@event4u/agent-config 10.1.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +110 -167
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
  5. package/dist/agent-src/commands/roadmap/create.md +4 -1
  6. package/dist/agent-src/commands/roadmap/next.md +38 -0
  7. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
  8. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  9. package/dist/agent-src/personas/advisors/first-principles.md +6 -0
  10. package/dist/agent-src/rules/active-remediation.md +53 -2
  11. package/dist/agent-src/rules/code-provenance.md +66 -4
  12. package/dist/agent-src/rules/content-quoting-floor.md +6 -0
  13. package/dist/agent-src/rules/context-hygiene.md +39 -0
  14. package/dist/agent-src/rules/design-fidelity.md +49 -3
  15. package/dist/agent-src/rules/token-budget-discipline.md +3 -2
  16. package/dist/agent-src/rules/token-efficiency.md +22 -0
  17. package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
  18. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
  20. package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
  21. package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
  22. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
  23. package/dist/agent-src/skills/roadmap-writing/SKILL.md +9 -9
  24. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
  25. package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
  26. package/dist/agent-src/templates/agent-settings.md +12 -28
  27. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  28. package/dist/agent-src/templates/persona.md +26 -0
  29. package/dist/agent-src/templates/roadmaps.md +27 -7
  30. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  31. package/dist/agent-src/templates/skill.md +35 -1
  32. package/dist/cli/commands/commands.candidates.test.js +257 -0
  33. package/dist/cli/commands/commands.candidates.test.js.map +1 -0
  34. package/dist/cli/commands/commands.js +199 -0
  35. package/dist/cli/commands/commands.js.map +1 -1
  36. package/dist/cli/main.js +1 -0
  37. package/dist/cli/main.js.map +1 -1
  38. package/dist/cli/registry.js +2 -2
  39. package/dist/cli/registry.js.map +1 -1
  40. package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
  41. package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
  42. package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
  43. package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
  44. package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
  45. package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-FV34EGEX.js} +166 -39
  46. package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
  47. package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
  48. package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
  49. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  50. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  51. package/dist/cli-delegate/cmd_conformance.js +881 -204
  52. package/dist/cli-delegate/cmd_doctor.js +3 -3
  53. package/dist/cli-delegate/cmd_explain.js +2 -2
  54. package/dist/cli-delegate/cmd_export.js +4 -4
  55. package/dist/cli-delegate/cmd_handoff.js +6 -5
  56. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  57. package/dist/cli-delegate/cmd_migrate.js +2 -2
  58. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  59. package/dist/cli-delegate/cmd_prune.js +4 -4
  60. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  61. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  62. package/dist/cli-delegate/cmd_session_recycle.js +44 -9
  63. package/dist/cli-delegate/cmd_settings_get.js +2 -2
  64. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  65. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  66. package/dist/cli-delegate/cmd_sync.js +4 -4
  67. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  68. package/dist/cli-delegate/cmd_update.js +1 -1
  69. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  70. package/dist/cli-delegate/cmd_validate.js +4 -4
  71. package/dist/cli-delegate/cmd_versions.js +1 -1
  72. package/dist/discovery/deprecation-report.md +1 -1
  73. package/dist/discovery/discovery-manifest.json +21 -21
  74. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  75. package/dist/discovery/discovery-manifest.summary.md +1 -1
  76. package/dist/discovery/dormancy-report.md +1 -1
  77. package/dist/discovery/orphan-report.md +1 -1
  78. package/dist/discovery/packs.json +2 -2
  79. package/dist/discovery/trust-report.md +1 -1
  80. package/dist/discovery/workspaces.json +2 -2
  81. package/dist/hooks/dispatch.js +204 -44
  82. package/dist/install/install.mjs +9 -21
  83. package/dist/mcp/registry-manifest.json +2 -2
  84. package/dist/mcp/server.json +2 -2
  85. package/dist/mcp/server.mjs +1337 -490
  86. package/dist/router.json +1 -1
  87. package/dist/scripts/_lib/agent_settings.js +25 -2
  88. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  89. package/dist/scripts/_lib/compile_time_toggles.js +4 -3
  90. package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
  91. package/dist/server/io/yamlIO.js +0 -2
  92. package/dist/server/io/yamlIO.js.map +1 -1
  93. package/dist/server/schemas/settings.js +3 -9
  94. package/dist/server/schemas/settings.js.map +1 -1
  95. package/dist/shared/settingsClasses.js +64 -2
  96. package/dist/shared/settingsClasses.js.map +1 -1
  97. package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
  98. package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
  99. package/dist/ui/index.html +1 -1
  100. package/docs/contracts/settings-classes.md +307 -145
  101. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
  102. package/docs/guidelines/agent-infra/layered-settings.md +1 -2
  103. package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  104. package/docs/guidelines/design-antipatterns.md +79 -10
  105. package/docs/guidelines/design-fidelity-mechanics.md +120 -2
  106. package/package.json +1 -1
  107. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  108. package/src/config/agent-settings.template.yml +12 -30
  109. package/src/config/gate-coverage.yml +42 -8
  110. package/src/config/gate-violation-baselines.json +15 -3
  111. package/src/config/profiles/balanced.ini +0 -2
  112. package/src/config/profiles/full.ini +1 -3
  113. package/src/config/profiles/minimal.ini +0 -2
  114. package/src/config/review-prompt-binding-baseline.json +19 -0
  115. package/src/config/rule-enforcement-baseline.json +0 -1
  116. package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
  117. package/src/scripts/_cli/handoff_sessions.ts +3 -2
  118. package/src/scripts/_dispatch.bash +1 -0
  119. package/src/scripts/_lib/agent_settings.ts +25 -2
  120. package/src/scripts/_lib/bench_telegraph.ts +1 -1
  121. package/src/scripts/_lib/cc_transcript.ts +25 -0
  122. package/src/scripts/_lib/compile_time_toggles.ts +4 -3
  123. package/src/scripts/ai_council/clients.ts +243 -34
  124. package/src/scripts/cache_realization_report.ts +3 -2
  125. package/src/scripts/check_ci_local_parity.ts +60 -6
  126. package/src/scripts/check_review_prompt_binding.ts +553 -0
  127. package/src/scripts/check_rule_projection_integrity.ts +3 -1
  128. package/src/scripts/check_suppression_hygiene.ts +19 -3
  129. package/src/scripts/conformance_scan.ts +282 -9
  130. package/src/scripts/cost/track.mjs +10 -4
  131. package/src/scripts/council_cli.ts +33 -3
  132. package/src/scripts/design_slop_rules.test.ts +37 -6
  133. package/src/scripts/design_slop_rules.ts +150 -22
  134. package/src/scripts/dispatch_r2_reviewer.ts +15 -1
  135. package/src/scripts/external_sources_denylist.json +3 -1
  136. package/src/scripts/hook_manifest.yaml +17 -6
  137. package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
  138. package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
  139. package/src/scripts/hooks/evidence_independence.ts +61 -9
  140. package/src/scripts/hooks/session_eol_hook.ts +123 -3
  141. package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
  142. package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
  143. package/src/scripts/lint_design_antipattern_parity.ts +201 -0
  144. package/src/scripts/lint_harvest_provenance.ts +483 -0
  145. package/src/scripts/lint_settings_classes.ts +273 -11
  146. package/src/scripts/lint_skill_router_head.ts +254 -0
  147. package/src/scripts/lint_symptom_intake.ts +241 -0
  148. package/src/scripts/report_consultation_rate.ts +128 -2
  149. package/src/scripts/report_skill_activation.ts +6 -4
  150. package/src/scripts/report_skill_obligation_violations.ts +2 -1
  151. package/src/scripts/run_skill_evals.ts +10 -5
  152. package/src/scripts/schemas/persona.schema.json +8 -0
  153. package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
  154. package/src/scripts/sweep_dead_scan_roots.ts +51 -0
  155. package/src/server/io/yamlIO.ts +0 -2
  156. package/src/server/schemas/settings.ts +3 -19
  157. package/src/shared/settingsClasses.ts +93 -2
  158. package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
