@rasensio/aidlc 1.86.0 → 1.88.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/dist/change/types.d.ts +1 -1
- package/dist/change/types.d.ts.map +1 -1
- package/dist/commands/amend.d.ts.map +1 -1
- package/dist/commands/amend.js +12 -2
- package/dist/commands/amend.js.map +1 -1
- package/dist/commands/autopilot.d.ts.map +1 -1
- package/dist/commands/autopilot.js +9 -0
- package/dist/commands/autopilot.js.map +1 -1
- package/dist/commands/claim.d.ts +11 -0
- package/dist/commands/claim.d.ts.map +1 -1
- package/dist/commands/claim.js +25 -11
- package/dist/commands/claim.js.map +1 -1
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +5 -0
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -10
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/transition.d.ts.map +1 -1
- package/dist/commands/transition.js +118 -29
- package/dist/commands/transition.js.map +1 -1
- package/dist/concurrency/claim-store.d.ts +24 -6
- package/dist/concurrency/claim-store.d.ts.map +1 -1
- package/dist/concurrency/claim-store.js +39 -15
- package/dist/concurrency/claim-store.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +3 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/next-steps.d.ts.map +1 -1
- package/dist/core/next-steps.js +8 -0
- package/dist/core/next-steps.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/doctor/migrations/instance-completion.d.ts +5 -0
- package/dist/doctor/migrations/instance-completion.d.ts.map +1 -1
- package/dist/doctor/migrations/instance-completion.js +13 -4
- package/dist/doctor/migrations/instance-completion.js.map +1 -1
- package/dist/doctor/migrations/prune-registry.js +2 -2
- package/dist/doctor/migrations/prune-registry.js.map +1 -1
- package/dist/events/event-bus.d.ts +2 -1
- package/dist/events/event-bus.d.ts.map +1 -1
- package/dist/events/event-bus.js +25 -11
- package/dist/events/event-bus.js.map +1 -1
- package/dist/hooks/approval-guard.d.ts +51 -0
- package/dist/hooks/approval-guard.d.ts.map +1 -0
- package/dist/hooks/approval-guard.js +293 -0
- package/dist/hooks/approval-guard.js.map +1 -0
- package/dist/hooks/human-only.d.ts +61 -0
- package/dist/hooks/human-only.d.ts.map +1 -0
- package/dist/hooks/human-only.js +82 -0
- package/dist/hooks/human-only.js.map +1 -0
- package/dist/hooks/install.d.ts +1 -1
- package/dist/hooks/install.d.ts.map +1 -1
- package/dist/hooks/install.js +56 -9
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/lifecycle-context.d.ts +1 -1
- package/dist/hooks/lifecycle-context.d.ts.map +1 -1
- package/dist/hooks/lifecycle-context.js +6 -2
- package/dist/hooks/lifecycle-context.js.map +1 -1
- package/dist/roadmap/promote.d.ts +32 -0
- package/dist/roadmap/promote.d.ts.map +1 -1
- package/dist/roadmap/promote.js +49 -1
- package/dist/roadmap/promote.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval guard: a Claude Code `PreToolUse` hook that blocks an agent from
|
|
3
|
+
* running the commands only a person may run, or writing the autopilot
|
|
4
|
+
* approval record (agent-cannot-approve-itself/AC-8..AC-13).
|
|
5
|
+
*
|
|
6
|
+
* The opposite contract to the phase guard (`guard.ts`), on purpose, and the
|
|
7
|
+
* same one as the reviewer guard (`compile/reviewer-guard.ts`): this is a
|
|
8
|
+
* boundary, so every way it can fail is a block. The script exits 2 on anything
|
|
9
|
+
* it cannot read, and the hook command turns any other exit — a missing script,
|
|
10
|
+
* a missing `node`, a crash — into 2 as well.
|
|
11
|
+
*
|
|
12
|
+
* It is a standalone script rather than an `aidlc hook` subcommand because it
|
|
13
|
+
* runs before every agent shell and file-write call: starting the CLI each time
|
|
14
|
+
* is slow, and a broken build would block every call (design D1). The logic is
|
|
15
|
+
* written once, here, as `guardVerdict`, and embedded in the script with
|
|
16
|
+
* `Function.prototype.toString` — so it must stay self-contained.
|
|
17
|
+
*
|
|
18
|
+
* It is a denylist, and a shell denylist can be talked around (`sh -c`, a
|
|
19
|
+
* variable, a script file). The CLI's terminal check (`human-only.ts`) is the
|
|
20
|
+
* line; this is the early, logged layer (design D2).
|
|
21
|
+
*
|
|
22
|
+
* Design: design.md C3
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { HUMAN_ONLY, SUBJECT_LIMIT } from './human-only.js';
|
|
27
|
+
import { LIFECYCLE_HOOK_MARKER } from './install.js';
|
|
28
|
+
export const APPROVAL_GUARD_PATH = '.claude/hooks/aidlc-approval-guard.mjs';
|
|
29
|
+
export const APPROVAL_GUARD_MATCHER = 'Bash|Edit|Write|MultiEdit|NotebookEdit';
|
|
30
|
+
/**
|
|
31
|
+
* The settings.json command. Exit 0 allows and 2 blocks with the script's own
|
|
32
|
+
* reason; any other status means the guard did not run, which blocks too, with a
|
|
33
|
+
* line saying so (AC-12). `$CLAUDE_PROJECT_DIR` because a hook runs in the
|
|
34
|
+
* session's working directory, which need not be the root.
|
|
35
|
+
*/
|
|
36
|
+
export function approvalGuardCommand() {
|
|
37
|
+
return (`node "$CLAUDE_PROJECT_DIR/${APPROVAL_GUARD_PATH}"; s=$?; [ "$s" = 0 ] && exit 0; ` +
|
|
38
|
+
`[ "$s" = 2 ] || echo 'AIDLC approval guard could not run, so the call is blocked; reinstall it with: aidlc hook install' >&2; ` +
|
|
39
|
+
`exit 2 ${LIFECYCLE_HOOK_MARKER}`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Block or pass one `PreToolUse` payload. Self-contained: no imports and no
|
|
43
|
+
* outer references, because the rendered script carries its source text.
|
|
44
|
+
*/
|
|
45
|
+
export function guardVerdict(input, rules) {
|
|
46
|
+
const STATE_FILE = 'aidlc/autopilot/state.json';
|
|
47
|
+
const FILE_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];
|
|
48
|
+
const unreadable = {
|
|
49
|
+
block: true,
|
|
50
|
+
rule: 'unreadable-input',
|
|
51
|
+
reason: 'the hook input could not be read as a tool call, so the approval guard blocks it',
|
|
52
|
+
subject: '',
|
|
53
|
+
};
|
|
54
|
+
if (typeof input !== 'object' || input === null)
|
|
55
|
+
return unreadable;
|
|
56
|
+
const toolName = input.tool_name;
|
|
57
|
+
const toolInput = input.tool_input;
|
|
58
|
+
if (typeof toolName !== 'string' || typeof toolInput !== 'object' || toolInput === null)
|
|
59
|
+
return unreadable;
|
|
60
|
+
if (FILE_TOOLS.includes(toolName)) {
|
|
61
|
+
const target = toolInput.file_path ?? toolInput.notebook_path;
|
|
62
|
+
if (typeof target !== 'string')
|
|
63
|
+
return unreadable;
|
|
64
|
+
if (!target.replace(/\\/g, '/').endsWith(STATE_FILE))
|
|
65
|
+
return { block: false };
|
|
66
|
+
return {
|
|
67
|
+
block: true,
|
|
68
|
+
rule: 'approval-record',
|
|
69
|
+
reason: 'autopilot approvals are recorded only by `aidlc autopilot approve`, run by a person in their own terminal',
|
|
70
|
+
subject: target,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (toolName !== 'Bash')
|
|
74
|
+
return { block: false };
|
|
75
|
+
const command = toolInput.command;
|
|
76
|
+
if (typeof command !== 'string')
|
|
77
|
+
return unreadable;
|
|
78
|
+
// Words as a POSIX shell would split them, for this purpose only: quotes and
|
|
79
|
+
// backslashes join, so a quoted mention is one word that names no program
|
|
80
|
+
// (AC-9). Operators end a segment. A `$(…)` or backtick body inside double
|
|
81
|
+
// quotes still runs, so it is scanned as a command of its own.
|
|
82
|
+
function split(text) {
|
|
83
|
+
const segments = [];
|
|
84
|
+
const nested = [];
|
|
85
|
+
let words = [];
|
|
86
|
+
let word = '';
|
|
87
|
+
let inWord = false;
|
|
88
|
+
const endWord = () => {
|
|
89
|
+
if (inWord)
|
|
90
|
+
words.push(word);
|
|
91
|
+
word = '';
|
|
92
|
+
inWord = false;
|
|
93
|
+
};
|
|
94
|
+
const endSegment = () => {
|
|
95
|
+
endWord();
|
|
96
|
+
if (words.length > 0)
|
|
97
|
+
segments.push(words);
|
|
98
|
+
words = [];
|
|
99
|
+
};
|
|
100
|
+
const closingParen = (from) => {
|
|
101
|
+
let depth = 1;
|
|
102
|
+
for (let j = from; j < text.length; j++) {
|
|
103
|
+
if (text[j] === '(')
|
|
104
|
+
depth++;
|
|
105
|
+
else if (text[j] === ')' && --depth === 0)
|
|
106
|
+
return j;
|
|
107
|
+
}
|
|
108
|
+
return text.length;
|
|
109
|
+
};
|
|
110
|
+
for (let i = 0; i < text.length; i++) {
|
|
111
|
+
const c = text[i];
|
|
112
|
+
if (c === "'") {
|
|
113
|
+
const end = text.indexOf("'", i + 1);
|
|
114
|
+
const stop = end === -1 ? text.length : end;
|
|
115
|
+
word += text.slice(i + 1, stop);
|
|
116
|
+
inWord = true;
|
|
117
|
+
i = stop;
|
|
118
|
+
}
|
|
119
|
+
else if (c === '"') {
|
|
120
|
+
inWord = true;
|
|
121
|
+
let j = i + 1;
|
|
122
|
+
for (; j < text.length && text[j] !== '"'; j++) {
|
|
123
|
+
const d = text[j];
|
|
124
|
+
if (d === '\\' && j + 1 < text.length) {
|
|
125
|
+
word += text[++j];
|
|
126
|
+
}
|
|
127
|
+
else if (d === '$' && text[j + 1] === '(') {
|
|
128
|
+
const end = closingParen(j + 2);
|
|
129
|
+
nested.push(text.slice(j + 2, end));
|
|
130
|
+
word += text.slice(j, end + 1);
|
|
131
|
+
j = end;
|
|
132
|
+
}
|
|
133
|
+
else if (d === '`') {
|
|
134
|
+
const end = text.indexOf('`', j + 1);
|
|
135
|
+
const stop = end === -1 ? text.length : end;
|
|
136
|
+
nested.push(text.slice(j + 1, stop));
|
|
137
|
+
word += text.slice(j, stop + 1);
|
|
138
|
+
j = stop;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
word += d;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
i = j;
|
|
145
|
+
}
|
|
146
|
+
else if (c === '\\') {
|
|
147
|
+
if (i + 1 < text.length && text[i + 1] !== '\n') {
|
|
148
|
+
word += text[i + 1];
|
|
149
|
+
inWord = true;
|
|
150
|
+
}
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
else if (c === ' ' || c === '\t' || c === '\r') {
|
|
154
|
+
endWord();
|
|
155
|
+
}
|
|
156
|
+
else if (c === '#' && !inWord) {
|
|
157
|
+
const nl = text.indexOf('\n', i);
|
|
158
|
+
i = nl === -1 ? text.length : nl - 1;
|
|
159
|
+
}
|
|
160
|
+
else if (';&|()\n`'.includes(c) || (c === '$' && text[i + 1] === '(')) {
|
|
161
|
+
endSegment();
|
|
162
|
+
if (c === '$')
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
else if (c === '<' || c === '>') {
|
|
166
|
+
endWord();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
word += c;
|
|
170
|
+
inWord = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
endSegment();
|
|
174
|
+
return { segments, nested };
|
|
175
|
+
}
|
|
176
|
+
function matchSegment(words) {
|
|
177
|
+
const at = words.findIndex((w) => w.split('/').pop() === 'aidlc' || w.endsWith('dist/cli.js'));
|
|
178
|
+
if (at === -1)
|
|
179
|
+
return null;
|
|
180
|
+
const args = words.slice(at + 1);
|
|
181
|
+
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
182
|
+
for (const rule of rules) {
|
|
183
|
+
if (!rule.words.every((w, k) => positionals[k] === w))
|
|
184
|
+
continue;
|
|
185
|
+
const flag = rule.flag;
|
|
186
|
+
if (flag !== null && !args.some((a) => a === flag || a.startsWith(`${flag}=`)))
|
|
187
|
+
continue;
|
|
188
|
+
return rule;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
function scan(text, depth) {
|
|
193
|
+
const { segments, nested } = split(text);
|
|
194
|
+
for (const words of segments) {
|
|
195
|
+
const hit = matchSegment(words);
|
|
196
|
+
if (hit !== null)
|
|
197
|
+
return hit;
|
|
198
|
+
}
|
|
199
|
+
if (depth >= 4)
|
|
200
|
+
return null;
|
|
201
|
+
for (const inner of nested) {
|
|
202
|
+
const hit = scan(inner, depth + 1);
|
|
203
|
+
if (hit !== null)
|
|
204
|
+
return hit;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const hit = scan(command, 0);
|
|
209
|
+
if (hit === null)
|
|
210
|
+
return { block: false };
|
|
211
|
+
return {
|
|
212
|
+
block: true,
|
|
213
|
+
rule: hit.rule,
|
|
214
|
+
reason: `${hit.label} needs a person: an agent may not approve, override or confirm its own gates. ` +
|
|
215
|
+
'Ask the person to run it in their own terminal',
|
|
216
|
+
subject: command,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** The script `aidlc hook install` writes to `APPROVAL_GUARD_PATH`. */
|
|
220
|
+
export function renderApprovalGuard() {
|
|
221
|
+
return `#!/usr/bin/env node
|
|
222
|
+
// Generated by \`aidlc hook install\` — do not edit by hand; the next install overwrites it.
|
|
223
|
+
//
|
|
224
|
+
// AIDLC approval guard. Claude Code runs it before every Bash, Edit, Write,
|
|
225
|
+
// MultiEdit and NotebookEdit call: exit 0 lets the call run, exit 2 blocks it and
|
|
226
|
+
// shows stderr to the agent. The hook command blocks on any other exit as well.
|
|
227
|
+
|
|
228
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
229
|
+
import { dirname, join, resolve } from 'node:path';
|
|
230
|
+
|
|
231
|
+
const RULES = ${JSON.stringify(HUMAN_ONLY)};
|
|
232
|
+
const SUBJECT_LIMIT = ${SUBJECT_LIMIT};
|
|
233
|
+
|
|
234
|
+
${guardVerdict.toString()}
|
|
235
|
+
|
|
236
|
+
// The refusal log: <git-common-dir>/aidlc/refusals.ndjson, found by reading
|
|
237
|
+
// files rather than spawning git; .aidlc/refusals.ndjson outside a repository.
|
|
238
|
+
function logPath(root) {
|
|
239
|
+
for (let dir = resolve(root); ; dir = dirname(dir)) {
|
|
240
|
+
const dotgit = join(dir, '.git');
|
|
241
|
+
try {
|
|
242
|
+
let gitdir = dotgit;
|
|
243
|
+
if (!statSync(dotgit).isDirectory()) {
|
|
244
|
+
const m = /^gitdir:\\s*(.+)$/m.exec(readFileSync(dotgit, 'utf8'));
|
|
245
|
+
if (m === null) break;
|
|
246
|
+
gitdir = resolve(dir, m[1].trim());
|
|
247
|
+
}
|
|
248
|
+
let common = gitdir;
|
|
249
|
+
try {
|
|
250
|
+
common = resolve(gitdir, readFileSync(join(gitdir, 'commondir'), 'utf8').trim());
|
|
251
|
+
} catch {
|
|
252
|
+
// a primary checkout has no commondir file
|
|
253
|
+
}
|
|
254
|
+
return join(common, 'aidlc', 'refusals.ndjson');
|
|
255
|
+
} catch {
|
|
256
|
+
// no .git here
|
|
257
|
+
}
|
|
258
|
+
if (dirname(dir) === dir) break;
|
|
259
|
+
}
|
|
260
|
+
return join(resolve(root), '.aidlc', 'refusals.ndjson');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let verdict;
|
|
264
|
+
let session;
|
|
265
|
+
try {
|
|
266
|
+
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
267
|
+
session = input?.session_id;
|
|
268
|
+
verdict = guardVerdict(input, RULES);
|
|
269
|
+
} catch {
|
|
270
|
+
verdict = guardVerdict(null, RULES);
|
|
271
|
+
}
|
|
272
|
+
if (!verdict.block) process.exit(0);
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const path = logPath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
276
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
277
|
+
const entry = {
|
|
278
|
+
at: new Date().toISOString(),
|
|
279
|
+
where: 'guard',
|
|
280
|
+
rule: verdict.rule,
|
|
281
|
+
reason: verdict.reason,
|
|
282
|
+
subject: String(verdict.subject).slice(0, SUBJECT_LIMIT),
|
|
283
|
+
};
|
|
284
|
+
if (typeof session === 'string') entry.session = session;
|
|
285
|
+
appendFileSync(path, JSON.stringify(entry) + '\\n');
|
|
286
|
+
} catch {
|
|
287
|
+
// a refusal that cannot be logged is still a refusal
|
|
288
|
+
}
|
|
289
|
+
process.stderr.write('AIDLC: ' + verdict.reason + '.\\n');
|
|
290
|
+
process.exit(2);
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=approval-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-guard.js","sourceRoot":"","sources":["../../src/hooks/approval-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commands only a person may run, and the check that refuses them to an
|
|
3
|
+
* agent (agent-cannot-approve-itself/AC-1..AC-6, AC-14, AC-15).
|
|
4
|
+
*
|
|
5
|
+
* Each command exists so that a person can say yes: carry a paused item on,
|
|
6
|
+
* pass the ownership gate, reopen from testing. An agent that runs one approves
|
|
7
|
+
* its own work, which empties "approved" of meaning.
|
|
8
|
+
*
|
|
9
|
+
* This check is the line; the approval guard (`approval-guard.ts`) is the early,
|
|
10
|
+
* logged layer in Claude Code (design D2). A shell denylist can always be talked
|
|
11
|
+
* around, but no agent harness gives its shell a terminal on stdin, so the
|
|
12
|
+
* terminal test catches every route the guard misses, in every harness.
|
|
13
|
+
*
|
|
14
|
+
* Design: design.md C1
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
export type HumanRule = 'autopilot-approve' | 'transition-override' | 'amend-confirm';
|
|
19
|
+
export interface HumanOnlyCommand {
|
|
20
|
+
rule: HumanRule;
|
|
21
|
+
/** How the command is named to whoever is refused. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** The positional words after `aidlc` that name the command. */
|
|
24
|
+
words: string[];
|
|
25
|
+
/** The flag that makes it need a person, or null when the command always does. */
|
|
26
|
+
flag: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** The one table. The approval guard embeds it, so both layers refuse the same set. */
|
|
29
|
+
export declare const HUMAN_ONLY: readonly HumanOnlyCommand[];
|
|
30
|
+
/** The longest command or path a log line keeps. */
|
|
31
|
+
export declare const SUBJECT_LIMIT = 500;
|
|
32
|
+
export interface RefusalEntry {
|
|
33
|
+
at: string;
|
|
34
|
+
where: 'cli' | 'guard';
|
|
35
|
+
rule: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
session?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Why no person is present, or null when one is. `AIDLC_UNATTENDED` comes first
|
|
42
|
+
* so the reason names it even at a terminal (AC-4). Only stdin is read: a person
|
|
43
|
+
* piping the output through `tee` is still a person.
|
|
44
|
+
*/
|
|
45
|
+
export declare function absenceReason(env: NodeJS.ProcessEnv, stdinIsTTY: boolean | undefined): string | null;
|
|
46
|
+
export declare function refusalMessage(cmd: HumanOnlyCommand, why: string): string;
|
|
47
|
+
/** Shared by every worktree, and never committed: it records what happened here. */
|
|
48
|
+
export declare function refusalsPath(projectRoot: string): string;
|
|
49
|
+
/** Append one line. Never throws: a refusal that cannot be logged is still a refusal (AC-15). */
|
|
50
|
+
export declare function logRefusal(projectRoot: string, entry: RefusalEntry): void;
|
|
51
|
+
/**
|
|
52
|
+
* Null when a person is present. Otherwise logs the refusal and returns the
|
|
53
|
+
* message, for the caller to print in its own way (`amend --json` wraps it).
|
|
54
|
+
*/
|
|
55
|
+
export declare function requireHuman(rule: HumanRule, opts: {
|
|
56
|
+
root: string;
|
|
57
|
+
env?: NodeJS.ProcessEnv;
|
|
58
|
+
stdinIsTTY?: boolean;
|
|
59
|
+
subject?: string;
|
|
60
|
+
}): string | null;
|
|
61
|
+
//# sourceMappingURL=human-only.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-only.d.ts","sourceRoot":"","sources":["../../src/hooks/human-only.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,SAAS,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAEtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kFAAkF;IAClF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,uFAAuF;AACvF,eAAO,MAAM,UAAU,EAAE,SAAS,gBAAgB,EAIjD,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpG;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,iGAAiG;AACjG,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CASzE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACtF,MAAM,GAAG,IAAI,CAaf"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commands only a person may run, and the check that refuses them to an
|
|
3
|
+
* agent (agent-cannot-approve-itself/AC-1..AC-6, AC-14, AC-15).
|
|
4
|
+
*
|
|
5
|
+
* Each command exists so that a person can say yes: carry a paused item on,
|
|
6
|
+
* pass the ownership gate, reopen from testing. An agent that runs one approves
|
|
7
|
+
* its own work, which empties "approved" of meaning.
|
|
8
|
+
*
|
|
9
|
+
* This check is the line; the approval guard (`approval-guard.ts`) is the early,
|
|
10
|
+
* logged layer in Claude Code (design D2). A shell denylist can always be talked
|
|
11
|
+
* around, but no agent harness gives its shell a terminal on stdin, so the
|
|
12
|
+
* terminal test catches every route the guard misses, in every harness.
|
|
13
|
+
*
|
|
14
|
+
* Design: design.md C1
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { gitCommonDir } from '../concurrency/common-dir.js';
|
|
21
|
+
/** The one table. The approval guard embeds it, so both layers refuse the same set. */
|
|
22
|
+
export const HUMAN_ONLY = [
|
|
23
|
+
{ rule: 'autopilot-approve', label: 'aidlc autopilot approve', words: ['autopilot', 'approve'], flag: null },
|
|
24
|
+
{ rule: 'transition-override', label: 'aidlc transition --override', words: ['transition'], flag: '--override' },
|
|
25
|
+
{ rule: 'amend-confirm', label: 'aidlc amend --confirm', words: ['amend'], flag: '--confirm' },
|
|
26
|
+
];
|
|
27
|
+
/** The longest command or path a log line keeps. */
|
|
28
|
+
export const SUBJECT_LIMIT = 500;
|
|
29
|
+
/**
|
|
30
|
+
* Why no person is present, or null when one is. `AIDLC_UNATTENDED` comes first
|
|
31
|
+
* so the reason names it even at a terminal (AC-4). Only stdin is read: a person
|
|
32
|
+
* piping the output through `tee` is still a person.
|
|
33
|
+
*/
|
|
34
|
+
export function absenceReason(env, stdinIsTTY) {
|
|
35
|
+
if (env.AIDLC_UNATTENDED === '1')
|
|
36
|
+
return 'AIDLC_UNATTENDED=1 says no person is attending this run';
|
|
37
|
+
if (stdinIsTTY !== true)
|
|
38
|
+
return 'stdin is not a terminal, so no person is at the keyboard';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function refusalMessage(cmd, why) {
|
|
42
|
+
return `${cmd.label} needs a person: ${why}. Run it yourself in your own terminal.`;
|
|
43
|
+
}
|
|
44
|
+
/** Shared by every worktree, and never committed: it records what happened here. */
|
|
45
|
+
export function refusalsPath(projectRoot) {
|
|
46
|
+
const common = gitCommonDir(projectRoot);
|
|
47
|
+
return common === null
|
|
48
|
+
? join(projectRoot, '.aidlc', 'refusals.ndjson')
|
|
49
|
+
: join(common, 'aidlc', 'refusals.ndjson');
|
|
50
|
+
}
|
|
51
|
+
/** Append one line. Never throws: a refusal that cannot be logged is still a refusal (AC-15). */
|
|
52
|
+
export function logRefusal(projectRoot, entry) {
|
|
53
|
+
try {
|
|
54
|
+
const path = refusalsPath(projectRoot);
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
const line = { ...entry, subject: entry.subject.slice(0, SUBJECT_LIMIT) };
|
|
57
|
+
appendFileSync(path, `${JSON.stringify(line)}\n`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// nothing to do: the caller refuses either way
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Null when a person is present. Otherwise logs the refusal and returns the
|
|
65
|
+
* message, for the caller to print in its own way (`amend --json` wraps it).
|
|
66
|
+
*/
|
|
67
|
+
export function requireHuman(rule, opts) {
|
|
68
|
+
const why = absenceReason(opts.env ?? process.env, 'stdinIsTTY' in opts ? opts.stdinIsTTY : process.stdin.isTTY);
|
|
69
|
+
if (why === null)
|
|
70
|
+
return null;
|
|
71
|
+
const cmd = HUMAN_ONLY.find((c) => c.rule === rule);
|
|
72
|
+
const message = refusalMessage(cmd, why);
|
|
73
|
+
logRefusal(opts.root, {
|
|
74
|
+
at: new Date().toISOString(),
|
|
75
|
+
where: 'cli',
|
|
76
|
+
rule,
|
|
77
|
+
reason: why,
|
|
78
|
+
subject: opts.subject ?? process.argv.slice(2).join(' '),
|
|
79
|
+
});
|
|
80
|
+
return message;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=human-only.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-only.js","sourceRoot":"","sources":["../../src/hooks/human-only.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/install.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
export type LifecyclePlatform = 'claude-code' | 'codex';
|
|
26
|
-
export type LifecycleHook = 'session-start' | 'guard';
|
|
26
|
+
export type LifecycleHook = 'session-start' | 'guard' | 'approval-guard';
|
|
27
27
|
export declare const LIFECYCLE_PLATFORMS: readonly LifecyclePlatform[];
|
|
28
28
|
export declare const LIFECYCLE_HOOK_MARKER = "#aidlc-lifecycle-hook";
|
|
29
29
|
export declare const GUARD_MATCHER = "Edit|Write|MultiEdit|NotebookEdit";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AASH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,OAAO,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEzE,eAAO,MAAM,mBAAmB,EAAE,SAAS,iBAAiB,EAA6B,CAAC;AAC1F,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAC7D,eAAO,MAAM,aAAa,sCAAsC,CAAC;AA2BjE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,iBAAiC,GAAG,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAO5G;AAkCD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAErF;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAElE;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAElE;AAsFD,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,QAAQ,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrD,WAAW,CAmBb;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,QAAQ,EAAE,iBAAiB,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GACzD,WAAW,CAoBb;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,aAAa,GACnB,OAAO,CAOT;AAED,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,EAAqC,MAAM,CAAC;AAEzE,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,QAA0B,CAAC"}
|
package/dist/hooks/install.js
CHANGED
|
@@ -25,13 +25,18 @@
|
|
|
25
25
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
26
|
import { dirname, join } from 'node:path';
|
|
27
27
|
import { cliEntryPath } from '../cost/hooks/entry.js';
|
|
28
|
+
import { APPROVAL_GUARD_MATCHER, APPROVAL_GUARD_PATH, approvalGuardCommand, renderApprovalGuard } from './approval-guard.js';
|
|
28
29
|
import { shimInvocation, shimProblem, writeShim } from '../cost/hooks/shim.js';
|
|
29
30
|
export const LIFECYCLE_PLATFORMS = ['claude-code', 'codex'];
|
|
30
31
|
export const LIFECYCLE_HOOK_MARKER = '#aidlc-lifecycle-hook';
|
|
31
32
|
export const GUARD_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
|
|
32
33
|
/** A hook needs only to start the CLI and read a few files; ten seconds is generous. */
|
|
33
34
|
const TIMEOUT_SECONDS = 10;
|
|
34
|
-
const EVENT = {
|
|
35
|
+
const EVENT = {
|
|
36
|
+
'session-start': 'SessionStart',
|
|
37
|
+
guard: 'PreToolUse',
|
|
38
|
+
'approval-guard': 'PreToolUse',
|
|
39
|
+
};
|
|
35
40
|
/**
|
|
36
41
|
* The two hook commands for a platform. Both run through the cost-capture shim,
|
|
37
42
|
* which finds an absolute `node` and this project's CLI at run time and never
|
|
@@ -43,6 +48,7 @@ export function lifecycleCommands(platform = 'claude-code') {
|
|
|
43
48
|
return {
|
|
44
49
|
'session-start': `${base} hook session-start ${LIFECYCLE_HOOK_MARKER}`,
|
|
45
50
|
guard: `${base} hook guard ${LIFECYCLE_HOOK_MARKER}`,
|
|
51
|
+
'approval-guard': approvalGuardCommand(),
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
/** The one table mapping a platform to its lifecycle hooks (agent-portability/AC-41). */
|
|
@@ -79,10 +85,31 @@ export function installCommand(platform) {
|
|
|
79
85
|
function isMarked(h) {
|
|
80
86
|
return typeof h.command === 'string' && h.command.endsWith(LIFECYCLE_HOOK_MARKER);
|
|
81
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Which lifecycle hook a marked command is, read from its text. The phase guard
|
|
90
|
+
* and the approval guard share `PreToolUse`, so identity by marker alone would
|
|
91
|
+
* have each install replace the other (agent-cannot-approve-itself, design C4).
|
|
92
|
+
*/
|
|
93
|
+
function kindOf(h) {
|
|
94
|
+
if (!isMarked(h))
|
|
95
|
+
return null;
|
|
96
|
+
if (h.command.includes(APPROVAL_GUARD_PATH))
|
|
97
|
+
return 'approval-guard';
|
|
98
|
+
if (h.command.includes(' hook guard '))
|
|
99
|
+
return 'guard';
|
|
100
|
+
return 'session-start';
|
|
101
|
+
}
|
|
102
|
+
function isKind(which) {
|
|
103
|
+
return which === null ? isMarked : (h) => kindOf(h) === which;
|
|
104
|
+
}
|
|
82
105
|
function groupFor(which, platform) {
|
|
83
106
|
const hook = { type: 'command', command: lifecycleCommands(platform)[which], timeout: TIMEOUT_SECONDS };
|
|
84
107
|
// No matcher on SessionStart, so it fires on startup, resume, clear and compact.
|
|
85
|
-
|
|
108
|
+
if (which === 'guard')
|
|
109
|
+
return { matcher: GUARD_MATCHER, hooks: [hook] };
|
|
110
|
+
if (which === 'approval-guard')
|
|
111
|
+
return { matcher: APPROVAL_GUARD_MATCHER, hooks: [hook] };
|
|
112
|
+
return { hooks: [hook] };
|
|
86
113
|
}
|
|
87
114
|
function load(path) {
|
|
88
115
|
if (!existsSync(path))
|
|
@@ -105,12 +132,13 @@ function save(path, raw, file) {
|
|
|
105
132
|
* Only a group this takes a command from is dropped for being left empty; a
|
|
106
133
|
* user's group is passed through as it was, empty or not.
|
|
107
134
|
*/
|
|
108
|
-
function withoutMarked(groups) {
|
|
135
|
+
function withoutMarked(groups, which = null) {
|
|
136
|
+
const matches = isKind(which);
|
|
109
137
|
let removed = 0;
|
|
110
138
|
const kept = [];
|
|
111
139
|
for (const g of groups ?? []) {
|
|
112
140
|
const hooks = Array.isArray(g.hooks) ? g.hooks : [];
|
|
113
|
-
const unmarked = hooks.filter((h) => !
|
|
141
|
+
const unmarked = hooks.filter((h) => !matches(h));
|
|
114
142
|
removed += hooks.length - unmarked.length;
|
|
115
143
|
if (unmarked.length === hooks.length)
|
|
116
144
|
kept.push(g);
|
|
@@ -122,7 +150,7 @@ function withoutMarked(groups) {
|
|
|
122
150
|
/** Exactly one marked command under the event, in the group shape the installer writes. */
|
|
123
151
|
function isCurrent(groups, which, platform) {
|
|
124
152
|
const expected = groupFor(which, platform);
|
|
125
|
-
const marked = (groups ?? []).filter((g) => (g.hooks ?? []).some(
|
|
153
|
+
const marked = (groups ?? []).filter((g) => (g.hooks ?? []).some(isKind(which)));
|
|
126
154
|
return marked.length === 1 && JSON.stringify(marked[0]) === JSON.stringify(expected);
|
|
127
155
|
}
|
|
128
156
|
function ensure(file, which, platform) {
|
|
@@ -131,7 +159,7 @@ function ensure(file, which, platform) {
|
|
|
131
159
|
// Left where it is when already current, so a re-install does not even reorder
|
|
132
160
|
// the file (AC-16).
|
|
133
161
|
if (!isCurrent(hooks[event], which, platform)) {
|
|
134
|
-
hooks[event] = [...withoutMarked(hooks[event]).groups, groupFor(which, platform)];
|
|
162
|
+
hooks[event] = [...withoutMarked(hooks[event], which).groups, groupFor(which, platform)];
|
|
135
163
|
}
|
|
136
164
|
file.hooks = hooks;
|
|
137
165
|
}
|
|
@@ -154,7 +182,26 @@ export function installLifecycleHooks(projectRoot, opts) {
|
|
|
154
182
|
ensure(file, 'session-start', opts.platform);
|
|
155
183
|
if (opts.guard)
|
|
156
184
|
ensure(file, 'guard', opts.platform);
|
|
157
|
-
|
|
185
|
+
// Not opt-in: an approval only means something when no agent can give it
|
|
186
|
+
// (agent-cannot-approve-itself/AC-16). Claude Code only; every harness gets the
|
|
187
|
+
// CLI's own terminal check.
|
|
188
|
+
let scriptWritten = false;
|
|
189
|
+
if (opts.platform === 'claude-code') {
|
|
190
|
+
ensure(file, 'approval-guard', opts.platform);
|
|
191
|
+
scriptWritten = writeApprovalGuard(projectRoot);
|
|
192
|
+
}
|
|
193
|
+
const settingsWritten = save(path, raw, file);
|
|
194
|
+
return { written: settingsWritten || scriptWritten, path };
|
|
195
|
+
}
|
|
196
|
+
/** Write the approval guard script when its text would change; true when written. */
|
|
197
|
+
function writeApprovalGuard(projectRoot) {
|
|
198
|
+
const path = join(projectRoot, APPROVAL_GUARD_PATH);
|
|
199
|
+
const text = renderApprovalGuard();
|
|
200
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === text)
|
|
201
|
+
return false;
|
|
202
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
203
|
+
writeFileSync(path, text);
|
|
204
|
+
return true;
|
|
158
205
|
}
|
|
159
206
|
/**
|
|
160
207
|
* Remove the lifecycle hooks from one platform's file, or only the guard
|
|
@@ -173,7 +220,7 @@ export function removeLifecycleHooks(projectRoot, opts) {
|
|
|
173
220
|
for (const event of events) {
|
|
174
221
|
if (!(event in file.hooks))
|
|
175
222
|
continue;
|
|
176
|
-
const result = withoutMarked(file.hooks[event]);
|
|
223
|
+
const result = withoutMarked(file.hooks[event], opts.guardOnly ? 'guard' : null);
|
|
177
224
|
if (result.removed === 0)
|
|
178
225
|
continue;
|
|
179
226
|
removed += result.removed;
|
|
@@ -194,7 +241,7 @@ export function removeLifecycleHooks(projectRoot, opts) {
|
|
|
194
241
|
export function lifecycleHookInstalled(projectRoot, platform, which) {
|
|
195
242
|
try {
|
|
196
243
|
const { file } = load(hookFilePath(projectRoot, platform));
|
|
197
|
-
return (file.hooks?.[EVENT[which]] ?? []).some((g) => (g.hooks ?? []).some(
|
|
244
|
+
return (file.hooks?.[EVENT[which]] ?? []).some((g) => (g.hooks ?? []).some(isKind(which)));
|
|
198
245
|
}
|
|
199
246
|
catch {
|
|
200
247
|
return false;
|