@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Does a completed synthesis keep the disagreement it started with?
3
+ *
4
+ * Three elements have to survive into the final text, and a synthesis that
5
+ * drops any of them has resolved the disagreement by not reporting it:
6
+ *
7
+ * 1. the unresolved disagreement itself — both sides, named;
8
+ * 2. the strongest evidence the minority side actually offered;
9
+ * 3. the observation that would settle it.
10
+ *
11
+ * Element 1 was already asked for by every lens template (`Clashes`,
12
+ * `Conflicts`, `Outliers`, or the mandated divergence prose). Element 2 was
13
+ * asked for by one. Element 3 was asked for by none, which is the gap
14
+ * {@link RESOLVING_EVIDENCE_SECTION} closes.
15
+ *
16
+ * WHY A CHECKER AND NOT ONLY A TEMPLATE CLAUSE. A template is an instruction to
17
+ * the host agent, and an instruction nobody can check is a hope. Both council
18
+ * seats that reviewed this refused a prose-only build in as many words. So the
19
+ * template carries the ask and this module makes the answer decidable: given a
20
+ * completed synthesis, {@link auditSynthesisElements} reports which of the three
21
+ * elements are present, by structure, without deciding whether their content is
22
+ * any good.
23
+ *
24
+ * WHAT IT DECIDES, AND WHAT IT DOES NOT. Presence and shape. A synthesis that
25
+ * names a clash, cites a minority exhibit and proposes a resolving measurement
26
+ * PASSES even when the measurement is a bad one — grading that needs the
27
+ * benchmark, and the benchmark has not run. This is the same honest scope the
28
+ * sibling minority-retention audit states for itself, and for the same reason:
29
+ * an audit that claimed to grade quality would be inventing the number.
30
+ *
31
+ * REPORT, NEVER THROW. Not every council pass produces dissent, and a synthesis
32
+ * of a unanimous pass has no disagreement to retain. So the audit reports
33
+ * `dissentPresent: false` and grades nothing, rather than failing a synthesis
34
+ * for the absence of a thing that did not happen. A caller with independent
35
+ * knowledge that the pass dissented passes {@link AuditOptions.dissentPresent}
36
+ * and gets the full grading regardless of what the text looks like.
37
+ *
38
+ * Pure, offline, no model call.
39
+ */
40
+
41
+ /** The three elements a dissenting synthesis has to carry. */
42
+ export type SynthesisElement =
43
+ | 'unresolved-disagreement'
44
+ | 'minority-evidence'
45
+ | 'resolving-evidence';
46
+
47
+ export const SYNTHESIS_ELEMENTS: readonly SynthesisElement[] = Object.freeze([
48
+ 'unresolved-disagreement',
49
+ 'minority-evidence',
50
+ 'resolving-evidence',
51
+ ]);
52
+
53
+ /**
54
+ * The section every lens template now closes its disagreement reporting with.
55
+ *
56
+ * One section rather than two because elements 2 and 3 are both per-clash
57
+ * follow-ups: splitting them would let a synthesis answer one clash's evidence
58
+ * and another clash's resolution and still look complete.
59
+ */
60
+ export const RESOLVING_EVIDENCE_SECTION = `### Resolving evidence
61
+ One line per unresolved disagreement above, in this shape:
62
+ \`<clash> — strongest minority evidence: <what the losing side actually
63
+ showed> · resolved by: <the observation that would settle it>\`.
64
+ Name a measurement, an artefact or a run — never a further opinion. Write
65
+ \`no resolving evidence identified — <why>\` when nothing observable would
66
+ settle it: an unanswerable disagreement is a finding, and silence is not.`;
67
+
68
+ /** Headings under which a lens reports an unresolved disagreement. */
69
+ const DISAGREEMENT_HEADINGS = ['Clashes', 'Conflicts', 'Outliers', 'Divergence'];
70
+
71
+ /** The heading element 3 lives under. */
72
+ export const RESOLVING_EVIDENCE_HEADING = 'Resolving evidence';
73
+
74
+ /** In-body markers the resolving-evidence section is asked to emit. */
75
+ const MINORITY_EVIDENCE_MARKER = /strongest minority evidence\s*:/i;
76
+ const RESOLVED_BY_MARKER = /resolved by\s*:/i;
77
+ const NO_RESOLVING_EVIDENCE_MARKER = /no resolving evidence identified\s*[—:-]/i;
78
+
79
+ /**
80
+ * Text markers that say a pass dissented, used only when the caller does not
81
+ * tell us. Deliberately narrow: a heading with a non-empty body, or an explicit
82
+ * split verdict. A loose match here would grade unanimous syntheses.
83
+ */
84
+ const SPLIT_VERDICT_MARKER = /^\s*\*{0,2}verdict\*{0,2}\s*:\s*split\b/im;
85
+
86
+ export interface ElementFinding {
87
+ readonly element: SynthesisElement;
88
+ readonly present: boolean;
89
+ /** The heading or marker that satisfied it, or `null`. */
90
+ readonly satisfiedBy: string | null;
91
+ }
92
+
93
+ export interface SynthesisElementAudit {
94
+ /** Whether the pass had a disagreement to retain at all. */
95
+ readonly dissentPresent: boolean;
96
+ /** How `dissentPresent` was decided — `caller` or `text`. */
97
+ readonly dissentSource: 'caller' | 'text';
98
+ readonly elements: readonly ElementFinding[];
99
+ readonly missing: readonly SynthesisElement[];
100
+ /** True when a dissenting pass rendered all three. Always true with no dissent. */
101
+ readonly complete: boolean;
102
+ }
103
+
104
+ export interface AuditOptions {
105
+ /**
106
+ * Override the text-derived dissent decision. Pass `true` when the run's own
107
+ * tally recorded a split; pass `false` to grade nothing.
108
+ */
109
+ readonly dissentPresent?: boolean;
110
+ }
111
+
112
+ function _headingBody(text: string, heading: string): string | null {
113
+ const lines = text.split('\n');
114
+ const headingRe = new RegExp(`^#{2,4}\\s+${heading}\\s*$`, 'i');
115
+ const idx = lines.findIndex((l) => headingRe.test(l));
116
+ if (idx === -1) return null;
117
+ let body = '';
118
+ for (let i = idx + 1; i < lines.length; i++) {
119
+ if (/^#{2,4}\s/.test(lines[i] as string)) break;
120
+ body += (lines[i] as string) + '\n';
121
+ }
122
+ return body;
123
+ }
124
+
125
+ /** A heading with a non-empty, non-placeholder body. */
126
+ function _nonEmptySection(text: string, heading: string): boolean {
127
+ const body = _headingBody(text, heading);
128
+ if (body === null) return false;
129
+ const stripped = body.replace(/^\s*[*_]*(none|n\/a|nothing)[.*_\s]*$/gim, '').trim();
130
+ return stripped.length > 0;
131
+ }
132
+
133
+ /** Audit a COMPLETED synthesis — the host- or chairman-authored text. */
134
+ export function auditSynthesisElements(
135
+ text: string,
136
+ opts: AuditOptions = {},
137
+ ): SynthesisElementAudit {
138
+ const disagreementHeading =
139
+ DISAGREEMENT_HEADINGS.find((h) => _nonEmptySection(text, h)) ?? null;
140
+
141
+ const dissentSource: 'caller' | 'text' = opts.dissentPresent === undefined ? 'text' : 'caller';
142
+ const dissentPresent =
143
+ opts.dissentPresent ?? (disagreementHeading !== null || SPLIT_VERDICT_MARKER.test(text));
144
+
145
+ const resolvingBody = _headingBody(text, RESOLVING_EVIDENCE_HEADING) ?? '';
146
+ const declaredNone = NO_RESOLVING_EVIDENCE_MARKER.test(resolvingBody);
147
+
148
+ const elements: ElementFinding[] = [
149
+ {
150
+ element: 'unresolved-disagreement',
151
+ present: disagreementHeading !== null,
152
+ satisfiedBy: disagreementHeading === null ? null : `### ${disagreementHeading}`,
153
+ },
154
+ {
155
+ element: 'minority-evidence',
156
+ present: MINORITY_EVIDENCE_MARKER.test(resolvingBody) || _nonEmptySection(text, 'Outliers'),
157
+ satisfiedBy: MINORITY_EVIDENCE_MARKER.test(resolvingBody)
158
+ ? 'strongest minority evidence:'
159
+ : _nonEmptySection(text, 'Outliers')
160
+ ? '### Outliers'
161
+ : null,
162
+ },
163
+ {
164
+ element: 'resolving-evidence',
165
+ present: RESOLVED_BY_MARKER.test(resolvingBody) || declaredNone,
166
+ satisfiedBy: RESOLVED_BY_MARKER.test(resolvingBody)
167
+ ? 'resolved by:'
168
+ : declaredNone
169
+ ? 'no resolving evidence identified'
170
+ : null,
171
+ },
172
+ ];
173
+
174
+ const missing = dissentPresent
175
+ ? elements.filter((e) => !e.present).map((e) => e.element)
176
+ : [];
177
+
178
+ return {
179
+ dissentPresent,
180
+ dissentSource,
181
+ elements,
182
+ missing,
183
+ complete: missing.length === 0,
184
+ };
185
+ }
186
+
187
+ /** Human-readable audit text. */
188
+ export function renderSynthesisElementAudit(a: SynthesisElementAudit): string {
189
+ if (!a.dissentPresent) {
190
+ return (
191
+ 'synthesis element audit: SKIPPED — no unresolved disagreement in this pass ' +
192
+ `(decided from ${a.dissentSource}); there is nothing to retain\n`
193
+ );
194
+ }
195
+ const rows = a.elements
196
+ .map(
197
+ (e) =>
198
+ ` - ${e.element}: ${e.present ? 'present' : 'MISSING'}` +
199
+ (e.satisfiedBy === null ? '' : ` [${e.satisfiedBy}]`),
200
+ )
201
+ .join('\n');
202
+ const verdict = a.complete
203
+ ? 'synthesis retains all three elements of the unresolved disagreement'
204
+ : `synthesis DROPS ${a.missing.length} of 3 elements: ${a.missing.join(', ')}`;
205
+ return `${verdict}\n${rows}\n`;
206
+ }