@@ -6,17 +6,31 @@
6
6
  * The scope constraint is the point, and it is a council ruling
7
7
  * (2026-08-06): *"a conformance scan that checks un-mechanised rules is
8
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.
9
+ * enforcement."* So every classifier is IMPORTED from the gate it measures
10
+ * rather than re-implemented here. A second copy of a classifier is the "second
11
+ * artefact to keep in sync" the repo's own principle forbids, and it would let
12
+ * the scan and the gate disagree silently.
14
13
  *
15
14
  * language-pin ← language_mirror_hook.classify
16
15
  * git-authorization ← git_authorization_hook.classifyAuthorization
17
16
  * + block_unauthorized_git.commandOp / BLOCK_OPS
18
17
  * vacuous-evidence ← before_complete_hook.isVacuousOutput / isCiPoll / pendingCount
19
18
  * evidence-steering ← evidence_independence.isEvaluationPrompt / preloadedVerdict
19
+ * completion-claim ← turn_end_gate_hook.detectCompletionClaim (round 7)
20
+ * task-completeness ← delegation_nudge_hook.enumeratedFileTokens /
21
+ * FILE_SIGNAL_FLOOR (below)
22
+ *
23
+ * The count was "exactly four checks, one per shipped gate" and both halves have
24
+ * since moved, so both are stated plainly rather than left to rot: there are
25
+ * SIX, and `task-completeness` is the one with NO gate behind it. That is not a
26
+ * quiet exception to the council ruling — it is the ruling's own logic run
27
+ * forwards. The ruling forbids presenting a post-hoc measurement AS enforcement;
28
+ * this check exists to decide whether a refusal is warranted at all, before one
29
+ * is built, because the alternative is a gate resting on an unmeasured premise.
30
+ * The pre-registered bar it will be read against is committed BEFORE the number
31
+ * (`road-to-completion-loop` Phase 2), and a published null — no gate — is an
32
+ * accepted outcome. Nothing here claims the check enforces anything: this file
33
+ * is a REPORT, as its exit contract below says.
20
34
  *
21
35
  * Deliberately NOT scanned: ask-shape, session-canary, promissory closings,
