@sabaiway/agent-workflow-kit 11.0.0 → 11.1.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 +48 -0
- package/README.md +7 -6
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/launchers/windsurf-workflow.md +2 -2
- package/package.json +1 -1
- package/references/contracts.md +1 -1
- package/references/modes/bootstrap.md +5 -5
- package/references/modes/recommendations.md +1 -0
- package/references/scripts/archiver-structure.test.mjs +48 -3
- package/references/scripts/install-git-hooks.test.mjs +31 -1
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/agent_rules.md +1 -1
- package/references/templates/changelog.md +1 -1
- package/references/templates/decisions.md +2 -2
- package/tools/recommendations.mjs +53 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ 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
|
+
## 11.1.0 — the bootstrap offers enforcement on every project, and the advisor closes the loop (AD-131)
|
|
8
|
+
|
|
9
|
+
Bootstrap steps 8–9 (the enforcement scripts + the pre-commit hook) used to decide by the step-1
|
|
10
|
+
recon — a Python, Go or Rust project was denied the caps gate, the archivers and the hook for no
|
|
11
|
+
proven reason, then `upgrade` reported `skipped-no-node-evidence` on the very tree the bootstrap left
|
|
12
|
+
bare. The scripts are dependency-free Node the agent host already runs for step 7's navigator.
|
|
13
|
+
Contract: `node-evidence.md` rev 2 (S8–S10 bound).
|
|
14
|
+
|
|
15
|
+
**The offer, on every project.** `references/modes/bootstrap.md` steps 8–9 offer the scripts and the
|
|
16
|
+
hook preview-first with a one-line why: the bootstrap seeds on `package-json` OR
|
|
17
|
+
`deployed-node-scripts` OR the fact that the agent host is running the kit; the only skip condition
|
|
18
|
+
is that the agent host cannot run them; the residual is stated (the hook runs `node scripts/<x>.mjs`,
|
|
19
|
+
so a committer without `node` on PATH gets the hook's own loud failure). `deploy-tail.md` carries the
|
|
20
|
+
same sentence once; the Windsurf launcher's step list names the offer once and copies nothing
|
|
21
|
+
unconditionally; the by-hand "Non-Node projects follow the policy by hand" claims left the READMEs.
|
|
22
|
+
The step-1 recon is a stack fact, never the criterion.
|
|
23
|
+
|
|
24
|
+
**The advisor's `enforcement` item.** On a `docs/ai` tree whose node evidence answers `none` and
|
|
25
|
+
whose `scripts/` is absent or holds no basename of the kit's own bundle (listed at run time through
|
|
26
|
+
the probe's own `scriptsBundleRoot` seam, never a hardcoded list), Recommendations renders ONE
|
|
27
|
+
optional offer: the exact bootstrap tail as a `HAND-APPLY:` line with both paths shell-quoted.
|
|
28
|
+
Every other cell is silence or a stated skip naming the path — `package-json` /
|
|
29
|
+
`deployed-node-scripts` render nothing and never list the bundle; a wrong-kind path, an unreadable
|
|
30
|
+
probe (`<root>: <code> on <rel>`), an existing, linked or unreadable destination each skip by name;
|
|
31
|
+
no `docs/ai` renders nothing. The probe spawns no git; the installer's refusal over a hook it did not
|
|
32
|
+
write is stated in the posture note (pinned). The commit-guard item's installer-absent skip carries
|
|
33
|
+
its own 155-char remedy (deploy the scripts, bootstrap steps 8–9, install the hook, re-run) on ANY
|
|
34
|
+
evidence — never "run upgrade first".
|
|
35
|
+
|
|
36
|
+
**A declined offer leaves no false claim.** Every install claim in the family is conditional on
|
|
37
|
+
the yes — `installed on a yes` in the READMEs and the seeded `changelog.md` / `decisions.md`
|
|
38
|
+
templates, `where installed` in the seeded `agent_rules.md`, `and the enforcement offer was
|
|
39
|
+
accepted` in both visibility contracts — pinned by one (file, literal, count) table in
|
|
40
|
+
`test/enforcement-offer-docs.test.mjs`; the visible wiring of `package.json` scripts is conditional
|
|
41
|
+
on an existing `package.json` AND an accepted offer; on a no the docs command slots of
|
|
42
|
+
`env_commands.md` and `AGENTS.md` carry a note (the caps + index gate run from the agent's own skill
|
|
43
|
+
copy; the archive rotations need the scripts deployed) and never the skill's absolute path.
|
|
44
|
+
|
|
45
|
+
**Pins, unchanged scripts.** The deployed scripts' basename fallback (no or a malformed
|
|
46
|
+
`package.json` → the directory basename) was already there — `install-git-hooks.test.mjs` and
|
|
47
|
+
`archiver-structure.test.mjs` pin it with a package.json-named control; no script changed.
|
|
48
|
+
|
|
49
|
+
**Measured on itself (the first-pass-quality series' acceptance metric).** Executed delegated on ONE
|
|
50
|
+
held codex session, eight accounted dispatches, every fold through it. Diff council: round 1 codex
|
|
51
|
+
revise 0 blockers + 3 majors, agy SHIP, lens 0 + 2 majors; rounds 2–3 resolved the decline-path
|
|
52
|
+
subarea at altitude; round 4 CLEAN from all three — four rounds against the 13-round baseline
|
|
53
|
+
(AD-130). Three fold-induced findings, each caught by a test or gate before a receipt attested it.
|
|
54
|
+
|
|
7
55
|
## 11.0.0 — the per-fold walk is a record, the cap is a refusal at the round table's own signal, and the fingerprint argv has one home (AD-130)
|
|
8
56
|
|
|
9
57
|
Plan 2's two attestation rounds on CONVERGED bytes each found one member of the kit's own robustness
|
package/README.md
CHANGED
|
@@ -117,12 +117,12 @@ your-repo/
|
|
|
117
117
|
├── tech_reference.md ← configs & patterns
|
|
118
118
|
├── pages/ ← one spec per page/route
|
|
119
119
|
└── history/ ← changelog archive (HOT→WARM→COLD)
|
|
120
|
-
+ scripts/ ← caps · index · archive (
|
|
121
|
-
+ pre-commit hook ← keeps it all honest (
|
|
120
|
+
+ scripts/ ← caps · index · archive (installed on a yes)
|
|
121
|
+
+ pre-commit hook ← keeps it all honest (installed on a yes)
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
The Markdown memory is **stack-agnostic**; the `scripts/` + pre-commit hook are the **Node path**
|
|
125
|
-
(dependency-free, `node --test`).
|
|
125
|
+
(dependency-free, `node --test`). They are offered on every project because the agent host runs them; a committer without Node on PATH gets a loud hook failure.
|
|
126
126
|
|
|
127
127
|
Two **visibility** modes, chosen at deploy time: **visible** (committed with the repo) or **hidden**
|
|
128
128
|
(same files in-tree but git-ignored via the project-local `.git/info/exclude`, so the repo "looks
|
|
@@ -225,7 +225,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
225
225
|
|
|
226
226
|
| Command | When | What happens |
|
|
227
227
|
|---------|------|--------------|
|
|
228
|
-
| `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data →
|
|
228
|
+
| `/agent-workflow-kit` | new / empty project | recon → **asks visible-or-hidden** + **conversational language** + **agent attribution** (default off) → deploys `AGENTS.md` + `docs/ai/` filled with real recon data → offers enforcement (installed on a yes) → **asks before committing** |
|
|
229
229
|
| `/agent-workflow-kit upgrade` | existing deployment | reads `docs/ai/.workflow-version`, shows the changelog diff, preserves your authored memory, applies migrations, re-stamps — then prints a **read-only** one-line backend-status line (what's set up vs missing); refreshes the already-placed bridges from the kit's bundled copies (never installs a new one — set one up with `/agent-workflow-kit setup`) |
|
|
230
230
|
| `/agent-workflow-kit help` | any time | **read-only command index** — every command, grouped (Inspect / Configure / Orchestrate / Lifecycle) and tagged read-only / writer / guarded. The discoverable entry point, and where any unrecognized invocation lands (always read-only — a garbage invocation never writes). Never writes, never commits, never runs a subscription CLI. |
|
|
231
231
|
| `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
|
|
@@ -270,7 +270,7 @@ It **never auto-commits** and **never overwrites** an existing `AGENTS.md` witho
|
|
|
270
270
|
- **Plan lifecycle** — a capped plan (goal and boundary, module ledger, verification), ephemeral plan files, and a mandatory Cleanup phase.
|
|
271
271
|
- **No silent failures** — every guard that rejects an action logs structured context.
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
The dependency-free **Node** enforcement scripts (`node --test`, no package manager assumed) and their hook are offered on every project because the agent host runs them; a committer without Node on PATH gets a loud hook failure.
|
|
274
274
|
|
|
275
275
|
---
|
|
276
276
|
|
|
@@ -284,7 +284,8 @@ agent-workflow-kit — the composition root (installed via npx … init)
|
|
|
284
284
|
on /agent-workflow-kit in a repo, the kit:
|
|
285
285
|
├─ delegates ─▶ memory substrate (healthy copy, else bundled fallback)
|
|
286
286
|
├─ injects ─▶ workflow methodology (live from the installed engine)
|
|
287
|
-
├─ deploys ─▶ AGENTS.md + docs/ai/
|
|
287
|
+
├─ deploys ─▶ AGENTS.md + docs/ai/
|
|
288
|
+
│ + Node scripts + pre-commit hook (installed on a yes)
|
|
288
289
|
├─ detects ─▶ optional backends (codex / agy, read-only)
|
|
289
290
|
└─ sets up ─▶ a bridge (opt-in) (place skill + link wrappers)
|
|
290
291
|
```
|
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: '11.
|
|
6
|
+
version: '11.1.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
package/capability.json
CHANGED
|
@@ -26,8 +26,8 @@ reinvent any of it here; the kit's `SKILL.md` is the single source of truth.
|
|
|
26
26
|
attribution) as ONE structured multi-question prompt; record each answer individually,
|
|
27
27
|
write nothing until ALL are answered** →
|
|
28
28
|
create `AGENTS.md` (+ `CLAUDE.md` symlink) from `<KIT_DIR>/references/templates/` →
|
|
29
|
-
deploy `docs/ai/` → copy `<KIT_DIR>/references/scripts/*.mjs
|
|
30
|
-
wire/hide per visibility →
|
|
29
|
+
deploy `docs/ai/` → offer the enforcement scripts + pre-commit hook (on a yes: copy `<KIT_DIR>/references/scripts/*.mjs`, install the hook) →
|
|
30
|
+
wire/hide per visibility → stamp `docs/ai/.workflow-version`.
|
|
31
31
|
4. Report what was filled with real data vs left as TODO, then **ask before committing** —
|
|
32
32
|
never auto-commit.
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.1.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",
|
package/references/contracts.md
CHANGED
|
@@ -17,7 +17,7 @@ The user chooses at bootstrap whether the AI artifacts are visible in the repo o
|
|
|
17
17
|
**explicit up-front question** (bootstrap step 2), never an assumed default. The two modes then
|
|
18
18
|
diverge:
|
|
19
19
|
|
|
20
|
-
- **visible** — artifacts are committed.
|
|
20
|
+
- **visible** — artifacts are committed. When a `package.json` exists and the enforcement offer was accepted, wire the project's `package.json` scripts (`docs:check` / `docs:index` / `docs:index:check` / `docs:archive` / `docs:archive:check` / `docs:archive:issues` / `docs:archive:issues:check` / `prepare: node scripts/install-git-hooks.mjs`); add a minimal `.gitignore` (`docs/plans/`, `.claude/settings.local.json`) either way. This is the canonical model.
|
|
21
21
|
- **hidden** (in-tree) — same files on disk, but the repo "looks normal": the AI/agent footprint is git-ignored via **one managed block in the project-local `.git/info/exclude`** (resolved with `git rev-parse --git-path info/exclude` — never the machine-global `core.excludesFile`, which would affect every repo on the host; **AD-014** amends **AD-006**). The kit's `tools/hide-footprint.mjs` is the single writer: it covers `KIT_OWN_PATHS ∪ the present subset of KNOWN_FOOTPRINT` (the full footprint table below — the kit's own artifacts **and** every other AI/agent tool's files). Per path: a **tracked** file → **ASK** (an exclude does nothing for it; only `git rm --cached` un-tracks it — the tool prints that command and never runs it); an untracked path already covered by a **tracked `.gitignore`** → dropped (redundant); a **present** file whose name is generic enough to be ambiguous (`falsePositiveRisk`) → **ASK**; everything else → **hidden**. `asks` are excluded from the block unless explicitly opted in. **Verify** treats a path as hidden only when it is **untracked AND ignored by our project-local block** (or a tracked `.gitignore`) — being ignored by the global excludes does **not** count. Re-running re-derives the block wholesale (sorted/deduped) → a clean re-run is a **zero-diff** no-op. On an existing global-excludes deployment the tool **detects + reports the residual legacy machine-global block and keeps it by default** (a harmless double-ignore; the local block wins precedence); removal is the explicit `--remove-global` (it prints the removed lines as a restorable backup), which **the agent only runs after asking** — another of the user's hidden repos on the same host may rely on the same root-anchored global lines. **Do not edit `package.json`** — that is a tracked change and would leak; the pre-commit hook (always untracked in `.git/hooks/`) calls the scripts via `node scripts/<x>.mjs` directly. Windows is supported (text edit, no symlinks; CRLF preserved).
|
|
22
22
|
|
|
23
23
|
**Visibility changes what git sees — never how the agent works (the load-bearing invariant).** Both modes carry the **same** working memory and run the **same** session protocol: `docs/ai` and the deployed workflow artifacts (`AGENTS.md`, the added `scripts/`) are read at session start and maintained on task completion **identically** whether visible or hidden. (Hidden mode git-ignores a *wider* footprint — other AI/agent tools' files too — but that is only a **visibility** surface; the working memory the agent actually reads and maintains is `docs/ai` plus those artifacts.) Hidden mode only makes the repo *look* normal to git; it is **never** a license to skip, defer, or down-prioritise a doc/state update. The single difference is the destination: in hidden mode those updates live on disk and never enter a commit (the footprint is excluded), so a commit there carries only the project's own tracked files while `docs/ai` is still kept current locally — **git-ignored ≠ optional-to-maintain**. An agent that reads "git-ignored / `git status` clean" and infers the working memory is optional has the model backwards. (This is why bootstrap/upgrade never wire hidden-mode docs into a commit, yet the session protocol still mandates maintaining them.)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
### Mode: bootstrap
|
|
2
2
|
|
|
3
|
-
<!-- opt-in-capability:
|
|
3
|
+
<!-- opt-in-capability: enforcement -->
|
|
4
4
|
|
|
5
5
|
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
6
6
|
|
|
@@ -23,9 +23,9 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
23
23
|
5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (step-1 recon), do **not** overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md` (the cross-agent standard — Codex / Cursor / Devin Desktop / Copilot read it natively) from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md`, and symlink `CLAUDE.md -> AGENTS.md` (`ln -s AGENTS.md CLAUDE.md`) for Claude Code — single source, no duplication. For nested context, add a subdir `AGENTS.md` (+ a `CLAUDE.md` symlink beside it for Claude Code).
|
|
24
24
|
6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` + the seed `adr/` store + the seed `specs/` store from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys every template except `AGENTS.md`, `adr-record.md` and `SPEC_TEMPLATE.md` — the latter two are skill-home authoring references, never deployed). This deploys the HOT ADR window **`docs/ai/decisions.md`** and the seed navigator **`docs/ai/adr/log.md`** (its `adr/` subdir copies in wholesale), the spec store root **`docs/ai/specs/index.md`** (its `specs/` subdir copies in wholesale), the `.md` docs, **and** the three seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — seeded as the no-consent EMPTY list; FILLING it is the consented `gates-init` preview, `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), and **`docs/ai/autonomy.json`** (the per-project autonomy policy — seeded SPARSE with the onboarding note only, so it is exactly the computed defaults until the user declares levels via `/agent-workflow-kit set-autonomy` or by hand). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
|
|
25
25
|
7. **Fill templates** per the table below. **Then materialize the navigator, as the last `docs/ai` step:** the entry point declares `docs/ai/index.md` always-loaded and it is a GENERATED artifact with no template, so run `node ${CLAUDE_SKILL_DIR}/references/scripts/check-docs-size.mjs --ensure-index --root=<project>` and relay its one outcome line (`ensure-index: regenerated` / `already-current`, or a `write-refused` / `probe-failed` line naming the offending path — either is a STOP: resolve it before reporting the deploy, since the navigator the entry point declares does not exist yet). The generator runs from **this skill's own directory**, so a project with **no Node runtime** is covered too: the agent host runs it, and step 8's copy is not a precondition.
|
|
26
|
-
8. **
|
|
27
|
-
9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)).
|
|
28
|
-
- **visible** —
|
|
26
|
+
8. **Offer enforcement on every project, preview-first.** Explain in one line that the dependency-free Node scripts are available because the agent host already runs their step-7 navigator; the only skip condition is that the agent host cannot run them. Preview the source and destination list and ask; on a yes copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs` (+ `*.test.mjs`) into the project's `scripts/`. On a no, skip the hook in step 9 and say so; the docs command slots then name no project-local command — fill `env_commands.md` and the `Docs caps + index freshness` row of `AGENTS.md` with the same note: the caps + index gate run from the agent's own skill copy (the step-7 `--root=<project>` form) and the archive rotations need the scripts deployed. Never write the skill's absolute path into a deployed doc. They self-configure (project name from `package.json` or, without one, the directory basename; hierarchical/on-demand sections auto-discovered). The hook calls `node scripts/<x>.mjs` directly, so a committer without `node` on PATH gets the hook's own loud failure.
|
|
27
|
+
9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). When the user accepts the enforcement offer, install the pre-commit hook: `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting. Once the gate declaration is final-run-capable, the installer's optional consented `--commit-guard <kit commit-guard.mjs path>` arm binds commits to the green `run-gates --final` receipt (the Recommendations advisor surfaces the exact one-liner).
|
|
28
|
+
- **visible** — when a `package.json` exists and the offer was accepted, wire its scripts (never create one for this); add the minimal `.gitignore` either way. **Do not run the hide tool.**
|
|
29
29
|
- **hidden** — run the kit's hide writer (one managed block in the **project-local** `.git/info/exclude`, covering the kit's own artifacts **and** every known AI/agent tool's footprint — never the machine-global excludes): `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --dry-run` **first** (changes nothing, prints the plan), then the same without `--dry-run`. Handle what it surfaces, in plain language (never the tool's internal terms):
|
|
30
30
|
- A path it reports as **already committed** can't be hidden by ignoring it and is **never un-tracked silently** — show the user the printed `git rm --cached <path>`, let them decide, then opt it in with `--include=<path>`.
|
|
31
31
|
- A **present file with a generic name** it flags (e.g. `GEMINI.md`) → ask before `--include=<path>`.
|
|
@@ -58,5 +58,5 @@ Fill strategy:
|
|
|
58
58
|
| `tech_reference.md` | Carry over real configs/patterns found in deps. |
|
|
59
59
|
| `active_plan.md`, `handover.md` | TODO seed (e.g. "Bootstrap session — fill domain sections after first real work"). |
|
|
60
60
|
| `decisions.md` | The HOT ADR window — seed `AD-001` (adopting this memory system), newest at the bottom. |
|
|
61
|
-
| `adr/log.md` | The seed navigator — deployed as-is (the generator over the seeded HOT); regenerated by `archive-decisions.mjs
|
|
61
|
+
| `adr/log.md` | The seed navigator — deployed as-is (the generator over the seeded HOT); regenerated by `archive-decisions.mjs` where the enforcement scripts are deployed (installed on a yes), otherwise it stays the seed — never hand-edited either way. |
|
|
62
62
|
| `known_issues.md`, `changelog.md`, `pages/shared-patterns.md` | Empty template / first bootstrap entry. |
|
|
@@ -33,6 +33,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
33
33
|
|
|
34
34
|
- `source-size` — the practice's arms differ by what the declared gate actually IS. **`adopted-elsewhere`** — the gate runs a DIFFERENT real copy of this checker (a vendored deployment): the practice is running, the advisor simply cannot resolve it as its own sibling, and the realpath anchor deliberately does not widen (it is what stops a lookalike from certifying). So the convergence is the ack writer's preview (`--lane source-size-copy` → `sourceSizeCopyAck`), never `--adopt`, which would mint the record and then be refused by the fill on the id already declared. The fingerprint binds the declared claims AS AUTHORED, sorted — not their resolved paths, which are machine-specific and would churn a committed ack between machines — so a second external copy re-fires it. **`id-squatter`** — a gate carries the `source-size` id and is not this checker under any reading: the id claims the practice while nothing measures module size, so it is **HAND-APPLY** (rename the id, or repoint the cmd at this checker's `--check` form). **`unminted`** stays what it was for both copies: a gate certain to refuse on every run, attention, and NO acknowledgement silences it — only the rendered verb differs (with the gate already declared elsewhere the missing half is the record alone, so the line is `--write-baseline`). Risk profile: `docs/ai/gates.json` is a PRIVILEGED file, so every arm here either records an acknowledgement or hands you an edit — the kit declares nothing on its own.
|
|
35
35
|
- `gate-hook` — the base arm is the ordinary opt-in wiring offer. The **`marker-stale`** arm is not: the placed hook validates your declaration through its OWN baked copy and goes dark on any key it does not know, so a declaration carrying the `lcovProducer` key under a hook that predates it silently turns auto-approval OFF, and every gate prompts again with no error anywhere. The condition is the key's PRESENCE, not its value — an older hook rejects a key it does not know whatever that key says, so `"lcovProducer": false` darkens it exactly as `true` does. The arm is deliberately marker-scoped — a stale hook is otherwise harmless — and its recovery is the writer's own: `gate-hook --apply` places only an ABSENT target, so converging means deleting the placed hook and re-placing it. That makes it a destructive **HAND-APPLY** (`rm` + `--apply`, absolute path so it can only delete this project's hook), never something the consent flow runs for you. When the read-lane is also enabled, this arm and `read-lane.stale` would report the same file with the same recovery, so exactly one renders — **this one**, because its cause is the true one: a hook that postdates the read-lane and merely predates the marker key reads `lanes.json` perfectly well, and the read-lane arm's wording would be false over it. Risk profile: deleting one placed hook file, then re-placing it from the bundle; the declaration is never touched.
|
|
36
|
+
- `enforcement` — copies the bundled `references/scripts/*.mjs` runnable scripts and their tests, then installs the pre-commit hook. It is HAND-APPLY: the agent never runs it; the offer renders only when `scripts/` is absent or a plain directory contains no bundled basename, so the copy never clobbers an existing file and never follows a link. The hook invokes `node` directly, so a committer without it on PATH gets a loud failure. The installer refuses to overwrite a pre-commit hook it did not write — on that refusal stop and merge the hook by hand (bootstrap step 9); the copied scripts stay in place. Risk profile: local tooling and a Git hook are added only when the maintainer applies the shown command.
|
|
36
37
|
- `mcp-channel` — the offer is to REGISTER the kit's read-only stdio MCP server in this project, which is a different kind of consent from the other velocity items: a registration is a command your MCP client will RUN, so the apply here is the mode's own **flagless preview**, which prints the exact entry and writes nothing — the `--apply` that follows is a SEPARATE step you run after reading that entry, and the consent flow never carries it. What the registration buys: path questions and literal searches become typed tool calls whose arguments are named JSON fields instead of a string handed to a shell, so a pipe, a redirect or a quote inside a pattern or a path stays DATA and is never interpreted — they are perfectly legal bytes to search for, there is simply no shell to read them as operators. Posture: the server is a **read-only child of your client** (path/type/size/line facts and literal search over this project root; no write and no exec API), and like the client itself it runs **outside the Bash sandbox** — the sandbox is not what bounds it, the server's own root containment is. The two allow rules it adds make those two tools promptless and nothing else. Two arms are **HAND-APPLY** and never run for you: **`.differing`** — an `agent-workflow` entry already stands in `.mcp.json` and **structurally differs** from what this kit copy would write (another kit copy, a hand-edited path, an added `env`; the comparison ignores key order, so re-serialized identical bytes are the same registration), and silently changing what an MCP server launches is exactly what consent must not slide past, so the remedy is your edit; and the **masked** arm, where an OS sandbox hides `.mcp.json` behind a device node — the kit cannot write there, so it hands you the text to paste from outside the sandbox. When the file is masked but the settings half is already complete, the item **does not render at all**: what is unobservable becomes a stated SKIP, so optimality is withheld rather than a registration you already made being offered again. Stated limit: this item does **not** detect a `disabledMcpjsonServers` veto, so a converged `mcp-channel` means *what the mode writes is in place*, not *the client will load it* — see `${CLAUDE_SKILL_DIR}/references/modes/mcp.md` for why that check was subtracted rather than half-built. Risk profile: a new read-only channel your client will launch; no write or exec exposure, and no existing declaration is touched.
|
|
37
38
|
- ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
|
|
38
39
|
- `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it writes the reader/checker pairs and the store root, create-only, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries a NAMED **HAND-APPLY alternative** — the DECLINE preview (`ack-write --lane spec-adoption`, a dry-run that prints the exact `--apply`), which records `specAdoptionAck` in the family-owned `docs/ai/acks.json`. **The two are exclusive, and the order is part of the contract:** ask which the maintainer wants BEFORE the confirmation; on "seed" run the apply; on "decline" run NOTHING — hand over the preview line and let them run it and its printed `--apply`; never run the seed after a decline was chosen, and never run the decline preview for them. **`adopting`** (a store with no live contract): an optional item naming the draft count; its apply is that same decline preview (a dry-run; after the SAME confirmation you run the `--apply` it prints), and the honest remedy is authoring a contract from the skill's `SPEC_TEMPLATE.md`. A recorded decline silences BOTH arms and shows on `status` as ` — declined`; one live contract makes the decline moot. An unreadable store is a stated skip, never `flow optimal`. Risk profile: the seed writes only create-only files under `scripts/` and `docs/ai/specs/`; the decline writes one key into the ack store; nothing here authors a spec or blocks a plan.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
8
|
// Structural pin (green by construction, a tripwire not a proof): all three archivers read
|
|
8
9
|
// documents through the ONE shared tokenizer. A fourth archiver — or a future edit that reaches
|
|
@@ -12,6 +13,50 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
13
|
|
|
13
14
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const ARCHIVERS = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-decisions.mjs'];
|
|
16
|
+
const ARCHIVER_CLOSURE = ['archive-changelog.mjs', 'archive-issues.mjs', 'archive-caps.mjs', 'markdown-blocks.mjs'];
|
|
17
|
+
const FIXED_DATE = '2026-09-02';
|
|
18
|
+
const MALFORMED_PACKAGE_JSON = '{ not json';
|
|
19
|
+
const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
|
|
20
|
+
const NAMED_FIXTURE = 'named-fixture';
|
|
21
|
+
|
|
22
|
+
const buildArchiveHeadings = async (packageJson) => {
|
|
23
|
+
const root = mkdtempSync(join(tmpdir(), 'archiver-name-'));
|
|
24
|
+
const scripts = join(root, 'scripts');
|
|
25
|
+
mkdirSync(scripts);
|
|
26
|
+
for (const name of ARCHIVER_CLOSURE) cpSync(join(DIR, name), join(scripts, name));
|
|
27
|
+
if (packageJson !== null) writeFileSync(join(root, 'package.json'), packageJson);
|
|
28
|
+
const changelog = await import(pathToFileURL(join(scripts, 'archive-changelog.mjs')).href);
|
|
29
|
+
const issues = await import(pathToFileURL(join(scripts, 'archive-issues.mjs')).href);
|
|
30
|
+
const result = {
|
|
31
|
+
fallback: basename(root),
|
|
32
|
+
changelog: changelog.buildRecent([], FIXED_DATE),
|
|
33
|
+
issues: issues.buildResolvedFile(null, [], FIXED_DATE),
|
|
34
|
+
};
|
|
35
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assertArchiveHeadings = (result, projectName) => {
|
|
40
|
+
assert.ok(result.changelog.includes(`# Changelog WARM Archive \u2014 ${projectName}`));
|
|
41
|
+
assert.ok(result.issues.includes(`# Resolved Issues \u2014 ${projectName}`));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('the archivers head their archives with the directory basename when no package.json, or a malformed one, names the project', () => {
|
|
45
|
+
it('no package.json names the directory basename', async () => {
|
|
46
|
+
const result = await buildArchiveHeadings(null);
|
|
47
|
+
assertArchiveHeadings(result, result.fallback);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('a malformed package.json names the directory basename', async () => {
|
|
51
|
+
const result = await buildArchiveHeadings(MALFORMED_PACKAGE_JSON);
|
|
52
|
+
assertArchiveHeadings(result, result.fallback);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('a named package.json wins', async () => {
|
|
56
|
+
const result = await buildArchiveHeadings(NAMED_PACKAGE_JSON);
|
|
57
|
+
assertArchiveHeadings(result, NAMED_FIXTURE);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
15
60
|
|
|
16
61
|
describe('every archiver reads through the tokenizer — no raw scan survives', () => {
|
|
17
62
|
for (const name of ARCHIVERS) {
|
|
@@ -8,12 +8,15 @@ import { describe, it } from 'node:test';
|
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
9
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, cpSync, existsSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join, dirname, resolve } from 'node:path';
|
|
11
|
+
import { basename, join, dirname, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
|
|
15
15
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const INSTALLER_SRC = join(HERE, 'install-git-hooks.mjs');
|
|
17
|
+
const MALFORMED_PACKAGE_JSON = '{ not json';
|
|
18
|
+
const NAMED_PACKAGE_JSON = '{"name":"named-fixture"}';
|
|
19
|
+
const NAMED_FIXTURE = 'named-fixture';
|
|
17
20
|
|
|
18
21
|
// The installer anchors ROOT at <its dir>/.. — deploy it into a fixture's scripts/ like a consumer.
|
|
19
22
|
const mkProject = () => {
|
|
@@ -39,6 +42,33 @@ const hooksPathOf = (root) => {
|
|
|
39
42
|
return resolve(root, r.stdout.trim());
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
const markerFor = (name) => `# ${name}:install-git-hooks.mjs`;
|
|
46
|
+
const readHook = (root) => readFileSync(join(hooksPathOf(root), 'pre-commit'), 'utf8');
|
|
47
|
+
|
|
48
|
+
describe('install-git-hooks \u2014 beside no package.json, or a malformed one, the marker names the directory basename', () => {
|
|
49
|
+
it('no package.json, then a MALFORMED one \u2014 the marker names the directory basename', () => {
|
|
50
|
+
const { root } = mkProject();
|
|
51
|
+
const expected = markerFor(basename(root));
|
|
52
|
+
const absent = runInstaller(root);
|
|
53
|
+
assert.equal(absent.status, 0, absent.stderr);
|
|
54
|
+
assert.ok(readHook(root).split('\n').includes(expected));
|
|
55
|
+
writeFileSync(join(root, 'package.json'), MALFORMED_PACKAGE_JSON);
|
|
56
|
+
const malformed = runInstaller(root);
|
|
57
|
+
assert.equal(malformed.status, 0, malformed.stderr);
|
|
58
|
+
assert.ok(readHook(root).split('\n').includes(expected));
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('a named package.json wins \u2014 the marker carries its name', () => {
|
|
63
|
+
const { root } = mkProject();
|
|
64
|
+
writeFileSync(join(root, 'package.json'), NAMED_PACKAGE_JSON);
|
|
65
|
+
const result = runInstaller(root);
|
|
66
|
+
assert.equal(result.status, 0, result.stderr);
|
|
67
|
+
assert.ok(readHook(root).split('\n').includes(markerFor(NAMED_FIXTURE)));
|
|
68
|
+
rmSync(root, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
42
72
|
describe('install-git-hooks — the hooks path comes from git plumbing (C7)', () => {
|
|
43
73
|
it('a normal repo installs at the git-path hooks location', () => {
|
|
44
74
|
const { root } = mkProject();
|
|
@@ -9,7 +9,7 @@ The non-obvious traps — scan these before bootstrapping or upgrading. Each is
|
|
|
9
9
|
- **`CLAUDE.md` is a symlink, not a copy.** `ln -s AGENTS.md CLAUDE.md` — single source, no duplication. A copy drifts; a symlink can't.
|
|
10
10
|
- **Never overwrite an existing entry point or hook.** If `AGENTS.md` / `CLAUDE.md` already exist, or the installer reports a pre-existing non-marker git hook, **stop and ask** the user to merge vs replace — don't clobber.
|
|
11
11
|
- **Unrecognized invocations are read-only.** Only a **known** subcommand reaches its mode; the **bare** invocation bootstraps (and an existing `docs/ai/` makes it ask upgrade-vs-bootstrap first, never overwrite); **any other / ambiguous** token routes to `help` (read-only). A garbage invocation never writes. The mapping is pinned by `tools/commands.mjs` `routeInvocation` (unit-tested) — don't hand-route around it.
|
|
12
|
-
- **
|
|
12
|
+
- **Enforcement is offered on every project.** The dependency-free scripts are available because the agent host already runs the navigator; for the offer, the only skip condition is that the agent host cannot run them. The hook calls `node scripts/<x>.mjs`, so a committer without `node` on PATH gets the hook's own loud failure. **A skip names a PROVEN fact, never a proxy:** upgrade ensures judge Node by EVIDENCE — a root `package.json` OR any kit-seeded `scripts/*.mjs` already deployed — and the `skipped-no-node-evidence` line lists every probe that answered absent; a reason the tool could disprove is a tool defect. **A skip line that contradicts the observed tree is raised as a finding in the report, never pasted as neutral** — report the contradiction with the proven fact and do not build on the skip.
|
|
13
13
|
- **Conversational language never translates artifacts.** It governs *dialogue only*. Code, identifiers, paths, commands, log output, abbreviations, and every deployed `docs/ai/` / `AGENTS.md` file stay in their source language. See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract).
|
|
14
14
|
- **Never auto-commit.** Report quality-gate results and wait for explicit approval — in both modes.
|
|
15
15
|
- **Never leak kit internals to the user — and a tool-COMPOSED user-facing line holds the same bar at the source.** No ADR ids, tool / function / operation names (`reconcile`, `inject`, `ensureSlot`), marker / slot / fragment / anchor terminology, or verbatim tool stderr **inside the human sentence** of anything the user reads. Translate every tool outcome into plain language a third-party user — who has never read this `SKILL.md` — can understand and act on (e.g. the cap-refusal report in `${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3). The composed lines themselves are **user-grade** language: machine tokens and tool self-labels belong to the **machine-line channel** — the `[run-gates] status=…` grammar (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`), a line's leading self-label/prefix, or a runnable command/path the user can act on — never mid-sentence; alarm words (`PARTIALLY`, `incomplete`, `failed`, `broken`, `persists`) render only in outcomes gated on a **detected abnormal condition**. ONE designed exception, stated not implied: the configuration ensures' LEADING outcome token — one closed-vocabulary token, a failure's closed cause word opening its detail line — is that contract's own machine slot, not a leak. The **verbatim**-paste contract stays: the agent pastes tool-composed outcome lines as written and never re-composes their facts — the lines are user-grade at the source, so pasting them verbatim IS the plain language.
|
|
@@ -37,7 +37,7 @@ Read in order, then confirm before starting:
|
|
|
37
37
|
Before claiming "done":
|
|
38
38
|
1. Run all quality gates (lint + type-check + tests) — all green.
|
|
39
39
|
2. Update docs: `current_state.md` (feature ready), `changelog.md` (entry), `handover.md` (**REPLACE** the last-session block — session delta, never append; older deltas live in `changelog.md` → `history/`), `pages/<page>.md` (matches implementation), the governing spec (its revision lands with the code: `draft` -> `live` on the landing row). Only bump "Last Updated" when content actually changed.
|
|
40
|
-
3. Run the docs cap-validator + index-freshness gate (pre-commit also enforces). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
|
|
40
|
+
3. Run the docs cap-validator + index-freshness gate (the pre-commit hook also enforces it where installed). On failure: trim the offending file, or run the changelog rotation if the offender is `changelog.md`.
|
|
41
41
|
4. If the work executed a plan file — run that plan's final **Phase: Cleanup** (see the planning skill / §5). Without it the plan is not done.
|
|
42
42
|
5. **Ask before committing** (§4): report lint / type-check / test counts + docs status, then wait for explicit approval. DO NOT auto-commit.
|
|
43
43
|
|
|
@@ -20,7 +20,7 @@ maxLines: 700
|
|
|
20
20
|
**Changes:**
|
|
21
21
|
- Created `AGENTS.md` (entry point) + `CLAUDE.md` symlink.
|
|
22
22
|
- Created `docs/ai/` with the spec files + `pages/`.
|
|
23
|
-
-
|
|
23
|
+
- Offered the docs cap-validator / index-freshness / archive scripts + pre-commit hook (installed on a yes).
|
|
24
24
|
- Recorded the real stack, scripts, and architecture into the relevant files.
|
|
25
25
|
|
|
26
26
|
**Quality gates:** n/a (no code changes).
|
|
@@ -13,7 +13,7 @@ maxLines: 500
|
|
|
13
13
|
> consequences, newest at the bottom. Link related ADRs with `[[AD-XXX]]`; retrieval is by the
|
|
14
14
|
> `AD-NNN` id (filename), grep over the flat store, or the `[[AD-NNN]]` supersession chain.
|
|
15
15
|
|
|
16
|
-
> **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically; no cap is ever raised and there is no monolithic ledger.
|
|
16
|
+
> **Archive:** older ADRs are stored one immutable file per record under [`adr/`](./adr/) — see the active-set navigator [`adr/log.md`](./adr/log.md). `archive-decisions.mjs` explodes the oldest entries beyond this window automatically where the enforcement scripts are deployed (installed on a yes); no cap is ever raised and there is no monolithic ledger.
|
|
17
17
|
|
|
18
18
|
## AD-001 — Adopt AI-agent memory system (`docs/ai/`)
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ maxLines: 500
|
|
|
22
22
|
|
|
23
23
|
**Context.** Multi-session AI work loses context between runs. Without a structured handover, each new session re-reads code, re-discovers decisions, and repeats past mistakes.
|
|
24
24
|
|
|
25
|
-
**Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via
|
|
25
|
+
**Decision.** Adopt a Memory Map in `AGENTS.md` (entry point — the cross-agent standard; tool aliases like `CLAUDE.md` symlink to it) + structured files under `docs/ai/`. Define three protocols (Start / During / Complete). Enforce frontmatter caps + index freshness + a one-file-per-ADR archive via the deployed scripts + pre-commit hook (offered on every project, installed on a yes). Deployed via the `agent-workflow-kit` skill.
|
|
26
26
|
|
|
27
27
|
**Rationale.** Single entry + structured spec files = constant boot-up cost regardless of project size. ADRs prevent litigating the same decision twice. `pages/<page>.md` keeps behaviour canonical (docs > assumptions). Caps + one-file-per-ADR archival keep every record scannable as the decision history grows without bound.
|
|
28
28
|
|
|
@@ -83,6 +83,7 @@ import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
|
83
83
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
84
84
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
85
85
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
86
|
+
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
86
87
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
87
88
|
import { surveyExecutorVehicle, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
88
89
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
@@ -108,7 +109,12 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
|
|
|
108
109
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
109
110
|
const toolPath = (rel) => join(HERE, rel);
|
|
110
111
|
const q = shellQuoteArg;
|
|
111
|
-
|
|
112
|
+
const ENFORCEMENT_ABSENT_CODE = 'ENOENT';
|
|
113
|
+
const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
|
|
114
|
+
const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
|
|
115
|
+
const ENFORCEMENT_WHAT = 'the enforcement scripts are not deployed beside docs/ai (no Node evidence) \u2014 the offer copies them and installs the pre-commit hook';
|
|
116
|
+
const ENFORCEMENT_APPLY_TEMPLATE = 'HAND-APPLY: mkdir -p {root}/scripts && cp {kit}/references/scripts/*.mjs {root}/scripts/ && node {root}/scripts/install-git-hooks.mjs';
|
|
117
|
+
const COMMIT_GUARD_INSTALLER_REMEDY = 'the hook installer (scripts/install-git-hooks.mjs) is not deployed \u2014 deploy the enforcement scripts (bootstrap steps 8\u20139) and install the hook, then re-run';
|
|
112
118
|
// ── the section contract tokens (doc-parity-bound in upgrade.md + the mode doc) ────────────────
|
|
113
119
|
export const RECOMMENDATIONS_SECTION_HEADER = '## Recommendations (agent-workflow)';
|
|
114
120
|
export const RECOMMENDATIONS_EMPTY_LINE = 'no recommendations — flow optimal.';
|
|
@@ -166,6 +172,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
166
172
|
// offer to enable something.
|
|
167
173
|
'gate-hook.marker-stale': SEVERITY_ATTENTION,
|
|
168
174
|
'commit-guard': SEVERITY_OPTIONAL,
|
|
175
|
+
enforcement: SEVERITY_OPTIONAL,
|
|
169
176
|
'read-lane': SEVERITY_OPTIONAL,
|
|
170
177
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
171
178
|
'read-lane.missing': SEVERITY_ATTENTION,
|
|
@@ -249,6 +256,7 @@ export const WHATS = Object.freeze({
|
|
|
249
256
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
250
257
|
'gate-hook.marker-stale': 'the declaration carries the lcovProducer key but the placed hook predates it — the hook goes dark and every gate prompts',
|
|
251
258
|
'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
|
|
259
|
+
enforcement: ENFORCEMENT_WHAT,
|
|
252
260
|
'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
|
|
253
261
|
'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
|
|
254
262
|
'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
|
|
@@ -313,6 +321,7 @@ export const BENEFITS = Object.freeze({
|
|
|
313
321
|
'source-size': 'maintainability — a module you can hold whole stays reviewable, and size drift becomes recorded, reasoned debt',
|
|
314
322
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
315
323
|
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
324
|
+
enforcement: 'enforcement — bundled checks and the pre-commit hook keep caps, archives, and the generated index current',
|
|
316
325
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
317
326
|
'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
|
|
318
327
|
'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
|
|
@@ -361,6 +370,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
361
370
|
{ id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
|
|
362
371
|
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
363
372
|
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
373
|
+
{ id: 'enforcement', mode: 'bootstrap', advisorKey: 'enforcement' },
|
|
364
374
|
{ id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
|
|
365
375
|
{ id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
|
|
366
376
|
{ id: 'mcp-channel', mode: 'mcp', advisorKey: 'mcp-channel' },
|
|
@@ -843,7 +853,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
843
853
|
if (!isFinalCapableDeclaration(gates, root)) return; // no receipt to guard yet — no offer
|
|
844
854
|
const installer = join(root, 'scripts', 'install-git-hooks.mjs');
|
|
845
855
|
if (!(deps.exists ?? existsSync)(installer)) {
|
|
846
|
-
skip('commit-guard', new Error(
|
|
856
|
+
skip('commit-guard', new Error(COMMIT_GUARD_INSTALLER_REMEDY));
|
|
847
857
|
return;
|
|
848
858
|
}
|
|
849
859
|
const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
|
|
@@ -866,6 +876,45 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
866
876
|
}
|
|
867
877
|
};
|
|
868
878
|
|
|
879
|
+
const readEnforcementNode = (path, lstat) => {
|
|
880
|
+
try {
|
|
881
|
+
return lstat(path);
|
|
882
|
+
} catch (err) {
|
|
883
|
+
if (err?.code === ENFORCEMENT_ABSENT_CODE) return null;
|
|
884
|
+
throw new Error(`${path}: ${err?.code ?? err?.message ?? err}`);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
export const probeEnforcement = ({ root, deps, add, skip }) => {
|
|
889
|
+
try {
|
|
890
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
891
|
+
const docsAi = join(root, 'docs', 'ai');
|
|
892
|
+
const docsAiNode = readEnforcementNode(docsAi, lstat);
|
|
893
|
+
if (docsAiNode === null) return;
|
|
894
|
+
if (!docsAiNode.isDirectory()) throw new Error(`${docsAi} is not a plain directory`);
|
|
895
|
+
const evidence = probeNodeEvidence(root, lstat);
|
|
896
|
+
if (evidence.state === NODE_EVIDENCE.PACKAGE_JSON || evidence.state === NODE_EVIDENCE.DEPLOYED_SCRIPTS) return;
|
|
897
|
+
if (evidence.state === NODE_EVIDENCE.UNREADABLE) throw new Error(`${root}: ${evidence.error}`);
|
|
898
|
+
if (evidence.state !== NODE_EVIDENCE.NONE) throw new Error(`unknown Node-evidence state: ${evidence.state}`);
|
|
899
|
+
if (evidence.wrongKind.length > 0) throw new Error(join(root, evidence.wrongKind[0]));
|
|
900
|
+
const readDir = deps.readdir ?? readdirSync;
|
|
901
|
+
const bundleRoot = deps.scriptsBundleRoot ?? ENFORCEMENT_BUNDLE_ROOT;
|
|
902
|
+
const bundleBasenames = readDir(bundleRoot).filter((name) => name.endsWith('.mjs')).sort();
|
|
903
|
+
if (NODE_EVIDENCE_SCRIPTS.some((name) => !bundleBasenames.includes(name))) throw new Error(`${bundleRoot}: a runnable enforcement script is missing`);
|
|
904
|
+
const scripts = join(root, 'scripts');
|
|
905
|
+
const scriptsNode = readEnforcementNode(scripts, lstat);
|
|
906
|
+
if (scriptsNode !== null && !scriptsNode.isDirectory()) throw new Error(`${scripts} is not a plain directory`);
|
|
907
|
+
for (const basename of scriptsNode === null ? [] : bundleBasenames) {
|
|
908
|
+
const destination = join(scripts, basename);
|
|
909
|
+
if (readEnforcementNode(destination, lstat) === null) continue;
|
|
910
|
+
throw new Error(`${destination} already exists; the enforcement copy would clobber it`);
|
|
911
|
+
}
|
|
912
|
+
add('enforcement', ENFORCEMENT_WHAT, fillTemplate(ENFORCEMENT_APPLY_TEMPLATE, { root: q(root), kit: q(ENFORCEMENT_KIT_ROOT) }));
|
|
913
|
+
} catch (err) {
|
|
914
|
+
skip('enforcement', err);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
869
918
|
// The read-lane offer (AD-055 Part II, Help-through-Recommendations): once the gate hook is PLACED
|
|
870
919
|
// and WIRED, offer to enable the opt-in read-only compound lane. Mutually exclusive with the
|
|
871
920
|
// gate-hook item BY CONSTRUCTION — that item keys on `!wired`, this one on `wired` — so an unplaced
|
|
@@ -1242,7 +1291,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
1242
1291
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
1243
1292
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
1244
1293
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
1245
|
-
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption']);
|
|
1294
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement']);
|
|
1246
1295
|
|
|
1247
1296
|
// The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
|
|
1248
1297
|
// zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
|
|
@@ -1547,6 +1596,7 @@ const PROBES = Object.freeze([
|
|
|
1547
1596
|
probeGatesInert,
|
|
1548
1597
|
probeSourceSize,
|
|
1549
1598
|
probeCommitGuard,
|
|
1599
|
+
probeEnforcement,
|
|
1550
1600
|
probeReadLane,
|
|
1551
1601
|
probeStateBlockHook,
|
|
1552
1602
|
probeCheapAgents,
|