From 6870b6109899cf7a4261e17302eddeec39cd91ad Mon Sep 17 00:00:00 2001 From: ojfbot <151410806+ojfbot@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:11:30 -0500 Subject: [PATCH] feat(skills): /wayfinder at user scope + full/lite charting modes (ADR-0101 rev A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /wayfinder was catalog-registered but never flagged scope:["user"], so despite the catalog-driven promise of adr:catalog-scoped-user-skills it resolved only inside ojfbot repos — charting unavailable exactly when fog is thickest, before an initiative has a repo. Flagged for user scope (catalog v1.21). User scope forces the rest, and surfaced four defects repo scope had hidden: - resolve-or-fail was unenforced prose. Adds scripts/resolve-anchor.mjs (--detect, --anchor=ns:#P), importing scripts/lib/northstar-fm.mjs rather than reimplementing the constrained frontmatter parse. - Map path was ambiguous: SKILL.md said repo-relative, map-format.md said core-absolute. Harmless inside core, a live bug from an arbitrary cwd. Full mode resolves to the core map library; lite mode to . - Lite mode for off-fleet charting: file-only map, no northstar anchor, no tracker projection, announced out loud. A *claimed* anchor that will not resolve stays an error — silent de-anchoring is the guarded failure. - The /frame-standup cross-ref SKILL.md claimed did not exist. Step 4.6 now routes an unassertable slice entrance to charting instead of leaving the slice queued indefinitely. Also wires the selfco vault as the charting-time reference layer per adr:bonded-pair-division-of-labor (draft) — design-time, read-only, one-way, full mode only. The operating-surface-bonded-pair map already did this by hand. Unchanged: file-canonical maps, serialized research, the two-ledger separation, and the MANDATORY trigger posture (upstream's disable-model-invocation stays rejected — under-firing is this fleet's measured failure, per ADR-0068). Upstream mattpocock/skills remains at v1.1.0, the version already absorbed; no upstream drift to reconcile. Files a defect in passing: northstar-fm.mjs does not strip YAML inline comments, so buddy-check's registry slug parses with its comment attached (91 chars). northstar-lint missed it because it resolves by path; resolve-anchor.mjs is the fleet's first slug-keyed consumer. Worked around for slug comparison only — a naive strip would truncate live roadmap values containing "PR #165". Co-Authored-By: Claude Opus 5 --- .claude/skills/frame-standup/SKILL.md | 7 + .../skill-loader/knowledge/skill-catalog.json | 7 +- .claude/skills/wayfinder/SKILL.md | 14 +- .../wayfinder/knowledge/fleet-substrate.md | 90 +++++++++ .../skills/wayfinder/knowledge/map-format.md | 8 +- .../wayfinder/scripts/resolve-anchor.mjs | 172 ++++++++++++++++++ decisions/adr/0101-wayfinder-decision-maps.md | 43 ++++- ...orthstar-fm-inline-comment-not-stripped.md | 22 +++ implementation-notes.md | 4 + scripts/templates/user-claude-md.template | 4 +- 10 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 .claude/skills/wayfinder/knowledge/fleet-substrate.md create mode 100644 .claude/skills/wayfinder/scripts/resolve-anchor.mjs create mode 100644 decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md diff --git a/.claude/skills/frame-standup/SKILL.md b/.claude/skills/frame-standup/SKILL.md index b9180b9c..fa089ee4 100644 --- a/.claude/skills/frame-standup/SKILL.md +++ b/.claude/skills/frame-standup/SKILL.md @@ -230,6 +230,13 @@ whose entrance criteria now look satisfied (offer to flip `status: queued → re THIS step's human judgment, the one thing the compiler never does), and `delivered` slices awaiting the merge ritual. These feed Step 5 ranking and Step 7b dispatch. +**A slice whose entrance you cannot assert either way has found fog** — the blocker is an open +question, not a missing deliverable. Route it to `/wayfinder` to chart rather than leaving it +`queued` indefinitely, which is how a slice quietly becomes permanent backlog. Surface it in the +day plan as "chart it", not as a dispatchable action; charting is a session of its own and closes +zero slices. (Boundary: *what/whether* → `/wayfinder`; *how to ship safely in stages* → +`/gated-slice`.) + ### Step 5 — Generate the day plan Cross-reference: daily-logger suggested actions + open action backlog + diff --git a/.claude/skills/skill-loader/knowledge/skill-catalog.json b/.claude/skills/skill-loader/knowledge/skill-catalog.json index b9989add..3c752e2d 100644 --- a/.claude/skills/skill-loader/knowledge/skill-catalog.json +++ b/.claude/skills/skill-loader/knowledge/skill-catalog.json @@ -1,6 +1,6 @@ { - "version": "1.20", - "updated": "2026-07-29", + "version": "1.21", + "updated": "2026-07-31", "skills": [ { "name": "opm", @@ -689,6 +689,9 @@ ], "layer_affinity": [ 0 + ], + "scope": [ + "user" ] }, { diff --git a/.claude/skills/wayfinder/SKILL.md b/.claude/skills/wayfinder/SKILL.md index c1ffdb96..cd9b9104 100644 --- a/.claude/skills/wayfinder/SKILL.md +++ b/.claude/skills/wayfinder/SKILL.md @@ -18,23 +18,26 @@ You are a wayfinder charting work that is too big for one agent session and wrap ## Core principles 1. **Plan, don't do.** A wayfinder session either charts the map or works exactly one ticket. Resolving tickets during charting is the signature failure. -2. **The map is file-canonical; issues are the projection.** The map lives at `decisions/wayfinder/.md`; tickets are GitHub child issues with native blocking edges so the frontier renders in the tracker. Fix the map by editing the file (same posture as roadmap files vs compiled beads). +2. **The map is file-canonical; issues are the projection.** In full mode the map lives at `/decisions/wayfinder/.md` — one library, so any surface can enumerate every open frontier in a single read; tickets are GitHub child issues with native blocking edges so the frontier renders in the tracker. Fix the map by editing the file (same posture as roadmap files vs compiled beads). In lite mode it lives at `/decisions/wayfinder/.md` with no tracker projection (`knowledge/fleet-substrate.md`). 3. **Placement litmus** (`adr:wayfinder-decision-maps`): can you state `success` + a machine-runnable `check`? → it's a roadmap slice, not a ticket here. Can you state only the question precisely? → wayfinder ticket. Can't state the question yet? → `## Not yet specified` fog. Fog graduates to a ticket when the question — not the answer — becomes statable. 4. **Facts are gathered; decisions are the user's.** Charting may explore the repo freely, but every ticket's decision belongs to the user (grilling tickets are HITL by definition — an agent answering its own ticket has broken the loop). 5. **Refer by name.** Tickets are referenced by title in prose, never bare issue numbers. ## Modes +This skill is installed at user scope, so it can fire anywhere on this machine. **Before anything else, decide the charting mode** — `scripts/resolve-anchor.mjs --detect` returns `full` (fleet substrate resolves) or `lite` (it doesn't). Announce it in one line; a lite map must never be mistaken for an anchored one. The full mode table is in `knowledge/fleet-substrate.md`. + ### Chart (default — new map) -1. **Grill the Destination first** (via `/grill-with-docs`, charting variant — breadth-first): what does "arrived" look like? Name it before anything else; if the map is anchored to a northstar, cite the properties (`ns:#P`, resolve-or-fail). The Destination fixes scope. +1. **Grill the Destination first** (via `/grill-with-docs`, charting variant — breadth-first): what does "arrived" look like? Name it before anything else; if the map is anchored to a northstar, cite the properties (`ns:#P`, resolve-or-fail — run `scripts/resolve-anchor.mjs --anchor=ns:#P` and don't write the anchor if it exits non-zero). The Destination fixes scope. +1b. **Read the reference layer** (full mode only): consult `~/selfco/wiki/` — `synthesis/`, `concepts/`, `entities/` — for prior lenses bearing on the Destination, and cite what you use under `## Notes`. Design-time, read-only, one-way: a vault page informs a ticket body, it never *is* a decision (`adr:bonded-pair-division-of-labor`, draft). 2. **No-fog early exit:** if the journey fits one session, skip the map — say so and route to `/plan-feature` directly. 3. Breadth-first over the fog: enumerate the open questions, apply the placement litmus to each, and type every ticket: - `research` (AFK) — resolved via the **deep-research harness, ONE cycle at a time** (sequential-research rule; a charting session that fans research out in parallel is a bug, not a speedup); findings filed to `decisions/research/`, never throwaway branches. - `grilling` (HITL, the default) — resolved via `/grill-with-docs`; its in-loop ADR stubs give `## Decisions so far` real `decisions/adr/` entries to index. - `prototype` (HITL) — resolved via `/prototype`; artifact linked from the ticket (disposition per `/prototype`, incl. the kept-branch primary-source option). - `task` (either) — manual unblocking work: provisioning, access, data moves. -4. Write the map file (`knowledge/map-format.md`), then create the child issues labelled `wayfinder:` in dependency order, wiring blocking edges with the tracker's native blocked-by relationship — the **frontier** (open + unblocked + unclaimed) must render in the tracker. Sized so each ticket fits one session. +4. Write the map file (`knowledge/map-format.md`) at the mode's path, then — **full mode only** — create the child issues labelled `wayfinder:` in dependency order, wiring blocking edges with the tracker's native blocked-by relationship, so the **frontier** (open + unblocked + unclaimed) renders in the tracker. In lite mode the Tickets table *is* the frontier. Sized so each ticket fits one session. 5. **Stop.** Zero tickets resolved in the charting session. ### Work (one ticket per session) @@ -64,7 +67,8 @@ The charted decisions are exactly what make `entrance`/`success`/`check` statabl - **Parallel research is the tempting bug.** Upstream's chart-time fan-out is rewritten to sequential here (2026-06-05 API-saturation failure; the verify stage collapses under concurrency). Slower charting is the accepted price — the SKILL says so, so don't "optimize" it back. - **Don't put deliveries on the map.** If a ticket's closure would be a merged PR, it's a roadmap slice that skipped the litmus — move it out. Mixing deliveries into the map (or questions into the roadmap) breaks both ledgers' closure semantics. - **The map body in the tracker is a projection.** Editing the GitHub issue body and not the file forks canon; the file wins, same as roadmaps vs beads. -- **A standup that can't assert a slice's entrance found fog.** The move is "chart it" — not leaving the slice queued forever (`/frame-standup` cross-ref). +- **A standup that can't assert a slice's entrance found fog.** The move is "chart it" — not leaving the slice queued forever (`/frame-standup` Step 4.6 routes here). +- **Lite mode is a degradation, not a dialect.** Say so out loud when it engages. The failure is a map that silently drops its `northstar:` anchor because the substrate wasn't found, then reads later as deliberately unanchored. A *claimed* anchor that won't resolve is an error — never write it and carry on. --- @@ -73,5 +77,7 @@ $ARGUMENTS ## See Also - `knowledge/map-format.md` — the map file schema + ticket issue template +- `knowledge/fleet-substrate.md` — full/lite modes, substrate read order, sharp edges (slug identity, vantage misses) +- `scripts/resolve-anchor.mjs` — `--detect` (mode) and `--anchor=ns:#P` (resolve-or-fail) - `decisions/adr/0101-wayfinder-decision-maps.md` — the governing decision (adapted from mattpocock/skills v1.1, verdicts D11–D13) - `/grill-with-docs` (grilling tickets, charting variant) · `/prototype` (prototype tickets) · `/gated-slice` (post-decision staging) · `/plan-feature --from-conversation` + `/orchestrate --emit=github-issues` (handoff) diff --git a/.claude/skills/wayfinder/knowledge/fleet-substrate.md b/.claude/skills/wayfinder/knowledge/fleet-substrate.md new file mode 100644 index 00000000..36aae2a7 --- /dev/null +++ b/.claude/skills/wayfinder/knowledge/fleet-substrate.md @@ -0,0 +1,90 @@ +# Wayfinder — fleet substrate and charting modes + +`/wayfinder` is installed at user scope (`scope: ["user"]` in `skill-catalog.json`), so it can +fire in any repo on this machine — including repos with no northstars, no `core/decisions/`, and +no fleet tracker. It therefore charts in one of **two modes**, decided by substrate +resolvability rather than by path. + +## Deciding the mode + +```bash +node "/scripts/resolve-anchor.mjs" --detect # human +node "/scripts/resolve-anchor.mjs" --detect --json # { mode, core_root, reason, registry_entries } +``` + +The script walks ancestors of the cwd looking for `decisions/northstar/README.md`, and for +`core/decisions/northstar/README.md` (the sibling-repo vantage — cwd is `~/ojfbot/`, core is +`~/ojfbot/core`). **Full** mode requires that registry to parse with at least one entry. +Everything else is **lite**. + +Detection is deliberately *not* a `~/ojfbot` path prefix: a core checkout moved elsewhere still +charts in full mode, and a scratch directory that happens to be named `ojfbot` does not. + +Announce the mode in one line at the start of a charting session. A lite map must never be +mistaken for an anchored one. + +## What changes between modes + +| | **full** (substrate resolves) | **lite** (it doesn't) | +|---|---|---| +| Map path | `/decisions/wayfinder/.md` | `/decisions/wayfinder/.md` | +| `northstar:` anchor | resolve-or-fail via the script | omitted — a claimed anchor is an **error**, never a silent skip | +| Ticket projection | GitHub child issues, `wayfinder:`, native blocked-by edges | table rows in the map only; `tracker_issue:` omitted | +| selfco reference read | yes (design-time, read-only) | skipped | +| Handoff | `/plan-feature --from-conversation` → `/orchestrate --emit=github-issues`, slices appended to the northstar's roadmap | `/plan-feature` only; no roadmap append | + +Full-mode maps all land in one library under core, which is what lets any surface enumerate every +open frontier in a single read. Lite-mode maps stay local to the repo that produced them. + +## Resolving an anchor + +```bash +node "/scripts/resolve-anchor.mjs" --anchor=ns:l2-ojfbot#P1 +``` + +Exit 0 with the resolved property, or exit 1 with the reason. This is the mechanism behind +"resolve-or-fail" — run it before writing a `northstar:` line, and do not write the anchor if it +fails. + +## Sharp edges + +- **Slugs are immutable identity** (ADR-0087) and are matched verbatim. `buddy-check` is + registered *without* the `l1-` prefix; `l1-virtuallight` is lowercase while its directory is + `virtualLight`. Never case-fold or prefix-normalize either — the script won't, and neither + should prose in a map. +- **Registered-but-absent is usually a vantage artifact,** not a registry lie: a sibling checkout + missing from this working copy. `northstar-lint.mjs` downgrades these to WARNs and `--detect` + keeps full mode; only anchors pointing into an absent file fail, and they fail individually + with `unreachable: true`. +- **Registry paths are core-root-relative** (`..//.claude/northstar.md`). Resolution belongs + to `northstar-fm.mjs`; don't hand-join paths. +- **The registry is markdown frontmatter, not JSON.** Only `northstar-fm.mjs`'s constrained, + `LIST_KEYS`-gated parser is safe on it. Import it; never reimplement or `JSON.parse` it. +- **A known parser gap:** inline YAML comments are not stripped, so `slug: buddy-check # NB: …` + parses with the comment attached. `resolve-anchor.mjs` works around this for slug comparison + only. Tracked as `decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md` — don't + "fix" it with a naive strip, which would truncate live roadmap values containing `PR #165`. + +## The selfco read (full mode, charting only) + +Per `adr:bonded-pair-division-of-labor` (**draft**), the operational spine carries *work objects* +(beads, registry, slices, instances) and the selfco vault carries *understanding objects* (lenses, +entity pages, syntheses), which the spine reads **at design time only**. + +Charting is design time. Before enumerating fog, consult `~/selfco/wiki/` — `synthesis/`, +`concepts/`, and `entities/` — for prior thinking that bears on the Destination, and cite what you +use under the map's `## Notes`. The `operating-surface-bonded-pair` map already does this by hand, +citing the `precedent-survey-methodology` corpus as its design-judgment source. + +Strictly read-only and one-way. Wayfinder never writes to the vault, and never treats a vault page +as a decision — vault pages inform ticket bodies; the decision still belongs to the user in that +ticket's session. Skip entirely when `~/selfco` is absent or in lite mode. + +## Read order for a charting session + +1. `resolve-anchor.mjs --detect` → mode. +2. Full mode only: `decisions/northstar/README.md` frontmatter → `registry:` + `roadmaps:`. +3. `resolve-anchor.mjs --anchor=…` for the Destination's claimed properties. +4. `~/selfco/wiki/` for prior lenses and syntheses on the Destination. +5. Existing maps in the map library — an initiative may already be charted, or its fog may + already be someone else's `## Out of scope`. diff --git a/.claude/skills/wayfinder/knowledge/map-format.md b/.claude/skills/wayfinder/knowledge/map-format.md index e928a491..ca95b960 100644 --- a/.claude/skills/wayfinder/knowledge/map-format.md +++ b/.claude/skills/wayfinder/knowledge/map-format.md @@ -1,7 +1,11 @@ # Wayfinder map format -The map is file-canonical at `core/decisions/wayfinder/.md`; GitHub child issues are the -projection (`adr:wayfinder-decision-maps`). Slug: kebab-case initiative name, no date. +The map is file-canonical; GitHub child issues are the projection +(`adr:wayfinder-decision-maps`). Slug: kebab-case initiative name, no date. + +Where the file lands depends on the charting mode (`knowledge/fleet-substrate.md`): +**full** → `/decisions/wayfinder/.md` (the one map library); +**lite** → `/decisions/wayfinder/.md`, with `northstar:` and `tracker_issue:` omitted. ## Map file diff --git a/.claude/skills/wayfinder/scripts/resolve-anchor.mjs b/.claude/skills/wayfinder/scripts/resolve-anchor.mjs new file mode 100644 index 00000000..3bc9dad6 --- /dev/null +++ b/.claude/skills/wayfinder/scripts/resolve-anchor.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node +/** + * resolve-anchor.mjs — deterministic substrate answers for /wayfinder. + * + * The SKILL mandates that a northstar anchor "resolve-or-fail" and that charting + * degrades to lite mode off-fleet. Both are facts, not judgment, so they live here + * (house pattern: deepen/scripts/measure-depth.mjs, adopt-stack/scripts/measure-pkg.mjs). + * + * --detect → { mode, core_root, reason, registry_entries } + * --anchor=ns:#P → { ok, anchor, ... } ; exit 1 with a reason when it doesn't resolve + * --json → machine output (default is human-readable) + * + * Imports the registry parser rather than reimplementing it: the registry is markdown + * frontmatter under a constrained schema, and northstar-fm.mjs's LIST_KEYS-gated parse + * is the only safe reader of it. + */ +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { loadAll, buildPropertyIndex } from '../../../../scripts/lib/northstar-fm.mjs'; + +const REGISTRY_REL = path.join('decisions', 'northstar', 'README.md'); + +/** + * Find the core checkout from a starting directory. + * + * Two shapes are walked at every ancestor, which covers both vantages that matter: + * /decisions/northstar/README.md → cwd is core itself, or below it + * /core/decisions/northstar/README.md → cwd is a sibling repo (anc = ~/ojfbot) + * + * Presence of the registry file is the signal — never a path prefix. A checkout moved + * out of ~/ojfbot still charts in full mode; a scratch dir named "ojfbot" does not. + */ +export function findCoreRoot(startDir) { + let dir = path.resolve(startDir); + for (;;) { + if (existsSync(path.join(dir, REGISTRY_REL))) return dir; + const sibling = path.join(dir, 'core'); + if (existsSync(path.join(sibling, REGISTRY_REL))) return sibling; + const parent = path.dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +/** + * Mode is decided by substrate *resolvability*, not location: a core root whose registry + * parses and is non-empty. A registry that parses to zero entries is lite — charting + * against an empty registry would let every anchor fail for the wrong reason. + * + * Registered-but-absent northstar files are deliberately NOT fatal here. northstar-lint.mjs + * downgrades those to vantage WARNs (`_unreachable`) because a sibling checkout missing from + * this working copy is a vantage artifact, not a registry lie. Full mode still applies; only + * anchors that actually point into an absent file fail, and they fail individually. + */ +export function detect(cwd = process.cwd()) { + const coreRoot = findCoreRoot(cwd); + if (!coreRoot) { + return { mode: 'lite', core_root: null, registry_entries: 0, reason: `no ${REGISTRY_REL} found in any ancestor of ${cwd} (or an ancestor's core/)` }; + } + const { error, entries } = loadAll(coreRoot); + if (error) return { mode: 'lite', core_root: coreRoot, registry_entries: 0, reason: error }; + if (!entries.length) { + return { mode: 'lite', core_root: coreRoot, registry_entries: 0, reason: `registry at ${path.join(coreRoot, REGISTRY_REL)} parsed but is empty` }; + } + return { mode: 'full', core_root: coreRoot, registry_entries: entries.length, reason: `registry resolves with ${entries.length} entries` }; +} + +const ANCHOR_RE = /^ns:([A-Za-z0-9._-]+)#(P\d+)$/; + +/** + * northstar-fm.mjs's `scalar()` does not strip YAML inline comments, so a registry entry + * written as `slug: buddy-check # NB: …` parses with the comment glued on (91 chars). + * The lint never noticed because it resolves entries by `path`, not by slug — this is the + * first slug-keyed consumer. + * + * Fixing it in the shared parser would need quote-aware stripping: the roadmap corpus has + * legitimate quoted values containing " #" (roadmap-l2-ojfbot.md:301 + * `title: "Calibrate judge #1 — …"`, plus entrance/deliverable prose citing `PR #165`), and a + * naive strip would silently truncate them and break an operational CI gate. So the repair is + * scoped to slug comparison, where it is unambiguously safe: slugs are kebab-case identifiers + * (ADR-0087) that can never legitimately contain " #". + * + * Filed as a defect rather than fixed here — see decisions/defects/. + */ +function slugOf(entry) { + return String(entry.slug ?? '').split(/\s+#/)[0].trim(); +} + +/** + * Resolve `ns:#P`. Slugs are immutable identity (ADR-0087) — matched verbatim, + * never case-folded or prefix-normalized. `buddy-check` is registered without the `l1-` + * prefix and `l1-virtuallight` is lowercase while its directory is `virtualLight`; + * normalizing either would resolve the wrong northstar or none at all. + */ +export function resolveAnchor(anchor, cwd = process.cwd()) { + const m = ANCHOR_RE.exec(anchor || ''); + if (!m) return { ok: false, anchor, reason: `malformed anchor ${JSON.stringify(anchor)} — expected ns:#P` }; + const [, slug, propId] = m; + + const det = detect(cwd); + if (det.mode !== 'full') return { ok: false, anchor, reason: `no fleet substrate: ${det.reason}`, mode: det.mode }; + + const { entries, northstars } = loadAll(det.core_root); + const entry = entries.find((e) => slugOf(e) === slug); + if (!entry) { + return { ok: false, anchor, reason: `northstar slug ${JSON.stringify(slug)} is not in the registry`, known_slugs: entries.map(slugOf) }; + } + + const ns = northstars.find((n) => slugOf(n) === slug); + if (ns && ns._missing) { + const why = ns._unreachable + ? `its repo root ${ns._repoRoot} is absent from this working copy (vantage, not a registry error)` + : `its file ${ns._abs} is missing`; + return { ok: false, anchor, reason: `northstar ${slug} is registered but unreadable — ${why}`, unreachable: Boolean(ns._unreachable) }; + } + + const hit = buildPropertyIndex(northstars).get(anchor); + if (!hit) { + const available = (ns?.properties || []).map((p) => p.id); + return { ok: false, anchor, reason: `${slug} has no property ${propId}`, available_properties: available }; + } + + const { property, northstar } = hit; + return { + ok: true, + anchor, + slug, + property_id: property.id, + name: property.name ?? null, + target: property.target ?? null, + current: property.current ?? null, + ladders_up_to: property.ladders_up_to ?? null, + tier: northstar.tier ?? null, + northstar_path: northstar._abs ?? null, + }; +} + +// ── CLI ───────────────────────────────────────────────────────────────────── +const args = process.argv.slice(2); +const json = args.includes('--json'); +const anchorArg = args.find((a) => a.startsWith('--anchor=')); + +if (args.includes('--help') || (!anchorArg && !args.includes('--detect'))) { + console.log(`resolve-anchor.mjs — substrate resolution for /wayfinder + + --detect report full|lite mode for the current directory + --anchor=ns:#P resolve a northstar anchor (exit 1 if it doesn't) + --json machine-readable output`); + process.exit(args.includes('--help') ? 0 : 2); +} + +if (anchorArg) { + const res = resolveAnchor(anchorArg.slice('--anchor='.length)); + if (json) console.log(JSON.stringify(res, null, 2)); + else if (res.ok) { + console.log(`OK ${res.anchor} → ${res.name ?? '(unnamed)'} [${res.tier ?? '?'}] current=${res.current ?? '?'} target=${res.target ?? '?'}`); + if (res.ladders_up_to) console.log(` ladders up to ${res.ladders_up_to}`); + } else { + console.error(`FAIL ${res.anchor}: ${res.reason}`); + if (res.available_properties?.length) console.error(` available: ${res.available_properties.join(', ')}`); + } + process.exit(res.ok ? 0 : 1); +} + +const det = detect(); +if (json) console.log(JSON.stringify(det, null, 2)); +else { + console.log(`mode: ${det.mode}`); + console.log(`core: ${det.core_root ?? '(none)'}`); + console.log(`why: ${det.reason}`); +} +process.exit(0); diff --git a/decisions/adr/0101-wayfinder-decision-maps.md b/decisions/adr/0101-wayfinder-decision-maps.md index 38f7a029..0fd3d7e9 100644 --- a/decisions/adr/0101-wayfinder-decision-maps.md +++ b/decisions/adr/0101-wayfinder-decision-maps.md @@ -1,8 +1,8 @@ # ADR-0101: /wayfinder — file-canonical decision maps upstream of the roadmap spine slug: wayfinder-decision-maps serial: 0101 -rev: -Date: 2026-07-22 +rev: A +Date: 2026-07-22 (revised 2026-07-31 — promoted to user scope; adds full/lite charting modes, an anchor resolver, and the selfco design-time read) Status: Accepted domain: workflow-engine type: tooling @@ -113,6 +113,45 @@ unacknowledged fog — the move is "chart it," not "leave it queued." | Extend `/gated-slice` with a "foggy mode" instead of a new skill | Conflates deciding with shipping; gated-slice's TPM/shadow/RIDM machinery presumes the destination is already decided. | | Keep using ad-hoc grill sessions + offsite docs | Status quo: no frontier, no dependency ordering, decisions scattered across transcripts with no index. | +## Rev A — user scope, two charting modes (2026-07-31) + +`/wayfinder` was catalog-registered but never flagged `scope: ["user"]`, so despite the +catalog-driven promise of `adr:catalog-scoped-user-skills` it resolved only inside ojfbot repos — +charting was unavailable exactly when fog is thickest, at the start of something that doesn't have +a repo yet. Flagged for user scope (catalog v1.21). The rest of Rev A is what user scope forces: + +- **Two charting modes.** `full` when the northstar registry resolves from the cwd, `lite` when it + doesn't. Lite writes the map to `/decisions/wayfinder/`, omits the `northstar:` anchor and + the tracker projection, and must announce itself. Rejected the alternative of refusing outright + off-fleet: the charting discipline is worth more than the anchoring, and a map that can't be + written is a map that gets written in a transcript instead. A *claimed* anchor that won't + resolve stays an error — silent de-anchoring is the failure mode this guards. +- **`scripts/resolve-anchor.mjs`.** Rev-zero mandated "resolve-or-fail" for `ns:#P` but + named no mechanism, so it was unenforced prose. The script (`--detect`, `--anchor=`) imports + `northstar-fm.mjs` and makes both the mode and the anchor deterministic — the house split where + facts belong to a script and judgment stays in the SKILL. +- **Map path disambiguated.** `SKILL.md` said `decisions/wayfinder/.md` while + `knowledge/map-format.md` said `core/decisions/wayfinder/.md`. Harmless inside core, a + live bug from an arbitrary cwd. Full mode resolves to the core library; one library is what lets + a surface enumerate every open frontier in a single read. +- **selfco read at charting time.** Per `adr:bonded-pair-division-of-labor` (draft) the spine reads + the vault's understanding objects at design time only; charting *is* design time, and the + `operating-surface-bonded-pair` map was already doing this by hand. Read-only, one-way, full mode + only — a vault page informs a ticket body, it never *is* a decision. +- **`/frame-standup` route made real.** Rev-zero's Gotcha claimed a standup cross-ref that did not + exist in `frame-standup`; Step 4.6 now routes an unassertable slice entrance to charting instead + of leaving it `queued` indefinitely. + +Unchanged: file-canonical maps, serialized research, the two-ledger separation, and the MANDATORY +trigger posture (upstream's `disable-model-invocation: true` stays rejected — under-firing, not +over-firing, is this fleet's measured failure per ADR-0068). + +Filed in passing: `decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md` — +`northstar-fm.mjs` doesn't strip YAML inline comments, so `buddy-check`'s registry slug parses with +its comment attached. `northstar-lint.mjs` missed it because it resolves by `path`; the resolver is +the fleet's first slug-keyed consumer. Worked around for slug comparison only, because a naive +strip would truncate live roadmap values containing `PR #165`. + ## Provenance | Field | Value | diff --git a/decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md b/decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md new file mode 100644 index 00000000..b5454af4 --- /dev/null +++ b/decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md @@ -0,0 +1,22 @@ +--- +type: defect-report +slug: northstar-fm-inline-comment-not-stripped +class: schema-drift +severity: medium +status: open +disposition: repair-mechanism +location: "core:scripts/lib/northstar-fm.mjs:17" +claim: "scalar() parses a constrained-YAML frontmatter value; the schema doc treats registry entries as YAML, in which a trailing ' # ...' is a comment." +actual: "Inline comments are not stripped. decisions/northstar/README.md:39 'slug: buddy-check # NB: ...' parses as a 91-char slug with the comment glued on. northstar-lint.mjs never caught it because it resolves entries by path, not slug; /wayfinder's resolve-anchor.mjs is the first slug-keyed consumer and had to work around it. A naive fix is unsafe: roadmap-l2-ojfbot.md:301 has title: \"Calibrate judge #1 — ...\" and entrance/deliverable prose citing 'PR #165', so stripping must be quote-aware or it will truncate live roadmap slices and break an operational CI gate." +claim_probe: "node --input-type=module -e \"import{loadRegistry}from'./scripts/lib/northstar-fm.mjs';process.exit(loadRegistry('.').entries.some(e=>/\\s#/.test(String(e.slug)))?0:1)\"" +filed: 2026-08-01 +filed_by: "agent:claude-opus-5" +evidence: "path:.claude/skills/wayfinder/scripts/resolve-anchor.mjs" +--- + +# scalar() parses a constrained-YAML frontmatter value; the schema doc treats registry entries as YAML, in which a trailing ' # ...' is a comment. +**Actual:** Inline comments are not stripped. decisions/northstar/README.md:39 'slug: buddy-check # NB: ...' parses as a 91-char slug with the comment glued on. northstar-lint.mjs never caught it because it resolves entries by path, not slug; /wayfinder's resolve-anchor.mjs is the first slug-keyed consumer and had to work around it. A naive fix is unsafe: roadmap-l2-ojfbot.md:301 has title: "Calibrate judge #1 — ..." and entrance/deliverable prose citing 'PR #165', so stripping must be quote-aware or it will truncate live roadmap slices and break an operational CI gate. +## Why it matters +_(unwritten — filed mid-work)_ +## Repair +_(unwritten)_ diff --git a/implementation-notes.md b/implementation-notes.md index d5325daa..27a0f554 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -14,6 +14,10 @@ The count is a **discovery rate, never a defect rate**: more entries is better. - Plan specified H8 Stage A as a `PreToolUse` hook on `Bash`. Territory: user-scope `PreToolUse(Bash)` fires synchronously on *every* Bash call in *every* session, so installing the `.mjs` directly would spawn node fleet-wide for a signal needed only on merges. Took the conservative option: added `merge-quiz.sh`, a bash prefilter that rejects non-merge commands in ~4ms, and installed that as the hook entry instead. - Plan's S0 assumed the `EXPECTED_ARTIFACT` audit would mostly confirm entries. Territory: `investigate` names an exact artifact path in its own SKILL.md but had no `pathPattern`, so any file written in-session satisfied it; `validate` declares `scheme: 'path'` while its SKILL.md says "No auto-fixes — findings and verdict only", i.e. the same unreachability as grill-with-docs. Fixed the evidenced one (`investigate`), and surfaced `validate`/`plan-feature` as annotated ambiguities rather than guessing — the file's own docblock requires surfacing over guessing, and resolving them changes what those skills are. +## Deviations — /wayfinder user-scope promotion 2026-07-31 + +- Plan listed `scripts/lib/northstar-fm.mjs` under "reused rather than rebuilt" and assumed slug lookups against the registry would just work. Territory: `scalar()` does not strip YAML inline comments, so `README.md:39` (`slug: buddy-check # NB: …`) parses as a 91-char slug. `northstar-lint.mjs` reports 19/19 present because it resolves entries by `path`; `resolve-anchor.mjs` is the fleet's first slug-keyed consumer, so it is the first thing to see the bug. Fixing `scalar()` looked like the clean repair until the corpus scan: `roadmap-l2-ojfbot.md:301` carries `title: "Calibrate judge #1 — …"` and several `entrance`/`deliverable` values cite `PR #165`, so a naive `\s+#.*$` strip would truncate live roadmap slices and break an operational CI gate. Took the conservative option: scoped the strip to slug comparison inside the new script (kebab slugs can never legitimately contain " #"), left the shared parser untouched, and filed `decisions/defects/dr-northstar-fm-inline-comment-not-stripped.md` (`repair-mechanism`, quote-aware strip) rather than absorbing it silently. + ## Deviations — GOLF UMBRELLA landing 2026-07-30 - Landing prompt placed the fairway itinerary row in "Leg 4"; disk itinerary has Leg 3 as the golf-only leg (2026-07-02 decision) and Leg 4 as GameWorld — rows landed in Leg 3, discrepancy noted in-row and in core#298. - Plan assumed the reference sweep could ride PR-1; gcgcca references live in four other repos, so the sweep distributed across core#297, mirrorworld#11, daily-logger#259, morning-cockpit#41. diff --git a/scripts/templates/user-claude-md.template b/scripts/templates/user-claude-md.template index 0387b18f..84d710e9 100644 --- a/scripts/templates/user-claude-md.template +++ b/scripts/templates/user-claude-md.template @@ -51,7 +51,9 @@ Every skill flagged `scope:["user"]` in core's `skill-catalog.json` is symlinked `~/.claude/skills/`, so it works in any session on this machine — not just inside ojfbot. Today that includes the Pocock stack (`/grill-with-docs`, `/tdd`, `/deepen`, `/triage`), session-boundary skills (`/bead`, `/resume`), the daily flow (`/frame-standup`, `/adr`, `/skill-loader`, `/daily-logger`), and -the lifecycle gates (`/investigate`, `/validate`, `/roadmap`). `/vault` is opt-in via `--with-selfco`. +the lifecycle gates (`/investigate`, `/validate`, `/roadmap`). `/wayfinder` sits upstream of all of +them — charting foggy, multi-session initiatives — and degrades to a file-only "lite" mode outside +the fleet, where there is no northstar registry to anchor to. `/vault` is opt-in via `--with-selfco`. The set is **catalog-driven** — flag a skill `scope:["user"]` in the catalog and re-run `core/scripts/install-agents.sh --user-scope` to sync. Skills resolve through symlinks to