@event4u/agent-config 14.21.0 → 14.22.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 (181) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -143,10 +143,68 @@ export const CAPSULE_SCHEMA_VERSION = 4;
143
143
  */
144
144
  export const ACCEPTED_CAPSULE_VERSIONS: readonly number[] = [3, 4];
145
145
 
146
- /** The two CHECKPOINT variants sharing this schema. */
147
- export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
146
+ /** The three CHECKPOINT variants sharing this schema. */
147
+ export const CAPSULE_VARIANTS = ['worker', 'main_session', 'continuity_record'] as const;
148
148
  export type CapsuleVariant = (typeof CAPSULE_VARIANTS)[number];
149
149
 
150
+ /**
151
+ * `continuity_record` — the DETERMINISTIC main-session variant.
152
+ *
153
+ * Why a variant and not a relaxed `main_session`. `main_session` requires
154
+ * `failed_approaches` and `successful_approaches` with at least one entry each,
155
+ * and requires the explicit token `none` when nothing failed or worked — the
156
+ * whole point of those fields being that silence and "nothing was abandoned"
157
+ * stay distinguishable. A writer that runs without model spend cannot know
158
+ * either. It could only write `none`, which asserts a judgement nobody made.
159
+ *
160
+ * The AI council of 2026-09-07 ruled exactly this: add a variant rather than
161
+ * stuffing absence markers into `main_session`, because explicit absence
162
+ * markers "misrepresent unavailable model judgments as values". This constant
163
+ * set is that ruling in code.
164
+ *
165
+ * The fields below are FORBIDDEN on this variant rather than optional, and the
166
+ * distinction is load-bearing. Optional would leave the door open for a later
167
+ * writer to fill one with a guess and for a reader to be unable to tell a
168
+ * derived record from a judged one. Forbidden makes the variant's meaning
169
+ * checkable: everything a `continuity_record` carries was computed from disk.
170
+ *
171
+ * Nothing here is invented — every name is an existing key of this same schema.
172
+ * The council's constraint was explicit: the required fields "must be derived
173
+ * from the existing validated envelope contract; this ruling does not authorize
174
+ * inventing parallel identity or directive fields."
175
+ *
176
+ * Version compatibility is unchanged and envelope-wide: a `continuity_record`
177
+ * declares the same `capsule_version`, and a reader that does not know the
178
+ * variant refuses it by name (see `validateRecycleEnvelope`) rather than
179
+ * guessing at its required set.
180
+ */
181
+ export const CONTINUITY_RECORD_FORBIDDEN_KEYS: ReadonlySet<string> = new Set([
182
+ 'decisions',
183
+ 'constraints',
184
+ 'do_not_touch',
185
+ 'assumptions',
186
+ 'failed_approaches',
187
+ 'successful_approaches',
188
+ 'open_questions',
189
+ 'not_carried_forward',
190
+ 'status_summary',
191
+ 'suggested_skills',
192
+ 'open_worker_envelopes',
193
+ ]);
194
+
195
+ /** Required on a `continuity_record` — each one computable from on-disk state. */
196
+ export const CONTINUITY_RECORD_REQUIRED_KEYS: readonly string[] = [
197
+ 'capsule_version',
198
+ 'variant',
199
+ 'summary',
200
+ 'task',
201
+ 'workspace',
202
+ 'written_at',
203
+ 'acceptance_criteria',
204
+ 'remaining',
205
+ 'predecessor',
206
+ ];
207
+
150
208
  /** A single-line token no longer than `max` chars. Rejects any body-shaped value. */
151
209
  function isShortLine(value: unknown, max: number): value is string {
152
210
  return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
@@ -757,6 +815,31 @@ export function validateRecycleEnvelope(input: unknown): string[] {
757
815
  }
758
816
  }
759
817
 
