@sabaiway/agent-workflow-kit 1.28.0 → 1.30.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 +93 -0
- package/README.md +4 -1
- package/SKILL.md +67 -6
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +145 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +169 -1
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +120 -3
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +132 -0
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/contracts.md +1 -0
- package/references/hooks/gate-approve.mjs +300 -0
- package/references/templates/agent_rules.md +3 -2
- package/references/templates/handover.md +1 -0
- package/tools/commands.mjs +24 -3
- package/tools/detect-backends.mjs +2 -0
- package/tools/family-registry.mjs +25 -0
- package/tools/gate-hook.mjs +387 -0
- package/tools/grounding.mjs +263 -0
- package/tools/known-footprint.mjs +1 -0
- package/tools/presentation.mjs +1 -0
- package/tools/procedures.mjs +50 -5
- package/tools/recipes.mjs +78 -12
- package/tools/renderers.mjs +6 -0
- package/tools/review-state.mjs +395 -0
- package/tools/set-recipe.mjs +15 -5
- package/tools/uninstall.mjs +175 -26
- package/tools/velocity-profile.mjs +21 -3
- package/tools/view-model.mjs +18 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gate-approve.mjs — the agent-workflow PreToolUse gate-approval hook. Bundled at
|
|
3
|
+
// references/hooks/gate-approve.mjs; PLACED (copied) by `/agent-workflow-kit hook` to
|
|
4
|
+
// <project>/.claude/hooks/agent-workflow-gates.mjs and wired as a `PreToolUse` "Bash" hook in
|
|
5
|
+
// .claude/settings.json.
|
|
6
|
+
//
|
|
7
|
+
// SELF-CONTAINED by contract: the placed copy must run on machines without the kit — no kit
|
|
8
|
+
// imports, dependency-free, Node >= 18, no side effects on import. Two constants are baked
|
|
9
|
+
// FROZEN COPIES of the velocity-profile.mjs exports (a placed file cannot import the kit):
|
|
10
|
+
// SEEDED_READONLY_CORE ≡ UNIVERSAL_READONLY_ALLOWLIST and RESIDUAL_FORMS ≡
|
|
11
|
+
// RUNTIME_RESIDUAL_FORMS — drift-guarded by the kit's test/gate-hook-core-parity.test.mjs.
|
|
12
|
+
//
|
|
13
|
+
// It reads <project root>/docs/ai/gates.json LIVE on every invocation (AD-035: one declaration,
|
|
14
|
+
// two consumers — editing gates.json never requires re-wiring). The project root resolves from
|
|
15
|
+
// $CLAUDE_PROJECT_DIR (the stdin `cwd` may be a subdirectory), falling back to the stdin `cwd`
|
|
16
|
+
// only when the env is absent.
|
|
17
|
+
//
|
|
18
|
+
// Decision ladder for every Bash PreToolUse call — first match wins:
|
|
19
|
+
// (a) declared-gate EXACT match → allow. Byte-identical (leading/trailing trim only — no
|
|
20
|
+
// whitespace collapsing, no quote/glob/variable interpretation, no prefix or pattern
|
|
21
|
+
// matching: patterns are exactly what made AD-021 auto-seeding fragile) to one declared
|
|
22
|
+
// gate `cmd`, AND the stdin `cwd` realpath-resolves to the project root (gates run FROM
|
|
23
|
+
// THE ROOT by contract — the same bytes from a subdirectory resolve relative paths
|
|
24
|
+
// differently), AND permission_mode is `default`/`acceptEdits` (an allow never loosens
|
|
25
|
+
// `plan`/`bypassPermissions`).
|
|
26
|
+
// (b) residual guard → ask. The command's leading tokens match the SEEDED read-only core AND
|
|
27
|
+
// the command carries a documented runtime residual (output redirection, command
|
|
28
|
+
// substitution, the bounded `--output` write-flag family) that a settings-level allow
|
|
29
|
+
// rule cannot see. Most-restrictive-wins: this surfaces a human prompt even where a
|
|
30
|
+
// seeded allow rule would have silently approved. Detection is deliberately string-level
|
|
31
|
+
// and conservative (no shell parsing in a dependency-free hook): a quoted metacharacter
|
|
32
|
+
// may over-ASK, never under-allow. Covers the kit-SEEDED core only, never arbitrary
|
|
33
|
+
// user-added rules.
|
|
34
|
+
// (c) everything else → NO decision: exit 0, no output — the normal permission flow proceeds
|
|
35
|
+
// unchanged. The hook NEVER emits `deny`.
|
|
36
|
+
//
|
|
37
|
+
// Fail-safe invariant, decoupled per function: a DECLARATION anomaly (missing / unreadable /
|
|
38
|
+
// malformed / schema-invalid gates.json) disables ONLY exact-gate approval (a) — the residual
|
|
39
|
+
// guard (b) needs no declaration and keeps running (a broken gates.json must not silently
|
|
40
|
+
// reopen the seeded-allowlist hole). Only an INPUT anomaly (unparseable stdin, non-Bash
|
|
41
|
+
// tool_name) disables the whole hook. EVERY anomaly path exits 0 — never exit 2 (exit 2 is an
|
|
42
|
+
// immediate block; a hook that fires on every Bash call must never become the blocker or the
|
|
43
|
+
// noise — run-gates.mjs already yells about a broken declaration at its own point of use).
|
|
44
|
+
//
|
|
45
|
+
// Trust posture: this hook only removes the PROMPT for commands the human already declared in
|
|
46
|
+
// gates.json — the same trust boundary as run-gates.mjs, which executes those commands with the
|
|
47
|
+
// caller's privileges. gates.json is thereby a privileged file; an invalid declaration approves
|
|
48
|
+
// NOTHING (strict-parse-or-no-decision).
|
|
49
|
+
|
|
50
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
51
|
+
import { join } from 'node:path';
|
|
52
|
+
import { pathToFileURL } from 'node:url';
|
|
53
|
+
|
|
54
|
+
export const HOOK_EVENT_NAME = 'PreToolUse';
|
|
55
|
+
export const BASH_TOOL_NAME = 'Bash';
|
|
56
|
+
export const GATES_REL = 'docs/ai/gates.json';
|
|
57
|
+
export const PROJECT_DIR_ENV = 'CLAUDE_PROJECT_DIR';
|
|
58
|
+
export const DECISION_ALLOW = 'allow';
|
|
59
|
+
export const DECISION_ASK = 'ask';
|
|
60
|
+
// The only modes an `allow` may be emitted under — an allowlist, so an unknown future mode is
|
|
61
|
+
// fenced closed by default. The residual guard (b) is NOT mode-fenced: an `ask` never loosens.
|
|
62
|
+
export const ALLOW_PERMISSION_MODES = Object.freeze(['default', 'acceptEdits']);
|
|
63
|
+
|
|
64
|
+
// Baked frozen copy of velocity-profile.mjs UNIVERSAL_READONLY_ALLOWLIST (the audited read-only
|
|
65
|
+
// core velocity seeds as settings allow rules). Drift-guarded — never edit here alone.
|
|
66
|
+
export const SEEDED_READONLY_CORE = Object.freeze([
|
|
67
|
+
'Bash(git status:*)',
|
|
68
|
+
'Bash(git diff:*)',
|
|
69
|
+
'Bash(git log:*)',
|
|
70
|
+
'Bash(git show:*)',
|
|
71
|
+
'Bash(git ls-files:*)',
|
|
72
|
+
'Bash(git check-ignore:*)',
|
|
73
|
+
'Bash(git branch --list:*)',
|
|
74
|
+
'Bash(npm view:*)',
|
|
75
|
+
'Bash(npm ls:*)',
|
|
76
|
+
'Bash(npm outdated:*)',
|
|
77
|
+
'Bash(ls:*)',
|
|
78
|
+
'Bash(cat:*)',
|
|
79
|
+
'Bash(head:*)',
|
|
80
|
+
'Bash(tail:*)',
|
|
81
|
+
'Bash(wc:*)',
|
|
82
|
+
'Bash(readlink:*)',
|
|
83
|
+
'Bash(which:*)',
|
|
84
|
+
'Bash(grep:*)',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Baked frozen copy of velocity-profile.mjs RUNTIME_RESIDUAL_FORMS (the documented residual a
|
|
88
|
+
// settings-level allow rule cannot see). Drift-guarded — never edit here alone.
|
|
89
|
+
export const RESIDUAL_FORMS = Object.freeze({
|
|
90
|
+
writeRedirections: Object.freeze(['>', '>>', '1>', '2>', '&>', '>|']),
|
|
91
|
+
// `$(…)` + backtick + process substitution `<(…)` all RUN a nested command (`>(…)` is caught by
|
|
92
|
+
// the `>` redirection scan). Bare `<` is input redirection (reads a file — read-only commands may
|
|
93
|
+
// already do that), so it is deliberately NOT here.
|
|
94
|
+
commandSubstitutions: Object.freeze(['$(', '`', '<(']),
|
|
95
|
+
// The `--output` write-flag family, matched as a raw SUBSTRING of the whole command (never a
|
|
96
|
+
// whitespace-token check): the hook sees the pre-shell command string, so `--output=f`,
|
|
97
|
+
// `"--output=f"`, `'--output' f`, and `\--output` must all trip it — over-asking on a benign
|
|
98
|
+
// `--output-indicator` is the safe direction (never under-allow a real write flag through quotes).
|
|
99
|
+
boundedWriteFlags: Object.freeze(['--output']),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const RESIDUAL_CLASS_REDIRECTION = 'output redirection';
|
|
103
|
+
export const RESIDUAL_CLASS_SUBSTITUTION = 'command substitution';
|
|
104
|
+
export const RESIDUAL_CLASS_OUTPUT_FLAG = 'a bounded --output write flag';
|
|
105
|
+
|
|
106
|
+
const EXIT_OK = 0;
|
|
107
|
+
const UTF8 = 'utf8';
|
|
108
|
+
const BASH_ALLOW_PATTERN = /^Bash\((.+):\*\)$/u;
|
|
109
|
+
const WHITESPACE_PATTERN = /\s+/u;
|
|
110
|
+
const GATE_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
111
|
+
const GATE_CMD_NEWLINE_PATTERN = /[\r\n]/u;
|
|
112
|
+
const GATE_KEYS = Object.freeze(['id', 'title', 'cmd']);
|
|
113
|
+
const README_KEY = '_README';
|
|
114
|
+
const GATES_KEY = 'gates';
|
|
115
|
+
|
|
116
|
+
// ── declaration (validation parity with run-gates.mjs) ────────────────────────────────
|
|
117
|
+
|
|
118
|
+
// Self-contained restatement of run-gates.mjs `validateDeclaration`, at EXACT parity including
|
|
119
|
+
// the optional `_README` string — a declaration the runner accepts is never rejected here (else
|
|
120
|
+
// every template-shaped declaration would approve nothing), and one the runner rejects never
|
|
121
|
+
// approves anything. Returns a boolean shape instead of throwing: anomalies go dark, not loud.
|
|
122
|
+
export const validateDeclarationShape = (parsed) => {
|
|
123
|
+
const invalid = { ok: false };
|
|
124
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return invalid;
|
|
125
|
+
for (const key of Object.keys(parsed)) {
|
|
126
|
+
if (key !== README_KEY && key !== GATES_KEY) return invalid;
|
|
127
|
+
}
|
|
128
|
+
if (parsed[README_KEY] !== undefined && typeof parsed[README_KEY] !== 'string') return invalid;
|
|
129
|
+
if (!Array.isArray(parsed[GATES_KEY])) return invalid;
|
|
130
|
+
const seenIds = new Set();
|
|
131
|
+
for (const gate of parsed[GATES_KEY]) {
|
|
132
|
+
if (gate === null || typeof gate !== 'object' || Array.isArray(gate)) return invalid;
|
|
133
|
+
for (const key of Object.keys(gate)) {
|
|
134
|
+
if (!GATE_KEYS.includes(key)) return invalid;
|
|
135
|
+
}
|
|
136
|
+
for (const key of GATE_KEYS) {
|
|
137
|
+
if (typeof gate[key] !== 'string' || gate[key].trim() === '') return invalid;
|
|
138
|
+
}
|
|
139
|
+
if (GATE_CMD_NEWLINE_PATTERN.test(gate.cmd)) return invalid;
|
|
140
|
+
if (!GATE_ID_PATTERN.test(gate.id)) return invalid;
|
|
141
|
+
if (seenIds.has(gate.id)) return invalid;
|
|
142
|
+
seenIds.add(gate.id);
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, gates: parsed[GATES_KEY] };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Read + strictly validate the LIVE declaration. Any declaration anomaly returns null — ladder
|
|
148
|
+
// (a) goes dark while the residual guard (b) keeps running (the decoupled fail-safe invariant).
|
|
149
|
+
export const readDeclarationGates = (projectRoot, deps = {}) => {
|
|
150
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
151
|
+
if (typeof projectRoot !== 'string' || projectRoot === '') return null;
|
|
152
|
+
try {
|
|
153
|
+
const validated = validateDeclarationShape(JSON.parse(readFile(join(projectRoot, GATES_REL), UTF8)));
|
|
154
|
+
return validated.ok ? validated.gates : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ── seeded-core prefix + residual detection (ladder b) ────────────────────────────────
|
|
161
|
+
|
|
162
|
+
const getAllowCommandPrefix = (pattern) => {
|
|
163
|
+
const match = pattern.match(BASH_ALLOW_PATTERN);
|
|
164
|
+
return match ? match[1] : undefined;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const tokenizeCommand = (command) => command.trim().split(WHITESPACE_PATTERN).filter(Boolean);
|
|
168
|
+
|
|
169
|
+
const CORE_PREFIX_TOKEN_LISTS = Object.freeze(
|
|
170
|
+
SEEDED_READONLY_CORE.map(getAllowCommandPrefix)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map((prefix) => Object.freeze(tokenizeCommand(prefix))),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// `Bash(git status:*)` approves `git status <anything>` at a word boundary — the guard matches
|
|
176
|
+
// the same way: a core pattern's tokens must be the command's LEADING tokens exactly.
|
|
177
|
+
export const matchSeededCorePrefix = (command) => {
|
|
178
|
+
const tokens = tokenizeCommand(command);
|
|
179
|
+
const matched = CORE_PREFIX_TOKEN_LISTS.find(
|
|
180
|
+
(prefixTokens) =>
|
|
181
|
+
prefixTokens.length <= tokens.length && prefixTokens.every((token, index) => tokens[index] === token),
|
|
182
|
+
);
|
|
183
|
+
return matched ? matched.join(' ') : null;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// String-level, conservative: the hook sees the PRE-SHELL command string, so every class is a raw
|
|
187
|
+
// substring scan (never a whitespace-token check — a token check misses `"--output=f"` / `'>' f`
|
|
188
|
+
// where the quotes are still in the string but the shell will strip them). A quoted metacharacter
|
|
189
|
+
// or write flag may over-ASK, never under-allow (no shell parsing in a dependency-free hook).
|
|
190
|
+
export const detectResidualClasses = (command) => {
|
|
191
|
+
const classes = [];
|
|
192
|
+
if (RESIDUAL_FORMS.writeRedirections.some((form) => command.includes(form))) {
|
|
193
|
+
classes.push(RESIDUAL_CLASS_REDIRECTION);
|
|
194
|
+
}
|
|
195
|
+
if (RESIDUAL_FORMS.commandSubstitutions.some((form) => command.includes(form))) {
|
|
196
|
+
classes.push(RESIDUAL_CLASS_SUBSTITUTION);
|
|
197
|
+
}
|
|
198
|
+
if (RESIDUAL_FORMS.boundedWriteFlags.some((flag) => command.includes(flag))) {
|
|
199
|
+
classes.push(RESIDUAL_CLASS_OUTPUT_FLAG);
|
|
200
|
+
}
|
|
201
|
+
return classes;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// ── the decision ladder (pure core) ───────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export const decideBashCall = ({ command, permissionMode, cwdIsProjectRoot, gates }) => {
|
|
207
|
+
const trimmed = command.trim();
|
|
208
|
+
// (a) declared-gate exact match — all three invariants (declaration valid, cwd = project
|
|
209
|
+
// root, mode in the allow fence) must hold; otherwise fall through, never error.
|
|
210
|
+
if (Array.isArray(gates) && cwdIsProjectRoot === true && ALLOW_PERMISSION_MODES.includes(permissionMode)) {
|
|
211
|
+
const declaredGate = gates.find((gate) => gate.cmd.trim() === trimmed);
|
|
212
|
+
if (declaredGate !== undefined) {
|
|
213
|
+
return {
|
|
214
|
+
permissionDecision: DECISION_ALLOW,
|
|
215
|
+
permissionDecisionReason: `agent-workflow gates: byte-exact match of declared gate "${declaredGate.id}" (${GATES_REL}), invoked from the project root`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// (b) residual guard — no declaration needed; not mode-fenced (an ask never loosens).
|
|
220
|
+
const corePrefix = matchSeededCorePrefix(trimmed);
|
|
221
|
+
if (corePrefix !== null) {
|
|
222
|
+
const residualClasses = detectResidualClasses(trimmed);
|
|
223
|
+
if (residualClasses.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
permissionDecision: DECISION_ASK,
|
|
226
|
+
permissionDecisionReason: `agent-workflow residual guard: read-only-seeded "${corePrefix}" carries ${residualClasses.join(' + ')} — a settings allow rule cannot see runtime shape; confirm by hand`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// (c) no decision — the normal permission flow proceeds unchanged.
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// ── stdin-driven main ─────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
const resolveRealpath = (maybePath, realpath) => {
|
|
237
|
+
if (typeof maybePath !== 'string' || maybePath === '') return null;
|
|
238
|
+
try {
|
|
239
|
+
return realpath(maybePath);
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Parse one PreToolUse stdin payload and run the ladder. Returns the decision object or null
|
|
246
|
+
// (null → no output, exit 0). Injectable deps keep the tests hermetic.
|
|
247
|
+
export const runHook = (rawInput, deps = {}) => {
|
|
248
|
+
const env = deps.env ?? process.env;
|
|
249
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
250
|
+
const parseInput = () => {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(rawInput);
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const input = parseInput();
|
|
258
|
+
// Input anomaly → the WHOLE hook goes dark (distinct from a declaration anomaly, which only
|
|
259
|
+
// darkens ladder (a)).
|
|
260
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
261
|
+
if (input.tool_name !== BASH_TOOL_NAME) return null;
|
|
262
|
+
const command = input.tool_input === null || typeof input.tool_input !== 'object' ? undefined : input.tool_input.command;
|
|
263
|
+
if (typeof command !== 'string' || command.trim() === '') return null;
|
|
264
|
+
|
|
265
|
+
const envRoot = env[PROJECT_DIR_ENV];
|
|
266
|
+
const projectRoot =
|
|
267
|
+
typeof envRoot === 'string' && envRoot !== '' ? envRoot : typeof input.cwd === 'string' ? input.cwd : null;
|
|
268
|
+
const rootReal = resolveRealpath(projectRoot, realpath);
|
|
269
|
+
const cwdReal = resolveRealpath(input.cwd, realpath);
|
|
270
|
+
return decideBashCall({
|
|
271
|
+
command,
|
|
272
|
+
permissionMode: input.permission_mode,
|
|
273
|
+
cwdIsProjectRoot: rootReal !== null && cwdReal !== null && rootReal === cwdReal,
|
|
274
|
+
gates: readDeclarationGates(projectRoot, deps),
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const formatDecision = (decision) =>
|
|
279
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: HOOK_EVENT_NAME, ...decision } });
|
|
280
|
+
|
|
281
|
+
const readStdin = async () => {
|
|
282
|
+
const chunks = [];
|
|
283
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
284
|
+
return Buffer.concat(chunks).toString(UTF8);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const main = async () => {
|
|
288
|
+
// Exit 0 on EVERY path — never exit 2 (an immediate block), never a top-level throw: this
|
|
289
|
+
// fires on every Bash call and must never become the blocker or the noise.
|
|
290
|
+
try {
|
|
291
|
+
const decision = runHook(await readStdin());
|
|
292
|
+
if (decision !== null) process.stdout.write(`${formatDecision(decision)}\n`);
|
|
293
|
+
} catch {
|
|
294
|
+
// Deliberately dark: anomalies are the runner's job to report at its own point of use.
|
|
295
|
+
}
|
|
296
|
+
return EXIT_OK;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
300
|
+
if (isDirectRun) main().then((code) => process.exit(code));
|
|
@@ -18,8 +18,9 @@ Every AI agent working on this project **must** adhere to this protocol before w
|
|
|
18
18
|
### 1.1. Start of Session
|
|
19
19
|
Read in order, then confirm before starting:
|
|
20
20
|
1. `docs/ai/handover.md` — where we left off.
|
|
21
|
-
2. `docs/ai/
|
|
22
|
-
3.
|
|
21
|
+
2. `docs/ai/orchestration.json` — the CONFIGURED orchestration recipes (per activity/slot). Honor them: a silent recipe downgrade is a forbidden substitution.
|
|
22
|
+
3. `docs/ai/active_plan.md` — pick ONE task from "Immediate Priority".
|
|
23
|
+
4. Confirm with the user: *"I'm taking task X. Confirm?"*
|
|
23
24
|
|
|
24
25
|
### 1.2. During Work
|
|
25
26
|
**Before any feature:** read the relevant page spec (`docs/ai/pages/<page>.md`). If behaviour changes, update the spec FIRST so docs and code never diverge.
|
|
@@ -14,6 +14,7 @@ maxLines: 80
|
|
|
14
14
|
|
|
15
15
|
**Last session:** {{DATE}}
|
|
16
16
|
**Branch:** {{BRANCH}}
|
|
17
|
+
**Active recipes:** not recorded yet — paste the configured-recipe line (composed from `docs/ai/orchestration.json`) and refresh it whenever the config changes.
|
|
17
18
|
|
|
18
19
|
## What was done last session
|
|
19
20
|
|
package/tools/commands.mjs
CHANGED
|
@@ -119,12 +119,19 @@ const CATALOG = [
|
|
|
119
119
|
kind: WRITER,
|
|
120
120
|
oneLine: 'Place bundled cheap-model subagent definitions for mechanical work — sweeps, changelog skeletons, gate triage (Claude Code; opt-in; preview first).',
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
key: 'hook',
|
|
124
|
+
invocation: invocationOf('hook'),
|
|
125
|
+
group: 'Configure',
|
|
126
|
+
kind: WRITER,
|
|
127
|
+
oneLine: 'Auto-approve your own declared gate commands (docs/ai/gates.json) via a Claude Code hook — exact matches only, previews first (opt-in).',
|
|
128
|
+
},
|
|
122
129
|
{
|
|
123
130
|
key: 'recipes',
|
|
124
131
|
invocation: invocationOf('recipes'),
|
|
125
132
|
group: 'Orchestrate',
|
|
126
133
|
kind: READ_ONLY,
|
|
127
|
-
oneLine: 'See the orchestration recipes (Solo / Reviewed / Council / Delegated)
|
|
134
|
+
oneLine: 'See the orchestration recipes (Solo / Reviewed / Council / Delegated), which one fits this environment, and the configured per-activity line to paste at session start.',
|
|
128
135
|
},
|
|
129
136
|
{
|
|
130
137
|
key: 'procedures',
|
|
@@ -140,6 +147,20 @@ const CATALOG = [
|
|
|
140
147
|
kind: WRITER,
|
|
141
148
|
oneLine: 'Set the orchestration recipe for an activity from plain language — previews the change, then writes the config when you confirm.',
|
|
142
149
|
},
|
|
150
|
+
{
|
|
151
|
+
key: 'review-state',
|
|
152
|
+
invocation: invocationOf('review-state'),
|
|
153
|
+
group: 'Orchestrate',
|
|
154
|
+
kind: READ_ONLY,
|
|
155
|
+
oneLine: 'Check that every configured review backend has receipted the current uncommitted tree with a fresh grounded review; --check turns it into a gate exit code.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'grounding',
|
|
159
|
+
invocation: invocationOf('grounding'),
|
|
160
|
+
group: 'Orchestrate',
|
|
161
|
+
kind: WRITER,
|
|
162
|
+
oneLine: 'Assemble the verified-facts payload a grounded review runs against — the entry-point Hard Constraints plus a plan’s decision sections; prints it, or writes ONE scratch file with --out.',
|
|
163
|
+
},
|
|
143
164
|
];
|
|
144
165
|
|
|
145
166
|
// Deep-freeze: freeze the array AND every entry, so the catalog is genuinely immutable at runtime
|
|
@@ -164,8 +185,8 @@ export const kindOf = (key) => byKey.get(key)?.kind ?? null;
|
|
|
164
185
|
// (`upgrade`) OR the full slash form (`/agent-workflow-kit upgrade`); the first word is significant
|
|
165
186
|
// and trailing args are ignored. Precise semantics:
|
|
166
187
|
// undefined / null / '' / whitespace-only / the exact bare invocation → 'bootstrap'
|
|
167
|
-
// a known first token (upgrade/status/setup/backends/recipes/procedures/velocity/
|
|
168
|
-
// → that mode
|
|
188
|
+
// a known first token (upgrade/status/setup/backends/recipes/procedures/velocity/agents/hook/
|
|
189
|
+
// gates/set-recipe/uninstall/help) → that mode
|
|
169
190
|
// anything else (unrecognized / ambiguous) → 'help' (read-only — NEVER a writer/guarded mode)
|
|
170
191
|
export const routeInvocation = (token) => {
|
|
171
192
|
if (token == null) return BARE_INVOCATION_MODE;
|
|
@@ -88,6 +88,7 @@ const RAW_BACKENDS = [
|
|
|
88
88
|
],
|
|
89
89
|
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
90
90
|
continue: [],
|
|
91
|
+
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); a write failure warns, never fails the review',
|
|
91
92
|
},
|
|
92
93
|
},
|
|
93
94
|
bin: 'codex',
|
|
@@ -120,6 +121,7 @@ const RAW_BACKENDS = [
|
|
|
120
121
|
'agy-review --continue [--decided @f] [--focus "…"]',
|
|
121
122
|
'agy-review --conversation <id> [--decided @f] [--focus "…"]',
|
|
122
123
|
],
|
|
124
|
+
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review",
|
|
123
125
|
},
|
|
124
126
|
},
|
|
125
127
|
bin: 'agy',
|
|
@@ -44,6 +44,10 @@ import {
|
|
|
44
44
|
readSettingsFile,
|
|
45
45
|
resolveEffectiveMode,
|
|
46
46
|
} from './velocity-profile.mjs';
|
|
47
|
+
// The gate-hook writer's own wired-detection + placed path — reused by the settings survey (one
|
|
48
|
+
// implementation; gate-hook imports only node builtins + velocity-profile, so no cycle).
|
|
49
|
+
import { HOOK_FILE_REL as GATE_HOOK_FILE_REL, isHookWired } from './gate-hook.mjs';
|
|
50
|
+
import { GATES_REL } from './run-gates.mjs';
|
|
47
51
|
// The status vocabulary (manifestState constants, internal→public maps, display names, the no-leak
|
|
48
52
|
// forbidden set) lives in the frozen labels.mjs LEAF (Plan §4.2 B1) so the import graph is acyclic —
|
|
49
53
|
// nothing imports family-registry for vocabulary. Imported here for internal use; the public subset is
|
|
@@ -438,11 +442,32 @@ export const surveyVelocity = (dir, deps = {}) => {
|
|
|
438
442
|
}
|
|
439
443
|
};
|
|
440
444
|
|
|
445
|
+
// gate hook: the opt-in PreToolUse gate-approval hook (Mode: hook) — wired (the settings entry, in
|
|
446
|
+
// EITHER settings file: the hooks contract merges both), the placed hook file, and the gate
|
|
447
|
+
// declaration it consumes. Read-only; the wired-detection is REUSED from the writer (gate-hook.mjs
|
|
448
|
+
// isHookWired — one implementation, never a drifting copy).
|
|
449
|
+
export const surveyGateHook = (dir, deps = {}) => {
|
|
450
|
+
try {
|
|
451
|
+
const d = resolve(dir);
|
|
452
|
+
const exists = deps.exists ?? existsSync;
|
|
453
|
+
const project = readSettingsFile(join(d, SETTINGS_FILE), { ...deps, cwd: d });
|
|
454
|
+
const local = readSettingsFile(join(d, SETTINGS_LOCAL_FILE), { ...deps, cwd: d });
|
|
455
|
+
return {
|
|
456
|
+
wired: isHookWired(project.data) || isHookWired(local.data),
|
|
457
|
+
filePlaced: Boolean(exists(join(d, GATE_HOOK_FILE_REL))),
|
|
458
|
+
declarationPresent: Boolean(exists(join(d, GATES_REL))),
|
|
459
|
+
};
|
|
460
|
+
} catch (err) {
|
|
461
|
+
return { error: localizeError(err) };
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
441
465
|
// the project-scoped settings survey (needs a project dir). Each area is independently localized-on-error.
|
|
442
466
|
export const surveySettings = (dir, deps = {}) => ({
|
|
443
467
|
recipes: surveyRecipes(dir, deps),
|
|
444
468
|
attribution: surveyAttribution(dir, deps),
|
|
445
469
|
velocity: surveyVelocity(dir, deps),
|
|
470
|
+
hook: surveyGateHook(dir, deps),
|
|
446
471
|
});
|
|
447
472
|
|
|
448
473
|
// bridges: HOST-scoped (no project needed). Wrapper command NAMES come from FAMILY_MEMBERS[].wrapperCmds
|