@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,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural conformance scan — replays the local transcript store through the
|
|
4
|
+
* checks this suite MECHANISES, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* The scope constraint is the point, and it is a council ruling
|
|
7
|
+
* (2026-08-06): *"a conformance scan that checks un-mechanised rules is
|
|
8
|
+
* theatre — if you can't gate it, don't pretend measuring it post-hoc is
|
|
9
|
+
* enforcement."* So this scan carries exactly four checks, one per shipped
|
|
10
|
+
* gate, and every classifier is IMPORTED from the gate it measures rather than
|
|
11
|
+
* re-implemented here. A second copy of a classifier is the "second artefact to
|
|
12
|
+
* keep in sync" the repo's own principle forbids, and it would let the scan and
|
|
13
|
+
* the gate disagree silently.
|
|
14
|
+
*
|
|
15
|
+
* language-pin ← language_mirror_hook.classify
|
|
16
|
+
* git-authorization ← git_authorization_hook.classifyAuthorization
|
|
17
|
+
* + block_unauthorized_git.commandOp / BLOCK_OPS
|
|
18
|
+
* vacuous-evidence ← before_complete_hook.isVacuousOutput / isCiPoll / pendingCount
|
|
19
|
+
* evidence-steering ← evidence_independence.isEvaluationPrompt / preloadedVerdict
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT scanned: ask-shape, session-canary, promissory closings,
|
|
22
|
+
* checkbox batching, symptom-vs-root-cause. Those are left as prose by
|
|
23
|
+
* `road-to-agent-behavior-conformance`, and measuring them here would claim a
|
|
24
|
+
* rigour the tree does not have.
|
|
25
|
+
*
|
|
26
|
+
* This is a REPORT, not a gate: exit 0 on every path except an unreadable
|
|
27
|
+
* store. It answers "did the shipped gates have anything to bite on?" —
|
|
28
|
+
* which is also the only honest way to find out whether they help.
|
|
29
|
+
*/
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
33
|
+
|
|
34
|
+
import { classify } from "./language_mirror_hook.js";
|
|
35
|
+
import { classifyAuthorization, type GitOp } from "./git_authorization_hook.js";
|
|
36
|
+
import { BLOCK_OPS, commandOp } from "./hooks/block_unauthorized_git.js";
|
|
37
|
+
import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.js";
|
|
38
|
+
import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evidence_independence.js";
|
|
39
|
+
|
|
40
|
+
export interface Violation {
|
|
41
|
+
check: "language-pin" | "git-authorization" | "vacuous-evidence" | "evidence-steering";
|
|
42
|
+
session: string;
|
|
43
|
+
at: string;
|
|
44
|
+
detail: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionReport {
|
|
48
|
+
session: string;
|
|
49
|
+
user_turns: number;
|
|
50
|
+
assistant_turns: number;
|
|
51
|
+
violations: Violation[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ScanReport {
|
|
55
|
+
scanned_at: string;
|
|
56
|
+
store: string;
|
|
57
|
+
sessions: number;
|
|
58
|
+
totals: Record<string, number>;
|
|
59
|
+
per_session: SessionReport[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Entry = Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
function _isObject(v: unknown): v is Record<string, unknown> {
|
|
65
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Text of a user-role entry, or null when it is not a real chat message. */
|
|
69
|
+
export function userText(entry: Entry): string | null {
|
|
70
|
+
if (entry["type"] !== "user" || entry["isSidechain"] === true) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const msg = entry["message"];
|
|
74
|
+
const content = _isObject(msg) ? msg["content"] : undefined;
|
|
75
|
+
let text = "";
|
|
76
|
+
if (typeof content === "string") {
|
|
77
|
+
text = content;
|
|
78
|
+
} else if (Array.isArray(content)) {
|
|
79
|
+
text = content
|
|
80
|
+
.filter((b) => _isObject(b) && b["type"] === "text")
|
|
81
|
+
.map((b) => String((b as Record<string, unknown>)["text"] ?? ""))
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
84
|
+
if (!text.trim()) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (text.includes("<system-reminder>") || text.startsWith("<local-command-stdout>")) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Assistant prose, or null. */
|
|
94
|
+
export function assistantText(entry: Entry): string | null {
|
|
95
|
+
if (entry["type"] !== "assistant" || entry["isSidechain"] === true) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const msg = entry["message"];
|
|
99
|
+
const content = _isObject(msg) ? msg["content"] : undefined;
|
|
100
|
+
if (!Array.isArray(content)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const text = content
|
|
104
|
+
.filter((b) => _isObject(b) && b["type"] === "text")
|
|
105
|
+
.map((b) => String((b as Record<string, unknown>)["text"] ?? ""))
|
|
106
|
+
.join("\n")
|
|
107
|
+
.trim();
|
|
108
|
+
return text || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Tool-use blocks from an assistant entry. */
|
|
112
|
+
function toolUses(entry: Entry): Array<{ name: string; input: Record<string, unknown> }> {
|
|
113
|
+
const msg = entry["message"];
|
|
114
|
+
const content = _isObject(msg) ? msg["content"] : undefined;
|
|
115
|
+
if (!Array.isArray(content)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return content
|
|
119
|
+
.filter((b) => _isObject(b) && b["type"] === "tool_use")
|
|
120
|
+
.map((b) => {
|
|
121
|
+
const o = b as Record<string, unknown>;
|
|
122
|
+
return {
|
|
123
|
+
name: String(o["name"] ?? ""),
|
|
124
|
+
input: _isObject(o["input"]) ? (o["input"] as Record<string, unknown>) : {},
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A skill / slash-command body arriving in the user role. This is the
|
|
131
|
+
* transcript shape that made the language defect invisible: it occupies the
|
|
132
|
+
* user role but is not a chat message, so it must never become the language
|
|
133
|
+
* trigger — the same reason `language_mirror_hook` pins at prompt-submit time.
|
|
134
|
+
*/
|
|
135
|
+
export function isInjectedBody(text: string): boolean {
|
|
136
|
+
if (/^(Base directory for this skill|<command-message>|<command-name>|<local-command)/.test(text)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return text.length > 2500 && classify(text).language === "en";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Harness-generated text that occupies an assistant turn but is not the
|
|
144
|
+
* assistant writing. Round-2 self-scan: 8 of 9 "violations" in one session were
|
|
145
|
+
* `API Error: 529 Overloaded` retry banners. Counting the harness as the model
|
|
146
|
+
* inflates the very number this scan exists to report honestly.
|
|
147
|
+
*/
|
|
148
|
+
const HARNESS_TEXT =
|
|
149
|
+
/^(API Error:|Request (timed out|was aborted)|\[Request interrupted|Error: |Credit balance is too low|You've hit your|Prompt is too long)/i;
|
|
150
|
+
|
|
151
|
+
/** Rough English-opener test for the language check (first prose line only). */
|
|
152
|
+
const EN_OPENER =
|
|
153
|
+
/^(let me|i'?ll |i'?m |found it|ok[,. ]|okay|alright|here'?s|now[,. ]|looking|checking|reading|running|the |this |that |there |we |you |good |right[,. ]|perfect|done[.,]|all |confirmed|correct)/i;
|
|
154
|
+
|
|
155
|
+
export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
156
|
+
const report: SessionReport = {
|
|
157
|
+
session: sessionId,
|
|
158
|
+
user_turns: 0,
|
|
159
|
+
assistant_turns: 0,
|
|
160
|
+
violations: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
let pinned: "de" | "en" | null = null;
|
|
164
|
+
let pendingPoll: { at: string; cmd: string } | null = null;
|
|
165
|
+
let authorized = new Set<GitOp>();
|
|
166
|
+
let sawPending = false;
|
|
167
|
+
let evaluationsThisTurn = 0;
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (!line.trim()) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
let entry: Entry;
|
|
174
|
+
try {
|
|
175
|
+
entry = JSON.parse(line) as Entry;
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Tool results arrive as `user` entries carrying `toolUseResult`.
|
|
181
|
+
if (entry["type"] === "user" && entry["toolUseResult"] !== undefined && pendingPoll !== null) {
|
|
182
|
+
const raw = entry["toolUseResult"];
|
|
183
|
+
const out = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
184
|
+
const pending = pendingCount(out);
|
|
185
|
+
if (pending !== null && pending > 0) {
|
|
186
|
+
sawPending = true;
|
|
187
|
+
} else if (isVacuousOutput(out) || (pending === 0 && !sawPending)) {
|
|
188
|
+
report.violations.push({
|
|
189
|
+
check: "vacuous-evidence",
|
|
190
|
+
session: sessionId,
|
|
191
|
+
at: pendingPoll.at,
|
|
192
|
+
detail: `CI poll read as settled without an in-flight observation: ${out.slice(0, 90)}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
pendingPoll = null;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ut = userText(entry);
|
|
200
|
+
if (ut !== null) {
|
|
201
|
+
if (isInjectedBody(ut)) {
|
|
202
|
+
// Not a chat message — it changes neither the pin nor the ledger.
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
report.user_turns += 1;
|
|
206
|
+
const c = classify(ut);
|
|
207
|
+
if (c.language !== "und") {
|
|
208
|
+
pinned = c.language;
|
|
209
|
+
}
|
|
210
|
+
authorized = new Set(classifyAuthorization(ut).authorized);
|
|
211
|
+
sawPending = false;
|
|
212
|
+
evaluationsThisTurn = 0;
|
|
213
|
+
pendingPoll = null;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (entry["type"] !== "assistant" || entry["isSidechain"] === true) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const at = String(entry["timestamp"] ?? "");
|
|
221
|
+
|
|
222
|
+
const prose = assistantText(entry);
|
|
223
|
+
if (prose !== null) {
|
|
224
|
+
report.assistant_turns += 1;
|
|
225
|
+
if (pinned === "de") {
|
|
226
|
+
const first = prose.split("\n").find((l) => l.trim()) ?? "";
|
|
227
|
+
if (HARNESS_TEXT.test(first.trim())) {
|
|
228
|
+
continue; // harness banner, not the assistant's prose
|
|
229
|
+
}
|
|
230
|
+
const c = classify(first);
|
|
231
|
+
if (first.length > 12 && (c.language === "en" || EN_OPENER.test(first.trim()))) {
|
|
232
|
+
report.violations.push({
|
|
233
|
+
check: "language-pin",
|
|
234
|
+
session: sessionId,
|
|
235
|
+
at,
|
|
236
|
+
detail: `German pin, English reply opener: "${first.slice(0, 110)}"`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const tu of toolUses(entry)) {
|
|
243
|
+
const cmd =
|
|
244
|
+
typeof tu.input["command"] === "string" ? (tu.input["command"] as string) : null;
|
|
245
|
+
if (cmd !== null) {
|
|
246
|
+
const op = commandOp(cmd);
|
|
247
|
+
if (op !== null && !authorized.has(op) && BLOCK_OPS.has(op)) {
|
|
248
|
+
report.violations.push({
|
|
249
|
+
check: "git-authorization",
|
|
250
|
+
session: sessionId,
|
|
251
|
+
at,
|
|
252
|
+
detail: `irreversible \`${op}\` with no authorization in the turn's prompt: ${cmd.slice(0, 110)}`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (isCiPoll(cmd)) {
|
|
256
|
+
// The RESULT of this call arrives on the NEXT entry, and that entry is
|
|
257
|
+
// role `user` — not assistant. The first version read
|
|
258
|
+
// `entry["toolUseResult"]` off the assistant entry, where it never
|
|
259
|
+
// exists: measured on a 20 MB transcript, 1913 entries carry the key
|
|
260
|
+
// and all 1913 are `user`. So this check could not fire at all and the
|
|
261
|
+
// scan printed a permanent `✅ vacuous-evidence 0` — a false green in
|
|
262
|
+
// the one report whose job is to say whether the gates bite.
|
|
263
|
+
pendingPoll = { at, cmd };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const prompt =
|
|
268
|
+
typeof tu.input["prompt"] === "string" ? (tu.input["prompt"] as string) : null;
|
|
269
|
+
if (prompt !== null && isEvaluationPrompt(prompt)) {
|
|
270
|
+
const preloaded = preloadedVerdict(prompt);
|
|
271
|
+
if (preloaded !== null) {
|
|
272
|
+
report.violations.push({
|
|
273
|
+
check: "evidence-steering",
|
|
274
|
+
session: sessionId,
|
|
275
|
+
at,
|
|
276
|
+
detail: `evaluation prompt pre-loads its verdict: "${preloaded}"`,
|
|
277
|
+
});
|
|
278
|
+
} else if (isSelfScoped(prompt)) {
|
|
279
|
+
if (evaluationsThisTurn >= 1) {
|
|
280
|
+
report.violations.push({
|
|
281
|
+
check: "evidence-steering",
|
|
282
|
+
session: sessionId,
|
|
283
|
+
at,
|
|
284
|
+
detail: "second self-review dispatch in one turn (verdict shopping)",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
evaluationsThisTurn += 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return report;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Default Claude Code transcript store for a project directory. */
|
|
297
|
+
export function defaultStore(projectDir: string): string {
|
|
298
|
+
// Claude Code slugs BOTH separators and dots: /Users/x/.claude → -Users-x--claude.
|
|
299
|
+
const slug = projectDir.replace(/[/.]/g, "-");
|
|
300
|
+
return path.join(process.env["HOME"] ?? "", ".claude", "projects", slug);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function scanStore(store: string, limit: number): ScanReport {
|
|
304
|
+
const files = fs
|
|
305
|
+
.readdirSync(store)
|
|
306
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
307
|
+
.map((f) => ({ f, m: fs.statSync(path.join(store, f)).mtimeMs }))
|
|
308
|
+
.sort((a, b) => b.m - a.m)
|
|
309
|
+
.slice(0, limit)
|
|
310
|
+
.map((x) => x.f);
|
|
311
|
+
|
|
312
|
+
const per_session: SessionReport[] = [];
|
|
313
|
+
for (const f of files) {
|
|
314
|
+
const lines = fs.readFileSync(path.join(store, f), "utf8").split("\n");
|
|
315
|
+
const r = scanSession(f.replace(/\.jsonl$/, "").slice(0, 8), lines);
|
|
316
|
+
if (r.assistant_turns > 0) {
|
|
317
|
+
per_session.push(r);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const totals: Record<string, number> = {
|
|
322
|
+
"language-pin": 0,
|
|
323
|
+
"git-authorization": 0,
|
|
324
|
+
"vacuous-evidence": 0,
|
|
325
|
+
"evidence-steering": 0,
|
|
326
|
+
};
|
|
327
|
+
for (const s of per_session) {
|
|
328
|
+
for (const v of s.violations) {
|
|
329
|
+
totals[v.check] = (totals[v.check] ?? 0) + 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
scanned_at: new Date().toISOString(),
|
|
335
|
+
store,
|
|
336
|
+
sessions: per_session.length,
|
|
337
|
+
totals,
|
|
338
|
+
per_session,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function render(report: ScanReport): string {
|
|
343
|
+
const lines: string[] = [];
|
|
344
|
+
lines.push(`conformance:behavior · ${report.sessions} session(s) · ${report.store}`);
|
|
345
|
+
for (const [k, v] of Object.entries(report.totals)) {
|
|
346
|
+
lines.push(` ${v === 0 ? "✅" : "⚠️ "} ${k.padEnd(18)} ${v}`);
|
|
347
|
+
}
|
|
348
|
+
const worst = [...report.per_session]
|
|
349
|
+
.filter((s) => s.violations.length > 0)
|
|
350
|
+
.sort((a, b) => b.violations.length - a.violations.length)
|
|
351
|
+
.slice(0, 5);
|
|
352
|
+
if (worst.length > 0) {
|
|
353
|
+
lines.push("");
|
|
354
|
+
lines.push(" Highest-violation sessions:");
|
|
355
|
+
for (const s of worst) {
|
|
356
|
+
lines.push(` ${s.session} ${s.violations.length} across ${s.assistant_turns} turns`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push(" Scanned checks are exactly the mechanised ones. Ask-shape, session-canary,");
|
|
361
|
+
lines.push(" promissory closings and checkbox batching are left as prose and NOT measured.");
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function main(argv?: string[]): number {
|
|
366
|
+
const args = argv ?? process.argv.slice(2);
|
|
367
|
+
let store: string | null = null;
|
|
368
|
+
let limit = 30;
|
|
369
|
+
let json = false;
|
|
370
|
+
let out: string | null = null;
|
|
371
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
372
|
+
const a = args[i];
|
|
373
|
+
if (a === "--store" && args[i + 1] !== undefined) {
|
|
374
|
+
store = args[++i] as string;
|
|
375
|
+
} else if (a === "--limit" && args[i + 1] !== undefined) {
|
|
376
|
+
limit = Number(args[++i]);
|
|
377
|
+
} else if (a === "--json") {
|
|
378
|
+
json = true;
|
|
379
|
+
} else if (a === "--output" && args[i + 1] !== undefined) {
|
|
380
|
+
out = args[++i] as string;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const resolved = store ?? defaultStore(process.cwd());
|
|
384
|
+
if (!fs.existsSync(resolved)) {
|
|
385
|
+
process.stderr.write(`conformance:behavior: no transcript store at ${resolved}\n`);
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
const report = scanStore(resolved, Number.isFinite(limit) && limit > 0 ? limit : 30);
|
|
389
|
+
if (out) {
|
|
390
|
+
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
391
|
+
}
|
|
392
|
+
// fs.writeSync, not process.stdout.write: `process.exit()` below does not
|
|
393
|
+
// flush an async pipe write, so `--json | jq` reproducibly received a
|
|
394
|
+
// truncated 64 KB document with no error (measured: 85,169 vs 65,536 bytes).
|
|
395
|
+
fs.writeSync(1, json ? `${JSON.stringify(report, null, 2)}\n` : `${render(report)}\n`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
400
|
+
function _isCliEntry(): boolean {
|
|
401
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if (process.argv[1] === undefined) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
408
|
+
if (import.meta.url === argvUrl) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
413
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
414
|
+
return here === argv;
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (_isCliEntry()) {
|
|
421
|
+
process.exit(main());
|
|
422
|
+
}
|
|
@@ -548,16 +548,38 @@ function reviewerPrompt(args: {
|
|
|
548
548
|
].join('\n');
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The findings skeleton, carrying `prompt_hash`.
|
|
553
|
+
*
|
|
554
|
+
* The contract's most dangerous residual is the PROMPT channel (§ 5): the
|
|
555
|
+
* dispatcher fixes the reviewer's inputs but not the instructions wrapped around
|
|
556
|
+
* them, so "a hash-verified artefact is not evidence of an unbiased review" — its
|
|
557
|
+
* recorded case zero is an honest-null whose prompt stated prior rounds were
|
|
558
|
+
* clean, where a later unsteered round on the identical scope found seven
|
|
559
|
+
* defects.
|
|
560
|
+
*
|
|
561
|
+
* Writing the hash of the prompt THIS dispatcher built makes the channel
|
|
562
|
+
* attributable: the artefact now records which prompt produced it, so a review
|
|
563
|
+
* run against a different prompt is distinguishable from one run against the
|
|
564
|
+
* dispatcher's. `--verify` re-derives it from the same inputs and compares.
|
|
565
|
+
*
|
|
566
|
+
* It does not CLOSE the residual and is not sold as doing so: the same host that
|
|
567
|
+
* authors a steered prompt could hash the steered text. The gain is an
|
|
568
|
+
* attributable record where there was none, which is what turns the audit trail
|
|
569
|
+
* (§ 2.7, round records are never edited in place) into something a contradicting
|
|
570
|
+
* later round can be checked against.
|
|
571
|
+
*/
|
|
551
572
|
function findingsSkeleton(args: {
|
|
552
573
|
slug: string;
|
|
553
574
|
headSha: string;
|
|
554
575
|
scopeHash: string;
|
|
555
576
|
reviewedDate: string;
|
|
556
577
|
manifest: string;
|
|
578
|
+
promptHash: string;
|
|
557
579
|
}): string {
|
|
558
580
|
return [
|
|
559
581
|
`# Findings: ${args.slug}`,
|
|
560
|
-
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} -->`,
|
|
582
|
+
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} | prompt_hash: ${args.promptHash} -->`,
|
|
561
583
|
'',
|
|
562
584
|
args.manifest,
|
|
563
585
|
'',
|
|
@@ -890,7 +912,14 @@ function runDispatch(args: Args): number {
|
|
|
890
912
|
acHash: hashes.ac_hash,
|
|
891
913
|
dispatched,
|
|
892
914
|
});
|
|
893
|
-
const skeleton = findingsSkeleton({
|
|
915
|
+
const skeleton = findingsSkeleton({
|
|
916
|
+
slug,
|
|
917
|
+
headSha,
|
|
918
|
+
scopeHash: hashes.scope_hash,
|
|
919
|
+
reviewedDate,
|
|
920
|
+
manifest,
|
|
921
|
+
promptHash: sha256(promptText),
|
|
922
|
+
});
|
|
894
923
|
|
|
895
924
|
fs.mkdirSync(inputDirAbs, { recursive: true });
|
|
896
925
|
fs.writeFileSync(path.join(inputDirAbs, 'diff.patch'), scopeDiffText, 'utf-8');
|