@sabaiway/agent-workflow-kit 10.1.0 → 10.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 +58 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +4 -2
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +288 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-verdict.test.mjs +109 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +19 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +5 -336
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/status.md +1 -1
- package/references/modes/upgrade.md +6 -4
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/agent_rules.md +3 -2
- package/tools/ack-store.mjs +57 -0
- package/tools/ack-write.mjs +1 -1
- package/tools/doc-parity.mjs +8 -0
- package/tools/ensure-ops.mjs +18 -9
- package/tools/ensure-specs.mjs +3 -4
- package/tools/ensure-vocabulary.mjs +5 -2
- package/tools/family-registry.mjs +32 -3
- package/tools/lens-region.mjs +4 -1
- package/tools/node-evidence.mjs +77 -0
- package/tools/recommendations.mjs +68 -67
- package/tools/renderers.mjs +9 -0
- package/tools/spec-adoption.mjs +71 -0
- package/tools/spec-check.mjs +2 -2
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/view-model.mjs +2 -0
|
@@ -10,283 +10,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// --add-dir escape — the staging dir's perms + the offloaded artifact's perms/contents WHILE they
|
|
19
|
-
// still exist (agy-review's trap removes the staging dir on exit). Kept inline so the file is
|
|
20
|
-
// standalone (the kit mirror is byte-equality; no shared helper grows that set).
|
|
21
|
-
// The envelope `agy --output-format json` returns: ONE object whose `response` carries the model's
|
|
22
|
-
// text VERBATIM. Encoded in node so an arbitrary body (quotes, backslashes, multibyte) round-trips
|
|
23
|
-
// exactly — hand-rolled JSON quoting in bash is the defect farm this fake exists to avoid.
|
|
24
|
-
const FAKE_ENVELOPE_ENCODER = [
|
|
25
|
-
'const text = require("node:fs").readFileSync(0, "utf8");',
|
|
26
|
-
'const [cid, status, shape] = process.argv.slice(1);',
|
|
27
|
-
'const envelope = { conversation_id: cid, status, response: text, duration_seconds: 1.5, num_turns: 1,',
|
|
28
|
-
' usage: { input_tokens: 10, output_tokens: 5, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 15 } };',
|
|
29
|
-
// The fed lane ROUTES later turns at this field, so the two ways it can be present-but-unusable —
|
|
30
|
-
// absent, and present with the wrong TYPE — are knobs, not just a bad-grammar string.
|
|
31
|
-
'if (shape === "missing") delete envelope.conversation_id;',
|
|
32
|
-
'if (shape === "number") envelope.conversation_id = 42;',
|
|
33
|
-
'process.stdout.write(`${JSON.stringify(envelope)}\\n`);',
|
|
34
|
-
].join('\n');
|
|
35
|
-
|
|
36
|
-
const FAKE_AGY = [
|
|
37
|
-
'#!/usr/bin/env bash',
|
|
38
|
-
'set -u',
|
|
39
|
-
// A capability probe is NOT a paid dispatch: --help / --version answer BEFORE any capture file is
|
|
40
|
-
// touched, so the wrapper's pre-spend door can never read as a spent run (the sentinel) nor shift
|
|
41
|
-
// the fed lane's per-turn counter. Keyed on the FIRST argument like the real CLI, so a prompt that
|
|
42
|
-
// merely CONTAINS "--help" is never intercepted.
|
|
43
|
-
'case "${1:-}" in',
|
|
44
|
-
' --help|-h)',
|
|
45
|
-
' if [[ -n "${AGY_FAKE_HELP_EXIT:-}" ]]; then echo "fake agy: help unavailable" >&2; exit "$AGY_FAKE_HELP_EXIT"; fi',
|
|
46
|
-
' for f in --output-format --json-schema --disable-slash-commands --effort --mode; do',
|
|
47
|
-
' if [[ "$f" == "${AGY_FAKE_HELP_OMIT:-}" ]]; then continue; fi',
|
|
48
|
-
' printf " %s fake capability line\\n" "$f"',
|
|
49
|
-
' done',
|
|
50
|
-
' if [[ -n "${AGY_FAKE_HELP_EXTRA:-}" ]]; then printf "%s\\n" "$AGY_FAKE_HELP_EXTRA"; fi',
|
|
51
|
-
' exit 0 ;;',
|
|
52
|
-
' --version) printf "%s\\n" "${AGY_FAKE_VERSION:-1.1.13}"; exit 0 ;;',
|
|
53
|
-
'esac',
|
|
54
|
-
': "${AGY_FAKE_ARGV:=/dev/null}"',
|
|
55
|
-
': "${AGY_FAKE_ENV:=/dev/null}"',
|
|
56
|
-
': "${AGY_FAKE_PROMPT:=/dev/null}"',
|
|
57
|
-
': "${AGY_FAKE_SENTINEL:=/dev/null}"',
|
|
58
|
-
'printf invoked > "$AGY_FAKE_SENTINEL"',
|
|
59
|
-
// The fed lane dispatches from the private staging dir (deliberately — see the wrapper), so the
|
|
60
|
-
// fake's own cwd is the only handle a test has on a directory the exit trap then removes.
|
|
61
|
-
'printf "%s" "$PWD" > "${AGY_FAKE_CWD:-/dev/null}"',
|
|
62
|
-
'{ for a in "$@"; do printf "%s\\n" "$a"; done; } > "$AGY_FAKE_ARGV"',
|
|
63
|
-
'{ echo "FOO_API_KEY=${FOO_API_KEY:-<unset>}"; echo "ANTIGRAVITY_API_KEY=${ANTIGRAVITY_API_KEY:-<unset>}"; } > "$AGY_FAKE_ENV"',
|
|
64
|
-
'prompt=""',
|
|
65
|
-
'prev=""; for a in "$@"; do if [[ "$prev" == "-p" ]]; then prompt="$a"; printf "%s" "$a" > "$AGY_FAKE_PROMPT"; fi; prev="$a"; done',
|
|
66
|
-
// Every answer leaves through ONE emitter, so the transport is a property of the INVOCATION
|
|
67
|
-
// (`--output-format json` ⇒ an envelope, otherwise text) and every existing output knob keeps
|
|
68
|
-
// meaning exactly what it meant. AGY_FAKE_RAW_STDOUT bypasses the encoder entirely — the
|
|
69
|
-
// unparseable-payload arm; AGY_FAKE_STDERR is the CLI's own diagnostic.
|
|
70
|
-
'aw_fmt=""',
|
|
71
|
-
'prev=""; for a in "$@"; do if [[ "$prev" == "--output-format" ]]; then aw_fmt="$a"; fi; prev="$a"; done',
|
|
72
|
-
// AGY_FAKE_BAD_TURN scopes the two transport-breaking knobs to ONE turn: a fed run whose turn 1
|
|
73
|
-
// answers normally and whose turn 2 does not is the only way to reach the "no LATER turn is spent"
|
|
74
|
-
// arms — an unscoped knob breaks turn 1 and the run never gets there.
|
|
75
|
-
'aw_fake_emit() {',
|
|
76
|
-
' local body="$1" aw_status="${AGY_FAKE_STATUS:-SUCCESS}" aw_raw=""',
|
|
77
|
-
' if [[ -n "${AGY_FAKE_RAW_STDOUT+x}" ]]; then aw_raw=1; fi',
|
|
78
|
-
' if [[ -n "${AGY_FAKE_BAD_TURN:-}" && "$turn" != "${AGY_FAKE_BAD_TURN}" ]]; then aw_status="SUCCESS"; aw_raw=""; fi',
|
|
79
|
-
' if [[ -n "${AGY_FAKE_STDERR:-}" ]]; then printf "%s\\n" "$AGY_FAKE_STDERR" >&2; fi',
|
|
80
|
-
' if [[ -n "$aw_raw" ]]; then printf "%s" "$AGY_FAKE_RAW_STDOUT"; return 0; fi',
|
|
81
|
-
' if [[ "$aw_fmt" != "json" ]]; then printf "%s\\n" "$body"; return 0; fi',
|
|
82
|
-
` printf "%s\\n" "$body" | node -e '${FAKE_ENVELOPE_ENCODER}' \\`,
|
|
83
|
-
' "${AGY_FAKE_CONV_ID:-11111111-2222-3333-4444-555555555555}" "$aw_status" "${AGY_FAKE_CONV_SHAPE:-ok}"',
|
|
84
|
-
'}',
|
|
85
|
-
'prev=""; for a in "$@"; do',
|
|
86
|
-
' if [[ "$prev" == "--add-dir" ]]; then',
|
|
87
|
-
' printf "%s" "$a" > "${AGY_FAKE_ADDDIR:-/dev/null}"',
|
|
88
|
-
' stat -c "%a" "$a" > "${AGY_FAKE_ADDDIR_MODE:-/dev/null}" 2>/dev/null || true',
|
|
89
|
-
' art="$a/precomputed-change-set"',
|
|
90
|
-
' if [[ -f "$art" ]]; then stat -c "%a" "$art" > "${AGY_FAKE_ARTIFACT_MODE:-/dev/null}" 2>/dev/null || true; cp "$art" "${AGY_FAKE_ARTIFACT_COPY:-/dev/null}" 2>/dev/null || true; fi',
|
|
91
|
-
' fi; prev="$a"',
|
|
92
|
-
'done',
|
|
93
|
-
'if [[ -n "${AGY_FAKE_SLEEP:-}" ]]; then sleep "$AGY_FAKE_SLEEP"; fi',
|
|
94
|
-
// ── multi-turn support (the fed lane) ──────────────────────────────────────────────────────────
|
|
95
|
-
// The single-file captures above record the LAST invocation; a chunked feed needs a PER-TURN
|
|
96
|
-
// record, so each invocation also writes prompt/argv to "<file>.<turn>" and bumps a counter file.
|
|
97
|
-
'turn=1',
|
|
98
|
-
'if [[ -n "${AGY_FAKE_TURNS:-}" ]]; then',
|
|
99
|
-
' if [[ -s "$AGY_FAKE_TURNS" ]]; then turn=$(( $(cat "$AGY_FAKE_TURNS") + 1 )); fi',
|
|
100
|
-
' printf "%s" "$turn" > "$AGY_FAKE_TURNS"',
|
|
101
|
-
' printf "%s" "$prompt" > "${AGY_FAKE_PROMPT}.$turn"',
|
|
102
|
-
' { for a in "$@"; do printf "%s\\n" "$a"; done; } > "${AGY_FAKE_ARGV}.$turn"',
|
|
103
|
-
'fi',
|
|
104
|
-
'if [[ -n "${AGY_FAKE_FAIL_TURN:-}" && "$turn" == "${AGY_FAKE_FAIL_TURN}" ]]; then',
|
|
105
|
-
' printf "FAKE_TURN_FAILURE\\n" >&2; exit 3',
|
|
106
|
-
'fi',
|
|
107
|
-
// A FEED turn: the model was told to reply OK only. This fake deliberately misbehaves — it emits a
|
|
108
|
-
// PREMATURE verdict — so the isolation invariant (feed output never reaches stdout or the parsed
|
|
109
|
-
// capture) is proven against the worst case, not the polite one.
|
|
110
|
-
'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"--- BEGIN CHANGE-SET PART "* && "$prompt" != *"Requested addresses"* ]]; then',
|
|
111
|
-
' aw_fake_emit "$(printf "PREMATURE_FEED_CHATTER\\n### Verdict\\nREWORK")"; exit 0',
|
|
112
|
-
'fi',
|
|
113
|
-
// The FINAL turn carries the delivery-proof request. The fake answers it the only honest way:
|
|
114
|
-
// by reading the bodies it was actually fed, turn by turn — so a wrapper that never delivered a
|
|
115
|
-
// part cannot be satisfied by this stub either.
|
|
116
|
-
'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"Requested addresses"* ]]; then',
|
|
117
|
-
' req="$(printf "%s" "$prompt" | awk "/^Requested addresses/{f=1; next} f && /^###/{exit} f{print}")"',
|
|
118
|
-
' entries=()',
|
|
119
|
-
' mapfile -t _items <<< "$req"',
|
|
120
|
-
' for _it in "${_items[@]}"; do',
|
|
121
|
-
' [[ -n "$_it" ]] || continue',
|
|
122
|
-
' k="$(printf "%s" "$_it" | awk "{print \\$2}")"; l="$(printf "%s" "$_it" | awk "{print \\$4}")"',
|
|
123
|
-
' src="$k"',
|
|
124
|
-
' if [[ "${AGY_FAKE_PROOF_DUP:-}" == "1" ]]; then src=1; fi',
|
|
125
|
-
' if [[ "${AGY_FAKE_PROOF_OMIT:-}" == "$k" ]]; then continue; fi',
|
|
126
|
-
' body="$(awk -v want="$l" "f && /^--- END CHANGE-SET PART /{exit} f{c++; if (c==want) {print; exit}} /^--- BEGIN CHANGE-SET PART /{f=1}" "${AGY_FAKE_PROMPT}.$src")"',
|
|
127
|
-
' if [[ "${AGY_FAKE_PROOF_CORRUPT:-}" == "$k" ]]; then body="${body}X"; fi',
|
|
128
|
-
' entry="$(printf "part %s line %s: %s" "$k" "$l" "$body")"',
|
|
129
|
-
// Shape knobs the grammar must survive (a bullet) or reject (everything else).
|
|
130
|
-
' if [[ "${AGY_FAKE_PROOF_BULLET:-}" == "1" ]]; then entry="- $entry"; fi',
|
|
131
|
-
' if [[ "${AGY_FAKE_PROOF_NESTED:-}" == "$k" ]]; then entry="note: I believe $entry"; fi',
|
|
132
|
-
' if [[ "${AGY_FAKE_PROOF_PAD:-}" == "1" ]]; then entry="$(printf "part %02d line %04d: %s" "$k" "$l" "$body")"; fi',
|
|
133
|
-
' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then entry="$(printf "Part %s Line %s: %s" "$k" "$l" "$body")"; fi',
|
|
134
|
-
' if [[ "${AGY_FAKE_PROOF_HUGE:-}" == "$k" ]]; then entry="$(printf "part %s line 99999999999999999999: %s" "$k" "$body")"; fi',
|
|
135
|
-
' entries+=("$entry")',
|
|
136
|
-
' if [[ "${AGY_FAKE_PROOF_TWICE:-}" == "1" ]]; then entries+=("$entry"); fi',
|
|
137
|
-
' done',
|
|
138
|
-
' if [[ "${AGY_FAKE_PROOF_EXTRA:-}" == "1" ]]; then entries+=("part 99 line 1: an address nobody asked for"); fi',
|
|
139
|
-
' if [[ "${AGY_FAKE_PROOF_HUGE_EXTRA:-}" == "1" ]]; then entries+=("part 99999999999999999999 line 1: an invented giant address"); fi',
|
|
140
|
-
' out="$( {',
|
|
141
|
-
' if [[ "${AGY_FAKE_PROOF_LATE:-}" == "1" ]]; then printf "### Verdict\\nSHIP\\n"; fi',
|
|
142
|
-
' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then printf "### Delivery Proof\\n"; else printf "### Delivery proof\\n"; fi',
|
|
143
|
-
' if [[ "${AGY_FAKE_PROOF_OUTSIDE:-}" == "1" ]]; then',
|
|
144
|
-
' printf "(nothing here)\\n### Verdict\\nSHIP\\n"',
|
|
145
|
-
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
146
|
-
' else',
|
|
147
|
-
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
148
|
-
' printf "### Verdict\\nSHIP\\n"',
|
|
149
|
-
' fi',
|
|
150
|
-
' } )"',
|
|
151
|
-
' aw_fake_emit "$out"',
|
|
152
|
-
' exit 0',
|
|
153
|
-
'fi',
|
|
154
|
-
// Unset AGY_FAKE_OUTPUT → a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
|
|
155
|
-
// the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
|
|
156
|
-
'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then aw_fake_emit "$(printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP")"; else aw_fake_emit "$AGY_FAKE_OUTPUT"; fi',
|
|
157
|
-
'exit "${AGY_FAKE_EXIT:-0}"',
|
|
158
|
-
'',
|
|
159
|
-
].join('\n');
|
|
160
|
-
|
|
161
|
-
// A PATH whose entries are symlinks to the real PATH binaries EXCEPT the excluded names. Excluding
|
|
162
|
-
// `agy-run` forces agy-review onto its `$HERE/agy.sh` fallback (the repo's CURRENT agy.sh, not a
|
|
163
|
-
// possibly-stale installed one), keeping the test hermetic; excluding `agy` ensures the only agy is
|
|
164
|
-
// our fake (prepended via $HOME/.local/bin). Ported from codex-review.test.mjs.
|
|
165
|
-
const makePathWithout = (root, exclude = []) => {
|
|
166
|
-
const skip = new Set(exclude);
|
|
167
|
-
const dir = mkdtempSync(join(root, 'nobin-'));
|
|
168
|
-
for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
|
|
169
|
-
let names;
|
|
170
|
-
try { names = readdirSync(d); } catch { continue; }
|
|
171
|
-
for (const name of names) {
|
|
172
|
-
if (skip.has(name)) continue;
|
|
173
|
-
const link = join(dir, name);
|
|
174
|
-
if (existsSync(link)) continue;
|
|
175
|
-
try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return dir;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
|
|
182
|
-
// shared: a per-run farm rebuild (thousands of symlinks) plus a per-test `git init`+commit were
|
|
183
|
-
// the suite's dominant wall cost, not the wrapper under test.
|
|
184
|
-
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'agy-review-shared-'));
|
|
185
|
-
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
186
|
-
const farms = new Map();
|
|
187
|
-
const farmFor = (exclude) => {
|
|
188
|
-
const key = exclude.join('|');
|
|
189
|
-
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
190
|
-
return farms.get(key);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const TEMPLATE_HOME = (() => {
|
|
194
|
-
const home = join(SHARED_ROOT, 'template-home');
|
|
195
|
-
const bin = join(home, '.local', 'bin');
|
|
196
|
-
mkdirSync(bin, { recursive: true });
|
|
197
|
-
writeFileSync(join(bin, 'agy'), FAKE_AGY, { mode: 0o755 });
|
|
198
|
-
const repo = join(home, 'repo');
|
|
199
|
-
mkdirSync(repo);
|
|
200
|
-
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
201
|
-
g('init', '-q');
|
|
202
|
-
g('config', 'user.email', 'probe@example.com');
|
|
203
|
-
g('config', 'user.name', 'probe');
|
|
204
|
-
writeFileSync(join(repo, 'base.txt'), 'committed base\n');
|
|
205
|
-
g('add', '-A');
|
|
206
|
-
g('commit', '-qm', 'base');
|
|
207
|
-
return home;
|
|
208
|
-
})();
|
|
209
|
-
|
|
210
|
-
// `clean: true` leaves a pristine committed tree (for the no-diff preflight); the default leaves one
|
|
211
|
-
// untracked file so `code` mode has a diff to review.
|
|
212
|
-
const makeSandbox = ({ clean = false } = {}) => {
|
|
213
|
-
const home = mkdtempSync(join(tmpdir(), 'agy-review-test-'));
|
|
214
|
-
cpSync(TEMPLATE_HOME, home, { recursive: true });
|
|
215
|
-
const bin = join(home, '.local', 'bin');
|
|
216
|
-
chmodSync(join(bin, 'agy'), 0o755);
|
|
217
|
-
const repo = join(home, 'repo');
|
|
218
|
-
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
219
|
-
if (!clean) writeFileSync(join(repo, 'pending.txt'), 'PENDING_UNTRACKED_BODY\n');
|
|
220
|
-
return { home, bin, repo, g };
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// Capture files are per-INVOCATION: a second run() on the same sandbox must not inherit the first
|
|
224
|
-
// run's turn counter or per-turn prompt files (the fed lane reads them back by turn index).
|
|
225
|
-
let runSeq = 0;
|
|
226
|
-
// ASYNCHRONOUS on purpose. A blocking spawnSync here holds the event loop for the whole dispatch,
|
|
227
|
-
// which pinned this file to ONE core: 208 points in a serial chain, 91.4s solo at 103% CPU while
|
|
228
|
-
// the other seven cores idled. Awaiting the child instead lets a `{ concurrency }` describe
|
|
229
|
-
// overlap its tests. The per-test environment still rides the CHILD's options — `process.env` is
|
|
230
|
-
// never mutated, which is what keeps overlapping tests from reading each other's PATH.
|
|
231
|
-
const run = (sb, { args, env = {}, cwd, wrapper } = {}) => new Promise((settle) => {
|
|
232
|
-
const { home, bin, repo } = sb;
|
|
233
|
-
const farm = farmFor(['agy', 'agy-run']);
|
|
234
|
-
const tag = `cap-${++runSeq}`;
|
|
235
|
-
const cap = {
|
|
236
|
-
argv: join(home, `${tag}-argv`), env: join(home, `${tag}-env`), prompt: join(home, `${tag}-prompt`),
|
|
237
|
-
sentinel: join(home, `${tag}-sentinel`), adddir: join(home, `${tag}-adddir`),
|
|
238
|
-
adddirMode: join(home, `${tag}-adddir-mode`), artifactMode: join(home, `${tag}-artifact-mode`),
|
|
239
|
-
artifactCopy: join(home, `${tag}-artifact-copy`), turns: join(home, `${tag}-turns`),
|
|
240
|
-
dispatchCwd: join(home, `${tag}-dispatch-cwd`),
|
|
241
|
-
};
|
|
242
|
-
const child = execFile('bash', [wrapper || WRAPPER, ...args], {
|
|
243
|
-
cwd: cwd || repo,
|
|
244
|
-
encoding: 'utf8',
|
|
245
|
-
timeout: 30000,
|
|
246
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
247
|
-
env: {
|
|
248
|
-
HOME: home,
|
|
249
|
-
PATH: `${bin}:${farm}`,
|
|
250
|
-
// Keep the wrapper's mktemp working when the suite runs inside an OS sandbox whose /tmp is
|
|
251
|
-
// read-only (only $TMPDIR is writable there).
|
|
252
|
-
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
253
|
-
AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
|
|
254
|
-
AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
|
|
255
|
-
AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
|
|
256
|
-
AGY_FAKE_TURNS: cap.turns, AGY_FAKE_CWD: cap.dispatchCwd,
|
|
257
|
-
...env,
|
|
258
|
-
},
|
|
259
|
-
}, (error, stdout, stderr) => {
|
|
260
|
-
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
261
|
-
// Per-turn captures are read EAGERLY: callers rmSync the sandbox before asserting.
|
|
262
|
-
const turns = existsSync(cap.turns) ? Number(readFileSync(cap.turns, 'utf8')) : 0;
|
|
263
|
-
const prompts = [];
|
|
264
|
-
const argvs = [];
|
|
265
|
-
for (let i = 1; i <= turns; i += 1) {
|
|
266
|
-
prompts.push(readIf(`${cap.prompt}.${i}`));
|
|
267
|
-
argvs.push(readIf(`${cap.argv}.${i}`));
|
|
268
|
-
}
|
|
269
|
-
settle({
|
|
270
|
-
status: error ? (error.code ?? 1) : 0, signal: error?.signal ?? null, stdout, stderr,
|
|
271
|
-
invoked: existsSync(cap.sentinel),
|
|
272
|
-
argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
|
|
273
|
-
adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
|
|
274
|
-
artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
|
|
275
|
-
dispatchCwd: readIf(cap.dispatchCwd).trim(), turns, prompts, argvs,
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
// The wrapper refuses many inputs BEFORE it reads stdin, so the pipe can already be closed here.
|
|
279
|
-
// The blocking spawn swallowed that; an async one throws EPIPE at the test. A closed pipe is the
|
|
280
|
-
// refusal working — but ONLY EPIPE is: any other write failure is a real fault and must reach
|
|
281
|
-
// the test instead of passing as a green.
|
|
282
|
-
child.stdin.on('error', (err) => { if (err.code !== 'EPIPE') throw err; });
|
|
283
|
-
child.stdin.end();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// runAsync was the async twin kept for the two sleep-bound timeout tests, back when run() blocked.
|
|
287
|
-
// run() IS that twin now, with the fuller capture surface, so the twin is one name pointing at it
|
|
288
|
-
// — two spawn paths could only drift.
|
|
289
|
-
const runAsync = run;
|
|
13
|
+
import {
|
|
14
|
+
WRAPPER, farmFor, makeSandbox, run, runAsync, readReceipts, RECEIPTS_REL,
|
|
15
|
+
ARTIFACT_HEADER, SHAPE_HEADER, FED_CAP, FED_WIDE_CAP, MAX_COUNTABLE_PROOF_ADDRESS,
|
|
16
|
+
isAssemblerBanner, seedFedChangeSet, inlineArtifactOf, bodyOf, requestedBlockOf, requestedOf, fedRun,
|
|
17
|
+
} from './agy-review-harness.test.mjs';
|
|
290
18
|
|
|
291
19
|
describe('agy-review.sh — model policy advisory (1)', { concurrency: 2 }, () => {
|
|
292
20
|
it('warns for a non-frontier model but still runs', async () => {
|
|
@@ -935,59 +663,6 @@ describe('agy-review.sh — repo file map budget (Phase 2)', { concurrency: 2 },
|
|
|
935
663
|
});
|
|
936
664
|
});
|
|
937
665
|
|
|
938
|
-
// ── the chunked-feed code review with PROVEN delivery (Phase 3) ──────────────────────────────────
|
|
939
|
-
// agy takes its prompt as ONE argv, and this host AUTO-DENIES agy's native read_file tool — so an
|
|
940
|
-
// over-cap change set can never be FETCHED by the model. It is DELIVERED instead: partitioned into
|
|
941
|
-
// under-cap parts, fed over continuation turns, then reviewed in a final turn. Delivery is PROVEN,
|
|
942
|
-
// never assumed: the wrapper picks a line from each part's body AFTER assembly and the final answer
|
|
943
|
-
// must reproduce every picked line verbatim. Envelope and body are formally separate — only BODIES
|
|
944
|
-
// concatenate, and they concatenate to the change set byte-for-byte.
|
|
945
|
-
const ARTIFACT_HEADER = '## The change set under review (assembled working-tree diff — repo-complete)';
|
|
946
|
-
const SHAPE_HEADER = '\n## Output — Markdown, this exact shape, nothing else';
|
|
947
|
-
const FED_CAP = 6000;
|
|
948
|
-
// The proof asks the model to COUNT to the address with no tool, so the address has to be reachable
|
|
949
|
-
// that way. The walk used to start at each part's middle: measured 847..1024 on a 701464-byte change
|
|
950
|
-
// set, which is where the three live false refusals sat. FED_WIDE_CAP gives the countability
|
|
951
|
-
// regression parts wide enough that a midpoint address is unmistakably out of reach — under the
|
|
952
|
-
// narrow FED_CAP the midpoint lands at 80, close enough to the bar that fixture drift could hide it.
|
|
953
|
-
const MAX_COUNTABLE_PROOF_ADDRESS = 40;
|
|
954
|
-
const FED_WIDE_CAP = 24000;
|
|
955
|
-
// The assembler's OWN section banners — the vocabulary the proof selector refuses, because it emits
|
|
956
|
-
// them for every change set and a model can rebuild the per-path forms from part 1's git-status
|
|
957
|
-
// block. A change set's own `=== … ===` line is NOT in this set and stays admissible.
|
|
958
|
-
const isAssemblerBanner = (line) =>
|
|
959
|
-
/^=== (repo file map|git status|staged diff|unstaged diff|untracked)/.test(line) && line.endsWith(' ===');
|
|
960
|
-
|
|
961
|
-
// A change set big enough to need several parts under FED_CAP.
|
|
962
|
-
const seedFedChangeSet = (sb, { lines = 400, multibyte = false } = {}) => {
|
|
963
|
-
const body = Array.from({ length: lines }, (_, i) =>
|
|
964
|
-
multibyte
|
|
965
|
-
? `line ${String(i).padStart(4, '0')} — multibyte marker ${'\u044e'.repeat(20)}`
|
|
966
|
-
: `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n');
|
|
967
|
-
writeFileSync(join(sb.repo, 'oversized.txt'), `${body}\n`);
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
const inlineArtifactOf = (prompt) => prompt.slice(prompt.indexOf(ARTIFACT_HEADER), prompt.indexOf(SHAPE_HEADER));
|
|
971
|
-
const bodyOf = (turnPrompt) => {
|
|
972
|
-
const begin = turnPrompt.match(/--- BEGIN CHANGE-SET PART \d+ OF \d+ ---\n/);
|
|
973
|
-
if (!begin) return null;
|
|
974
|
-
const start = begin.index + begin[0].length;
|
|
975
|
-
return turnPrompt.slice(start, turnPrompt.indexOf('\n--- END CHANGE-SET PART ', start));
|
|
976
|
-
};
|
|
977
|
-
// The addresses ride ONE PER LINE — that format is what makes a collision with a proof candidate
|
|
978
|
-
// constructively impossible, so the parser reads lines, never a delimiter-joined field.
|
|
979
|
-
const requestedBlockOf = (finalPrompt) => {
|
|
980
|
-
const start = finalPrompt.indexOf('Requested addresses');
|
|
981
|
-
assert.notEqual(start, -1, 'the final turn states which lines it requires');
|
|
982
|
-
const after = finalPrompt.slice(finalPrompt.indexOf('\n', start) + 1);
|
|
983
|
-
return after.slice(0, after.indexOf('\n###')).split('\n').filter(Boolean);
|
|
984
|
-
};
|
|
985
|
-
const requestedOf = (finalPrompt) => requestedBlockOf(finalPrompt).map((item) => {
|
|
986
|
-
const [, part, line] = item.match(/^part (\d+) line (\d+)$/);
|
|
987
|
-
return { part: Number(part), line: Number(line) };
|
|
988
|
-
});
|
|
989
|
-
const fedRun = (sb, extraEnv = {}) =>
|
|
990
|
-
run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), ...extraEnv } });
|
|
991
666
|
|
|
992
667
|
describe('agy-review.sh — chunked feed: the change set is DELIVERED (Phase 3)', { concurrency: 2 }, () => {
|
|
993
668
|
it('an over-cap code review feeds every part and the concatenated BODIES reproduce the change set exactly', async () => {
|
|
@@ -2368,12 +2043,6 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
|
|
|
2368
2043
|
const RECEIPT_FIXTURE = JSON.parse(
|
|
2369
2044
|
'{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<display>"},"delivery":"inline"}',
|
|
2370
2045
|
);
|
|
2371
|
-
const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
2372
|
-
const readReceipts = (repo) => {
|
|
2373
|
-
const p = join(repo, RECEIPTS_REL);
|
|
2374
|
-
if (!existsSync(p)) return [];
|
|
2375
|
-
return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
2376
|
-
};
|
|
2377
2046
|
const sha256HexOf = async (buf) => {
|
|
2378
2047
|
const { createHash } = await import('node:crypto');
|
|
2379
2048
|
return createHash('sha256').update(buf).digest('hex');
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.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",
|
|
@@ -33,6 +33,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
33
33
|
- `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.
|
|
34
34
|
- `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.
|
|
35
35
|
- 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.
|
|
36
|
+
- `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.
|
|
36
37
|
- `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
|
|
37
38
|
|
|
38
39
|
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
|
@@ -11,7 +11,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]
|
|
|
11
11
|
1. **Versions — a status-only render from `installed[]` + `deploymentHead`** (this is **NOT** the shared notes-based version block — see the separation note below): the **`docs/ai` structure version** (named as such, never "lineage head"), then each member by its `display` showing its `version` (or, when there is no version, the plain phrase for its `state`, mapped above), plus the two-axes disambiguation. **Freshness comes from `installed[].refresh`, not from `notes`:** for each member whose **`refresh.behind`** is `true`, show a **localized "needs refresh"** label and the **verbatim `refresh.recommend`** command **exactly once** (the command/package name stays source-language; **do not also paste the English `notes` caveats** — `refresh.recommend` is the single source of the recovery step, so the command is never duplicated on this surface). A member whose **`refresh.freshness`** is **`unknown`** is surfaced too — a localized *"couldn't be checked"* label; it is **never counted as current and never as behind** (its `notes` caveat carries the detail on the notes-based surfaces; here the label is enough). Lead with a one-line **headline count** derived from `installed[].state` + `refresh.behind` + `refresh.freshness` (e.g. *"5 members installed · 1 needs a refresh · 1 couldn't be checked"* — omit a zero count).
|
|
12
12
|
|
|
13
13
|
> **Status reads `refresh`; the shared version block + the bootstrap/upgrade footers stay `notes`-based (unchanged this release).** `${CLAUDE_SKILL_DIR}/references/modes/status.md` has its OWN status-only render (above), keyed on `installed[].refresh.behind` / `refresh.recommend`. The shared **version block** (under *The version block + welcome mat* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and the bootstrap (step 11) + every upgrade (steps 4 / 8) report footer still consume `installed[].notes` verbatim — that wiring is deliberately **untouched** here (their migration onto `refresh` is deferred). Do not rewrite those footers onto `refresh`.
|
|
14
|
-
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`** or **`old-unrotated`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits) — the note is the SAME for both, they differ only in how the older layout was detected (`old` = a retired archive file is still on disk; `old-unrotated` = the project's deployed rotation script predates the store); `migrated` / `none` need no note.
|
|
14
|
+
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`** or **`old-unrotated`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits) — the note is the SAME for both, they differ only in how the older layout was detected (`old` = a retired archive file is still on disk; `old-unrotated` = the project's deployed rotation script predates the store); `migrated` / `none` need no note. Then **one `specs` line, always** (from `project.specs` — the feature-spec layer's adoption state, judged from the store alone): `not adopted` (no `docs/ai/specs/` — the layer was never seeded), `adopting (N draft)` (a store with no live contract yet), `adopted (N live, M draft)`, or `could not be read — <reason>` (the store or a document in it could not be observed; never counted as any of the other three). A recorded decline (`project.specs.declined`) appends ` — declined` to the first two. The internal tokens are `not-adopted` / `adopting` / `adopted` / `unreadable`; render the plain phrases above, never the tokens. An envelope without the field says the installed kit predates it — say so rather than inventing a state.
|
|
15
15
|
3. **Settings (`--dir`, one line each)** (from `project.settings`):
|
|
16
16
|
- **recipes** — the effective recipe per slot (detail → `/agent-workflow-kit procedures` / `recipes`); a `recipes.detectError` → say the backends couldn't be checked, so recipes floored at solo.
|
|
17
17
|
- **attribution** — `includeCoAuthoredBy` effective; call out a **local override** only when `local` is non-null **and** differs from `project` (a `null` `local` means the key is absent there, so the project value stands — that is not an override).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
### Mode: upgrade
|
|
2
2
|
|
|
3
3
|
<!-- opt-in-capability: family-freshness -->
|
|
4
|
+
<!-- opt-in-capability: spec-adoption -->
|
|
4
5
|
|
|
5
6
|
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
7
|
|
|
@@ -10,7 +11,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
10
11
|
|
|
11
12
|
1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
|
|
12
13
|
2. `footprint` — `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` → visibility: visible · ambiguous · hidden — consent (conditional): ambiguous → ask which it is BEFORE anything; hidden → the conditional re-run without `--dry-run` (its surfaced paths ask per bootstrap step 9).
|
|
13
|
-
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node` / `old-adr-layout-migration-instructed` / `failed`.
|
|
14
|
+
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed`.
|
|
14
15
|
4. `gates-migration` — `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` → the preview plan · INERT checker · CUSTOMIZED entries, each named — consent: apply only on an explicit yes, re-run with `--apply`.
|
|
15
16
|
5. `bridges` — `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` → per-bridge lines: refreshed · already current · skipped — with its stated reason (not placed / newer than the bundle / unsupported host) · `skipped-readonly` · could not refresh.
|
|
16
17
|
6. `lens` — `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md` → per section: refreshed · already current · custom edit preserved · file absent / engine too old — skipped · over the line cap — refused · section absent — noted · a fully absent/invalid engine → hard STOP.
|
|
@@ -30,7 +31,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
30
31
|
|
|
31
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.
|
|
32
33
|
|
|
33
|
-
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy 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 and autonomy → `seeded` / `already-present`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node`; 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`, `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 six 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).
|
|
34
|
+
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all six** ensures described below — orchestration config · gate declaration · autonomy 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 and autonomy → `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 six 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).
|
|
34
35
|
|
|
35
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.)
|
|
36
37
|
|
|
@@ -38,11 +39,11 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
38
39
|
|
|
39
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).
|
|
40
41
|
|
|
41
|
-
**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),
|
|
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 three 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.
|
|
42
43
|
|
|
43
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.
|
|
44
45
|
|
|
45
|
-
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`, so behind a custom checker the store root is NOT seeded and the line names the remedy (copy the pair from `${CLAUDE_SKILL_DIR}/references/scripts/` by hand, re-run). One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node`
|
|
46
|
+
**What the spec-layer ensure does.** A deployment older than the feature-spec layer lacks `scripts/spec-schema.mjs` and `docs/ai/specs/index.md`, and its deployed `check-docs-size.mjs` predates the store collapse. In ONE order: the reader pair, then the checker pair — each seeded when absent and REFRESHED only while a file's bytes are a body a release shipped (an append-only digest catalog → `refreshed`; an edited body is preserved verbatim and withholds the writes that depend on it, the checker lane waiting on a byte-current reader pair); the store root is seeded from the bundled template, date rendered, ONLY behind a checker pair current after the run — an older or edited checker renders the store row by row and reds the hook's `--check-index`, so behind a custom checker the store root is NOT seeded and the line names the remedy (copy the pair from `${CLAUDE_SKILL_DIR}/references/scripts/` by hand, re-run). One token by precedence: `seeded` > `refreshed` > `customized-preserved` (an edited pair, and this run wrote nothing) > `already-present`; `skipped-no-node-evidence` only when neither a root `package.json` nor any kit-seeded `scripts/*.mjs` is present (the same Node-evidence probe the enforcement-script ensure uses; an unreadable probe is `failed` / `node-evidence-unverifiable`); every line states what this run did, and a write that stops partway names what landed. The legacy-ADR instruct never withholds it. **The adoption state this seed leaves behind is reported, never assumed:** the Recommendations section (step 4 / 8) carries the `spec-adoption` item — `not adopted` (the store is absent: the seed offered, the decline a named hand-apply alternative), `adopting` (a store with no live contract: the decline offered), silenced by a recorded decline (`ack-write --lane spec-adoption`), and a store the probe cannot read is a stated skip — and `status` prints the same state on its own line.
|
|
46
47
|
|
|
47
48
|
**`gates-migration` — legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
|
|
48
49
|
|
|
@@ -88,6 +89,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
88
89
|
flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
|
|
89
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.
|
|
90
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
|
+
- **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.
|
|
91
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 **six project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (orchestration config, gate declaration, autonomy 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`).
|
|
92
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*.
|
|
93
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).
|
|
@@ -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
|
-
- **No Node runtime → skip enforcement.** If the project has no Node (recon step 1), skip bootstrap steps 8–9 (scripts + hook) and follow the cap/archive/index policy manually, or port the scripts to the project's language.
|
|
12
|
+
- **No Node runtime → skip enforcement.** If the project has no Node (recon step 1), skip bootstrap steps 8–9 (scripts + hook) and follow the cap/archive/index policy manually, or port the scripts to the project's language. **A skip names a PROVEN fact, never a proxy:** the upgrade ensures judge Node by EVIDENCE — a root `package.json` OR any kit-seeded `scripts/*.mjs` already deployed — and their `skipped-no-node-evidence` line lists every probe that answered absent; a `skipped-*` outcome whose reason the tool could itself disprove is a tool defect, not an outcome. **A skip line that contradicts the observed tree is raised as a finding in the report, never pasted as neutral** — when the tree you can see disproves a skip's stated reason, report the contradiction with the fact named 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.
|
|
@@ -23,7 +23,7 @@ Read in order, then confirm before starting:
|
|
|
23
23
|
4. Confirm with the user: *"I'm taking task X. Confirm?"*
|
|
24
24
|
|
|
25
25
|
### 1.2. During Work
|
|
26
|
-
**Before any feature:** name its governing spec(s) — the feature spec under `docs/ai/specs/` for every touched spec-covered slice (zero
|
|
26
|
+
**Before any feature:** name its governing spec(s) — the feature spec under `docs/ai/specs/` for every touched spec-covered slice (a zero names the adoption state it relies on — not adopted, adopting, or nothing spec-covered touched; each cited spec's Out of scope bounds that slice's work). Where only a page spec (`docs/ai/pages/<page>.md`) exists it governs as an ADOPTION SHIM: state Out of scope + Revision inline in the plan. If the contract changes, the spec revision is authored WITH the plan (visible at review) and lands with the code, so docs and code never diverge.
|
|
27
27
|
|
|
28
28
|
**For every code change:**
|
|
29
29
|
1. Grep for similar implementations — reuse existing patterns.
|
|
@@ -72,13 +72,14 @@ Apply this as part of §2 before any user-facing summary:
|
|
|
72
72
|
- **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
|
|
73
73
|
- **Live host/session facts are tool-composed only.** Any claim 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**; 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.
|
|
74
74
|
- **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.
|
|
75
|
+
- **A skip that contradicts the tree is a finding.** A tool-composed `skipped-*` line whose stated reason the observed tree disproves (a "no Node" skip beside deployed Node scripts) is raised as a FINDING in the report, never pasted as a neutral outcome — and a tool may not emit a skip whose reason it could itself disprove.
|
|
75
76
|
|
|
76
77
|
### 2.6. Planning, review & process-fidelity invariants
|
|
77
78
|
Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
|
|
78
79
|
- **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
|
|
79
80
|
- **Finding scope (plan-execution) — name the invariant BEFORE the edit.** During EXECUTION only — a plan under authoring has no shipped behaviour to call a live defect in, so plan-review carries none of this. Every finding names the invariant its fix would enforce, and where that invariant already lives decides the disposition: already an acceptance criterion of the phase → **fold here**; it would have to be ADDED → ship the **narrow fix** for the found site (red first, then green) and queue ONLY the generalization — a deferral row carries the invariant, the origin `file:line`, the narrow fix, its proof and a residual exposure declared NOT live; no correct narrow fix → **blocking**: the phase does not close, and it is **never queued**. Two bars declared before each round: a finding counts only if it changes a **WRITE/REMOVE decision** or is a false statement in shipped text; a repeat finding in one subarea **routes to SUBTRACTION**, not a fourth patch.
|
|
80
81
|
- **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
|
|
81
|
-
- **Spec-first.** A plan names its GOVERNING spec(s) — zero, one or many, one per touched spec-covered slice (the feature spec under `docs/ai/specs/`; page-only coverage governs as an ADOPTION SHIM, with Out of scope + Revision stated inline in the plan). Each cited spec's Out of scope bounds that slice's work and the plan's non-goals restate it per slice — no global union; a cross-spec conflict is resolved by a spec revision BEFORE approval, never by silent precedence. A NEW feature's draft spec exists AT plan review (a `create` row); a change to a governed contract rides the plan as its proposed revision (a `modify` row); approval confirms plan and contract atomically, and the revision lands with the code. Scenario bindings are per scenario: a new scenario is `unbound` until its test lands in the same plan, and a status never regresses for an extension.
|
|
82
|
+
- **Spec-first.** A plan names its GOVERNING spec(s) — zero, one or many, one per touched spec-covered slice (the feature spec under `docs/ai/specs/`; page-only coverage governs as an ADOPTION SHIM, with Out of scope + Revision stated inline in the plan). A ZERO names the adoption state it relies on — `not adopted` (no store) or `adopting` (a store with no live contract), either with a recorded decline, or `nothing spec-covered touched` (a store with live contracts) — a bare zero is never a licence; the store's own state is what `status` and the upgrade advisor report. Each cited spec's Out of scope bounds that slice's work and the plan's non-goals restate it per slice — no global union; a cross-spec conflict is resolved by a spec revision BEFORE approval, never by silent precedence. A NEW feature's draft spec exists AT plan review (a `create` row); a change to a governed contract rides the plan as its proposed revision (a `modify` row); approval confirms plan and contract atomically, and the revision lands with the code. Scenario bindings are per scenario: a new scenario is `unbound` until its test lands in the same plan, and a status never regresses for an extension.
|
|
82
83
|
- **No code-mechanics in the plan.** A ledger row carries its path and anchor, and Verification carries the exact commands (the plan-shape canon) — checked syntax: the plan's own Verification runs them against an explicit expected outcome or gate; the only other syntax a plan may carry is a literal fixture/schema fragment a named test copies or validates. Un-run, logic-bearing syntax — control-flow, a regex, a glob, a grammar, an algorithm body, a mini-DSL — never lives in plan prose, however plausible or shell-verified it looks: a fold or draft that wants one is the trigger to write the test instead.
|
|
83
84
|
- **Test-as-spec.** Fold a code-touching finding into a red→green TEST, not a prose paragraph — the gate is the only deterministic checker; a paragraph cannot self-check.
|
|
84
85
|
- **Characterize-first.** Before editing UNCOVERED code, pin its current behavior in a green test, then edit — any unintended change goes red. Never edit what has no checker; first give it one. Keep edits atomic/reversible; prefer SUBTRACTIVE folds.
|