818
+ // VARIANT FIRST, then version. The order is the contract, not a style
819
+ // choice. Required fields are variant-specific, so a reader that does not
820
+ // recognise the variant cannot know what to demand — and a reader that
821
+ // reports twenty field violations against a variant it never understood has
822
+ // told the operator nothing useful and, worse, has implied the record was
823
+ // malformed when the truth is that the reader is old. One refusal naming
824
+ // the variant is the honest output, and it is what makes an old reader
825
+ // meeting a new record a legible event rather than a pile of noise.
826
+ const variant = e['variant'];
827
+ if (typeof variant !== 'string' || !(CAPSULE_VARIANTS as readonly string[]).includes(variant)) {
828
+ return [
829
+ `unknown variant ${JSON.stringify(variant)} — this reader knows ` +
830
+ `${CAPSULE_VARIANTS.join(' | ')}. A reader that does not know a variant must not ` +
831
+ 'guess at its required fields, so no further field checks were run.',
832
+ ];
833
+ }
834
+ if (variant === 'worker') {
835
+ return [
836
+ "variant 'worker' is a subagent generation handover and is validated by the worker " +
837
+ 'path, not by the main-session recycle validator — routing it here would apply ' +
838
+ 'the wrong required set to a well-formed record.',
839
+ ];
840
+ }
841
+ const isDerived = variant === 'continuity_record';
842
+
760
843
  const version = e['capsule_version'];
