@sabaiway/agent-workflow-kit 13.1.0 → 13.3.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 +140 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/upgrade.md +4 -4
- package/references/templates/vehicles.json +8 -0
- package/tools/checkpoint-core.mjs +118 -0
- package/tools/checkpoint-restore.mjs +126 -0
- package/tools/checkpoint.mjs +146 -0
- package/tools/ensure-configs.mjs +8 -7
- package/tools/ensure-ops.mjs +15 -7
- package/tools/ensure-vocabulary.mjs +1 -1
- package/tools/plan-files.mjs +2 -1
- package/tools/review-state.mjs +1 -1
- package/tools/task-brief.mjs +270 -0
- package/tools/vehicle-settings.mjs +127 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,146 @@ 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
|
+
## 13.3.0 — the executor vehicle's model becomes a project setting: `vehicle-settings.mjs` reads `docs/ai/vehicles.json` through two readers over one three-state table and resolves it to one posture, the seed `vehicles.json` ships, and the config ensure gains a seventh op, `vehicles`, fourth in the fixed order (AD-142)
|
|
8
|
+
|
|
9
|
+
**MINOR: one new tool, one new seed template, one new ensure op; no existing verb, token, cause or order of the six
|
|
10
|
+
prior ops moves.** The placed executor vehicle carried one model hard-wired in the bundled template — a project that
|
|
11
|
+
wanted another edited the placed file, which the placer then reported as a customization it never refreshed, and
|
|
12
|
+
when that model's quota ran out the slice died with it. This release ships the setting and its readers (story S1 of
|
|
13
|
+
the epic `EXECUTOR-VEHICLE-PINS-ONE-MODEL-AND-DIES-ON-ITS-QUOTA`); deriving the placed body from it and retrying a
|
|
14
|
+
refused slice on the fallback are the epic's second story. The contract is `docs/ai/specs/kit/vehicle-settings.md`
|
|
15
|
+
(live, revision 1, S1–S5 bound).
|
|
16
|
+
|
|
17
|
+
- **`tools/vehicle-settings.mjs` (127 lines, imports only `node:fs` and `node:path`).** The setting is ONE
|
|
18
|
+
strict-JSON file, `docs/ai/vehicles.json`, beside `orchestration.json` and `autonomy.json` — never a key of
|
|
19
|
+
`orchestration.json`. Its shape is closed: an optional `_README` string, then exactly one `executor` section of
|
|
20
|
+
exactly three non-empty bare-token string keys `model`, `effort` and `fallback` (`fallback` differing from `model`,
|
|
21
|
+
since an equal fallback retries into the same quota); every other form is MALFORMED, the reason naming the rel
|
|
22
|
+
path and the offending key or value. `readVehicles(cwd, readFile, lstat)` probes with lstat FIRST and never throws:
|
|
23
|
+
ENOENT alone answers `{ state: 'absent' }`; a directory, a symlink dangling or not, any other node kind, a
|
|
24
|
+
permission or read error, a parse error and a malformed body answer `{ state: 'unreadable', reason }` — the door
|
|
25
|
+
the read-only surfaces walk through; a well-formed regular file answers `{ state: 'present', settings }`.
|
|
26
|
+
`loadVehicles` is the same table with `unreadable` raised as the module-local `fail(1, reason)` (the
|
|
27
|
+
orchestration-config idiom, defined locally as every sibling reader does, so the module stays a leaf the read-only
|
|
28
|
+
surfaces can import). `resolveExecutor(answer)` is pure and answers `{ posture, reason }`: `present` the file's
|
|
29
|
+
three values with `source: 'file'`; `absent` the ONE frozen `EXECUTOR_DEFAULTS` — `model: 'opus'`, `effort: 'high'`,
|
|
30
|
+
`fallback: 'sonnet'` — with `source: 'default'`; `unreadable` a `null` posture and the reader's reason, so a broken
|
|
31
|
+
file is never mistaken for a healthy default. Exactly six exports: `VEHICLES_REL`, `EXECUTOR_DEFAULTS`,
|
|
32
|
+
`validateVehicles`, `readVehicles`, `loadVehicles`, `resolveExecutor`; no side effects on import.
|
|
33
|
+
- **`references/templates/vehicles.json` (new).** The seed: a `_README` sentence, then `executor` at the three
|
|
34
|
+
defaults; the suite pins its parsed `executor` equal to `EXECUTOR_DEFAULTS`, so no second hand-copy exists.
|
|
35
|
+
- **The seventh config ensure, `vehicles`, fourth in the fixed order after `autonomy`.** `ENSURE_OPS` in
|
|
36
|
+
`tools/ensure-vocabulary.mjs` reads `orchestration, gates, autonomy, vehicles, scripts, specs, index`;
|
|
37
|
+
`ensureVehicles` in `tools/ensure-ops.mjs` rides the existing `seedFromTemplate` door — create-only from the
|
|
38
|
+
bundled template, a present file preserved byte for byte and never read (a malformed present file is preserved,
|
|
39
|
+
not repaired: the readers are what name it), `wrong-node-kind` loud, `would-seed` under `--dry-run`, the outcome
|
|
40
|
+
tokens the seed family already had. `tools/ensure-configs.mjs` composes the table as before and says seven; the
|
|
41
|
+
op counts and banner ordinals inside `ensure-ops.mjs` were removed rather than renumbered, so the order lives in
|
|
42
|
+
`ENSURE_OPS` alone.
|
|
43
|
+
- **The mode docs count what runs.** `references/modes/upgrade.md`'s `configs` step performs all seven ensures and
|
|
44
|
+
names the vehicles declaration after the autonomy declaration with the shared `seeded` / `already-present` tokens
|
|
45
|
+
(both report clauses say seven); `references/modes/bootstrap.md` step 6 names the fourth seeded strict-JSON
|
|
46
|
+
config, `docs/ai/vehicles.json`, which the template loop now deploys on a fresh kit-path deployment. A deployment
|
|
47
|
+
on the memory package's own templates gets the file at its first upgrade run's ensure.
|
|
48
|
+
- **Tests.** `tools/vehicle-settings.test.mjs` (315 lines): one case per scenario S1–S5, the module and the ensure op
|
|
49
|
+
imported dynamically with throwing fallbacks so the suite loaded red on the tree before either existed; S3 walks
|
|
50
|
+
the whole malformed-form table, S4 six node kinds and IO errors with the lstat/readFile call order recorded, S5
|
|
51
|
+
the ensure's six outcomes plus the registry and the order. `ensure-configs.test.mjs`'s three outcome sequences
|
|
52
|
+
carry `vehicles: seeded` fourth; `composed-lines-ux.test.mjs`'s slot count reads `ENSURE_OPS.length`; the shipped
|
|
53
|
+
tarball pin is 311 (309 + the tool + the seed).
|
|
54
|
+
- **How it was built.** The story ran on the task tier as the pilot's measurement: ten ledger rows became ten
|
|
55
|
+
briefed tasks on wave `p1` — an author dispatch wrote each `TASK-` brief (kit-stamped against a fresh checkpoint),
|
|
56
|
+
an execute dispatch landed it — twenty codex-exec threads on one held session, every one folded first pass; the
|
|
57
|
+
diff council's findings were folded before the release; the plan council took six rounds. Figures:
|
|
58
|
+
`docs/ai/history/pilot-measure-2026-09-13.md`.
|
|
59
|
+
|
|
60
|
+
## 13.2.0 — the task tier lands: `checkpoint.mjs` mints, orders, verifies and prunes tree snapshots of a plan's sequence, `checkpoint-restore.mjs` restores one in three proven steps, `task-brief.mjs` stamps and checks a brief against the newest checkpoint, and `TASK-` is a scratch name (AD-141)
|
|
61
|
+
|
|
62
|
+
**MINOR: three new tools and one added arm of the scratch-name predicate; no existing verb, record, transition or
|
|
63
|
+
metric moves.** Since 13.1.0 the delegation ledger could measure a thread against a `checkpoint` base — a tree the
|
|
64
|
+
orchestrator had to compute by hand. Now the kit mints it, keeps it in a sequence, restores to it and binds the
|
|
65
|
+
task's brief to it. The contract is `docs/ai/specs/kit/checkpoint/index.md` with its parts `restore.md` and
|
|
66
|
+
`task-brief.md` (live, revision 2, S1–S10 bound).
|
|
67
|
+
|
|
68
|
+
- **`tools/checkpoint.mjs` (146 lines).** `mint --plan docs/plans/<stem>.md` writes ONE tree over the snapshot
|
|
69
|
+
scope of 13.1.0 (`snapshotScope`, the newest checkpoint as the base tree) through its own temporary index — a
|
|
70
|
+
forced add of every present regular file or symlink, one `write-tree`, the index removed before every return —
|
|
71
|
+
so `docs/ai/` deliverables are in the snapshot and the brief under `docs/plans/` never is; an absent leaf and a
|
|
72
|
+
directory at a former file leaf are deletions; a present non-regular path refuses by name and writes nothing;
|
|
73
|
+
the real index, the work tree and every other ref are untouched. The plan must be a readable regular file that is
|
|
74
|
+
a DIRECT child of `docs/plans/`, not a scratch name, with a stem `git check-ref-format` admits, else `stem`. The
|
|
75
|
+
ref of checkpoint `n` is `refs/agent-workflow/checkpoints/<stem>/<n>` and points at the TREE (reachable across
|
|
76
|
+
`gc`, never a branch, a fetch or a push); numbers run contiguously from 0; a mint whose tree equals the newest
|
|
77
|
+
ref's writes nothing and prints the newest's line; the line is `checkpoint <stem>/<n> <oid>`. `newest --plan`
|
|
78
|
+
prints the greatest or refuses `no-checkpoint`; `verify <oid>` recomputes with the target as base and prints
|
|
79
|
+
CLEAN (exit 0) or DIRTY (exit 1) with both oids, a lost snapshot path counted as dirt; `prune --plan` deletes the
|
|
80
|
+
plan's refs (an empty sequence exits 0) and refuses `open-thread`, deleting nothing, while any delegation-ledger
|
|
81
|
+
thread whose dispatch base is one of those trees is OPEN — judged by base oid alone, so another sequence's thread
|
|
82
|
+
holds the ref exactly when the two share the tree, and an unfolded success return still holds it; a malformed
|
|
83
|
+
ledger refuses. A non-tree id refuses through 13.1.0's `resolveTreeObject` wording. Exactly three exit codes:
|
|
84
|
+
0 accept or CLEAN, 1 refuse or DIRTY, 2 usage; `main(argv, deps)` returns and never exits; git runs through the
|
|
85
|
+
same guarded runner discipline as the 13.1.0 leaf (a thrown runner is a failed spawn).
|
|
86
|
+
- **`tools/checkpoint-core.mjs` (118 lines).** The git seam the two halves share — the guarded runner, the
|
|
87
|
+
repository resolution, the sequence-ref reader (a listing line that is not `<ref> <oid>` refuses `sequence`),
|
|
88
|
+
the temporary-index discipline, the scope lstat and the snapshot write — so `checkpoint.mjs` and
|
|
89
|
+
`checkpoint-restore.mjs` both import the leaf; `checkpoint.mjs` still imports the restore module, which no longer
|
|
90
|
+
imports `checkpoint.mjs`. The pair had imported each other;
|
|
91
|
+
the tools graph's acyclic pin (`read-graph-purity`) caught it at the release gate, after the council.
|
|
92
|
+
- **`tools/checkpoint-restore.mjs` (126 lines) and the `restore <oid>` verb.** Whole-tree over the snapshot scope,
|
|
93
|
+
ORDERED, idempotent and proven. Refused BEFORE the first write on exactly five states: a non-tree target;
|
|
94
|
+
`target-scope` — a target leaf outside the domain (an excluded path such as `docs/plans/x`), under a `.git`
|
|
95
|
+
segment, with a gitlink mode, or a tree no sequence ref carries; `ledger` — an unreadable or malformed ledger
|
|
96
|
+
(an absent one holds no thread); `open-thread` — an OPEN thread whose base oid is in the HELD SET, the target
|
|
97
|
+
plus every tree of every sequence carrying it (a disjoint sequence and a head base proceed); `type-conflict` — a
|
|
98
|
+
directory (empty or not) where the target holds a blob or a symlink, or a file or symlink at a parent segment
|
|
99
|
+
where the target needs a directory. Then MATERIALIZE (`read-tree` + `checkout-index -a -f` under a temporary
|
|
100
|
+
index: a blob lands as a file, a symlink entry as a symlink), EXTRAS (every domain path the target does not hold
|
|
101
|
+
is unlinked at its own lstat-ed leaf, never through a link; a parent emptied by that deletion is removed, never
|
|
102
|
+
the repository top, never a directory that still holds anything), PROOF (the scope tree recomputed as `verify`
|
|
103
|
+
computes it; equal → `checkpoint restore <oid> proof <oid>`, exit 0; unequal → exit 1 naming both oids, never a
|
|
104
|
+
restore claimed). Each step is a pure function of the target and the tree, so a re-run after an interruption at
|
|
105
|
+
any step converges; a restored ignore file that UNCOVERS a path the first domain did not hold surfaces as a
|
|
106
|
+
proof mismatch; re-running the restore converges. The real index is never a domain nor written (a staged change survives; the work tree under it is the
|
|
107
|
+
snapshot's); `docs/plans/`, the three settings files, the footprint and `node_modules` are never read, written or
|
|
108
|
+
deleted; earlier checkpoints stay; a restore to an older checkpoint is legal and undoes every task after it.
|
|
109
|
+
- **`tools/task-brief.mjs` (270 lines).** A brief is `docs/plans/TASK-<stem>-T<n>.md`, its execute attempt's
|
|
110
|
+
dispatch file `TASK-<stem>-T<n>-a<attempt>.md` beside it — both scratch, both outside the scope. The grammar is
|
|
111
|
+
closed: `# Task: <name>`, the story line byte-equal to the plan's, `## Slice` (`Plan:`, `Row:`, `Grouping:`,
|
|
112
|
+
`Files:` of `- <path> :: <tag>`), `## Reads`, `## Acceptance` (`- <command> :: <expected>`), `## Negative cases`,
|
|
113
|
+
`## Budget` (`- <path> :: <max lines>`, one per file), at most one `aw-task-binding` block. Exactly seven states
|
|
114
|
+
refuse `shape`; a tag outside test/impl/pin, a `Grouping:` not equal to the present tags in that order, or the
|
|
115
|
+
pin rule broken (exactly one pin line naming the package-content path on that row, none elsewhere) refuse
|
|
116
|
+
`grouping`; a `Row:` naming no row, an impl path that is not the row's path or a sweep expansion (the anchor is
|
|
117
|
+
never a ground), or a test path not co-located refuse `row`; the story line absent or unequal refuses `story`.
|
|
118
|
+
`stamp <brief>` judges the grammar, then writes the ONE block `{ schema: 1, plan: { path, sha256 }, reads: [{
|
|
119
|
+
path, sha256 }], checkpoint: <newest tree oid>, head: <commit oid> }` over zero or one existing block and prints
|
|
120
|
+
`task-brief digest sha256:<hex>` of the whole stamped file; a plan or read that is not a readable regular file
|
|
121
|
+
inside the repository refuses `reads` (the descriptor-bound no-follow door), an empty sequence `no-checkpoint`,
|
|
122
|
+
an unresolvable HEAD `head`. `check <brief> [--dispatch <file>]` recomputes everything and refuses each stale
|
|
123
|
+
state by one name: `binding`, `no-checkpoint`, `reads`, `binding-plan`, `binding-read`, `binding-checkpoint`,
|
|
124
|
+
`checkpoint-stale` (the tree DIRTY against the bound checkpoint — mint again, stamp again), `binding-head`, and
|
|
125
|
+
with `--dispatch` `dispatch-inputs` when the attempt's `aw-dispatch-contract` header does not carry the brief
|
|
126
|
+
path and the current digest literal. Checkpoint facts are read through `checkpoint.mjs`'s own exports.
|
|
127
|
+
- **`tools/plan-files.mjs`.** `TASK-` joins `isScratchPlanName`, so a brief or a dispatch file is never an
|
|
128
|
+
in-flight plan and neither `plan-shape --in-flight` nor `review-state` judges it; the review-state mode document
|
|
129
|
+
and header name the prefix.
|
|
130
|
+
- **Tarball 305 → 309.** The four modules are pinned by name. Suites: `tools/checkpoint.test/mint.test.mjs`
|
|
131
|
+
(50), `tools/checkpoint.test/restore.test.mjs` (42), `tools/task-brief.test.mjs` (69), `tools/plan-files.test.mjs`
|
|
132
|
+
(2): every case of the first session written before the code existed and red, the three cases of the diff
|
|
133
|
+
review's fold red on the pre-fold code, then green; the release gate's coverage checker named the uncovered
|
|
134
|
+
sites and five cases closed them (each CLI run as a process, the malformed listing for both verbs, the
|
|
135
|
+
non-numeric suffix) — 311 of 311 across the seven files the story touches.
|
|
136
|
+
- **Stated limits.** A present non-regular scope path refuses the restore at the proof step, after the writes; the
|
|
137
|
+
extras of a restore are judged under the ignore rules the tree carries BEFORE step (1), so a restored ignore file
|
|
138
|
+
that RE-COVERS a path (the task had un-ignored a build directory the snapshot never held) deletes that path as an
|
|
139
|
+
extra with a matching proof — a task that edits an ignore file is restored under the rules it left behind; a
|
|
140
|
+
retry of a terminal failure opens on the same base BEFORE Cleanup's prune, because after the prune no sequence
|
|
141
|
+
ref carries the base and the restore the retry needs refuses `target-scope` (the tree object itself is not
|
|
142
|
+
deleted, only unreachable by name); the precondition of a restore is no concurrent in-scope writer — a parallel session's `docs/ai/` write
|
|
143
|
+
during the task is restored away with a matching proof, so holding it is the orchestrator's; the process-group
|
|
144
|
+
stop of a running wrapper stays the bridge's contract, the kit's door is the OPEN-thread refusal; no parallel
|
|
145
|
+
tasks over one plan and no satellite worktrees; the `task` and `epic` rows of the activity table are story S7's.
|
|
146
|
+
|
|
7
147
|
## 13.1.0 — the delegation ledger learns a base: a dispatch records `head` or `checkpoint`, every measurement of a checkpoint thread is base-relative over the snapshot scope, and a checkpoint thread holds the delegate's session (AD-140)
|
|
8
148
|
|
|
9
149
|
**MINOR: one optional key on the dispatch record, one new flag on `open`, one new IO leaf; every record minted
|
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: '13.
|
|
6
|
+
version: '13.3.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": "13.
|
|
3
|
+
"version": "13.3.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",
|
|
@@ -21,7 +21,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
21
21
|
3. **Choose conversational language — answered in the step-2 batch.** Which language should the agent *talk to them* in — questions, explanations, summaries, status updates? Offer the language they're already writing in as the default. Carry the answer into the `{{COMM_LANGUAGE}}` slot of the *Communication language* block when `AGENTS.md` is created (step 5). See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract). This sets the **dialogue** language only — never the files.
|
|
22
22
|
4. **Choose agent attribution — answered in the step-2 batch.** May the agent attribute work to itself / to AI — `Co-Authored-By` trailers, "Generated with …" footers, "AI"/agent/model mentions in code, comments, commit messages, PR titles/bodies, or docs? **Default to `off`** (no agent/AI mention anywhere) unless they opt in — people are routinely surprised to find an AI listed as a repo contributor. Carry the answer into the `{{AGENT_ATTRIBUTION}}` slot of the *Attribution* block when `AGENTS.md` is created (step 5). **If `off` and the project uses Claude Code**, also set `"includeCoAuthoredBy": false` in the project's `.claude/settings.json` (create it if absent) — the trailer is added by the harness, so a doc directive alone won't stop it. See [Attribution contract](${CLAUDE_SKILL_DIR}/references/contracts.md#attribution-contract).
|
|
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
|
-
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
|
|
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 four 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`), **`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), and **`docs/ai/vehicles.json`** (the executor vehicle's model, effort and fallback as a setting the kit's readers resolve into one posture; the placed executor's body follows it from the release that derives it — seeded once, hand-edited, never clobbered). 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
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
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).
|
|
@@ -7,7 +7,7 @@ Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shippe
|
|
|
7
7
|
Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
|
|
8
8
|
|
|
9
9
|
1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, the `held session: <id> — <n> fold(s) rode it` / `none` / substitution line when configured delegated execution arms it, and per-backend receipt state (current / stale / ungrounded / probe / rejected / missing) with verdict + grounding + timestamp.
|
|
10
|
-
2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute = delegated` with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. For a head-base thread, the accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. A checkpoint-based thread's substitution closes only by its own ledger `degrade` (which also withdraws the expectation, so the next fold establishes HELD) — no core-evidence degrade lifts it, and the report's recovery hint says so. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a cwd where git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree location that passes — `error`, `redirected`, `no-work-tree` and `env-only` refuse by their state name once a receipt could be required), or every recipe-named backend SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a not-a-repository cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY review-backend escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (every edit the payload can see moves the fingerprint, and no more), ungrounded, vetoed, or unrecognized. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
|
|
10
|
+
2. **`--check`** → the gate exit code. Before the review-recipe early return, a configured `plan-execution.execute = delegated` with a plan in flight and a dirty, fingerprintable tree audits the delegation ledger and refuses a substituted held session; an absent ledger is inert, while an unreadable, malformed, foreign, audit-refused ledger or failed HEAD read fails closed with its own reason. For a head-base thread, the accepted-replacement escape is a `codex-exec` degrade at the substituted return's tree, standing after every later fold, never the review receipt id `codex`. A checkpoint-based thread's substitution closes only by its own ledger `degrade` (which also withdraws the expectation, so the next fold establishes HELD) — no core-evidence degrade lifts it, and the report's recovery hint says so. The remaining **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a CONFIGURED solo recipe (after the held-session arm above) (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`/`TASK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a cwd where git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree location that passes — `error`, `redirected`, `no-work-tree` and `env-only` refuse by their state name once a receipt could be required), or every recipe-named backend SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a not-a-repository cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY review-backend escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (every edit the payload can see moves the fingerprint, and no more), ungrounded, vetoed, or unrecognized. Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
|
|
11
11
|
3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** The candidate line for your own `docs/ai/gates.json`: `{ "id": "review-state", "title": "Review receipts current for the uncommitted tree", "cmd": "node <path-to-this-skill>/tools/review-state.mjs --check" }` — with the path your project actually reaches the kit by, QUOTED so a path with spaces survives, executable from the project root. The consent-gated gates-init preview — `node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>` (dry-run: it prints the derived entries and writes NOTHING; `--apply --only review-state` writes exactly this one on your explicit yes) — offers exactly this entry, path resolved and quoted, ONLY when your `docs/ai/orchestration.json` declares `reviewed`/`council` on `plan-execution.review` (the slot this checker enforces). The full consent-fill contract is in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate — and `run-gates --final` requires this check among its canonical core gates.
|
|
12
12
|
4. **`--await [--timeout <s>]`** (BUGFREE-3 / AD-049) → BLOCK until every recipe-named backend is SATISFIED for the current tree (i.e. until `--check` would PASS), or the bounded timeout elapses (a loud exit 1; default 900s). Run it after dispatching the review bridges to WAIT for their receipts to land instead of hand-polling a pid: the durable completion signal is the **receipt**, never a process event (a harness "completed" notification fires early; a bridge's output late-flushes). It inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. A held-session refusal and an **AUTHORITATIVE veto terminate the wait loudly BEFORE the deadline**: neither can be moved by waiting; only the held-session retry/degrade lane or a fresh review can move its corresponding refusal. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly. For waiting on **one dispatch's arrival** (never obligation satisfaction) use the receipt-arrival deadline runner, `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md`.
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
31
31
|
|
|
32
32
|
**`footprint` — hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** — the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
|
|
33
33
|
|
|
34
|
-
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all
|
|
34
|
+
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all seven** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · enforcement scripts · spec layer · navigator index — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every SEED is **create-only** (an existing file is preserved byte-for-byte); the three refresh-class ops are named apart — the orchestration onboarding note (refreshed only while it still matches a canonical this kit shipped), the spec-layer reader and checker pairs (below) and the **navigator index**, a GENERATED artifact regenerated whenever it is missing or stale (never authored content, so there is nothing to preserve). One ensure failing **never** skips the others: each reports its own outcome and the run exits non-zero when any of them `failed`. The outcome tokens, by ensure: orchestration → `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved`; gates, autonomy and vehicles → `seeded` / `already-present`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node-evidence`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node-evidence`; index → `regenerated` / `already-current`; and any ensure may report `failed`, whose line OPENS with the cause (relay it with that cause — never soften it into a skip; an op that copies file by file also states when it stopped partway). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `node-evidence-unverifiable`, `wrong-node-kind`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **A non-zero exit STOPs this upgrade** — report the failed line and stop there, before the equal-head exit, the migrations and the re-stamp. Add `--dry-run` to preview without writing a byte. Like the pointer slots + the footprint reconcile, all seven reach an equal-head deployment **without a lineage-head bump or a migration file** (they are `.json` / `scripts/` / a generated artifact / a seeded store root, inherently outside the docs cap-validator).
|
|
35
35
|
|
|
36
36
|
**What the orchestration-config ensure does.** `docs/ai/orchestration.json` must exist **and its onboarding note must be current**: created from the canonical seed if missing; if it already exists, **every activity/slot the user set is preserved** and ONLY the `_README` note is refreshed, and only when the existing one still matches a known prior canonical — the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (it normalizes CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim → `customized-preserved`; a *malformed* existing config is **preserved untouched + LOUD** → `malformed-preserved`, never clobbered and never silently skipped). The current note points at `/agent-workflow-kit set-recipe`. **Kit-owned:** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and this ensure applies the `_README` refresh; in the **fallback** path it does both. (Memory stays standalone.)
|
|
37
37
|
|
|
@@ -39,7 +39,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
39
39
|
|
|
40
40
|
**What the autonomy-declaration ensure does.** `docs/ai/autonomy.json` must exist: created from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing (the kit's own template twin — a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content). 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).
|
|
41
41
|
|
|
42
|
-
**What the enforcement-script ensure does.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. The pairs must exist in the project's `scripts/`: `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs`, copied from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Nothing else is seeded — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECTED FIRST, never auto-migrated (AD-051, Decision 13):** on a `docs/ai/history/decisions-archive*.md` monolith (`old`), or no monolith but a deployed rotator predating the store (`old-unrotated`), the project is on the RETIRED 3-tier cascade, so the ensure writes NOTHING and reports `old-adr-layout-migration-instructed` (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Relay it as the LOUD instruct it is: the fix is the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits), and the seed lands on the next upgrade. A layout the ensure cannot READ is `failed`, not a seed — it never writes on an unverifiable tree. The seed applies ONLY to a clean layout (neither signal). **Whether Node runs here is PROVEN, never proxied:** the ensure seeds on a regular `package.json` at the root OR on any kit-seeded `scripts/*.mjs` already deployed (a bootstrap places them into projects that never carry a `package.json`); only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the
|
|
42
|
+
**What the enforcement-script ensure does.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. The pairs must exist in the project's `scripts/`: `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs`, copied from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Nothing else is seeded — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECTED FIRST, never auto-migrated (AD-051, Decision 13):** on a `docs/ai/history/decisions-archive*.md` monolith (`old`), or no monolith but a deployed rotator predating the store (`old-unrotated`), the project is on the RETIRED 3-tier cascade, so the ensure writes NOTHING and reports `old-adr-layout-migration-instructed` (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Relay it as the LOUD instruct it is: the fix is the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits), and the seed lands on the next upgrade. A layout the ensure cannot READ is `failed`, not a seed — it never writes on an unverifiable tree. The seed applies ONLY to a clean layout (neither signal). **Whether Node runs here is PROVEN, never proxied:** the ensure seeds on a regular `package.json` at the root OR on any kit-seeded `scripts/*.mjs` already deployed (a bootstrap places them into projects that never carry a `package.json`); only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the config ensures still run; a probe that cannot be read is `failed` with the cause `node-evidence-unverifiable`, nothing written. **A `skipped-*` outcome whose stated reason this tool could itself disprove may not exist** — the retired `skipped-no-node` was exactly that, printed beside twenty deployed Node scripts. 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.
|
|
43
43
|
|
|
44
44
|
**What the navigator ensure does.** `docs/ai/index.md` is the always-loaded navigator the entry point declares, and it is GENERATED — no template ships it, so a deployment that never ran the generator boots from a broken entry point (and, on a Node project, carries a pre-commit hook that fails its own index check). The ensure runs the bundled generator's finalizer and reports `regenerated` (it was missing or stale — it was written) or `already-current` (nothing written). It never skips a No-Node project: the generator runs from `${CLAUDE_SKILL_DIR}/references/scripts/` on the agent host, not from the project's `scripts/`. **Its position in the run-list is EARLY and therefore NOT authoritative** — `lens` (and, on the migrated path, steps 6–7) still change `docs/ai` afterwards — so the authoritative run is the LATE `--only index` rung documented at both exits; the early one is idempotent and costs at most an `already-current` line.
|
|
45
45
|
|
|
@@ -90,7 +90,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
90
90
|
**The LATE navigator finalizer — the AUTHORITATIVE run, after the last `docs/ai` mutation.** `lens` above rewrites `docs/ai/agent_rules.md`, so the step-3 index ensure is already behind by the time the run-list ends. Re-run that ONE op here, before the step-4 report: `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`. Relay **this** line in the report (it supersedes the early one; an untouched tree reports `already-current`, a failure STOPs the upgrade like any other ensure). On the migrated path the same rung runs again at the END of step 7 — after the migrations, before the step-8 re-stamp.
|
|
91
91
|
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:
|
|
92
92
|
- **A skip line that contradicts the observed tree is a FINDING, never pasted as neutral.** Every ensure skip names the fact it proved (the probes that answered absent, the layout it read); if the tree you can see disproves that reason — a "no Node" skip beside deployed Node scripts, a "not deployed" skip beside a stamped `docs/ai` — do not relay the line as an outcome: raise it in the report as a defect of the tool, with the contradicting fact named, and stop short of any step that would build on the skip.
|
|
93
|
-
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (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); the **
|
|
93
|
+
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (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); the **seven project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (orchestration config, gate declaration, autonomy declaration, vehicles declaration, enforcement scripts, spec layer, navigator index — their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`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** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`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`).
|
|
94
94
|
- **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*.
|
|
95
95
|
- **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).
|
|
96
96
|
- **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`.
|
|
@@ -99,4 +99,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
99
99
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
100
100
|
6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
|
|
101
101
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.) **Then, before the stamp, re-run the LATE navigator finalizer:** the migrations of steps 6–7 may have added or changed `docs/ai` files, so run the one op again — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>` — and relay THAT line in the step-8 report (idempotent: an untouched tree reports `already-current`; a failure STOPs this upgrade like any other ensure).
|
|
102
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **
|
|
102
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **seven project-configuration ensure** (`configs`) lines (orchestration config, gate declaration, autonomy declaration, vehicles declaration, enforcement scripts, spec layer, navigator index) — the navigator one being the late `--only index` run at the end of step 7, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** outcomes (same outcome sets as step 4), and the **bridge-settings reconcile** (`bridge-settings`) outcome (line(s) pasted verbatim) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: 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, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows only from the release that derives it; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model.",
|
|
3
|
+
"executor": {
|
|
4
|
+
"model": "opus",
|
|
5
|
+
"effort": "high",
|
|
6
|
+
"fallback": "sonnet"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// spec:checkpoint — docs/ai/specs/kit/checkpoint/index.md
|
|
2
|
+
import { lstatSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { snapshotScope } from './dispatch-baseline.mjs';
|
|
7
|
+
import { isNeverCommittableStat } from './core-evidence.mjs';
|
|
8
|
+
import { isThreadTerminalRecord } from './dispatch-record.mjs';
|
|
9
|
+
import { GIT_MAX_BUFFER, resolveGitLocation, withGitPath } from './git-env.mjs';
|
|
10
|
+
|
|
11
|
+
export const CHECKPOINT_REF_PREFIX = 'refs/agent-workflow/checkpoints/';
|
|
12
|
+
const ACCEPT = 0;
|
|
13
|
+
const REFUSE = 1;
|
|
14
|
+
const INCREMENT = 1;
|
|
15
|
+
const EMPTY = '';
|
|
16
|
+
const SPACE = ' ';
|
|
17
|
+
const NEWLINE = '\n';
|
|
18
|
+
const UTF8 = 'utf8';
|
|
19
|
+
const TEMP_PREFIX = 'aw-checkpoint-';
|
|
20
|
+
const INDEX_NAME = 'index';
|
|
21
|
+
const LITERAL_PATHSPECS = '1';
|
|
22
|
+
const WORK_TREE = 'work-tree';
|
|
23
|
+
const MISSING_PATH_CODES = ['ENOENT', 'ENOTDIR'];
|
|
24
|
+
const OID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
25
|
+
const NAMES = { scope: 'scope', sequence: 'sequence' };
|
|
26
|
+
const DISPATCH_KIND = 'dispatch';
|
|
27
|
+
const CHECKPOINT_KIND = 'checkpoint';
|
|
28
|
+
const LOCATION_ERROR = 'checkpoint requires a git work tree';
|
|
29
|
+
const SEQUENCE_ERROR = 'expected contiguous numbered refs on trees';
|
|
30
|
+
const STAT_ERROR = 'cannot classify path';
|
|
31
|
+
const TYPE_ERROR = 'present path is not a regular file, symlink or directory';
|
|
32
|
+
const OID_ERROR = 'git did not return a tree oid';
|
|
33
|
+
const GIT = 'git';
|
|
34
|
+
const GIT_ARGS = {
|
|
35
|
+
refs: ['for-each-ref', '--format=%(refname) %(objectname)'],
|
|
36
|
+
empty: ['read-tree', '--empty'],
|
|
37
|
+
add: ['add', '-f', '--'],
|
|
38
|
+
tree: ['write-tree'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const refuse = (reason, code = REFUSE) => ({ ok: false, code, reason });
|
|
42
|
+
const defaultRunGit = (args, cwd, input, env) => spawnSync(GIT, args, {
|
|
43
|
+
cwd, input, env: withGitPath(env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true,
|
|
44
|
+
});
|
|
45
|
+
export const makeRunner = ({ runGit = defaultRunGit, env = process.env } = {}) => (args, cwd, input, override = env) => {
|
|
46
|
+
try { return runGit(args, cwd, input, override); }
|
|
47
|
+
catch (error) { return { error, status: null, stdout: Buffer.alloc(ACCEPT) }; }
|
|
48
|
+
};
|
|
49
|
+
export const readGitBytes = ({ run, top, env }, args, override = env) => {
|
|
50
|
+
const result = run(args, top, undefined, override);
|
|
51
|
+
if (result == null || result.error || result.signal || result.status !== ACCEPT || result.stdout == null) {
|
|
52
|
+
throw new Error(`${GIT} ${args.join(SPACE)} failed: ${result?.error?.message ?? String(result?.stderr ?? EMPTY).trim()}`);
|
|
53
|
+
}
|
|
54
|
+
return Buffer.from(result.stdout);
|
|
55
|
+
};
|
|
56
|
+
export const readGit = (context, args, override = context.env) => readGitBytes(context, args, override).toString(UTF8).trimEnd();
|
|
57
|
+
export const withRepository = (cwd, io, action) => {
|
|
58
|
+
try {
|
|
59
|
+
const env = io.env ?? process.env;
|
|
60
|
+
const run = makeRunner({ ...io, env });
|
|
61
|
+
const location = resolveGitLocation(cwd, { env,
|
|
62
|
+
spawn: (command, args, options) => run(args, options.cwd, undefined, options.env) });
|
|
63
|
+
if (location.state !== WORK_TREE) return refuse(location.cause ?? LOCATION_ERROR);
|
|
64
|
+
return action({ cwd, top: location.top, env, run, io: { ...io, env, runGit: run } });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return refuse(error.message);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
export const readCheckpointRefs = (context, prefix = CHECKPOINT_REF_PREFIX) => {
|
|
70
|
+
const lines = readGit(context, [...GIT_ARGS.refs, prefix]).split(NEWLINE).filter(Boolean);
|
|
71
|
+
return lines.map((line) => {
|
|
72
|
+
const [ref, oid, extra] = line.split(SPACE);
|
|
73
|
+
if (!ref.startsWith(prefix) || !OID_PATTERN.test(oid) || extra !== undefined) {
|
|
74
|
+
throw new Error(`${NAMES.sequence}: ${SEQUENCE_ERROR}`);
|
|
75
|
+
}
|
|
76
|
+
return { ref, oid };
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
export const withTemporaryIndex = (env, action) => {
|
|
80
|
+
const directory = mkdtempSync(join(tmpdir(), TEMP_PREFIX));
|
|
81
|
+
try {
|
|
82
|
+
return action({ ...env, GIT_INDEX_FILE: join(directory, INDEX_NAME), GIT_LITERAL_PATHSPECS: LITERAL_PATHSPECS });
|
|
83
|
+
} finally {
|
|
84
|
+
rmSync(directory, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export const readScopeStat = (context, path) => {
|
|
88
|
+
try {
|
|
89
|
+
const stat = (context.io.lstat ?? lstatSync)(join(context.top, path));
|
|
90
|
+
if (stat == null) throw new Error(STAT_ERROR);
|
|
91
|
+
return stat;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (MISSING_PATH_CODES.includes(error.code)) return null;
|
|
94
|
+
throw new Error(`${NAMES.scope}: ${path}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
export const writeSnapshot = (context, baseTreeOid) => {
|
|
98
|
+
const scope = snapshotScope(context.top, { baseTreeOid }, context.io);
|
|
99
|
+
if (!scope.ok) return refuse(scope.reason);
|
|
100
|
+
const paths = scope.paths.filter((path) => {
|
|
101
|
+
const stat = readScopeStat(context, path);
|
|
102
|
+
if (stat === null) return false;
|
|
103
|
+
if (isNeverCommittableStat(stat) || !(stat.isFile() || stat.isSymbolicLink() || stat.isDirectory())) {
|
|
104
|
+
throw new Error(`${NAMES.scope}: ${path}: ${TYPE_ERROR}`);
|
|
105
|
+
}
|
|
106
|
+
return stat.isFile() || stat.isSymbolicLink();
|
|
107
|
+
});
|
|
108
|
+
return withTemporaryIndex(context.env, (env) => {
|
|
109
|
+
readGit(context, GIT_ARGS.empty, env);
|
|
110
|
+
for (const path of paths) readGit(context, [...GIT_ARGS.add, path], env);
|
|
111
|
+
const oid = readGit(context, GIT_ARGS.tree, env);
|
|
112
|
+
if (!OID_PATTERN.test(oid)) return refuse(`${NAMES.scope}: ${OID_ERROR}`);
|
|
113
|
+
return { ok: true, oid };
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
export const findOpenCheckpointThread = (records, held) => records.find((record, index) =>
|
|
117
|
+
record.kind === DISPATCH_KIND && record.baseline?.kind === CHECKPOINT_KIND && held.has(record.baseline.treeOid)
|
|
118
|
+
&& !records.slice(index + INCREMENT).some((successor) => successor.nonce === record.nonce && isThreadTerminalRecord(successor)));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// spec:checkpoint — docs/ai/specs/kit/checkpoint/restore.md
|
|
2
|
+
import { unlinkSync, rmdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { resolveTreeObject, snapshotScope } from './dispatch-baseline.mjs';
|
|
5
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
6
|
+
import { CHECKPOINT_REF_PREFIX, withRepository, readGit, readGitBytes, readCheckpointRefs,
|
|
7
|
+
withTemporaryIndex, readScopeStat, writeSnapshot, findOpenCheckpointThread } from './checkpoint-core.mjs';
|
|
8
|
+
|
|
9
|
+
const REFUSE = 1;
|
|
10
|
+
const START = 0;
|
|
11
|
+
const NEXT = 1;
|
|
12
|
+
const LAST = -1;
|
|
13
|
+
const SLASH = '/';
|
|
14
|
+
const NUL = '\0';
|
|
15
|
+
const UTF8 = 'utf8';
|
|
16
|
+
const ROOT = '.';
|
|
17
|
+
const FORBIDDEN_SEGMENTS = ['', '.', '..', '.git'];
|
|
18
|
+
const ENTRY_PATTERN = /^([0-9]{6}) [^ ]+ [0-9a-f]+\t([\s\S]+)$/;
|
|
19
|
+
const SEQUENCE_REF_PATTERN = /^[^/]+\/(?:0|[1-9][0-9]*)$/;
|
|
20
|
+
const ALLOWED_MODES = ['100644', '100755', '120000'];
|
|
21
|
+
const LEDGER_ABSENT = 'absent';
|
|
22
|
+
const LEDGER_OK = 'ok';
|
|
23
|
+
const NONEMPTY_CODES = ['ENOTEMPTY', 'EEXIST'];
|
|
24
|
+
const NAMES = { scope: 'target-scope', ledger: 'ledger', open: 'open-thread', conflict: 'type-conflict' };
|
|
25
|
+
const ENCODING_ERROR = 'target tree paths are not lossless UTF-8';
|
|
26
|
+
const ENTRY_ERROR = 'unreadable target tree entry';
|
|
27
|
+
const UNREFERENCED_ERROR = 'no checkpoint sequence carries target';
|
|
28
|
+
const MISMATCH = 'checkpoint restore mismatch';
|
|
29
|
+
const GIT_ARGS = {
|
|
30
|
+
resolve: ['rev-parse', '--verify'],
|
|
31
|
+
entries: ['ls-tree', '-r', '-z'],
|
|
32
|
+
read: ['read-tree'],
|
|
33
|
+
checkout: ['checkout-index', '-a', '-f'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const refuse = (reason) => ({ ok: false, code: REFUSE, reason });
|
|
37
|
+
const readTargetEntries = (context, target) => {
|
|
38
|
+
const bytes = readGitBytes(context, [...GIT_ARGS.entries, target]);
|
|
39
|
+
const text = bytes.toString(UTF8);
|
|
40
|
+
if (!Buffer.from(text, UTF8).equals(bytes)) throw new Error(`${NAMES.scope}: ${ENCODING_ERROR}`);
|
|
41
|
+
return text.split(NUL).filter(Boolean).map((entry) => {
|
|
42
|
+
const parsed = ENTRY_PATTERN.exec(entry);
|
|
43
|
+
if (parsed === null) throw new Error(`${NAMES.scope}: ${ENTRY_ERROR}`);
|
|
44
|
+
const [, mode, path] = parsed;
|
|
45
|
+
if (!ALLOWED_MODES.includes(mode) || path.split(SLASH).some((segment) => FORBIDDEN_SEGMENTS.includes(segment))) {
|
|
46
|
+
throw new Error(`${NAMES.scope}: ${path}`);
|
|
47
|
+
}
|
|
48
|
+
return path;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const readHeldSet = (context, target) => {
|
|
52
|
+
const refs = readCheckpointRefs(context).filter(({ ref }) => SEQUENCE_REF_PATTERN.test(ref.slice(CHECKPOINT_REF_PREFIX.length)));
|
|
53
|
+
const sequences = new Set(refs.filter(({ oid }) => oid === target).map(({ ref }) => dirname(ref)));
|
|
54
|
+
if (sequences.size === START) throw new Error(`${NAMES.scope}: ${UNREFERENCED_ERROR}: ${target}`);
|
|
55
|
+
return new Set([target, ...refs.filter(({ ref }) => sequences.has(dirname(ref))).map(({ oid }) => oid)]);
|
|
56
|
+
};
|
|
57
|
+
const checkLedger = (context, held) => {
|
|
58
|
+
const ledger = readDelegationLedger(context.cwd, context.env);
|
|
59
|
+
if (ledger.state !== LEDGER_ABSENT && ledger.state !== LEDGER_OK) return refuse(`${NAMES.ledger}: ${ledger.reason}`);
|
|
60
|
+
const open = findOpenCheckpointThread(ledger.records ?? [], held);
|
|
61
|
+
return open === undefined ? { ok: true } : refuse(`${NAMES.open}: ${open.nonce}`);
|
|
62
|
+
};
|
|
63
|
+
const listParents = (path) => {
|
|
64
|
+
const parts = path.split(SLASH).slice(START, LAST);
|
|
65
|
+
return parts.map((part, index) => parts.slice(START, index + NEXT).join(SLASH));
|
|
66
|
+
};
|
|
67
|
+
const checkTypes = (context, paths) => {
|
|
68
|
+
for (const path of paths) {
|
|
69
|
+
for (const parent of listParents(path)) {
|
|
70
|
+
const stat = readScopeStat(context, parent);
|
|
71
|
+
if (stat === null) break;
|
|
72
|
+
if (!stat.isDirectory()) return refuse(`${NAMES.conflict}: ${parent}: ${path}`);
|
|
73
|
+
}
|
|
74
|
+
if (readScopeStat(context, path)?.isDirectory()) return refuse(`${NAMES.conflict}: ${path}`);
|
|
75
|
+
}
|
|
76
|
+
return { ok: true };
|
|
77
|
+
};
|
|
78
|
+
const removeEmptyParents = (context, path) => {
|
|
79
|
+
const parent = dirname(path);
|
|
80
|
+
if (parent === ROOT) return;
|
|
81
|
+
const stat = readScopeStat(context, parent);
|
|
82
|
+
if (stat === null || !stat.isDirectory()) return;
|
|
83
|
+
try {
|
|
84
|
+
rmdirSync(join(context.top, parent));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!NONEMPTY_CODES.includes(error.code)) throw error;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
removeEmptyParents(context, parent);
|
|
90
|
+
};
|
|
91
|
+
const removeExtras = (context, domain, targets) => {
|
|
92
|
+
for (const path of domain) {
|
|
93
|
+
if (targets.has(path) || path.split(SLASH).some((segment) => FORBIDDEN_SEGMENTS.includes(segment))) continue;
|
|
94
|
+
if (!listParents(path).every((parent) => readScopeStat(context, parent)?.isDirectory())) continue;
|
|
95
|
+
const stat = readScopeStat(context, path);
|
|
96
|
+
if (stat === null || !(stat.isFile() || stat.isSymbolicLink())) continue;
|
|
97
|
+
unlinkSync(join(context.top, path));
|
|
98
|
+
removeEmptyParents(context, path);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const restoreCheckpoint = (cwd, oid, io = {}) => withRepository(cwd, io, (context) => {
|
|
103
|
+
const tree = resolveTreeObject(context.top, oid, context.io);
|
|
104
|
+
if (!tree.ok) return refuse(tree.reason);
|
|
105
|
+
const target = readGit(context, [...GIT_ARGS.resolve, oid]);
|
|
106
|
+
const entries = readTargetEntries(context, target);
|
|
107
|
+
const scope = snapshotScope(context.top, { baseTreeOid: target }, context.io);
|
|
108
|
+
if (!scope.ok) return refuse(scope.reason);
|
|
109
|
+
const domain = new Set(scope.paths);
|
|
110
|
+
const outside = entries.find((path) => !domain.has(path));
|
|
111
|
+
if (outside !== undefined) return refuse(`${NAMES.scope}: ${outside}`);
|
|
112
|
+
const held = readHeldSet(context, target);
|
|
113
|
+
const ledger = checkLedger(context, held);
|
|
114
|
+
if (!ledger.ok) return ledger;
|
|
115
|
+
const types = checkTypes(context, entries);
|
|
116
|
+
if (!types.ok) return types;
|
|
117
|
+
withTemporaryIndex(context.env, (env) => {
|
|
118
|
+
readGit(context, [...GIT_ARGS.read, target], env);
|
|
119
|
+
readGit(context, GIT_ARGS.checkout, env);
|
|
120
|
+
});
|
|
121
|
+
removeExtras(context, scope.paths, new Set(entries));
|
|
122
|
+
const snapshot = writeSnapshot(context, target);
|
|
123
|
+
if (!snapshot.ok) return snapshot;
|
|
124
|
+
return snapshot.oid === target ? { ok: true, target, proof: snapshot.oid }
|
|
125
|
+
: refuse(`${MISMATCH}: ${target} ${snapshot.oid}`);
|
|
126
|
+
});
|