Topic-parameterised external-intelligence scan + digest. Default topic is the AI/Claude Code/PKM landscape; pass a topic name (e.g. `cybersec`) to run a different profile. Curated-source scan, user-provided URL pile, or both — assessed through the active profile's lens.
Installs into .claude/skills of the current project.
Are you the author of Landscape Scan?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/opencairn-landscape-scan)
---
name: landscape-scan
description: Topic-parameterised external-intelligence scan + digest. Default topic is the AI/Claude Code/PKM landscape; pass a topic name (e.g. `cybersec`) to run a different profile. Curated-source scan, user-provided URL pile, or both — assessed through the active profile's lens.
---
# Landscape Scan — External Intelligence (Scan + Digest)
A focused external-intelligence pass. The **engine** (this file) is domain-agnostic orchestration: mode selection, fetch mechanics, verification discipline, supply-chain cooldown, report wrapper. The **profile** (a file under `landscape-profiles/`) supplies everything domain-specific: which sources to scan, what to read for context, how to assess findings, and which report sections to emit.
Two modes that can run separately or together:
- **Scan mode** — go check the profile's curated source list (proactive).
- **Digest mode** — the user has dropped a pile of URLs / tweets / posts they want reviewed (reactive).
- **Both** — run scan, then layer digest on top.
## Philosophy
Research that ends in trials. Surface patterns, architecture insights, and (for threat-style profiles) exposure, then turn the strongest findings into hands-on trials. Dismiss a lead only on evidence about the thing itself — a trial, a primary source, or a completed second pass — never on how it was packaged or promoted. A run with nothing to try is valid only when every candidate's second pass has been run and reported.
The assessment *frame* lives in the profile, because what "actionable" means is domain-specific:
- a tool-adoption profile (the default `ai-cc-pkm`) asks "should this change how I work?" — **fit** + **capability** passes;
- a threat-intel profile (`cybersec`) asks "does this reach something I run, and what do I do about it?" — **exposure** + **action** passes.
The engine never collapses a profile's passes into a single verdict, and never skips the second pass based on the first.
## Instructions
### Standing gate — binds every step and the report, for every profile
**⛔ Read-the-source gate (mandatory).** Any *comparative claim that names a specific local skill* — that an external finding obsoletes / upgrades / replaces / collides with / is inferior or superior to / makes redundant `$X` — MUST be backed by reading `$X`'s own source *this run*, **wherever the claim appears** (Step 7's fit pass, Step 8, or any section of the report). Reviewer seats and external write-ups only ever see a name plus a one-line description, so their comparative claims are *structurally speculation*. **Name/description overlap is a prompt to open the file, never a verdict.**
- **Who reads:** the actor that writes the claim. A reviewer seat that can't load local skills emits "name overlap — source unread" and leaves the verdict to the orchestrator, which opens the named source itself before accepting or reporting it.
- **What to read:** the named Codex skill at `$SKILLS_ROOT/<name>/SKILL.md`. If it has not been ported, read the Claude source at `~/.claude/commands/<name>.md` and label that source explicitly. Read enough of the body to establish its operating layer and mechanism, not just the frontmatter one-liner.
- **If no source file exists, the claim is *more* suspect, not less.** Confirm the skill exists at all; absence of a file is grounds to **drop the claim**, never wave it through.
- **Checkable:** every comparative verdict naming a skill traces to a source read this run, recorded as `Source read: <path>` (or `no file — claim dropped`).
This gate lives here, outside the numbered steps, because it is not conditional on any profile key — a profile that skips the obsolescence check is still bound by it.
### 0. Resolve vault path and load shared rules
```bash
"${VAULT_PATH:?VAULT_PATH not set}/.claude/scripts/resolve-vault.sh"
```
If error, abort. Set **`SKILLS_ROOT`** to the first candidate below that contains both `landscape-scan/landscape-profiles/` and `_shared-rules.md`:
1. `${CODEX_HOME:-$HOME/.codex}/skills`;
2. `$HOME/.agents/skills`;
3. the nearest repository `.agents/skills` directory visible from the current working directory;
4. `~/repos/OpenCairn/codex/skills` for a canonical development checkout.
**If no candidate resolves, abort** and state which paths were tried. If more than one resolves, use the first and record the fact under `## Run diagnostics`: installed and canonical copies should match, so divergence belongs in the installation's template-sync workflow if one exists. Set `SKILL_DIR="$SKILLS_ROOT/landscape-scan"`, read `$SKILLS_ROOT/_shared-rules.md`, and apply it throughout. All code below uses `{VAULT}`, `$SKILLS_ROOT`, and `$SKILL_DIR` as placeholders to substitute with resolved absolute paths.
**Environment assumption:** this engine assumes a GNU/Linux shell (the profiles call `date -d`, `lsb_release`, `dpkg-query`), current web-search tooling, and the fetch ladder in `_shared-rules-content.md` §26. On a non-GNU shell, substitute the platform equivalent. If a configured fetch rung is unavailable, continue down §26's ladder and record the gap.
### 1. Resolve topic and load the profile
Determine the **topic** from the invocation:
- A topic word in the triggering message or arguments (e.g. `$landscape-scan cybersec`, "run a cybersec landscape scan") → that topic.
- No topic word → default topic **`ai-cc-pkm`** (preserves the original AI/Claude Code/PKM behaviour exactly).
Resolve the profile file at `$SKILL_DIR/landscape-profiles/<topic>.md`. If the named profile doesn't exist, list the available profiles (`ls "$SKILL_DIR/landscape-profiles/"`) and ask the user which to run — don't silently fall back to default when a topic was explicitly named.
**Read the profile now.** It defines, for the rest of this run (step numbers below refer to this engine's steps):
- `One-liner` — the profile's one-line self-description; surface it in the Step 10 report header.
- `Contextualising reads` — what to load in Step 4.
- `Sources` — the curated list for Step 5 (scan mode), plus any source-specific coverage rules.
- `Assessment frame` — the passes to run in Step 7 and their definitions.
- `Report sections` — the finding-sections Step 9 classifies into and Step 10 inserts (the engine supplies the common wrapper).
- `File naming` — the filename suffix + prior-scan glob (default profile: none; others: `-<topic>`).
- `Obsolescence check` — its **value** governs whether Step 8 runs (tool-adoption profiles only).
Everything below that says "per the profile" reads from this file.
### 2. Current date + file naming
```bash
date +"%G-W%V"
```
File naming: `YYYY-Www<suffix>.md` (ISO week). Use `%G` (ISO week-numbering year), **not** `%Y` (calendar year) — the two disagree in late-Dec / early-Jan and `%Y-W%V` produces self-contradictory labels (e.g. `2024-W01`) that break the prior-scan delta glob across the year boundary. The default `ai-cc-pkm` profile uses no suffix (`2026-W16.md`) so existing files and delta logic are unaffected; other profiles use `-<topic>` (`2026-W16-cybersec.md`). Prior-scan lookups in later steps glob **only the active profile's pattern** so topics don't cross-contaminate each other's deltas.
### 3. Mode selection
Determine which mode(s) apply, in precedence order:
- **Bare invocation — just `$landscape-scan` (or `$landscape-scan <topic>`), no URLs and no extra natural-language request** → **scan mode** on the resolved profile. Don't ask; this is the original default behaviour. (The clarifying question below is only for an ambiguous *natural-language* scan request.)
- **User said "both" or similar** → both modes. (Explicit user intent wins.)
- **URLs present AND a scan verb ("scan" / "landscape scan" / "run landscape scan")** → both modes. The skill invocation itself is **not** a scan verb — `$landscape-scan <topic>` plus a URL pile and nothing else is digest mode, since the bare-invocation rule above is scoped to runs with no URLs. The verb has to appear in the natural-language request.
- **URLs present in the triggering message (or as args), no scan verb** → digest mode only.
- **Scan verb, no URLs** → **ask first**: "Scan mode (curated sources only), or do you also have URLs for a digest pass?" Default to scan-only if the user confirms no URLs. Do NOT lock in scan-only silently — the cost of asking is one question; the cost of mis-classification is the user having to supply URLs mid-run, which forces a mode switch and re-planning of the report structure.
- **Ambiguous** → ask the user: "Scan (curated sources), digest (you have URLs), or both?"
### 4. Contextualising preamble (ALWAYS — before any assessment)
Relevance judgement requires knowing what the user is *already doing* (and, for tool profiles, what they're *publicly positioning*; for threat profiles, *what they actually run*). Without this, assessments default to generic-audience relevance and miss what matters.
**Load the profile's `Contextualising reads` list.** Every entry in that list is marked **[required]** or **[optional]**:
- **[required]** entries load on every run, whatever the run's scope. If a required read is missing or unusable, follow the profile's stated handling for it — never proceed by inferring what it would have said. A profile marks an entry required when its assessment is unsound without it (e.g. `cybersec`'s stack inventory *is* the allow-list its exposure pass runs against; without it, findings get judged on product-name matches).
- **[optional]** entries load proportionate to the scope of the run. These are the ones "keep it light" applies to.
An unmarked entry is treated as **[required]** — the safe default, since the failure mode of skipping a genuinely required read is a confident assessment built on nothing.
**Emit the resolved reads as an observable (required output)**, before any assessment — a gate that only asserts is a gate an executor can skip silently:
```
Contextualising reads: [required] 2/2 loaded | [optional] 1/2 | missing: <name> — <handling applied>
```
Carry the same line into the report's `## Contextualising reads` section. A run that reaches assessment without having displayed it has not passed this step.
Also:
- **Prior scan** — the most recent one for *this topic* regardless of age (it stays the delta anchor after a multi-week gap; note it under `## Run diagnostics` if it's older than ~2 weeks), from `{VAULT}/06 Archive/Landscape Scans/` — use the profile's **concrete prior-scan glob** (an executable shell pattern, e.g. `20[0-9][0-9]-W[0-9][0-9]-cybersec.md`, not the display template `YYYY-Www.md`). **Pick the most recent by the ISO week label parsed from the filename, not by mtime** — per `_shared-rules.md` §22, a filename that encodes the date is the authority, and Step 10 appends addenda in place, so a re-run of an older scan makes the stale file the newest by mtime and silently mis-anchors the delta. Sort lexically (`ls -1 … | sort -r`); mtime breaks ties only. Quote the directory but never the glob — `ls -1 "{VAULT}/06 Archive/Landscape Scans/"<glob> | sort -r` — a fully-quoted path makes the brackets literal and false-reports a cold start; an unquoted path breaks on the directory's spaces. Don't re-report things already classified unless their status changed. If the glob returns nothing, list the directory before concluding: no landscape files at all → genuine cold start, say so; files from other topics present but none matching this profile's pattern → report "no prior scans matched glob `<glob>`" under `## Run diagnostics` so a mis-written pattern surfaces instead of silently reading as a cold start.
- **Just-in-time routing-table context** — if a specific finding later touches an `AGENTS.md` routing-table topic, load that context before assessing that finding.
Keep the **[optional]** reads light — for those, the goal is *calibration*, not total recall. This softener does not reach `[required]` entries.
### 5. Scan mode — curated sources
Only if scan mode is active. Work the profile's `Sources` list; for each, focus on what's new or changed since the last scan for this topic. Honour any source-specific coverage rules the profile defines (e.g. the default profile's Zvi tool-tip sections).
If a source is unreachable or a repo has been deleted/moved, note it as "unavailable" and move on. Flag it in the *Sources list updates* section so the profile file gets cleaned up.
### 6. Digest mode — user-provided URL pile
Only if digest mode is active. These verification disciplines are domain-agnostic — they apply whatever the topic:
- **Fetch all URLs in parallel.** Don't serialise.
- **Handle fetch failures gracefully.** Follow `_shared-rules-content.md` §26. For 403/429/timeout/blocked pages, begin at its configured-reader rung; a credits/quota error counts as unavailable. If all routes fail, log the URL under "unfetchable" and continue.
- **Verify suspicious sources before trusting the pitch.** Red flags: engagement-ratio anomalies (many retweets, zero likes → bot amplification); self-promotion by the author without independent endorsement; unverifiable superlatives ("99% accuracy," "permanent fix"); brand-new repo with improbable star counts (astroturfing). For threat profiles, add: unsubstantiated severity claims, vendor marketing dressed as advisory, and "patch available" claims with no CVE or commit to point to.
- **Fetch the underlying primary source, not just the pitch.** A tweet/blog pointing to a tool → fetch the repo/product page (last commit, issues, licence). A post describing a vulnerability → fetch the CVE record / vendor advisory / commit, and report what's actually confirmed.
- **Check commit activity on `main`, not just releases.** Stale release ≠ dead; active releases ≠ live `main`. Independent signals — look at both.
- **Verify falsifiable structural claims against raw source.** If a summary asserts a project is archived/deprecated/forked/superseded/renamed/rewritten — or that a vuln is patched/exploited-in-the-wild/disputed — re-fetch the raw source (e.g. `raw.githubusercontent.com/<owner>/<repo>/main/README.md`, the NVD entry, the vendor bulletin) and quote verbatim before acting. Summarisers occasionally confabulate plausible structural claims; high-stakes assertions need verbatim confirmation.
- **Flag red flags explicitly in the output.** Don't smooth them over, but don't let them pre-empt the profile's second-pass assessment either. Promotional packaging (hype, templated or coordinated posts, amplification) is a reason to verify the underlying thing, never a classification by itself: a finding whose only defect is how it was promoted still gets both passes.
### 7. Assessment — per the profile's frame (BOTH modes)
For every finding worth reporting — from scan sources *or* digest pile — run the profile's `Assessment frame`. Each profile defines two passes; **run both, report both, never collapse to one verdict, never skip the second based on the first.**
The active profile's frame is authoritative; the two below are illustrative of the shipped profiles, not a closed set:
- Default `ai-cc-pkm`: **Fit** (does it replace / extend / lose-to what we have?) + **Capability** (what does it unlock that isn't being done today?).
- `cybersec`: **Exposure** (does it reach something on the stack inventory, and by what path?) + **Action** (severity → patch now / cooldown-then-patch / monitor / not-exposed-note).
The default profile's fit pass may name a specific local skill — that triggers the ⛔ Read-the-source gate (the standing gate at the top of `## Instructions`). The cybersec profile's exposure pass may name a specific stack component — that requires checking it against the stack-inventory doc, not asserting exposure from a product-name match.
### 8. Obsolescence / collision check — only if the profile's `Obsolescence check` value calls for it
*Tool-adoption profiles only.* Branch on the **value** of the profile's `Obsolescence check` key, not on whether the heading exists — every profile defines the key, so heading-presence is not the signal. Run this step only if that value specifies a check: the default `ai-cc-pkm` does; `cybersec` sets it to "does not run," so skip the step entirely for it.
Check whether any existing skill domains now have mature external alternatives that didn't exist (or weren't mature enough) when the skill was written. Lightweight, not a deep audit.
Every comparative claim this step produces is bound by the **⛔ Read-the-source gate** at the top of Instructions — read `/X`'s source this run, record `Source read: <path>`, drop the claim if no file exists.
If the vault contains a capability-audit project doc, update the relevant domain section. Locate it rather than assuming a filename — `find "{VAULT}/03 Projects" -maxdepth 1 -type f -iname '*capability audit*.md' -print` — and if nothing matches, say so instead of creating one. Write it through `locked-edit.sh` per `_shared-rules.md` §5.
### 9. Classify findings
Classification labels come from the profile's `Report sections`. For each finding, state what it is and its significance in the active profile's terms — tool-adoption profiles: what problem it solves (handled or not today); threat profiles: what it reaches and the action trigger — and place it in the right section.
**No dismissal without the second pass.** Every entry in a dismissal section (e.g. Skip) carries its second-pass line: what the finding would unlock, and why that is not worth trying now. An entry without it goes back through Step 7.
**Bias to try.** Where the profile defines a trial section, place every finding whose second pass shows real capability there, ranked, each with one concrete first trial step, where it would run, and the observation that decides keep or drop. When the invocation names a frame project with a trial list, route those entries to it.
**For any finding whose action is "install / adopt a package" (either profile — a Claude Code tool, or a security tool), compute supply-chain cooldown explicitly.** Days since latest release (npm/PyPI/Docker) or last commit (source installs). Compare against a **7-day** cooldown window measured from that date; a **3-day** window applies only when pinning to an *earlier* version that has itself been public ≥7 days. Don't install a package the day it publishes — let downstream users surface supply-chain compromises (malicious publishes, typosquats, compromised maintainer accounts) first. State the recommendation — **install now / pin earlier version vX.Y.Z / defer until YYYY-MM-DD** — don't leave arithmetic to the user. **Exception:** an exposed, exploited-in-the-wild or critical-reachable security patch overrides the cooldown — when the profile's action pass says *patch now*, apply it immediately and state why (active exploitation outweighs the fresh-publish risk); the cooldown still governs optional tool/source adoption. (This discipline is doubly load-bearing for the `cybersec` profile, whose own beat is supply-chain attacks — the override resolves the irony that a rushed patch can itself be poisoned, but an exploited critical can't wait.)
### 10. Generate scan report
```bash
mkdir -p "{VAULT}/06 Archive/Landscape Scans"
```
Draft the report outside the vault. Install `{VAULT}/06 Archive/Landscape Scans/YYYY-Www<suffix>.md` through `locked-edit.sh`: use `--append` for a missing file. If it already exists (a second run this week), append a dated addendum through `--append` rather than overwriting, and never invent a suffixed filename (`…b.md`) — the profiles' prior-scan globs won't match it. The engine supplies the common wrapper; the profile supplies the middle finding-sections:
```markdown
# Landscape Scan (<topic>) — YYYY-Www
**Topic / profile:** <topic> — <profile `One-liner`>
**Modes run:** [scan / digest / both]
**Date generated:** [YYYY-MM-DD (day-of-week), location if relevant]
## Contextualising reads
- [files/sources loaded in step 4]
## Scan sources checked *(scan mode only)*
- [N sources; flag any unavailable]
## URLs digested *(digest mode only)*
- [N, list each]
## Derived via abstraction-expansion *(only if rule fired this run)*
- [N sources surfaced via the Guidelines category-abstraction rule — NOT in the input pile]
## Unfetchable
- [URLs that failed every rung of the §26 fetch ladder, with reason; or "None this run."]
## Run diagnostics
- [Step 0 multiple commands dirs resolved (which, and that resolution was heuristic); prior scan older than ~2 weeks; no prior scans matched the glob; or "None this run."]
---
*Convention: single-value fields inline at top (`**X:** Y`); multi-item fields as section headings below.*
<<< PROFILE REPORT SECTIONS — insert the active profile's `Report sections` here >>>
## Red flags flagged
- [Source] — [what was suspicious; was the underlying thing verified or not]
## No change since last scan
- [Sources that had nothing new]
## Sources list updates
- [New sources to add or stale ones to remove from the profile file]
## Actions executed this run
- [Any edits made to AGENTS.md, context files, skills, profile files, tasks added to This Week / project hubs, or config — anything the scan surfaced that resulted in immediate in-session action. Makes the report self-contained. If none: "None."]
```
### 11. Update the profile's source list
If any sources should be added or removed (repos deleted, new high-quality sources discovered), edit the **profile file** directly (not this engine file).
### 12. Display confirmation
```
Landscape scan (<topic>) saved to: 06 Archive/Landscape Scans/YYYY-Www<suffix>.md
Modes run: [scan / digest / both]
Sources checked: N URLs digested: N Derived: N Unfetchable: N
Findings: [per profile's section counts]
Red flags flagged: N
Obsolescence check: [N domains / n/a for this profile]
Actions executed this run: N
Source list updates: [any changes made to the profile file]
```
## Guidelines
- **Research that ends in trials.** "How did they solve X?" / "Does this reach me?" come first; then "what is the smallest hands-on trial of this?"
- **Two passes, always.** First-pass dismissal does not close the second pass. Report both, per the profile's frame.
- **Delta over repeat.** Only report what's new since the last scan *for this topic*. Don't re-list stable sources that haven't changed.
- **Verify before trusting pitches.** Tweets/blogs/vendor marketing hype; repos, product pages, CVE records, and commits ship. Fetch the underlying thing.
- **Update the profile source list,** not this engine, when a source goes stale or a new one emerges.
- **Quick and focused.** Scan mode: 10–20 min for a light delta with few new items; allow longer for a cold start or a large gap. Follow the profile’s coverage scope and read relevant sections properly rather than expanding into unrelated material. Digest mode: scales with URL count but stays snappy.
- **Expand category abstraction when a direct-match search returns nothing.** If an exact-match search yields zero hits, don't conclude "none exists." Abstract up one level and search again. Direct-match null is a second-pass failure mode if not expanded.
- **Honest about signal.** A null result is valid only with each candidate's second pass shown — say so, and say what would change it.
## Frequency
Run weekly (typical) per topic, or whenever the user explicitly requests it — including ad-hoc digest-only runs when they've batched up a pile of URLs.
## Adding a new topic
Drop a `landscape-profiles/<topic>.md` file defining the seven profile keys (`One-liner`, `Contextualising reads`, `Sources`, `Assessment frame`, `Report sections`, `File naming`, `Obsolescence check` — the last with an explicit value, e.g. "does not run", since Step 8 branches on the value).
**Mark every `Contextualising reads` entry `[required]` or `[optional]`** — Step 4's "keep it light" reaches the optional ones only, and an unmarked entry defaults to required. Mark an entry required when the profile's assessment is unsound without it, and state there what to do when it's missing.
Most new topics need no engine edits. Two caveats: Step 8's vocabulary (skill obsolescence) and Step 9's install-cooldown framing are tool-adoption specifics hard-wired outside the seven keys, so a profile in a different domain may need them generalised. Keep profiles template-suitable where possible: cite the user's real infrastructure by pointing the profile at a vault doc (e.g. a stack-inventory note) rather than hardcoding personal specifics into the profile.
## Integration with Other Commands
- **$quarterly-review:** quarterly's strategic-alignment step would benefit from accumulated landscape findings. *(Intended complement — not yet wired into that skill.)*
- **$weekly-review:** weekly is internal (vault health, progress); landscape-scan is external. *(Intended complement — not yet wired into that skill.)*
- **The `cybersec` profile and an internal security-audit skill:** the audit is internal posture (what's misconfigured on the machine *now*); the cybersec scan is external threat intel (what got disclosed that touches the stack). Neither subsumes the other. The template ships no security-audit skill — if you run one, wire the pair by having both read the same stack-inventory doc (the one the cybersec profile's `Contextualising reads` names).