761
844
  if (typeof version !== 'number' || !ACCEPTED_CAPSULE_VERSIONS.includes(version)) {
762
845
  errors.push(`capsule_version must be one of ${ACCEPTED_CAPSULE_VERSIONS.join(' | ')}`);
@@ -766,9 +849,35 @@ export function validateRecycleEnvelope(input: unknown): string[] {
766
849
  // Demanding the new fields of an already-written record would be a
767
850
  // retroactive requirement, which is the one thing the recorded schema lock
768
851
  // forbids ("add fields or variants, never repurpose or remove one").
769
- const atV4 = version === 4;
770
- if (e['variant'] !== 'main_session') {
771
- errors.push("variant must be 'main_session'");
852
+ //
853
+ // A `continuity_record` is exempt from the two v4 judgement fields for the
854
+ // opposite reason: it is forbidden from carrying them at all (below), so
855
+ // requiring them would make the variant unsatisfiable.
856
+ const atV4 = version === 4 && !isDerived;
857
+
858
+ if (isDerived) {
859
+ // Forbidden, not merely absent: a derived record that carries a
860
+ // judgement field is either a `main_session` mislabelled, or a
861
+ // deterministic writer that invented a value. Both are worth refusing
862
+ // loudly, and neither is distinguishable after the fact.
863
+ for (const key of Object.keys(e)) {
864
+ if (CONTINUITY_RECORD_FORBIDDEN_KEYS.has(key)) {
865
+ errors.push(
866
+ `field "${key}" is forbidden on variant 'continuity_record' — it carries a ` +
867
+ 'model judgement, and a record written without model spend cannot have made ' +
868
+ "one. Use variant 'main_session' for a judged record.",
869
+ );
870
+ }
871
+ }
872
+ for (const key of CONTINUITY_RECORD_REQUIRED_KEYS) {
873
+ if (e[key] === undefined) {
874
+ errors.push(
875
+ `field "${key}" is required on variant 'continuity_record' — it is computable ` +
876
+ 'from on-disk state, so its absence is a writer defect rather than an ' +
877
+ 'unavailable judgement.',
878
+ );
879
+ }
880
+ }
772
881
  }
773
882
  if (!isShortLine(e['summary'], MAX_LINE_CHARS)) {
774
883
  errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
@@ -789,7 +898,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
789
898
  errors.push('acceptance_criteria must carry at least one entry — the successor cannot know "done" without it');
790
899
  }
791
900
  checkList(errors, 'remaining', e['remaining'], MAX_LINE_CHARS, true);
792
- checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, true);
901
+ checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, !isDerived);
793
902
  checkList(errors, 'decisions', e['decisions'], MAX_LINE_CHARS, false);
794
903
  errors.push(...decisionTagErrors(e['decisions']));
795
904
  checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
@@ -809,7 +918,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
809
918
 
810
919
  // Required with ≥ 1 entry (Phase 2.3): a session that abandoned nothing
811
920
  // writes `none`. Absence must never be readable as "nothing failed".
812
- checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, true);
921
+ checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, !isDerived);
813
922
  if (Array.isArray(e['failed_approaches']) && e['failed_approaches'].length === 0) {
814
923
  errors.push(
815
924
  'failed_approaches must carry at least one entry — write "none" explicitly, so ' +
@@ -835,7 +944,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
835
944
  // is a reader waiting for something that will not arrive.
836
945
  const predecessor = e['predecessor'];
837
946
  if (predecessor === undefined) {
838
- if (atV4) {
947
+ if (atV4 || isDerived) {
839
948
  errors.push(
840
949
  'predecessor is required — name the session this one continues, or write "none" ' +
841
950
  'explicitly for the first session in a workspace',
@@ -47,6 +47,11 @@ export function curlJsonPost(url: string, extraHeaders: string[], body: unknown)
47
47
  // before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
48
48
  // A full 16k-token generation legitimately runs several minutes, so the
49
49
  // ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
50
+ // Both ceilings are PER CALL, never a run total. Members are iterated
51
+ // serially, so a run's worst case is members x passes x 290s, and nothing
52
+ // cancels a call mid-flight: `spawnSync` takes no signal, which is the
53
+ // synchronous contract the module docblock states. The only run-level bound
54
+ // is the USD cap, and it is checked AFTER a call returns.
50
55
  args.push('--connect-timeout', '30', '--max-time', '290');
51
56
  args.push('-w', '\n%{http_code}', '--data-binary', '@-');
52
57
  const r = spawnSync('curl', args, {
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Two per-run deliberation measurements, both computed from outputs the run has
3
+ * already paid for: a per-correction stage attribution, and the
4
+ * `zero_marginal_value_call_rate`.
5
+ *
6
+ * Pure and offline. No model call, no new similarity measure — every judgement
7
+ * below reuses `is_near_duplicate` from `debate_gates.ts`, which is the
8
+ * repository's existing answer to "did this reply add anything", and the
9
+ * objection vocabulary that module already publishes. Forking a second
10
+ * similarity notion here would be two answers to one question, and the second
11
+ * is the one nobody updates.
12
+ *
13
+ * WHAT A CORRECTION IS, AND WHAT IT IS NOT.
14
+ *
15
+ * A correction is an objection-bearing sentence: one that carries a marker from
16
+ * {@link CORRECTION_MARKERS} and therefore names something it is pushing back
17
+ * on. Its FIRST STAGE is the earliest stage whose text contains that sentence
18
+ * or a near-duplicate of it — so a point raised in round 1 and repeated in
19
+ * round 3 is attributed to round 1, which is the whole question 10.2 asks.
20
+ *
21
+ * It decides ATTRIBUTION, never USEFULNESS. Whether a correction improved the
22
+ * verdict is a benchmark question and the benchmark has not run; a mechanism
23
+ * here that claimed to score usefulness would be inventing the number the
24
+ * forbidden-claims list exists to prevent. {@link attributeCorrections} answers
25
+ * "where did this first appear", and the word "useful" in the step it serves is
26
+ * a filter a human applies to the output, not one this code applies for them.
27
+ *
28
+ * WHAT ZERO MARGINAL VALUE IS.
29
+ *
30
+ * A call has zero marginal value when its text near-duplicates something the
31
+ * run had ALREADY received when the call was made. Not "duplicates the member's
32
+ * own prior round" — a member restating a peer's answer verbatim adds exactly
33
+ * as little, and the per-member reading would score it as novel. So the
34
+ * comparison set is every successful call that preceded it in run order.
35
+ *
36
+ * The first successful call is never zero-marginal: nothing preceded it. That
37
+ * makes the denominator at least 1 whenever the run produced any usable call,
38
+ * which is why the rate is a number rather than a `null` on every real run —
39
+ * and a run that produced NO usable call gets `null` with a stated reason,
40
+ * because zero calls is not a rate of zero.
41
+ *
42
+ * HONEST SCOPE. This measures textual novelty against an 0.8 Jaccard bar. It
43
+ * does not measure whether a call changed the verdict, and no consumer may read
44
+ * it as decision-quality evidence. The threshold is the shipped
45
+ * `NOVELTY_DUP_THRESHOLD`, inherited rather than tuned here; nothing has been
46
+ * benchmarked against it.
47
+ */
48
+ import type { CouncilResponse } from './clients.js';
49
+ import { is_near_duplicate, NOVELTY_DUP_THRESHOLD } from './debate_gates.js';
50
+
51
+ /**
52
+ * Objection markers. Deliberately the same vocabulary the shipped dissent
53
+ * counter uses, kept as its own constant because that one counts members and
54
+ * this one classifies sentences.
55
+ */
56
+ export const CORRECTION_MARKERS =
57
+ /\b(disagree|disagrees|object|objection|reject|rejects|however|but|flaw|flawed|wrong|incorrect|counter|counter-position|dissent|contradict|contradicts|refute|refutes|mistaken|overstates|understates)\b/i;
58
+
59
+ /** One call the run made, in the order the run made it. */
60
+ export interface DeliberationCall {
61
+ /** Stage the call belongs to — a round label, a phase name, whatever the caller uses. */
62
+ readonly stage: string;
63
+ /** `provider/model`, or any stable member identity. */
64
+ readonly member: string;
65
+ /** The reply text. Empty or error replies are not scored. */
66
+ readonly text: string;
67
+ /** True when the transport reported a failure; such a call is not scored. */
68
+ readonly failed?: boolean;
69
+ }
70
+
71
+ /** Where one correction first entered the deliberation. */
72
+ export interface CorrectionAttribution {
73
+ /** The correction sentence, trimmed and whitespace-collapsed. */
74
+ readonly correction: string;
75
+ /** Stage label of the earliest call carrying it. */
76
+ readonly firstStage: string;
77
+ /** Zero-based index of that call in run order. */
78
+ readonly firstCallIndex: number;
79
+ /** Member whose call first carried it. */
80
+ readonly firstMember: string;
81
+ /** How many later calls restated it. 0 means it was said once. */
82
+ readonly restatements: number;
83
+ }
84
+
85
+ /**
86
+ * One deliberation stage and what it produced — the record shape
87
+ * `CouncilRouteRecord` carries, filled from a real run rather than by a test.
88
+ */
89
+ export interface StageProduction {
90
+ readonly stage: string;
91
+ readonly produced: number;
92
+ readonly calls: number;
93
+ }
94
+
95
+ export interface ZeroMarginalValueReading {
96
+ /**
97
+ * Zero-marginal calls over scored calls, or `null` when the run produced no
98
+ * scorable call at all. `null` is "not measurable here", never zero.
99
+ */
100
+ readonly rate: number | null;
101
+ /** Why the rate is `null`. Empty string when it is not. */
102
+ readonly unavailableReason: string;
103
+ /** Every call the caller passed, failed and empty ones included. */
104
+ readonly callsTotal: number;
105
+ /** Calls that carried usable text — the denominator. */
106
+ readonly callsScored: number;
107
+ /** Calls that near-duplicated something already received — the numerator. */
108
+ readonly callsZeroMarginal: number;
109
+ /** The Jaccard bar used, published so the number can be re-derived. */
110
+ readonly threshold: number;
111
+ }
112
+
113
+ /** Decimals the published rate is rounded to, so a JSON round-trip is exact. */
114
+ export const RATE_PRECISION = 6;
115
+
116
+ function _round(value: number): number {
117
+ const f = 10 ** RATE_PRECISION;
118
+ return Math.round(value * f) / f;
119
+ }
120
+
121
+ function _usable(c: DeliberationCall): boolean {
122
+ return c.failed !== true && c.text.trim().length > 0;
123
+ }
124
+
125
+ /** Split a reply into sentences. Crude on purpose: no NLP, no dependency. */
126
+ function _sentences(text: string): string[] {
127
+ return text
128
+ .split(/(?<=[.!?])\s+|\n+/)
129
+ .map((s) => s.replace(/\s+/g, ' ').trim())
130
+ .filter((s) => s.length > 0);
131
+ }
132
+
133
+ /**
134
+ * Correction sentences in one reply.
135
+ *
136
+ * A minimum length is applied because a bare "But." carries a marker and no
137
+ * position; the bar is characters rather than a judgement so it stays
138
+ * predictable.
139
+ */
140
+ export const MIN_CORRECTION_CHARS = 25;
141
+
142
+ export function correctionSentences(text: string): string[] {
143
+ return _sentences(text).filter(
144
+ (s) => s.length >= MIN_CORRECTION_CHARS && CORRECTION_MARKERS.test(s),
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Attribute every correction to the earliest call that carried it.
150
+ *
151
+ * Calls are read in the order given, which must be run order — the attribution
152
+ * is only meaningful if "earlier in the list" means "earlier in the run".
153
+ */
154
+ export function attributeCorrections(
155
+ calls: readonly DeliberationCall[],
156
+ threshold: number = NOVELTY_DUP_THRESHOLD,
157
+ ): CorrectionAttribution[] {
158
+ const out: CorrectionAttribution[] = [];
159
+ const restatements: number[] = [];
160
+
161
+ calls.forEach((call, idx) => {
162
+ if (!_usable(call)) return;
163
+ for (const sentence of correctionSentences(call.text)) {
164
+ const seen = out.findIndex(
165
+ (a) =>
166
+ a.correction === sentence || is_near_duplicate(a.correction, sentence, threshold),
167
+ );
168
+ if (seen !== -1) {
169
+ restatements[seen] = (restatements[seen] ?? 0) + 1;
170
+ continue;
171
+ }
172
+ restatements.push(0);
173
+ out.push({
174
+ correction: sentence,
175
+ firstStage: call.stage,
176
+ firstCallIndex: idx,
177
+ firstMember: call.member,
178
+ restatements: 0,
179
+ });
180
+ }
181
+ });
182
+
183
+ return out.map((a, i) => ({ ...a, restatements: restatements[i] ?? 0 }));
184
+ }
185
+
186
+ /**
187
+ * Per-stage production, derived from the attribution so the two can never
188
+ * disagree: `produced` counts the corrections FIRST attributed to that stage.
189
+ */
190
+ export function stageProduction(
191
+ calls: readonly DeliberationCall[],
192
+ attributions: readonly CorrectionAttribution[],
193
+ ): StageProduction[] {
194
+ const order: string[] = [];
195
+ const callCount = new Map<string, number>();
196
+ for (const c of calls) {
197
+ if (!callCount.has(c.stage)) {
198
+ order.push(c.stage);
199
+ callCount.set(c.stage, 0);
200
+ }
201
+ callCount.set(c.stage, (callCount.get(c.stage) ?? 0) + 1);
202
+ }
203
+ const produced = new Map<string, number>();
204
+ for (const a of attributions) {
205
+ produced.set(a.firstStage, (produced.get(a.firstStage) ?? 0) + 1);
206
+ }
207
+ return order.map((stage) => ({
208
+ stage,
209
+ produced: produced.get(stage) ?? 0,
210
+ calls: callCount.get(stage) ?? 0,
211
+ }));
212
+ }
213
+
214
+ /**
215
+ * The `zero_marginal_value_call_rate` for one run.
216
+ *
217
+ * Every scored call is compared against every scored call that preceded it. The
218
+ * comparison is O(n squared) in the number of calls, which is fine: a council
219
+ * run is single digits of calls per round.
220
+ */
221
+ export function zeroMarginalValueRate(
222
+ calls: readonly DeliberationCall[],
223
+ threshold: number = NOVELTY_DUP_THRESHOLD,
224
+ ): ZeroMarginalValueReading {
225
+ const received: string[] = [];
226
+ let zero = 0;
227
+
228
+ for (const call of calls) {
229
+ if (!_usable(call)) continue;
230
+ const dup = received.some((prior) => is_near_duplicate(prior, call.text, threshold));
231
+ if (dup) zero += 1;
232
+ received.push(call.text);
233
+ }
234
+
235
+ if (received.length === 0) {
236
+ return {
237
+ rate: null,
238
+ unavailableReason:
239
+ 'no scorable call — every call failed or returned empty text, and zero calls is not a rate of zero',
240
+ callsTotal: calls.length,
241
+ callsScored: 0,
242
+ callsZeroMarginal: 0,
243
+ threshold,
244
+ };
245
+ }
246
+
247
+ return {
248
+ rate: _round(zero / received.length),
249
+ unavailableReason: '',
250
+ callsTotal: calls.length,
251
+ callsZeroMarginal: zero,
252
+ callsScored: received.length,
253
+ threshold,
254
+ };
255
+ }
256
+
257
+ /** Everything this module emits for one run, ready to serialise. */
258
+ export interface DeliberationMetrics {
259
+ readonly zero_marginal_value_call_rate: number | null;
260
+ readonly zero_marginal_value_unavailable_reason: string;
261
+ readonly calls_total: number;
262
+ readonly calls_scored: number;
263
+ readonly calls_zero_marginal: number;
264
+ readonly novelty_threshold: number;
265
+ readonly stage_outputs: readonly StageProduction[];
266
+ readonly correction_attributions: readonly CorrectionAttribution[];
267
+ }
268
+
269
+ /** Compute both measurements in one pass over the run's calls. */
270
+ export function measureDeliberation(
271
+ calls: readonly DeliberationCall[],
272
+ threshold: number = NOVELTY_DUP_THRESHOLD,
273
+ ): DeliberationMetrics {
274
+ const attributions = attributeCorrections(calls, threshold);
275
+ const zmv = zeroMarginalValueRate(calls, threshold);
276
+ return {
277
+ zero_marginal_value_call_rate: zmv.rate,
278
+ zero_marginal_value_unavailable_reason: zmv.unavailableReason,
279
+ calls_total: zmv.callsTotal,
280
+ calls_scored: zmv.callsScored,
281
+ calls_zero_marginal: zmv.callsZeroMarginal,
282
+ novelty_threshold: zmv.threshold,
283
+ stage_outputs: stageProduction(calls, attributions),
284
+ correction_attributions: attributions,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * The parts of one council run that made provider calls, in the order the run
290
+ * made them.
291
+ *
292
+ * Assembled here rather than at the call site because "which calls did this run
293
+ * make, and at which stage" is this module's own subject, and the CLI had the
294
+ * list only as a flat array with the stage boundaries implicit in the order.
295
+ * Both readings come out of one walk, so the billing total and the attribution
296
+ * can never be computed over different sets.
297
+ */
298
+ export interface RunCallParts {
299
+ /** The deliberation responses `consult()` returned. */
300
+ readonly deliberation: readonly CouncilResponse[];
301
+ readonly peerReview?: readonly CouncilResponse[] | null;
302
+ readonly consensusExtraction?: readonly CouncilResponse[] | null;
303
+ readonly consensusScoring?: readonly CouncilResponse[] | null;
304
+ readonly chairman?: CouncilResponse | null;
305
+ readonly stanceRepairs?: readonly CouncilResponse[] | null;
306
+ }
307
+
308
+ /** The six stage labels, in run order. */
309
+ export const RUN_STAGES = Object.freeze([
310
+ 'deliberation',
311
+ 'peer-review',
312
+ 'consensus-extraction',
313
+ 'consensus-scoring',
314
+ 'chairman',
315
+ 'stance-repair',
316
+ ] as const);
317
+
318
+ export interface RunCalls {
319
+ /** Stage-labelled calls, for {@link measureDeliberation}. */
320
+ readonly calls: readonly DeliberationCall[];
321
+ /** The same calls as raw responses, in the same order, for cost aggregation. */
322
+ readonly responses: CouncilResponse[];
323
+ }
324
+
325
+ function _asCall(stage: string, r: CouncilResponse): DeliberationCall {
326
+ return {
327
+ stage,
328
+ member: `${r.provider}/${r.model}`,
329
+ text: r.text,
330
+ failed: r.error !== null && r.error !== undefined,
331
+ };
332
+ }
333
+
334
+ /** Walk one run's parts into stage-labelled calls plus the flat response list. */
335
+ export function runCallsFrom(parts: RunCallParts): RunCalls {
336
+ const groups: ReadonlyArray<readonly [string, readonly CouncilResponse[]]> = [
337
+ ['deliberation', parts.deliberation],
338
+ ['peer-review', parts.peerReview ?? []],
339
+ ['consensus-extraction', parts.consensusExtraction ?? []],
340
+ ['consensus-scoring', parts.consensusScoring ?? []],
341
+ ['chairman', parts.chairman === null || parts.chairman === undefined ? [] : [parts.chairman]],
342
+ ['stance-repair', parts.stanceRepairs ?? []],
343
+ ];
344
+ const calls: DeliberationCall[] = [];
345
+ const responses: CouncilResponse[] = [];
346
+ for (const [stage, group] of groups) {
347
+ for (const r of group) {
348
+ calls.push(_asCall(stage, r));
349
+ responses.push(r);
350
+ }
351
+ }
352
+ return { calls, responses };
353
+ }
@@ -25,6 +25,7 @@
25
25
  import { wrapUntrustedBlocks } from '../_lib/untrusted_content.js';
26
26
 
27
27
  import type { ProjectContext } from './project_context.js';
28
+ import { RESOLVING_EVIDENCE_SECTION } from './synthesis_disagreement.js';
28
29
 
29
30
  // Python: NEUTRALITY_PREAMBLE = """\...""".strip()
30
31
  export const NEUTRALITY_PREAMBLE = `You are an independent reviewer. You have NOT seen any prior reasoning,
@@ -302,6 +303,8 @@ read it directly from a response.
302
303
  A single sentence: which course the host agent should advise the
303
304
  user to take, grounded in the strongest converged point.
304
305
 
306
+ ${RESOLVING_EVIDENCE_SECTION}
307
+
305
308
  ### Kill criteria
306
309
  Observable conditions that would falsify this recommendation without
307
310
  re-convening the council. Each entry names a threshold or a specific
@@ -329,6 +332,8 @@ and the host agent confirms are load-bearing. Maximum five.
329
332
  APPROVE / REQUEST_CHANGES / REJECT and a single sentence justifying
330
333
  the verdict, anchored on the strongest consensus or must-fix line.
331
334
 
335
+ ${RESOLVING_EVIDENCE_SECTION}
336
+
332
337
  ### Kill criteria
333
338
  Observable conditions that would falsify this verdict without re-convening
334
339
  the council (a failing check, a missing test, a regression signal). Each
@@ -363,6 +368,8 @@ separate certainty word for that; \`evidence:\` measures how well a
363
368
  finding is supported, and being alone in raising it is a different
364
369
  property the section already carries.
365
370
 
371
+ ${RESOLVING_EVIDENCE_SECTION}
372
+
366
373
  ### Kill criteria
367
374
  Observable conditions that would falsify the analysis's leading finding
368
375
  without re-convening the council. Each entry names a threshold or event.
@@ -381,6 +388,8 @@ export const CREATIVE_SYNTHESIS = `Write a free-form convergence / divergence
381
388
  synthesis of the council in prose — no fixed section shape for the body.
382
389
  Then close with exactly these two sections:
383
390
 
391
+ ${RESOLVING_EVIDENCE_SECTION}
392
+
384
393
  ### Kill criteria
385
394
  Observable conditions that would falsify the direction you converged on,
386
395
  without re-convening the council. Each entry names a threshold or event.