@sabaiway/agent-workflow-kit 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +58 -7
- package/references/modes/migrate-adr-store.md +4 -2
- package/references/modes/recommendations.md +2 -0
- package/references/modes/status.md +1 -1
- package/references/modes/upgrade.md +2 -2
- package/references/modes/velocity.md +3 -2
- package/references/scripts/archive-decisions.mjs +14 -3
- package/references/scripts/archive-decisions.test.mjs +27 -0
- package/references/shared/command-shapes.md +25 -24
- package/tools/family-registry.mjs +78 -12
- package/tools/migrate-adr-store.mjs +177 -8
- package/tools/path-inventory.mjs +516 -0
- package/tools/recommendations.mjs +41 -2
- package/tools/renderers.mjs +8 -2
- package/tools/repo-search.mjs +217 -29
- package/tools/velocity-profile.mjs +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,88 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 4.5.0 — the ADR-store migration finds the projects that could never hear about it (AD-083)
|
|
8
|
+
|
|
9
|
+
**If your project still keeps its decisions in one big archive file, this release is the first thing
|
|
10
|
+
that will actually tell you so.** A whole class of projects was invisible to the old check.
|
|
11
|
+
|
|
12
|
+
**1. The old-layout check stopped guessing from filenames.** It used to say «old layout» only when a
|
|
13
|
+
retired `docs/ai/history/decisions-archive*.md` file was on disk. A project that never grew one — its
|
|
14
|
+
decisions file simply never got big enough to roll over — was reported as having nothing to do, and
|
|
15
|
+
running the migration greeted it with *«a fresh new-scheme tree»*. Its own pre-commit check agreed:
|
|
16
|
+
«OK — every tier is within its cap», forever, describing a retired layout as healthy. The check now
|
|
17
|
+
looks at the rotation script your project actually deploys: the current one knows about the
|
|
18
|
+
`docs/ai/adr/` store, the older one does not. That is a fact about your tree, not a guess — and it
|
|
19
|
+
deliberately stays quiet for two groups it would otherwise nag wrongly: projects whose current script
|
|
20
|
+
already reds their own commit with an actionable fix, and projects that run no Node at all.
|
|
21
|
+
|
|
22
|
+
**2. `migrate-adr-store` now finishes the job on those projects, and can always be re-run.** On a
|
|
23
|
+
project with no archive file to explode it snapshots, refreshes the enforcement scripts and seeds the
|
|
24
|
+
store (`docs/ai/adr/` + the navigator + a regenerated `docs/ai/index.md`). Interrupt it anywhere and
|
|
25
|
+
run it again: a store folder alone is no longer mistaken for «done» (the navigator has to exist and
|
|
26
|
+
your own check has to pass), the script refresh is re-planned in full each run, and a failed index
|
|
27
|
+
regeneration now fails the run loudly instead of reporting success. The preview earns its go-ahead —
|
|
28
|
+
it runs exactly the checks the real run does and writes nothing.
|
|
29
|
+
|
|
30
|
+
**3. `upgrade` stops saying «flow optimal» to a project on the old layout.** The recommendations
|
|
31
|
+
section now carries the migration as an item. It is HAND-APPLY on purpose: the one-liner it hands you
|
|
32
|
+
is the **preview**, and the real migration is a separate step you run only after seeing the plan and
|
|
33
|
+
agreeing to it — this crossing overwrites and deletes files your project already has, which is why it
|
|
34
|
+
never rides the ordinary one-click apply. Honest limit, stated plainly:
|
|
35
|
+
this reaches you through `status` and `upgrade`, so it mechanizes a door you already walk through — it
|
|
36
|
+
is not a new one for someone who never runs either.
|
|
37
|
+
|
|
38
|
+
## 4.4.0 — the guard stops charging you for answers it already has (AD-082)
|
|
39
|
+
|
|
40
|
+
**If the agent keeps asking you to approve its own `2>/dev/null`, this release is for you.** Two
|
|
41
|
+
things changed, and neither refuses anything.
|
|
42
|
+
|
|
43
|
+
**1. The ask now talks to the AGENT, not only to you.** Until now, when the guard stopped a read
|
|
44
|
+
carrying a redirection it explained itself in *your* approval dialog — and the agent that composed the
|
|
45
|
+
command never heard a word of it. So it composed the same shape again next turn. The ask now also
|
|
46
|
+
carries a hint back to the agent naming the lane that would not have prompted at all. Nothing is
|
|
47
|
+
blocked and nothing new is approved; if your host does not support the field, you get exactly the old
|
|
48
|
+
behaviour. **You should see the same class of prompt arrive less often, not disappear** — the honest
|
|
49
|
+
claim, stated because the alternative would be a promise this cannot keep.
|
|
50
|
+
|
|
51
|
+
**2. Two lanes so several small questions stop becoming one composed shell.**
|
|
52
|
+
|
|
53
|
+
- **`path-inventory.mjs` (new).** Does this exist, what type is it, how big, how many lines, what is
|
|
54
|
+
in that directory, what does this small config say — for any number of paths, in ONE call, promptless.
|
|
55
|
+
**A path that does not exist is a normal answer** (`exists:false`, exit 0), because "does either of
|
|
56
|
+
these exist" is a question whose interesting answer is "no", and a tool that errors on it sends you
|
|
57
|
+
straight back to writing a shell one-liner. Symlinks are reported by type and never followed;
|
|
58
|
+
binaries are reported, never decoded; every bound names itself instead of truncating quietly.
|
|
59
|
+
- **`repo-search.mjs --paths-file`.** The pattern half already had a file lane; the TARGET half did
|
|
60
|
+
not, so a path containing a backtick or `$(` could not be searched without a prompt however you
|
|
61
|
+
quoted it. Now it can. Both lane files are excluded from the search itself, by real path.
|
|
62
|
+
|
|
63
|
+
**What you must do to get the promptless part.** Re-run `/agent-workflow-kit velocity --kit-tools`.
|
|
64
|
+
Allow rules are values already written into your `.claude/settings.json`; upgrading the kit does not
|
|
65
|
+
edit them. The advisor now tells you exactly which entries you are missing.
|
|
66
|
+
|
|
67
|
+
**Also in this release:** the guard's recovery advice is derived per tool (it used to name
|
|
68
|
+
`--pattern-file` for every scanned tool, which was wrong the moment a second one existed), and it now
|
|
69
|
+
names both possibilities — pass the byte out of band if it is part of an argument, drop it if it is a
|
|
70
|
+
real redirect — because the guard cannot tell those apart and should not pretend to.
|
|
71
|
+
|
|
72
|
+
**One consequence worth knowing about.** The guard's scanned-tool match is a substring match across
|
|
73
|
+
the command, deliberately inclusive because an over-match only ever costs an extra prompt. Adding a
|
|
74
|
+
second tool extends that: a command that merely MENTIONS `path-inventory.mjs` and carries a
|
|
75
|
+
redirection now asks where it previously got no decision at all. That is the same licensed over-ask
|
|
76
|
+
`repo-search.mjs` has had since 4.2.0, on one more path string.
|
|
77
|
+
|
|
78
|
+
**One rule worth knowing if you pass paths to either tool.** A target must name exactly one thing:
|
|
79
|
+
no empty value, no NUL byte, no `..` component (Node collapses it before the filesystem is consulted,
|
|
80
|
+
so the tool could answer about a different file than you named). A trailing `/` or `/.` is not
|
|
81
|
+
rejected — it asserts "this is a directory", exactly as it does to your shell. Awkward names are
|
|
82
|
+
still supported: spaces at the edges, backticks, control bytes — that is what the file lanes carry.
|
|
83
|
+
|
|
84
|
+
**Not in this release, deliberately.** A guard rung that DENIES instead of asking was designed,
|
|
85
|
+
reviewed and withdrawn — for the second time — because a command substitution can embed an arbitrary
|
|
86
|
+
command with no separator byte, so any such refusal can destroy work it never meant to touch. The
|
|
87
|
+
guard still never denies.
|
|
88
|
+
|
|
7
89
|
## 4.3.0 — the coverage gate no longer certifies evidence it cannot bind to your tree (AD-081)
|
|
8
90
|
|
|
9
91
|
**Read this if you have ever run `coverage-check --check` on its own.** Until now it read whatever
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '4.
|
|
6
|
+
version: '4.5.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
// (d) everything else → NO decision: exit 0, no output — the normal permission flow proceeds
|
|
46
46
|
// unchanged. The hook NEVER emits `deny`.
|
|
47
47
|
//
|
|
48
|
+
// EVERY ASK ALSO CARRIES A CALLER-FACING HINT (`additionalContext`). The ask itself goes to the
|
|
49
|
+
// HUMAN and its reason never reaches the caller that composed the command — so the guard knew the
|
|
50
|
+
// right lane and could not say it, which is the one thing every withdrawn deny design was buying.
|
|
51
|
+
// The hint is that channel without the refusal, and a host that ignores the field degrades to
|
|
52
|
+
// silence, i.e. to the previous behaviour. See the note above `HINT_CORE`.
|
|
53
|
+
//
|
|
48
54
|
// WHY THERE IS NO DENY RUNG, AND WHY THE DIRECTION IS RETIRED (kit 4.0.0 built one, AD-078
|
|
49
55
|
// removed it; the class was closed from the other side instead). The withdrawn rung refused a
|
|
50
56
|
// seeded read-only command that provably DISCARDED its output, on the argument that such a refusal
|
|
@@ -272,7 +278,16 @@ export const matchSeededCorePrefix = (command) => {
|
|
|
272
278
|
// review killed it: it would pull ANY unrelated `repo-search.mjs` into rung (b2), turning a
|
|
273
279
|
// pre-existing NO decision into an ASK — a change to a decision path this release has no business
|
|
274
280
|
// touching.
|
|
275
|
-
|
|
281
|
+
// The RECOVERY is per tool, not a constant. When repo-search was the only entry the reason named the
|
|
282
|
+
// literal `--pattern-file`, and that was wrong the moment a second tool joined: wrong for a tool with
|
|
283
|
+
// no such flag, and already incomplete for repo-search itself once its TARGET half gained a lane. An
|
|
284
|
+
// ask whose named recovery does not exist is worse than an ask with no advice, so the advice is
|
|
285
|
+
// derived from whichever tool matched.
|
|
286
|
+
export const SCANNED_TOOL_LANES = Object.freeze({
|
|
287
|
+
'agent-workflow-kit/tools/repo-search.mjs': '--pattern-file / --paths-file',
|
|
288
|
+
'agent-workflow-kit/tools/path-inventory.mjs': '--paths-file',
|
|
289
|
+
});
|
|
290
|
+
export const SCANNED_TOOL_PATHS = Object.freeze(Object.keys(SCANNED_TOOL_LANES));
|
|
276
291
|
|
|
277
292
|
// Quotes stripped and separators canonicalised, so a relative, absolute, quoted or Windows-separated
|
|
278
293
|
// spelling all compare the same. SUBSTRING, not equality, and scanned across EVERY token rather than
|
|
@@ -282,10 +297,13 @@ export const SCANNED_TOOL_PATHS = Object.freeze(['agent-workflow-kit/tools/repo-
|
|
|
282
297
|
// surface this exists to cover, while a spurious match merely over-asks — so inclusiveness wins.
|
|
283
298
|
const canonicalToken = (token) => token.replace(/["']/gu, '').replace(/\\/gu, '/');
|
|
284
299
|
|
|
285
|
-
export const matchScannedToolPrefix = (command) =>
|
|
300
|
+
export const matchScannedToolPrefix = (command) => matchScannedTools(command)[0] ?? null;
|
|
301
|
+
|
|
302
|
+
// ALL matches, not just the first. A command naming two scanned tools would otherwise be advised
|
|
303
|
+
// about whichever happens to sit earlier in the registry, and the residual may belong to the other.
|
|
304
|
+
export const matchScannedTools = (command) => {
|
|
286
305
|
const tokens = tokenizeCommand(command).map(canonicalToken);
|
|
287
|
-
|
|
288
|
-
return hit ?? null;
|
|
306
|
+
return SCANNED_TOOL_PATHS.filter((path) => tokens.some((token) => token.includes(path)));
|
|
289
307
|
};
|
|
290
308
|
|
|
291
309
|
// String-level, conservative: the hook sees the PRE-SHELL command string, so every class is a raw
|
|
@@ -381,6 +399,35 @@ export const isReadLaneCommand = (command) => {
|
|
|
381
399
|
});
|
|
382
400
|
};
|
|
383
401
|
|
|
402
|
+
// ── the caller-facing hint (rides an ask; refuses nothing) ────────────────────────────
|
|
403
|
+
//
|
|
404
|
+
// WHY THIS EXISTS, AND WHY IT IS NOT A DENY. An ask is answered by the HUMAN: `permissionDecisionReason`
|
|
405
|
+
// is context for THEIR dialog and never reaches the caller that composed the command. So for the whole
|
|
406
|
+
// life of this guard it has known the right lane and had no way to say it — which is the single reason
|
|
407
|
+
// every withdrawn deny design looked attractive, since a deny is refusal-shaped and does reach the
|
|
408
|
+
// caller. `additionalContext` is the same channel without the refusal. Probed live on 2026-07-28 with
|
|
409
|
+
// a throwaway hook: a distinct marker was delivered to the model on `allow`, on `ask`, AND on no
|
|
410
|
+
// decision at all.
|
|
411
|
+
// FAIL-SAFE BY CONSTRUCTION: a host that does not honour the field simply drops it, which is exactly
|
|
412
|
+
// the behaviour that existed before — silence. Nothing is refused, so a hint that is wrong for a
|
|
413
|
+
// particular command costs the caller a sentence, never their work. That is the property no deny
|
|
414
|
+
// design could ever claim.
|
|
415
|
+
export const HINT_CORE = 'agent-workflow: this read left the promptless lane — a plain read-only command with no redirection, no banner and no quotes stays inside it. If the byte is a decoration you do not need, drop it and re-issue; if it is ordinary text inside an argument, a search belongs in `repo-search.mjs` (`--pattern-file`), and several path questions (exists / size / line count / listing / small-file contents) are ONE `path-inventory.mjs` call. If the byte is a deliberate operator, keep it and confirm by hand.';
|
|
416
|
+
|
|
417
|
+
// The hint must not GUESS which of the two causes produced the residual — the guard cannot tell a
|
|
418
|
+
// byte that is ordinary text inside an argument from a real shell operator, and pretending otherwise
|
|
419
|
+
// is the exact claim AD-079 closed. So it names BOTH recoveries and lets the caller pick: the one
|
|
420
|
+
// that fits is always present, and the one that does not costs a clause.
|
|
421
|
+
// The hint never tells the caller to delete an operator it cannot read the intent of: dropping a
|
|
422
|
+
// real `> report.json` would change what the command DOES, and a guard that silently edits meaning is
|
|
423
|
+
// the failure this whole area was built to avoid. It names the out-of-band lane, notes that these
|
|
424
|
+
// tools already bound and print their own result (so a decoration is usually redundant), and leaves a
|
|
425
|
+
// deliberate operator to the human confirmation that already exists.
|
|
426
|
+
const hintForScannedTools = (tools) => {
|
|
427
|
+
const lanes = tools.map((tool) => `\`${tool}\` → ${SCANNED_TOOL_LANES[tool]}`).join('; ');
|
|
428
|
+
return `agent-workflow: the residual guard stopped this. If the byte is part of an ARGUMENT, pass it out of band — ${lanes} — and its bytes then never enter the command string, so this guard has nothing to scan (whether the clean form still prompts depends on your settings allow rules, which this hook does not read). If the byte is a decoration you do not need, drop it: these tools bound and print their own result. If it is a deliberate operator, keep it and confirm by hand — that is what the prompt is for.`;
|
|
429
|
+
};
|
|
430
|
+
|
|
384
431
|
// ── the decision ladder (pure core) ───────────────────────────────────────────────────
|
|
385
432
|
|
|
386
433
|
export const decideBashCall = ({ command, permissionMode, cwdIsProjectRoot, gates, readLaneOn = false }) => {
|
|
@@ -404,19 +451,23 @@ export const decideBashCall = ({ command, permissionMode, cwdIsProjectRoot, gate
|
|
|
404
451
|
return {
|
|
405
452
|
permissionDecision: DECISION_ASK,
|
|
406
453
|
permissionDecisionReason: `agent-workflow residual guard: read-only-seeded "${corePrefix}" carries ${residualClasses.join(' + ')} — a settings allow rule cannot see runtime shape; confirm by hand`,
|
|
454
|
+
additionalContext: HINT_CORE,
|
|
407
455
|
};
|
|
408
456
|
}
|
|
409
457
|
}
|
|
410
458
|
// (b2) the same guard over the scanned kit tools. The refusal NAMES the lane that avoids it, so a
|
|
411
459
|
// caller who picked the inline lane for a shell-significant pattern is corrected by the mechanism
|
|
412
460
|
// rather than expected to have remembered the rule.
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
461
|
+
const scannedTools = matchScannedTools(trimmed);
|
|
462
|
+
if (scannedTools.length > 0) {
|
|
415
463
|
const residualClasses = detectResidualClasses(trimmed);
|
|
416
464
|
if (residualClasses.length > 0) {
|
|
465
|
+
const named = scannedTools.map((tool) => `"${tool}"`).join(' + ');
|
|
466
|
+
const lanes = [...new Set(scannedTools.map((tool) => SCANNED_TOOL_LANES[tool]))].join(' / ');
|
|
417
467
|
return {
|
|
418
468
|
permissionDecision: DECISION_ASK,
|
|
419
|
-
permissionDecisionReason: `agent-workflow residual guard:
|
|
469
|
+
permissionDecisionReason: `agent-workflow residual guard: ${named} carries ${residualClasses.join(' + ')} — pass the byte-carrying argument out of band with ${lanes} (its bytes then never enter the command string), or confirm by hand if the byte is really part of the invocation`,
|
|
470
|
+
additionalContext: hintForScannedTools(scannedTools),
|
|
420
471
|
};
|
|
421
472
|
}
|
|
422
473
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
### Mode: migrate-adr-store
|
|
2
2
|
|
|
3
|
-
<!-- opt-in-capability:
|
|
3
|
+
<!-- opt-in-capability: adr-store-migration -->
|
|
4
4
|
|
|
5
5
|
The **guarded ADR-store migration** — a one-time, opt-in move of an existing project's `docs/ai` from the retired 3-tier ADR cascade (HOT `decisions.md` → the WARM/COLD `decisions-archive*.md` monoliths) to the durable **one-file-per-ADR store** (HOT `decisions.md` + `docs/ai/adr/AD-NNN-slug.md` records + the `docs/ai/adr/log.md` navigator). **In-agent, opt-in**, and reached ONLY here: a normal `upgrade` never installs the new-scheme rotator into an un-migrated project — the new rotator arrives ONLY through this mode, which migrates in the same step (AD-051). Run **`--dry-run` first, always**, show the user the plan in plain language, get explicit consent, then re-run with `--apply`. It **never commits**.
|
|
6
6
|
|
|
7
|
-
When to run it: `status` (or `upgrade`) reports an *old ADR layout
|
|
7
|
+
When to run it: `status` (or `upgrade`) reports an *old ADR layout*. That covers **two** shapes, and the remedy is the same for both — (1) a `decisions-archive*.md` monolith is still on disk, and (2) **no monolith was ever produced**: the project's deployed `scripts/archive-decisions.mjs` simply predates the store, so its own gate reports the retired three-tier world as healthy and would never mention the store. The second shape is detected by that deployed script's own provenance — never by "has `decisions.md`, lacks `adr/`", which would falsely accuse a project whose NEW rotator already reds its gate with an actionable fix, and every project that runs no Node. A project already on the one-file-per-ADR store with a fresh navigator — or a fresh deployment seeded with it — needs nothing here (the mode is a stated no-op).
|
|
8
|
+
|
|
9
|
+
On the no-monolith shape `--apply` does the same work minus the explosion: snapshot → refresh the deployed enforcement scripts → **seed** the store (`docs/ai/adr/` + the navigator + a regenerated `docs/ai/index.md`) → verify. It is **re-runnable to completion from any interruption**: a store directory alone never counts as finished (the navigator must exist and the project's own `--check` must pass), the whole script refresh is re-planned on every run with the rotation script written last, and a failed index regeneration fails the run **closed** rather than reporting success.
|
|
8
10
|
|
|
9
11
|
Run `node ${CLAUDE_SKILL_DIR}/tools/migrate-adr-store.mjs [--dry-run | --apply] [--cwd <project>]`:
|
|
10
12
|
|
|
@@ -27,6 +27,8 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
27
27
|
- `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is STALE (an enabled lane over an old hook) or MISSING, the item instead surfaces a **delete-to-reseed** / re-place recovery (a destructive `rm` + `--apply`, an attention item — never the safe preview). Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
|
|
28
28
|
- `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
|
|
29
29
|
|
|
30
|
+
- `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
|
|
31
|
+
|
|
30
32
|
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
|
31
33
|
|
|
32
34
|
- **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): nothing beyond the `--bridge-tier` wiring — the tier's `excludedCommands` already routes the wrappers OUTSIDE the sandbox, so they never consult `allowedDomains`/`allowWrite`. Record the ack (the ack-write one-liner) once the tier is confirmed.
|
|
@@ -11,7 +11,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]
|
|
|
11
11
|
1. **Versions — a status-only render from `installed[]` + `deploymentHead`** (this is **NOT** the shared notes-based version block — see the separation note below): the **`docs/ai` structure version** (named as such, never "lineage head"), then each member by its `display` showing its `version` (or, when there is no version, the plain phrase for its `state`, mapped above), plus the two-axes disambiguation. **Freshness comes from `installed[].refresh`, not from `notes`:** for each member whose **`refresh.behind`** is `true`, show a **localized "needs refresh"** label and the **verbatim `refresh.recommend`** command **exactly once** (the command/package name stays source-language; **do not also paste the English `notes` caveats** — `refresh.recommend` is the single source of the recovery step, so the command is never duplicated on this surface). A member whose **`refresh.freshness`** is **`unknown`** is surfaced too — a localized *"couldn't be checked"* label; it is **never counted as current and never as behind** (its `notes` caveat carries the detail on the notes-based surfaces; here the label is enough). Lead with a one-line **headline count** derived from `installed[].state` + `refresh.behind` + `refresh.freshness` (e.g. *"5 members installed · 1 needs a refresh · 1 couldn't be checked"* — omit a zero count).
|
|
12
12
|
|
|
13
13
|
> **Status reads `refresh`; the shared version block + the bootstrap/upgrade footers stay `notes`-based (unchanged this release).** `${CLAUDE_SKILL_DIR}/references/modes/status.md` has its OWN status-only render (above), keyed on `installed[].refresh.behind` / `refresh.recommend`. The shared **version block** (under *The version block + welcome mat* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and the bootstrap (step 11) + every upgrade (steps 4 / 8) report footer still consume `installed[].notes` verbatim — that wiring is deliberately **untouched** here (their migration onto `refresh` is deferred). Do not rewrite those footers onto `refresh`.
|
|
14
|
-
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits); `migrated` / `none` need no note.
|
|
14
|
+
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`** or **`old-unrotated`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits) — the note is the SAME for both, they differ only in how the older layout was detected (`old` = a retired archive file is still on disk; `old-unrotated` = the project's deployed rotation script predates the store); `migrated` / `none` need no note.
|
|
15
15
|
3. **Settings (`--dir`, one line each)** (from `project.settings`):
|
|
16
16
|
- **recipes** — the effective recipe per slot (detail → `/agent-workflow-kit procedures` / `recipes`); a `recipes.detectError` → say the backends couldn't be checked, so recipes floored at solo.
|
|
17
17
|
- **attribution** — `includeCoAuthoredBy` effective; call out a **local override** only when `local` is non-null **and** differs from `project` (a `null` `local` means the key is absent there, so the project value stands — that is not an override).
|
|
@@ -28,7 +28,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
28
28
|
|
|
29
29
|
**Autonomy-declaration ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/autonomy.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing** (the kit's OWN template twin, mirrored from memory — so a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content — never clobbered, never refreshed in place). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand). Report it in the step 4 / step 8 success report (*seeded* / *already present, preserved*). Like the other config ensures, no lineage-head bump or migration file (a `.json`, outside the docs cap-validator).
|
|
30
30
|
|
|
31
|
-
**Enforcement-script ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. Ensure the pair exists in the project's `scripts/`: **copy `archive-decisions.mjs` + `archive-decisions.test.mjs` from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing** (the kit's own fallback copies — byte-identical to the memory canon by the mirror guard); **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration, not this ensure). **OLD ADR-store layout — DETECT FIRST, never auto-migrate (AD-051, Decision 13):** if
|
|
31
|
+
**Enforcement-script ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. Ensure the pair exists in the project's `scripts/`: **copy `archive-decisions.mjs` + `archive-decisions.test.mjs` from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing** (the kit's own fallback copies — byte-identical to the memory canon by the mirror guard); **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration, not this ensure). **OLD ADR-store layout — DETECT FIRST, never auto-migrate (AD-051, Decision 13):** if `status` surfaces `adrLayout: "old"` (a `docs/ai/history/decisions-archive*.md` monolith) or `"old-unrotated"` (no monolith, but the deployed rotator predates the store), it is on the RETIRED 3-tier cascade — **do NOT seed or refresh the now one-file-per-ADR rotator here** (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Instead surface a LOUD instruct: run the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits). The seed-if-missing above applies ONLY to a clean layout (neither signal). The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (re-run `node scripts/install-git-hooks.mjs` after the ensure and it will refuse a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook. Skip this ensure on a No-Node project (the scripts are Node enforcement). Report it in the step 4 / step 8 success report (*added* / *already present* / *old ADR layout — migration instructed*).
|
|
32
32
|
|
|
33
33
|
**Placed-bridge refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
|
|
34
34
|
`node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` and **paste its per-bridge
|
|
@@ -63,7 +63,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
63
63
|
**NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
|
|
64
64
|
flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
|
|
65
65
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
66
|
-
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed
|
|
66
|
+
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed* (either shape); the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
67
67
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
68
68
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
69
69
|
- **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
|
|
@@ -33,11 +33,12 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
|
|
|
33
33
|
- `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd ${PROJECT_ROOT}` (wildcard — the read-only deployment advisor; its rendered apply one-liners are writers and still prompt)
|
|
34
34
|
- `node ${CLAUDE_SKILL_DIR}/tools/manifest/validate.mjs --strict <skill-dir>` (wildcard)
|
|
35
35
|
- `node ${CLAUDE_SKILL_DIR}/tools/release-scan.mjs <path>` (wildcard)
|
|
36
|
-
- `node ${CLAUDE_SKILL_DIR}/tools/repo-search.mjs --pattern <literal> --path <p>` (wildcard — the LITERAL search lane; for a pattern carrying a shell-significant byte use `--pattern-file <p>` instead,
|
|
36
|
+
- `node ${CLAUDE_SKILL_DIR}/tools/repo-search.mjs --pattern <literal> --path <p>` (wildcard — the LITERAL search lane; for a pattern carrying a shell-significant byte use `--pattern-file <p>` instead, and for a TARGET PATH carrying one use `--paths-file <p>` — either way the bytes never enter the command string. one of the TWO tier tools that carry hook residual coverage — see the honesty note below)
|
|
37
|
+
- `node ${CLAUDE_SKILL_DIR}/tools/path-inventory.mjs --path <p>` (wildcard — the read-only inventory lane: exists / type / size / `wc -l` lines / one-level listing / small-file contents for N paths in ONE call, so small path questions never need a composed shell. `--paths-file <p>` is its out-of-band target lane; hook residual coverage as with repo-search)
|
|
37
38
|
- `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs --cwd ${PROJECT_ROOT}` — **EXACT byte-string only**, and honestly **project-exec, not read-only**: it runs YOUR declared `docs/ai/gates.json` commands — the same trust boundary the opt-in hook grants byte-exact per-cmd. A wildcard would be BROADER than that boundary (`--cwd <dir>` executes another project's declared gates), so the bare cwd-defaulting form, any other `--cwd`, `--only`, and **`--final`** forms all still prompt (`--final` WRITES the final-run receipt into the core-evidence store — a recording run is never auto-approved).
|
|
38
39
|
- Writer previews, **exact arg-free dry-run byte-strings only** (the SEEDED tier byte-string is the arg-free preview of each): `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs` — every `--apply`/`--write`/`--yes` still prompts, always. (`gate-hook` also has a **`--read-lane`** flagged preview — the opt-in read-only compound lane, `${CLAUDE_SKILL_DIR}/references/modes/hook.md`; that flagged form is NOT the seeded arg-free byte-string, so it may **prompt once** — it IS a consent flow, stated, no silent cap.)
|
|
39
40
|
|
|
40
|
-
Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with
|
|
41
|
+
Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with TWO deliberate exceptions, `repo-search.mjs` and `path-inventory.mjs`**, whose prefixes are in the hook's scanned list because they take caller-supplied argument bytes: a redirection or substitution on THEIR invocation raises the ask, whose reason names that tool's out-of-band lane (`--pattern-file` / `--paths-file`) — context for the HUMAN deciding, not a message to the caller. Neither is in the seeded core, so neither inherits the read-lane allow. Every other tier entry: the opt-in hook's residual ask-net guards only the seeded read-only CORE prefixes, so the tier rides the same settings-level residual posture as the core (redirection / command substitution are not inspectable at the settings layer; see the residual notice). A skill or project path that cannot survive UNQUOTED in a byte-exact rule (spaces, metacharacters, non-POSIX) **STOPs the tier up front with a clear error** — nothing is seeded. Anything you want covered beyond the tier — such paths, this repo's own relative-path spellings, other tools — stays a **BY-HAND add** to your settings, with the path your project actually reaches the kit by. Pre-existing `node …` allow entries that do NOT match the seeded tier byte-forms stay flagged by the advisory for hand review.
|
|
41
42
|
|
|
42
43
|
**Invariants:** creates `.claude/` if absent and writes **only** `.claude/settings.json` (no other file); **never** allowlists commit/push/publish; **never** writes `settings.local.json`; never commits; opt-in `acceptEdits`, never silent.
|
|
43
44
|
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
// HOT preamble, and only THEN removes the monoliths — gated on conservation AND
|
|
30
30
|
// the snapshot. Re-run skips byte-identical records (crash-resumable).
|
|
31
31
|
// --write-navigator regenerate docs/ai/adr/log.md AND re-trigger the index regen (the authoring /
|
|
32
|
-
// supersession write-side; the --write-index analog).
|
|
32
|
+
// supersession write-side; the --write-index analog). With --dry-run it runs
|
|
33
|
+
// EXACTLY the same validation (parse, half-migrated guard, store integrity) and
|
|
34
|
+
// stops before every write — the read-only preflight a guarded caller needs to
|
|
35
|
+
// earn a go-ahead without risking a partial write.
|
|
33
36
|
// --dry-run print the planned rotation move-set, change nothing.
|
|
34
37
|
// --today=YYYY-MM-DD pin the lastUpdated stamp (tests / reproducible runs).
|
|
35
38
|
//
|
|
@@ -693,7 +696,7 @@ const runMigrate = (root, flags, today, deps, log, logError) => {
|
|
|
693
696
|
return 0;
|
|
694
697
|
};
|
|
695
698
|
|
|
696
|
-
const runWriteNavigator = (root, today, deps, log, logError) => {
|
|
699
|
+
const runWriteNavigator = (root, flags, today, deps, log, logError) => {
|
|
697
700
|
if (!existsSync(resolve(root, HOT_REL)) && !existsSync(resolve(root, ADR_DIR_REL))) {
|
|
698
701
|
log(`[archive-decisions] SKIP — no ADR substrate (neither ${HOT_REL} nor ${ADR_DIR_REL}); nothing to write.`);
|
|
699
702
|
return 0;
|
|
@@ -704,6 +707,14 @@ const runWriteNavigator = (root, today, deps, log, logError) => {
|
|
|
704
707
|
const adrEntries = loadAdrStore(root);
|
|
705
708
|
assertStoreIntegrity(hotEntries, adrEntries); // never emit a duplicate-row / corrupt navigator
|
|
706
709
|
const corpus = [...hotEntries, ...adrEntries];
|
|
710
|
+
// --dry-run runs EXACTLY the validation above and stops before every write: the parse, the
|
|
711
|
+
// half-migrated guard and the store-integrity check are the same code the write path uses, so a
|
|
712
|
+
// caller (the guarded ADR-store crossing) can earn a go-ahead without a partial write. A separate
|
|
713
|
+
// re-implementation of these checks would be an approximation that can disagree with the writer.
|
|
714
|
+
if (flags.dryRun) {
|
|
715
|
+
log(`[archive-decisions] --write-navigator DRY-RUN — no files will be changed; ${corpus.length} ADR(s) validated.`);
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
707
718
|
writeNavigatorFile(root, corpus, today);
|
|
708
719
|
const regen = (deps.regenerateIndex ?? defaultRegenerateIndex)(root, today);
|
|
709
720
|
log(`[archive-decisions] wrote ${NAV_REL} (${corpus.length} ADRs in the corpus).`);
|
|
@@ -828,7 +839,7 @@ export const runCli = (argv, deps = {}) => {
|
|
|
828
839
|
const today = todayOpt ?? new Date().toISOString().slice(0, 10);
|
|
829
840
|
|
|
830
841
|
if (flags.migrate) return runMigrate(root, flags, today, deps, log, logError);
|
|
831
|
-
if (flags.writeNavigator) return runWriteNavigator(root, today, deps, log, logError);
|
|
842
|
+
if (flags.writeNavigator) return runWriteNavigator(root, flags, today, deps, log, logError);
|
|
832
843
|
if (flags.check) return runCheck(root, today, log, logError);
|
|
833
844
|
|
|
834
845
|
if (!existsSync(resolve(root, HOT_REL))) {
|
|
@@ -643,6 +643,33 @@ describe('1.5 navigator — governing heads (computed), superseded drop out but
|
|
|
643
643
|
assert.doesNotMatch(gov, /\| AD-002 \|/, 'the Proposed ADR is NOT a governing head (accepted & not-superseded only)');
|
|
644
644
|
});
|
|
645
645
|
|
|
646
|
+
// The read-only preflight a guarded caller needs: the SAME parse / half-migrated guard / store
|
|
647
|
+
// integrity the writer runs, stopping before every write. Without it a caller can only learn that
|
|
648
|
+
// seeding is safe by seeding.
|
|
649
|
+
it('--write-navigator --dry-run validates and writes NOTHING', () => {
|
|
650
|
+
const root = makeRoot();
|
|
651
|
+
seedMigrated(root, { hotIds: ['005', '006'], storeIds: ['001'] });
|
|
652
|
+
const navBefore = readFileSync(join(root, NAV_REL), 'utf8');
|
|
653
|
+
rmSync(join(root, NAV_REL));
|
|
654
|
+
const r = run(['--write-navigator', '--dry-run', '--today=2026-07-09'], root);
|
|
655
|
+
assert.equal(r.code, 0);
|
|
656
|
+
assert.match(r.out.join('\n'), /DRY-RUN/, 'the run states it changed nothing');
|
|
657
|
+
assert.equal(existsSync(join(root, NAV_REL)), false, 'the navigator was NOT written by the dry-run');
|
|
658
|
+
// and the real run still produces exactly what the dry-run validated
|
|
659
|
+
assert.equal(run(['--write-navigator', '--today=2026-07-09'], root).code, 0);
|
|
660
|
+
assert.equal(readFileSync(join(root, NAV_REL), 'utf8'), navBefore);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('--write-navigator --dry-run REFUSES a corrupt store instead of green-lighting it', () => {
|
|
664
|
+
const root = makeRoot();
|
|
665
|
+
seedMigrated(root, { hotIds: ['005', '006'], storeIds: ['001'] });
|
|
666
|
+
// a record whose id also lives in the HOT window — the store would hold the ADR twice
|
|
667
|
+
writeFileSync(join(root, ADR_DIR_REL, 'AD-005-dup.md'), '---\n---\n\n## AD-005 — Dup\n\nBody.\n');
|
|
668
|
+
const r = run(['--write-navigator', '--dry-run', '--today=2026-07-09'], root);
|
|
669
|
+
assert.notEqual(r.code, 0, 'a corrupt store fails the preflight');
|
|
670
|
+
assert.doesNotMatch(r.out.join('\n'), /DRY-RUN/, 'no go-ahead is printed for a tree that cannot converge');
|
|
671
|
+
});
|
|
672
|
+
|
|
646
673
|
it('authoring a new HOT ADR then --write-navigator keeps --check green; a stale nav with NO write → exit 1, then --write-navigator fixes it', () => {
|
|
647
674
|
const root = makeRoot();
|
|
648
675
|
seedMigrated(root, { hotIds: ['005', '006'], storeIds: ['001'] });
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
### Command shapes — the promptless bar for instructed reads & probes
|
|
2
2
|
|
|
3
|
-
When a mode doc tells you to read a file or probe
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
When a mode doc tells you to read a file or probe state WITHOUT prescribing the exact command (a
|
|
4
|
+
recon read, the version-stamp read, any "check X"), the shape is yours — and improvised shapes are
|
|
5
|
+
where approval prompts come from. The bar:
|
|
6
6
|
|
|
7
|
-
- **Reads ride the host's file-read tool** (Read/Grep/Glob
|
|
8
|
-
|
|
7
|
+
- **Reads ride the host's file-read tool** (Read/Grep/Glob, or your agent's equivalent) whenever one
|
|
8
|
+
exists — a file-read tool never fires a shell approval prompt.
|
|
9
9
|
- **No file-read tool → ONE plain undecorated command per probe:** no `;`/`&&` compounds, no
|
|
10
|
-
redirects, no pipes, no command substitution — one probe per invocation
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **Improvised file writes ride the host's file-edit tools** (Write/Edit or the equivalent) —
|
|
18
|
-
never an ad-hoc heredoc or shell-redirect write.
|
|
10
|
+
redirects, no pipes, no command substitution — one probe per invocation (the lens’s «plain pipeline
|
|
11
|
+
per call», stricter). A decorated or chained form is outside this contract’s guarantee: behavior is
|
|
12
|
+
host/config-dependent — it may prompt, an opt-in read-lane may auto-approve a vetted compound, and
|
|
13
|
+
command substitution may even slip past a prefix allow rule. The plain single-command shape is the
|
|
14
|
+
only shell fallback that is promptless by construction.
|
|
15
|
+
- **Improvised file writes ride the host's file-edit tools** (Write/Edit or equivalent) — never an
|
|
16
|
+
ad-hoc heredoc or shell-redirect write.
|
|
19
17
|
- **Searching for TEXT is its own case.** A pattern carrying `>`, `` ` `` or `$(` prompts on a
|
|
20
|
-
seeded-core command however
|
|
21
|
-
|
|
22
|
-
one; else,
|
|
23
|
-
switching to `--pattern-file <path>` for a byte-carrying pattern
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
the
|
|
18
|
+
seeded-core command however quoted — the guard scans the raw string and a quote-stripped copy.
|
|
19
|
+
Quoting is not a workaround. (`|`/`&&` do not trip it.) Use the host's search tool if it has
|
|
20
|
+
one; else, with the kit tier seeded, `node <kit>/tools/repo-search.mjs --pattern <literal>`,
|
|
21
|
+
switching to `--pattern-file <path>` for a byte-carrying pattern and `--paths-file <path>` for a
|
|
22
|
+
byte-carrying TARGET — written with the file-write tool above, so their bytes never enter the
|
|
23
|
+
command string; else one plain command, taking the prompt. A wrong lane earns an ask naming the
|
|
24
|
+
right lane back to YOU, not just the human. **PATH questions too:** exists / size / lines / listing
|
|
25
|
+
(file text needs `--contents`), any number of paths, is ONE
|
|
26
|
+
`node <kit>/tools/path-inventory.mjs --path <p>` — not a bannered compound. Residual: a bare `grep` still prompts, and the file lanes need
|
|
27
|
+
promptless writes.
|
|
27
28
|
|
|
28
|
-
**Scope — improvised shapes only.** The
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
**Scope — improvised shapes only.** The commands a mode doc itself prescribes (`node …/tools/…`
|
|
30
|
+
dispatch lines, `--apply` lanes, install/symlink steps) are OUTSIDE this contract: run them exactly
|
|
31
|
+
as prescribed, as plain single invocations.
|