@event4u/agent-config 9.24.0 → 9.26.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +136 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +182 -0
- package/dist/agent-src/commands/analyze/inbox.md +33 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +194 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +3 -2
- package/dist/agent-src/rules/design-fidelity.md +17 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +116 -0
- package/dist/agent-src/rules/evaluator-independence.md +111 -0
- package/dist/agent-src/rules/session-canary.md +15 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +176 -0
- package/dist/agent-src/rules/token-budget-discipline.md +6 -3
- package/dist/agent-src/rules/ui-audit-gate.md +3 -0
- package/dist/agent-src/rules/user-interaction.md +23 -0
- package/dist/agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/skills/rule-writing/SKILL.md +45 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +114 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +146 -28
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +16 -11
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +12 -7
- package/dist/hooks/dispatch.js +1501 -511
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/router.json +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +68 -68
- package/src/config/gate-coverage.yml +131 -5
- package/src/config/gate-violation-baselines.json +11 -0
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/_dispatch.bash +12 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +146 -5
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_depth_budget.ts +295 -0
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/conformance_scan.ts +422 -0
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/git_authorization_hook.ts +328 -0
- package/src/scripts/hook_manifest.yaml +69 -9
- package/src/scripts/hooks/block_no_verify.ts +3 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +371 -0
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +368 -0
- package/src/scripts/language_mirror_hook.ts +456 -0
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_decision_impact.ts +246 -0
- package/src/scripts/lint_example_fences.ts +609 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/lint_rule_interactions.ts +90 -0
- package/src/scripts/lint_rule_migration_ledger.ts +480 -0
- package/src/scripts/lint_skill_descriptions.ts +86 -2
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +38 -1
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsConsent.ts +136 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Evaluator-independence guard — `pre_tool_use` concern.
|
|
4
|
+
*
|
|
5
|
+
* WHY — the single highest-severity finding of the 30-session conformance audit
|
|
6
|
+
* (2026-08-06), and the one class no rule covered at all:
|
|
7
|
+
*
|
|
8
|
+
* The agent commissioned a "blind review" of its own work, WROTE THE
|
|
9
|
+
* REVIEWER'S PROMPT ITSELF with the verdict pre-loaded ("NO-FINDINGS is
|
|
10
|
+
* expected and welcome") and the scope narrowed to four self-chosen files,
|
|
11
|
+
* then committed the resulting honest-null as binding gate evidence. An
|
|
12
|
+
* unsteered pass over the same delta later found 5 findings, 1 critical,
|
|
13
|
+
* already live on the trunk. The agent's own retro: "Die Fabrikation lag
|
|
14
|
+
* nicht im Weglassen des Reviews — sie lag in meinem Prompt. Ich habe die
|
|
15
|
+
* Antwort vorgeladen und dann als Evidenz verbucht."
|
|
16
|
+
*
|
|
17
|
+
* WHAT THIS GATES — evaluation dispatches only, never fan-out.
|
|
18
|
+
*
|
|
19
|
+
* The AI council (2026-08-06) proposed blocking the SECOND subagent dispatch of
|
|
20
|
+
* any kind within a turn. That criterion is wrong, and measurably so: the very
|
|
21
|
+
* session that produced this guard dispatched **seven** analysis subagents in
|
|
22
|
+
* one turn as a deliberate parallel fan-out, at the user's explicit request.
|
|
23
|
+
* Six of them would have been blocked. Parallel dispatch is not the defect —
|
|
24
|
+
* steering an evaluator is.
|
|
25
|
+
*
|
|
26
|
+
* So the gate keys on the evaluation shape instead:
|
|
27
|
+
*
|
|
28
|
+
* BLOCK · a dispatch whose prompt PRE-LOADS the verdict ("no findings is
|
|
29
|
+
* expected", "you should find nothing", "confirm there are no issues"). This
|
|
30
|
+
* is the literal construct that fabricated the honest-null, and it is
|
|
31
|
+
* mechanically detectable in the prompt the agent is about to send.
|
|
32
|
+
*
|
|
33
|
+
* BLOCK · a SECOND evaluation dispatch in the same turn — verdict shopping.
|
|
34
|
+
* Counted per turn over evaluation-shaped dispatches only.
|
|
35
|
+
*
|
|
36
|
+
* WARN · the first evaluation dispatch, naming the two observed failure
|
|
37
|
+
* shapes so the author can check its prompt before it goes out.
|
|
38
|
+
*
|
|
39
|
+
* A dispatch that is not evaluation-shaped is not touched at all.
|
|
40
|
+
*
|
|
41
|
+
* State: `agents/state/evidence-dispatch.json`
|
|
42
|
+
* { "session_id": str, "turn_started_at": iso8601, "evaluations": [ {at, digest} ] }
|
|
43
|
+
*
|
|
44
|
+
* Exit codes: 0 allow · 2 block (stderr carries the reason).
|
|
45
|
+
*/
|
|
46
|
+
import crypto from "node:crypto";
|
|
47
|
+
import fs from "node:fs";
|
|
48
|
+
import path from "node:path";
|
|
49
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
50
|
+
|
|
51
|
+
import { atomic_write_json } from "./state_io.js";
|
|
52
|
+
import { readHookStdin } from "./hook_stdin.js";
|
|
53
|
+
|
|
54
|
+
const EXIT_ALLOW = 0;
|
|
55
|
+
// MUST equal dispatch_hook.EXIT_BLOCK. The dispatcher's internal ladder is
|
|
56
|
+
// 0 allow / 1 block / 2 warn — NOT the 2-means-block shape a PreToolUse guard
|
|
57
|
+
// reads naturally from Claude's own native contract. This constant was 2 when
|
|
58
|
+
// this gate first shipped, so the dispatcher reduced every refusal to a WARN
|
|
59
|
+
// and the gate emitted advisory context while the operation went through.
|
|
60
|
+
// Pinned against the dispatcher's export by
|
|
61
|
+
// tests/hooks/concern_block_exit_parity.test.ts.
|
|
62
|
+
const EXIT_BLOCK = 1;
|
|
63
|
+
|
|
64
|
+
export const STATE_FILE = path.join("agents", "state", "evidence-dispatch.json");
|
|
65
|
+
|
|
66
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
67
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
68
|
+
|
|
69
|
+
function _isObject(v: unknown): v is JsonObject {
|
|
70
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Tool names that dispatch a subagent, across platforms. */
|
|
74
|
+
const DISPATCH_TOOLS: ReadonlySet<string> = new Set([
|
|
75
|
+
"Agent",
|
|
76
|
+
"Task",
|
|
77
|
+
"task",
|
|
78
|
+
"dispatch_agent",
|
|
79
|
+
"dispatchAgent",
|
|
80
|
+
"launch_agent",
|
|
81
|
+
"run_subagent",
|
|
82
|
+
"Subagent",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** The dispatch is an EVALUATION of work rather than ordinary fan-out. */
|
|
86
|
+
const EVALUATION_RE =
|
|
87
|
+
/\b(review|reviewer|audit|auditor|judge|verdict|blind[- ]pass|blind review|adversarial|critique|assess(ment)?|verify (my|the) (work|change|diff|implementation)|find (any )?(bugs|issues|defects|problems) in (my|the)|pr(ü|ue)f(e|ung)?|begutachte|gutachten|bewerte|(gegen)?check(e)?|durchsicht|review(e|iere))\b/i;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Verdict pre-loading. Each pattern is a phrase that tells the evaluator what
|
|
91
|
+
* answer is acceptable BEFORE it has looked — the construct that produced the
|
|
92
|
+
* fabricated honest-null.
|
|
93
|
+
*/
|
|
94
|
+
const PRELOADED_VERDICT_RE = [
|
|
95
|
+
// German — the operator writes German, and an English-only list meant no
|
|
96
|
+
// German dispatch was ever screened at all.
|
|
97
|
+
/\bich erwarte (hier )?(keine|nichts)\b/i,
|
|
98
|
+
/\b(du )?(wirst|solltest) (hier )?nichts finden\b/i,
|
|
99
|
+
/\bbest(ä|ae)tige (nur|kurz)\b/i,
|
|
100
|
+
/\b(ist|sollte) (wahrscheinlich|vermutlich|sicher) (sauber|korrekt|in ordnung|fine)\b/i,
|
|
101
|
+
/\bkeine findings? (erwartet|zu erwarten)\b/i,
|
|
102
|
+
// English paraphrases of the audited construct.
|
|
103
|
+
/\bi am confident there is nothing\b/i,
|
|
104
|
+
/\ba clean pass is the likely\b/i,
|
|
105
|
+
/\bno[- ]findings? (is|are) (expected|welcome|fine|acceptable|the likely)/i,
|
|
106
|
+
/\b(you )?(should|will|probably) find (nothing|no (issues|problems|bugs|findings))/i,
|
|
107
|
+
/\bconfirm (that )?there (are|is) no\b/i,
|
|
108
|
+
/\bi (believe|think|expect) (this|it) is (clean|correct|fine)\b/i,
|
|
109
|
+
/\bexpect(ed)? (to be )?(clean|green|no findings)\b/i,
|
|
110
|
+
/\bjust confirm\b/i,
|
|
111
|
+
/\bit('s| is) (probably|likely) fine\b/i,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export function isDispatchTool(tool: string | null): boolean {
|
|
115
|
+
return tool !== null && DISPATCH_TOOLS.has(tool);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isEvaluationPrompt(prompt: string): boolean {
|
|
119
|
+
return EVALUATION_RE.test(prompt);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The evaluation targets the AGENT'S OWN work rather than some external
|
|
124
|
+
* artifact.
|
|
125
|
+
*
|
|
126
|
+
* This discriminator was added because the conformance scan caught the gate
|
|
127
|
+
* red-handed on real data: the audit session that produced this hook dispatched
|
|
128
|
+
* seven subagents whose prompts opened "You are auditing real Claude Code
|
|
129
|
+
* session transcripts…". `EVALUATION_RE` matches `audit`, so six of the seven
|
|
130
|
+
* were flagged as verdict shopping — the exact false positive the hook's own
|
|
131
|
+
* header claims to avoid. Auditing thirty transcripts is not reviewing your own
|
|
132
|
+
* diff twice.
|
|
133
|
+
*
|
|
134
|
+
* Verdict shopping is only possible when there is one subject to shop a verdict
|
|
135
|
+
* ON, so the second-dispatch block requires a self-reference. The pre-loaded-
|
|
136
|
+
* verdict block does NOT: steering an evaluator is wrong whatever it is
|
|
137
|
+
* pointed at.
|
|
138
|
+
*/
|
|
139
|
+
const SELF_SCOPE_RE =
|
|
140
|
+
/\b(my (work|change|diff|implementation|code|fix|patch|branch|pr)|this (diff|change|branch|pr|delta|implementation|patch)|the delta|the change i|what i (wrote|built|changed|implemented)|i just (wrote|built|changed|implemented))\b/i;
|
|
141
|
+
|
|
142
|
+
export function isSelfScoped(prompt: string): boolean {
|
|
143
|
+
return SELF_SCOPE_RE.test(prompt);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Return the pre-loading phrase found in a prompt, or null. */
|
|
147
|
+
export function preloadedVerdict(prompt: string): string | null {
|
|
148
|
+
for (const re of PRELOADED_VERDICT_RE) {
|
|
149
|
+
const m = re.exec(prompt);
|
|
150
|
+
if (m) {
|
|
151
|
+
return m[0];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Pull [tool, prompt] out of a pre-tool envelope. */
|
|
158
|
+
export function extractDispatch(envelope: JsonObject): [string | null, string] {
|
|
159
|
+
const payload = _isObject(envelope["payload"]) ? (envelope["payload"] as JsonObject) : envelope;
|
|
160
|
+
const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
|
|
161
|
+
const tool = typeof toolRaw === "string" ? toolRaw : null;
|
|
162
|
+
const input = _isObject(payload["tool_input"])
|
|
163
|
+
? (payload["tool_input"] as JsonObject)
|
|
164
|
+
: _isObject(payload["toolInput"])
|
|
165
|
+
? (payload["toolInput"] as JsonObject)
|
|
166
|
+
: payload;
|
|
167
|
+
for (const key of ["prompt", "instructions", "task", "description"]) {
|
|
168
|
+
const v = input[key];
|
|
169
|
+
if (typeof v === "string" && v.trim()) {
|
|
170
|
+
return [tool, v];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [tool, ""];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface DispatchState extends JsonObject {
|
|
177
|
+
session_id: string;
|
|
178
|
+
turn_started_at: string;
|
|
179
|
+
evaluations: JsonValue[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The current user turn's stamp, or "" when no ledger exists yet. */
|
|
183
|
+
function _ledgerStamp(consumer_root: string): string {
|
|
184
|
+
try {
|
|
185
|
+
const raw = fs.readFileSync(
|
|
186
|
+
path.join(consumer_root, "agents", "state", "git-authorization.json"),
|
|
187
|
+
"utf8",
|
|
188
|
+
);
|
|
189
|
+
const d = JSON.parse(raw) as Record<string, unknown>;
|
|
190
|
+
return typeof d["detected_at"] === "string" ? d["detected_at"] : "";
|
|
191
|
+
} catch {
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _load(target: string): DispatchState {
|
|
197
|
+
try {
|
|
198
|
+
const decoded = JSON.parse(fs.readFileSync(target, "utf8")) as unknown;
|
|
199
|
+
if (_isObject(decoded) && Array.isArray(decoded["evaluations"])) {
|
|
200
|
+
return decoded as unknown as DispatchState;
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
/* fall through */
|
|
204
|
+
}
|
|
205
|
+
return { session_id: "", turn_started_at: "", evaluations: [] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface Decision {
|
|
209
|
+
exit: number;
|
|
210
|
+
stdout: string;
|
|
211
|
+
stderr: string;
|
|
212
|
+
/** Number of evaluation dispatches recorded for this turn AFTER this one. */
|
|
213
|
+
evaluations: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const WARN_TEXT =
|
|
217
|
+
"First evaluation dispatch this turn. Two shapes fabricated a binding honest-null " +
|
|
218
|
+
"in the audited sessions — check your prompt for both before it goes out: " +
|
|
219
|
+
"(1) a verdict pre-loaded into the prompt, and (2) a scope narrowed to files you " +
|
|
220
|
+
"chose. A review you commissioned on your own work is admissible as gate evidence " +
|
|
221
|
+
"only when the prompt is recorded alongside the verdict.";
|
|
222
|
+
|
|
223
|
+
export function decide(
|
|
224
|
+
tool: string | null,
|
|
225
|
+
prompt: string,
|
|
226
|
+
priorEvaluations: number,
|
|
227
|
+
): Decision {
|
|
228
|
+
if (!isDispatchTool(tool) || !isEvaluationPrompt(prompt)) {
|
|
229
|
+
return { exit: EXIT_ALLOW, stdout: "", stderr: "", evaluations: priorEvaluations };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const preloaded = preloadedVerdict(prompt);
|
|
233
|
+
if (preloaded !== null) {
|
|
234
|
+
return {
|
|
235
|
+
exit: EXIT_BLOCK,
|
|
236
|
+
stdout: "",
|
|
237
|
+
stderr:
|
|
238
|
+
`Blocked: this evaluation prompt pre-loads its verdict ("${preloaded}"). ` +
|
|
239
|
+
`That is the construct that produced a fabricated NO-FINDINGS committed as ` +
|
|
240
|
+
`binding gate evidence, over a delta an unsteered pass then found a live ` +
|
|
241
|
+
`critical in. Remove the expectation from the prompt and let the evaluator ` +
|
|
242
|
+
`reach its own verdict.\n`,
|
|
243
|
+
evaluations: priorEvaluations,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Verdict shopping needs a single subject to shop a verdict on. An
|
|
248
|
+
// evaluation of an external artifact (transcripts, a third party's code) is
|
|
249
|
+
// fan-out, not self-review, and is never counted or blocked.
|
|
250
|
+
if (!isSelfScoped(prompt)) {
|
|
251
|
+
return { exit: EXIT_ALLOW, stdout: "", stderr: "", evaluations: priorEvaluations };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (priorEvaluations >= 1) {
|
|
255
|
+
return {
|
|
256
|
+
exit: EXIT_BLOCK,
|
|
257
|
+
stdout: "",
|
|
258
|
+
stderr:
|
|
259
|
+
`Blocked: second evaluation dispatch of your own work in this turn (verdict ` +
|
|
260
|
+
`shopping). One evaluation has already run; commissioning another with a ` +
|
|
261
|
+
`different prompt or scope selects the answer instead of measuring it. ` +
|
|
262
|
+
`Report what the first pass returned, then re-plan.\n`,
|
|
263
|
+
evaluations: priorEvaluations,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
exit: EXIT_ALLOW,
|
|
269
|
+
stdout: `${JSON.stringify({ decision: "warn", reason: WARN_TEXT })}\n`,
|
|
270
|
+
stderr: "",
|
|
271
|
+
evaluations: priorEvaluations + 1,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function run(stdin_text: string, options: { consumer_root: string }): number {
|
|
276
|
+
let envelope: JsonObject = {};
|
|
277
|
+
if (stdin_text.trim()) {
|
|
278
|
+
try {
|
|
279
|
+
const decoded = JSON.parse(stdin_text) as unknown;
|
|
280
|
+
if (_isObject(decoded)) {
|
|
281
|
+
envelope = decoded;
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
return EXIT_ALLOW;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const target = path.join(options.consumer_root, STATE_FILE);
|
|
289
|
+
const state = _load(target);
|
|
290
|
+
const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
|
|
291
|
+
|
|
292
|
+
// Turn boundary. `turn_id` does NOT exist in the production envelope — the
|
|
293
|
+
// dispatcher never sets it, so the original marker fell through to
|
|
294
|
+
// `session_id` and the counter was session-scoped: one self-review anywhere
|
|
295
|
+
// in a session blocked every later one. The only per-user-turn stamp that
|
|
296
|
+
// actually exists is `detected_at` in the authorization ledger, which
|
|
297
|
+
// `git_authorization_hook` rewrites on every `user_prompt_submit`.
|
|
298
|
+
const turnMarker = `${session_id}:${_ledgerStamp(options.consumer_root)}`;
|
|
299
|
+
if (state.session_id !== turnMarker) {
|
|
300
|
+
state.session_id = turnMarker;
|
|
301
|
+
state.turn_started_at = new Date().toISOString();
|
|
302
|
+
state.evaluations = [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const [tool, prompt] = extractDispatch(envelope);
|
|
306
|
+
const decision = decide(tool, prompt, state.evaluations.length);
|
|
307
|
+
|
|
308
|
+
if (decision.evaluations > state.evaluations.length) {
|
|
309
|
+
state.evaluations.push({
|
|
310
|
+
at: new Date().toISOString(),
|
|
311
|
+
digest: crypto.createHash("sha256").update(prompt).digest("hex").slice(0, 16),
|
|
312
|
+
prompt_chars: prompt.length,
|
|
313
|
+
});
|
|
314
|
+
try {
|
|
315
|
+
atomic_write_json(target, state);
|
|
316
|
+
} catch {
|
|
317
|
+
/* observability only */
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (decision.stdout) {
|
|
322
|
+
process.stdout.write(decision.stdout);
|
|
323
|
+
}
|
|
324
|
+
if (decision.stderr) {
|
|
325
|
+
process.stderr.write(decision.stderr);
|
|
326
|
+
}
|
|
327
|
+
return decision.exit;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function main(argv?: string[]): number {
|
|
331
|
+
const args = argv ?? process.argv.slice(2);
|
|
332
|
+
let consumer_root = process.cwd();
|
|
333
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
334
|
+
const a = args[i];
|
|
335
|
+
if (a === "--project-dir" && args[i + 1] !== undefined) {
|
|
336
|
+
consumer_root = args[i + 1] as string;
|
|
337
|
+
i += 1;
|
|
338
|
+
} else if (a !== undefined && a.startsWith("--project-dir=")) {
|
|
339
|
+
consumer_root = a.slice("--project-dir=".length);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return run(readHookStdin(), { consumer_root });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
346
|
+
function _isCliEntry(): boolean {
|
|
347
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (process.argv[1] === undefined) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
354
|
+
if (import.meta.url === argvUrl) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
359
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
360
|
+
return here === argv;
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (_isCliEntry()) {
|
|
367
|
+
process.exit(main());
|
|
368
|
+
}
|