22
36
  * checkbox batching, symptom-vs-root-cause. Those are left as prose by
@@ -34,6 +48,7 @@ import path from "node:path";
34
48
  import { fileURLToPath, pathToFileURL } from "node:url";
35
49
 
36
50
  import { censusRuleDir } from "./_lib/carrier_divergence.js";
51
+ import { projectStoreSlug } from "./_lib/cc_transcript.js";
37
52
  import { entryText, isSidechain } from "./_lib/transcript_entry.js";
38
53
 
39
54
  import { classify } from "./language_mirror_hook.js";
@@ -45,6 +60,10 @@ import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.
45
60
  // than reimplemented, so the measured rate and the gate cannot disagree.
46
61
  import { detectCompletionClaim } from "./hooks/turn_end_gate_hook.js";
47
62
  import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evidence_independence.js";
63
+ // Phase 1 of `road-to-completion-loop` — the SAME prompt-signal extraction the
64
+ // delegation nudge classifies on, so the measurement cannot see a different
65
+ // deliverable set than the classifier does.
66
+ import { enumeratedFileTokens, FILE_SIGNAL_FLOOR } from "./hooks/delegation_nudge_hook.js";
48
67
 
49
68
  /**
50
69
  * This module's own repo root, NOT `process.cwd()`.
@@ -65,7 +84,8 @@ export interface Violation {
65
84
  | "git-authorization"
66
85
  | "vacuous-evidence"
67
86
  | "evidence-steering"
68
- | "completion-claim";
87
+ | "completion-claim"
88
+ | "task-completeness";
69
89
  session: string;
70
90
  at: string;
71
91
  detail: string;
@@ -79,6 +99,19 @@ export interface Violation {
79
99
  */
80
100
  turns_since_prompt?: number;
81
101
  compaction_since_prompt?: boolean;
102
+ /**
103
+ * `task-completeness` only. The deliverable set's size, how many of it the
104
+ * reply window touched, and the tokens it never touched.
105
+ *
106
+ * Structured rather than prose-only because 1.3 requires HAND-VALIDATING every
107
+ * hit: a validator needs the exact missed tokens to open the window and decide
108
+ * whether the omission was real or one of the legitimate shapes (a blocking
109
+ * question, a hand-back, a user-fenced scope, an explicitly deferred item).
110
+ * A count alone cannot be checked, which is the failure this field prevents.
111
+ */
112
+ enumerated?: number;
113
+ addressed?: number;
114
+ missed?: string[];
82
115
  }
83
116
 
84
117
  export interface SessionReport {
@@ -97,6 +130,18 @@ export interface SessionReport {
97
130
  de_pin_turns: number;
98
131
  /** First entry timestamp, so a reader can era-split without a second pass. */
99
132
  first_at: string;
133
+ /**
134
+ * `task-completeness`'s DENOMINATOR: reply windows that met the file floor and
135
+ * contained real assistant work, i.e. the windows on which the check could fire
136
+ * at all.
137
+ *
138
+ * Counted here beside the numerator for the same reason `de_pin_turns` is — a
139
+ * rate whose two halves are derived from different populations is the defect
140
+ * round 7 § 6.2 found in the language figure, where the same 152 violations read
141
+ * 6.5 % over all turns and 9.2 % over the eligible ones. Without this a hit
142
+ * COUNT is all there is, and a count is not a rate.
143
+ */
144
+ completeness_windows: number;
100
145
  }
101
146
 
102
147
  /**
@@ -177,6 +222,17 @@ export interface RateRecord {
177
222
  de_pin_turns?: number;
178
223
  /** The same numerator over `de_pin_turns`. Reported beside `rate_pct`, never instead. */
179
224
  rate_pct_de_pin?: number;
225
+ /**
226
+ * `task-completeness`'s two halves, optional for the same archive reason as the
227
+ * four above: no line written before this check existed carries them.
228
+ *
229
+ * The rate is over WINDOWS, not turns or sessions — the only denominator on
230
+ * which the check can fire — and it is persisted beside the count so a later
231
+ * reader cannot mistake a hit count for a rate.
232
+ */
233
+ completeness_windows?: number;
234
+ task_completeness?: number;
235
+ task_completeness_rate_pct?: number;
180
236
  /** The corpus straddles a carrier landing, so no band verdict. */
181
237
  era_spanning?: boolean;
182
238
  /** Which carrier, when `era_spanning` — empty otherwise. */
@@ -265,6 +321,7 @@ export const CHECK_IDS: readonly CheckId[] = [
265
321
  "vacuous-evidence",
266
322
  "evidence-steering",
267
323
  "completion-claim",
324
+ "task-completeness",
268
325
  ];
269
326
 
270
327
  /**
@@ -297,6 +354,36 @@ export const CHECK_MEANINGS: Record<CheckId, string> = {
297
354
  "each followed by the user handing the work back; this is the same " +
298
355
  "predicate `turn-end-gate`'s completion detector refuses on, so the rate " +
299
356
  "and the gate cannot disagree. A session that never polled CI can never hit.",
357
+ "task-completeness":
358
+ "A reply window that left at least one file the prompt ENUMERATED " +
359
+ "untouched — the token appears in neither any tool-call input nor any " +
360
+ "assistant prose before the next user prompt. Scope is deliberately narrow: " +
361
+ `only prompts naming >= ${FILE_SIGNAL_FLOOR} distinct file tokens are ` +
362
+ "measured, the same floor `delegation_nudge_hook` uses for its FILE shape, " +
363
+ "and the token list comes from that module rather than a second regex.\n" +
364
+ " What distinguishes a hit from a false read: a hit is EVIDENCE OF AN " +
365
+ "OMISSION, not proof of one. Four legitimate shapes produce the same " +
366
+ "signature and are NOT excluded here — a blocking question, a hand-back, a " +
367
+ "user-fenced scope, and an explicitly deferred item all address fewer files " +
368
+ "than the prompt named, correctly. That is why every hit is hand-validated " +
369
+ "and a precision is published beside the rate; an unvalidated count from " +
370
+ "this check means nothing.\n" +
371
+ " What it cannot see, stated so a zero is not misread as health: the " +
372
+ "ordered-plan, explicit-count, for-each and conjunction shapes name a " +
373
+ "COUNT of deliverables but not their identities, so this check is blind to " +
374
+ "them by construction — it does not score them complete, it does not score " +
375
+ "them at all. A prose deliverable that is not a filename is invisible to it.\n" +
376
+ " MEASURED PRECISION: 0 of 3 hits, over 4 eligible windows in 28 sessions " +
377
+ "(2026-08-12). All three were false positives from ONE extraction defect: the " +
378
+ "token list is taken from the whole prompt, so files quoted inside material " +
379
+ "the user PASTED — a review, a log, an example — read as deliverables. In all " +
380
+ "three, the user's own ask contributed zero tokens. `isInjectedBody` did not " +
381
+ "filter them because it only excludes long ENGLISH text, and this corpus is " +
382
+ "mostly German (12 295 and 7 656 chars, both classified `de`, both passed); " +
383
+ "the one English case missed the 2 500-char cut by 12. So a hit here is not " +
384
+ "evidence of anything until the ask is separated from the pasted material. " +
385
+ "Detector D was NOT built on this result — see " +
386
+ "`agents/evidence/analysis/task-completeness-measurement.md`.",
300
387
  };
301
388
 
302
389
  /**
@@ -333,6 +420,18 @@ export function renderWhy(report: ScanReport, id: CheckId): string {
333
420
  `compaction_since_prompt=${compacted ?? "—"}`,
334
421
  );
335
422
  }
423
+ // `task-completeness` provenance. Printed as its own line because 1.3
424
+ // requires hand-validating every hit, and a validator needs the missed
425
+ // tokens verbatim — not a truncated detail string.
426
+ const missed = (v as { missed?: string[] }).missed;
427
+ if (missed !== undefined) {
428
+ const enumerated = (v as { enumerated?: number }).enumerated;
429
+ const addressed = (v as { addressed?: number }).addressed;
430
+ lines.push(
431
+ ` provenance: enumerated=${enumerated ?? "—"} · ` +
432
+ `addressed=${addressed ?? "—"} · missed=[${missed.join(", ")}]`,
433
+ );
434
+ }
336
435
  }
337
436
  return lines.join("\n");
338
437
  }
@@ -418,6 +517,54 @@ export const HARNESS_TEXT =
418
517
  const EN_OPENER =
419
518
  /^(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;
420
519
 
520
+ /**
521
+ * One prompt's reply window, for the `task-completeness` check.
522
+ *
523
+ * A "turn" in this store is one ENTRY, and a single reply spans several of them
524
+ * (one prose entry plus one per tool call). Scoring per entry would report every
525
+ * tool-call entry as incomplete, so the unit is the window from a counted user
526
+ * prompt to the next one — which is what step 1.1's "the turn's own user prompt"
527
+ * means in the transcript's actual shape. The departure is recorded rather than
528
+ * silently taken.
529
+ */
530
+ export interface CompletenessWindow {
531
+ /** The PROMPT's timestamp, not an assistant entry's: it is where a validator opens the window. */
532
+ at: string;
533
+ tokens: string[];
534
+ addressed: Set<string>;
535
+ /**
536
+ * The window contained at least one assistant prose entry or tool call.
537
+ *
538
+ * An empty window is a truncated or abandoned session, not an omission — and
539
+ * counting it would make the rate a measure of how often a store ends
540
+ * mid-reply. That is the "detector reproduces the defect it measures" trap
541
+ * this check's own risk register names.
542
+ */
543
+ worked: boolean;
544
+ }
545
+
546
+ /** Mark every deliverable token this text mentions as addressed. */
547
+ export function markAddressed(text: string, w: CompletenessWindow): void {
548
+ if (w.tokens.length === 0) {
549
+ return;
550
+ }
551
+ const hay = text.toLowerCase();
552
+ for (const t of w.tokens) {
553
+ if (!w.addressed.has(t) && hay.includes(t)) {
554
+ w.addressed.add(t);
555
+ }
556
+ }
557
+ }
558
+
559
+ /** The window's verdict: the untouched tokens, or `null` for no finding. */
560
+ export function evaluateWindow(w: CompletenessWindow): { missed: string[] } | null {
561
+ if (!w.worked || w.tokens.length === 0) {
562
+ return null;
563
+ }
564
+ const missed = w.tokens.filter((t) => !w.addressed.has(t));
565
+ return missed.length > 0 ? { missed } : null;
566
+ }
567
+
421
568
  export function scanSession(sessionId: string, lines: string[]): SessionReport {
422
569
  const report: SessionReport = {
423
570
  session: sessionId,
@@ -426,6 +573,7 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
426
573
  violations: [],
427
574
  de_pin_turns: 0,
428
575
  first_at: "",
576
+ completeness_windows: 0,
429
577
  };
430
578
 
431
579
  let pinned: "de" | "en" | null = null;
@@ -443,6 +591,44 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
443
591
  // measured failure is a completion claim in a LATER turn than the poll.
444
592
  let ciSeen = false;
445
593
  let ciSettled = false;
594
+ // Phase 1 of `road-to-completion-loop` — the open reply window, or null when
595
+ // the last prompt named no deliverable set worth scoring.
596
+ let openWindow: CompletenessWindow | null = null;
597
+
598
+ /**
599
+ * Close the open window and record a finding if anything went untouched.
600
+ * Called on the NEXT counted prompt and once after the loop — a window left
601
+ * open at end-of-file is the most common shape in a live store and dropping it
602
+ * would silently bias the rate toward long sessions.
603
+ */
604
+ const closeWindow = (): void => {
605
+ if (openWindow === null) {
606
+ return;
607
+ }
608
+ // The denominator counts windows the check COULD fire on. A window with no
609
+ // assistant work is excluded from both halves — `evaluateWindow` already
610
+ // refuses to score it, and counting it below while never counting it above
611
+ // would deflate the rate by however often a store ends mid-reply.
612
+ if (openWindow.worked) {
613
+ report.completeness_windows += 1;
614
+ }
615
+ const verdict = evaluateWindow(openWindow);
616
+ if (verdict !== null) {
617
+ const addressed = openWindow.tokens.length - verdict.missed.length;
618
+ report.violations.push({
619
+ check: "task-completeness",
620
+ session: sessionId,
621
+ at: openWindow.at,
622
+ detail:
623
+ `prompt enumerated ${openWindow.tokens.length} file(s), reply window touched ` +
624
+ `${addressed}; untouched: ${verdict.missed.join(", ").slice(0, 160)}`,
625
+ enumerated: openWindow.tokens.length,
626
+ addressed,
627
+ missed: verdict.missed,
628
+ });
629
+ }
630
+ openWindow = null;
631
+ };
446
632
 
447
633
  for (const line of lines) {
448
634
  if (!line.trim()) {
@@ -516,6 +702,21 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
516
702
  continue;
517
703
  }
518
704
  report.user_turns += 1;
705
+ // Phase 1 — the previous window ends HERE, at the next real chat message.
706
+ // Ordered deliberately before the new window opens: the filters above
707
+ // (`isCompactSummary`, `isSyntheticPrompt`, `isInjectedBody`) all `continue`
708
+ // without closing, which is correct — an injected skill body does not end
709
+ // the user's reply window.
710
+ closeWindow();
711
+ const deliverables = enumeratedFileTokens(ut);
712
+ if (deliverables.length >= FILE_SIGNAL_FLOOR) {
713
+ openWindow = {
714
+ at: String(entry["timestamp"] ?? ""),
715
+ tokens: deliverables,
716
+ addressed: new Set<string>(),
717
+ worked: false,
718
+ };
719
+ }
519
720
  turnsSincePrompt = 0;
520
721
  compactionSincePrompt = false;
521
722
  const c = classify(ut);
@@ -538,6 +739,18 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
538
739
  if (prose !== null) {
539
740
  report.assistant_turns += 1;
540
741
  turnsSincePrompt += 1;
742
+ const firstLine = (prose.split("\n").find((l) => l.trim()) ?? "").trim();
743
+ // Phase 1 — prose is half the window's evidence (tool inputs are the other
744
+ // half, marked below). Placed before the language block for the same reason
745
+ // the completion-claim check is: that block `continue`s on a harness banner,
746
+ // and evidence collected after it would be skipped without saying so.
747
+ // A harness retry banner is not the assistant writing, so it marks nothing
748
+ // AND does not make the window count as worked — otherwise a session that
749
+ // died on a 529 would read as an omission.
750
+ if (openWindow !== null && !HARNESS_TEXT.test(firstLine)) {
751
+ openWindow.worked = true;
752
+ markAddressed(prose, openWindow);
753
+ }
541
754
  // Round 7 § 6.2 — the denominator the language check can actually fire on.
542
755
  // Counted here, beside the numerator, so the two cannot be derived from
543
756
  // different populations later.
@@ -578,6 +791,15 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
578
791
  }
579
792
 
580
793
  for (const tu of toolUses(entry)) {
794
+ // Phase 1 — the other half of the window's evidence. Serialised whole
795
+ // rather than field-picked: a deliverable can be named in `file_path`,
796
+ // `command`, `pattern`, `old_string` or a prompt, and enumerating the
797
+ // fields that count is how a reader of `file_path` alone would score a
798
+ // `grep`-then-`sed` edit as untouched.
799
+ if (openWindow !== null) {
800
+ openWindow.worked = true;
801
+ markAddressed(`${tu.name} ${JSON.stringify(tu.input)}`, openWindow);
802
+ }
581
803
  const cmd =
582
804
  typeof tu.input["command"] === "string" ? (tu.input["command"] as string) : null;
583
805
  if (cmd !== null) {
@@ -628,6 +850,11 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
628
850
  }
629
851
  }
630
852
 
853
+ // The last prompt's window never sees a following prompt, so it is closed
854
+ // here. Dropping it would bias the rate toward sessions that happen to end on
855
+ // a user turn.
856
+ closeWindow();
857
+
631
858
  return report;
632
859
  }
633
860
 
@@ -644,10 +871,27 @@ export const DEFAULT_RATE_SERIES = path.join(
644
871
  "conformance-rates.jsonl",
645
872
  );
646
873
 
647
- /** Default Claude Code transcript store for a project directory. */
874
+ /**
875
+ * Default Claude Code transcript store for a project directory.
876
+ *
877
+ * Claude Code slugs every character outside `[A-Za-z0-9-]`, not just the
878
+ * separator and the dot: `/Users/x/.claude` → `-Users-x--claude`, and a
879
+ * worktree named `feat+turn-end-gate-always-on` → `feat-turn-end-gate-always-on`.
880
+ *
881
+ * The narrower `[/.]` this replaced is why the 2026-08-12 session audit could
882
+ * not scan the worktree it was running in: the `+` survived into the computed
883
+ * slug, no such directory exists, and the scan printed "no transcript store"
884
+ * and measured NOTHING. A measurement tool that silently reports an empty
885
+ * corpus for a whole class of paths is worse than one that errors, because the
886
+ * zero is indistinguishable from a clean result.
887
+ *
888
+ * The character class is generalised rather than extended by one `+` because
889
+ * every one of the 56 store names on this machine matches `^[A-Za-z0-9-]+$` —
890
+ * there is no observed counter-example of a preserved special character, and
891
+ * guessing which OTHER character is next would repeat this defect.
892
+ */
648
893
  export function defaultStore(projectDir: string): string {
649
- // Claude Code slugs BOTH separators and dots: /Users/x/.claude → -Users-x--claude.
650
- const slug = projectDir.replace(/[/.]/g, "-");
894
+ const slug = projectStoreSlug(projectDir);
651
895
  return path.join(process.env["HOME"] ?? "", ".claude", "projects", slug);
652
896
  }
653
897
 
@@ -742,6 +986,16 @@ export function scanStore(store: string, limit: number): ScanReport {
742
986
  const de_pin_turns = per_session.reduce((n, s) => n + s.de_pin_turns, 0);
743
987
  const rate_pct_de_pin =
744
988
  de_pin_turns === 0 ? 0 : Number(((100 * language_pin) / de_pin_turns).toFixed(1));
989
+ // `task-completeness` over its own denominator. Kept separate from the language
990
+ // rate on purpose: the two checks fire on different populations, and a single
991
+ // `rate_pct` covering both would be the pooled figure round 7 § 6.1 had to
992
+ // retract.
993
+ const completeness_windows = per_session.reduce((n, s) => n + s.completeness_windows, 0);
994
+ const task_completeness = totals["task-completeness"] ?? 0;
995
+ const task_completeness_rate_pct =
996
+ completeness_windows === 0
997
+ ? 0
998
+ : Number(((100 * task_completeness) / completeness_windows).toFixed(1));
745
999
  const era = spansCarrierChange(per_session.map((s) => s.first_at));
746
1000
  const delivered = measureDelivered(
747
1001
  path.join(REPO_ROOT, ".claude", "rules"),
@@ -764,6 +1018,9 @@ export function scanStore(store: string, limit: number): ScanReport {
764
1018
  rate_pct,
765
1019
  de_pin_turns,
766
1020
  rate_pct_de_pin,
1021
+ completeness_windows,
1022
+ task_completeness,
1023
+ task_completeness_rate_pct,
767
1024
  era_spanning: era.spans,
768
1025
  era_reason: era.what,
769
1026
  band: bandVerdict(rate_pct, assistant_turns, { spansCarrier: era.spans }),
@@ -848,6 +1105,22 @@ export function render(report: ScanReport): string {
848
1105
  );
849
1106
  lines.push(' is the comparable one and the second is the honest one. Both are printed.');
850
1107
  }
1108
+ // `task-completeness`, on its own denominator. Optional-read for the same
1109
+ // archive reason as the two above: a `rate` block recorded before this check
1110
+ // existed carries neither field, and the series exists to be read back.
1111
+ const cWindows = report.rate.completeness_windows;
1112
+ const cRate = report.rate.task_completeness_rate_pct;
1113
+ if (typeof cWindows === "number" && typeof cRate === "number") {
1114
+ lines.push("");
1115
+ lines.push(
1116
+ ` task-completeness ${cRate.toFixed(1)}% ` +
1117
+ `(${report.rate.task_completeness ?? 0} of ${cWindows} eligible reply windows)`,
1118
+ );
1119
+ lines.push(" A hit is EVIDENCE of an omission, never proof: a blocking question, a");
1120
+ lines.push(" hand-back, a fenced scope and a deferred item all produce this signature");
1121
+ lines.push(" correctly. Read the hand-validated precision beside this figure or not");
1122
+ lines.push(" at all — `--why task-completeness` lists every hit with its missed tokens.");
1123
+ }
851
1124
  if (report.rate.band === "era-spanning") {
852
1125
  lines.push(
853
1126
  ` Not compared: the corpus SPANS a carrier landing — ${report.rate.era_reason || 'see CARRIER_CHANGES'}.`,
@@ -116,10 +116,16 @@ function dedupKey(rec) {
116
116
  return `${id} ${reqId}`;
117
117
  }
118
118
 
119
- // Claude Code encodes BOTH separators and dots: `/x/.claude/y` → `-x--claude-y`.
120
- // Replacing only `/` never resolves a dotted path segment, so any cwd under
121
- // `.claude/worktrees/` silently missed its project dir and exited 2.
122
- function encodeProjectPath(cwd) { return cwd.replace(/[/.]/g, '-'); }
119
+ // Claude Code slugs every character outside [A-Za-z0-9-], not just separators
120
+ // and dots: `/x/.claude/y` → `-x--claude-y`, and `feat+wt` → `feat-wt`.
121
+ //
122
+ // This is an inline copy of `_lib/cc_transcript.projectStoreSlug` — a module
123
+ // boundary, not an oversight: this file imports node builtins only. Keep the
124
+ // two in step. It was narrowed twice for the same reason (first `/` alone
125
+ // missed dotted segments; then `[/.]` missed a `+` worktree and silently
126
+ // resolved no project dir at all), which is why the class is now closed rather
127
+ // than extended one character per incident.
128
+ function encodeProjectPath(cwd) { return cwd.replace(/[^A-Za-z0-9-]/g, '-'); }
123
129
 
124
130
  function findProjectDir(cwd) {
125
131
  const c = join(PROJECTS_DIR, encodeProjectPath(cwd));
@@ -777,7 +777,17 @@ function _postRunQuorum(
777
777
  * "attendance is telemetry, never a silent drop" applies to the estimate
778
778
  * preview too, not only to a completed pass.
779
779
  */
780
- function _format_quorum_line(q: QuorumResult): string {
780
+ function _format_quorum_line(q: QuorumResult, phase?: QuorumEventPhase): string {
781
+ // The phase tag is not decoration. A degraded run prints attendance TWICE —
782
+ // the pre-run reading before the estimate table, the post-run reading after
783
+ // the consult — and the two disagree by construction: `2/2 present …
784
+ // concluded` then `0/2 present … INCONCLUSIVE`. Same prefix, opposite
785
+ // content, and until 2026-08-12 neither line said which was which, so an
786
+ // operator skimming (or anything grepping `council:quorum ·`) could take the
787
+ // stale one. The event log had solved this from the start with an explicit
788
+ // `phase` field; stdout had not. Optional, because the estimate path prints
789
+ // exactly one line and a tag there would claim a distinction it does not make.
790
+ const tag = phase === undefined ? '' : `${phase === 'pre_run' ? 'before the run' : 'after the run'} · `;
781
791
  const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
782
792
  // Round 7 § 5.3 — a degraded pass says so. `1/2 present, needed 1 —
783
793
  // concluded` is literally true and reads as agreement; with a threshold of 1
@@ -786,7 +796,7 @@ function _format_quorum_line(q: QuorumResult): string {
786
796
  // what was missing is the word that stops a reader inferring convergence.
787
797
  const degraded =
788
798
  q.present < q.total ? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.` : '';
789
- return `council:quorum · ${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
799
+ return `council:quorum · ${tag}${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
790
800
  }
791
801
 
792
802
  function build_members(settings: Dict, opts: BuildMembersOptions = {}): ExternalAIClient[] {
@@ -2621,7 +2631,9 @@ function cmd_run(
2621
2631
  _stdout(format_install_hints(skipped) + '\n');
2622
2632
  }
2623
2633
  if (quorum_out.result !== null) {
2624
- _stdout(_format_quorum_line(quorum_out.result) + '\n');
2634
+ // Tagged, because `cmd_run` prints attendance a SECOND time after the
2635
+ // consult and on a degraded pass the two readings contradict each other.
2636
+ _stdout(_format_quorum_line(quorum_out.result, 'pre_run') + '\n');
2625
2637
  }
2626
2638
  _stdout(
2627
2639
  format_estimate_table(billable, estimates, {
@@ -2777,6 +2789,19 @@ function cmd_run(
2777
2789
  lens: question.mode,
2778
2790
  invocation: String(_getattr(args, 'invocation', 'agent')),
2779
2791
  });
2792
+ // The post-run reading also has to REACH THE OPERATOR, not just the event
2793
+ // log and the payload. Measured 2026-08-12: a pass where BOTH members
2794
+ // errored printed `2/2 present, needed 1 — concluded` — the pre-run banner
2795
+ // from the estimate block above — then `wrote …json`, and nothing else. The
2796
+ // payload said `{status: "inconclusive", present: 0}` and the absent list
2797
+ // named both, so the record was honest the whole time; the only place the
2798
+ // failure was invisible was the stream the operator actually reads. Printing
2799
+ // it unconditionally, rather than only when it differs from the pre-run
2800
+ // value, is deliberate: "attendance is telemetry, never a silent drop"
2801
+ // (`_format_quorum_line`) reads the same way for an unchanged reading, and a
2802
+ // conditional print would make the absence of a line mean two different
2803
+ // things.
2804
+ _stdout(_format_quorum_line(post_run.quorum, 'post_run') + '\n');
2780
2805
  // Phase 4.1 — verdict → handoff envelope. Same tally `render()` computes
2781
2806
  // internally for the Vote Tally block (mirrored here, not imported from
2782
2807
  // there, because `render()` re-derives it from the SAVED payload on a
@@ -4024,5 +4049,10 @@ export {
4024
4049
  _parse_siblings_overrides,
4025
4050
  _synthesize_ai_council_block,
4026
4051
  _postRunQuorum,
4052
+ // Exported for the phase-tag test only. The two-contradictory-banners shape
4053
+ // it guards cannot be produced through `cmd_run` with an injected roster
4054
+ // (the pre-run print is gated on `build_members` having populated
4055
+ // `quorum_out`), and constructing a real roster would spend money.
4056
+ _format_quorum_line,
4027
4057
  CouncilDisabledError,
4028
4058
  };
@@ -100,16 +100,11 @@ const FIXTURES: Record<string, { ext: string; positive: string; negative: string
100
100
  negative:
101
101
  '<h1>Get Started</h1><p>Deploys are marked ✅ when healthy.</p><td>✅</td><button>Track progress</button>',
102
102
  },
103
- "slop-lock-shape": {
103
+ "slop-v8-lock-shape": {
104
104
  ext: "css",
105
105
  positive: ".a{border-radius:4px}.b{border-radius:8px}.c{border-radius:12px}.d{border-radius:20px}",
106
106
  negative: ".a{border-radius:8px}.b{border-radius:12px}",
107
107
  },
108
- "slop-lock-colour": {
109
- ext: "css",
110
- positive: ".a{color:#e11d48}.b{background:#2563eb}.c{border-color:#16a34a}",
111
- negative: ".a{color:#111827}.b{background:#6b7280}.c{border-color:#2563eb}",
112
- },
113
108
  "slop-l9-section-monotony": {
114
109
  ext: "html",
115
110
  // 8 identical media+text sections → 1 distinct layout family (< 4)
@@ -129,6 +124,42 @@ const FIXTURES: Record<string, { ext: string; positive: string; negative: string
129
124
  // only 2 consecutive → within cap
130
125
  negative: '<section><div class="grid"><img src="x"><h2>A</h2><p>t</p></div></section>'.repeat(2),
131
126
  },
127
+ "slop-v4-over-rounded": {
128
+ ext: "css",
129
+ // 24px radius on an 80px chip — mid-range rounding on a small surface
130
+ positive: ".chip{width:80px;border-radius:24px;background:#1f5f9c}",
131
+ // a deliberate pill is a shape decision, not over-rounding
132
+ negative: ".chip{width:80px;border-radius:9999px;background:#1f5f9c}",
133
+ },
134
+ "slop-c3-dark-glow": {
135
+ ext: "css",
136
+ // zero-offset saturated glow inside a declared dark surface
137
+ positive: ".dark .btn{background:#0b0f14;box-shadow:0 0 18px #22d3ee}",
138
+ // same dark surface, ordinary offset depth — not a glow
139
+ negative: ".dark .btn{background:#0b0f14;box-shadow:0 2px 8px #22d3ee}",
140
+ },
141
+ "slop-t9-uppercase-body": {
142
+ ext: "css",
143
+ positive: "article p{text-transform:uppercase;font-size:16px}",
144
+ // a short field label is the legitimate home of uppercase
145
+ negative: ".field-label{text-transform:uppercase;font-size:12px}",
146
+ },
147
+ "slop-t10-wide-tracking-body": {
148
+ ext: "css",
149
+ positive: "article p{letter-spacing:0.12em;font-size:16px}",
150
+ negative: "article p{letter-spacing:0.01em;font-size:16px}",
151
+ },
152
+ "slop-m1-bounce-easing": {
153
+ ext: "css",
154
+ // y1 = 1.56 overshoots the target and returns — the bounce signature
155
+ positive: ".card{transition:transform .3s cubic-bezier(.34,1.56,.64,1)}",
156
+ negative: ".card{transition:transform .3s cubic-bezier(.4,0,.2,1)}",
157
+ },
158
+ "slop-m3-img-hover-transform": {
159
+ ext: "css",
160
+ positive: ".tile:hover img{transform:scale(1.05)}",
161
+ negative: ".tile:hover .tile__title{color:#16202b}",
162
+ },
132
163
  };
133
164
 
134
165
  function findingsFor(ruleId: string, content: string, ext: string, ctx = NO_CTX) {