opencode-swarm 7.146.2 → 7.146.4

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 (86) hide show
  1. package/.opencode/skills/swarm-pr-review/SKILL.md +27 -44
  2. package/.opencode/skills/swarm-pr-review/references/findings-persistence-contract.md +52 -12
  3. package/.opencode/skills/swarm-pr-review/references/lane-output-recoverability.md +6 -3
  4. package/.opencode/skills/swarm-pr-review/references/parser-dry-run.md +19 -1
  5. package/.opencode/skills/swarm-pr-review/references/verdict-settlement-contract.md +41 -0
  6. package/dist/agents/council-prompts.d.ts +3 -3
  7. package/dist/background/candidate-contract.d.ts +21 -1
  8. package/dist/background/candidate-parser.d.ts +21 -1
  9. package/dist/background/pending-delegations.d.ts +8 -1
  10. package/dist/background/pr-review-collection-receipt.d.ts +70 -0
  11. package/dist/cli/{config-doctor-vstrprja.js → config-doctor-578ebqhn.js} +2 -2
  12. package/dist/cli/{core-3wzpwe5z.js → core-2ne03a43.js} +1 -1
  13. package/dist/cli/{curation-policy-x19e8bqc.js → curation-policy-g353f457.js} +6 -6
  14. package/dist/cli/{curator-llm-factory-6bra0bfd.js → curator-llm-factory-6hgn7p6n.js} +27 -27
  15. package/dist/cli/{curator-cvy1s7wd.js → curator-w52frg17.js} +27 -27
  16. package/dist/cli/{evidence-summary-service-yhap2xph.js → evidence-summary-service-nj4p6a0d.js} +11 -11
  17. package/dist/cli/{gate-evidence-vxbyq5sb.js → gate-evidence-4vnjdh9y.js} +6 -6
  18. package/dist/cli/{guardrail-explain-h8cnwa02.js → guardrail-explain-rq01z1yz.js} +28 -28
  19. package/dist/cli/{guardrail-log-syb7tz2w.js → guardrail-log-ratb651f.js} +3 -3
  20. package/dist/cli/{hive-promoter-3tex870c.js → hive-promoter-pbm9kee1.js} +27 -27
  21. package/dist/cli/{index-yxhcy5cm.js → index-1mbdwy12.js} +55 -1
  22. package/dist/cli/{index-01ybcsa5.js → index-3neh2mxk.js} +2 -2
  23. package/dist/cli/{index-5e5a1y2r.js → index-3p4wgm0d.js} +1 -1
  24. package/dist/cli/{index-4vwn66wk.js → index-5cbbtbvf.js} +3 -3
  25. package/dist/cli/{index-z5ppkn8y.js → index-6eycwm3v.js} +6 -6
  26. package/dist/cli/{index-bghwrr59.js → index-6q35swn2.js} +1 -1
  27. package/dist/cli/{index-qbpxrw95.js → index-8py21zn6.js} +2 -2
  28. package/dist/cli/{index-tj861zhd.js → index-96zkay2e.js} +4 -4
  29. package/dist/cli/{index-y3hmyvt6.js → index-bq4mvwht.js} +2 -2
  30. package/dist/cli/{index-rfph0nhf.js → index-brb64k96.js} +1426 -635
  31. package/dist/cli/{index-p0m03xa6.js → index-df8xj9hr.js} +4 -4
  32. package/dist/cli/{index-cr90rcg3.js → index-dp5w9s5z.js} +7 -7
  33. package/dist/cli/{index-w0cj7zyd.js → index-f75qjtn5.js} +1 -1
  34. package/dist/cli/{index-vmfaafte.js → index-ffqdah8s.js} +5 -5
  35. package/dist/cli/{index-f3x2gsby.js → index-fp2d44f5.js} +1 -1
  36. package/dist/cli/{index-jv9xs6mb.js → index-g2r8nknz.js} +2 -2
  37. package/dist/cli/{index-n4k0pt5f.js → index-g335vrc7.js} +4 -4
  38. package/dist/cli/{index-z9ed68jt.js → index-gr3zfcsh.js} +18 -4
  39. package/dist/cli/{index-w5r84rd1.js → index-hcb25p12.js} +1 -1
  40. package/dist/cli/{index-ycgm9pzg.js → index-hw1yghpg.js} +7 -6
  41. package/dist/cli/{index-a1febb2k.js → index-jgwhpza8.js} +2 -2
  42. package/dist/cli/{index-cx67qa9g.js → index-kjmcp9mf.js} +1 -1
  43. package/dist/cli/{index-42nq97rb.js → index-kv2j4cnq.js} +6 -6
  44. package/dist/cli/{index-zkaz3fhw.js → index-p3k155dv.js} +2 -2
  45. package/dist/cli/{index-qyvjp0bq.js → index-q81s0m8k.js} +66 -37
  46. package/dist/cli/{index-97psh79d.js → index-rpy0bent.js} +178 -18
  47. package/dist/cli/{index-r46q6996.js → index-t798v5pq.js} +29 -29
  48. package/dist/cli/{index-e1hjd15c.js → index-tp5ytzzg.js} +1 -1
  49. package/dist/cli/{index-vj9bba5k.js → index-vdrwj7z0.js} +4 -4
  50. package/dist/cli/{index-46cypnz3.js → index-wesj94x0.js} +4 -4
  51. package/dist/cli/{index-c9jr4j2q.js → index-xsxj4vbf.js} +1 -1
  52. package/dist/cli/index.js +27 -27
  53. package/dist/cli/{knowledge-escalator-mn4sx4qd.js → knowledge-escalator-jbazxmk5.js} +11 -11
  54. package/dist/cli/{knowledge-events-x975ngy4.js → knowledge-events-jyqy3st8.js} +9 -9
  55. package/dist/cli/{knowledge-link-8pr02pza.js → knowledge-link-jkx8xjxp.js} +5 -5
  56. package/dist/cli/{knowledge-store-33p762ey.js → knowledge-store-e2defagd.js} +6 -6
  57. package/dist/cli/{knowledge-validator-q5jc0scs.js → knowledge-validator-pnmdsw2b.js} +8 -8
  58. package/dist/cli/{pending-delegations-kkrczqv7.js → pending-delegations-k2yqg1sj.js} +3 -3
  59. package/dist/cli/{pr-subscriptions-2xn238xt.js → pr-subscriptions-js5v2xr6.js} +3 -3
  60. package/dist/cli/{runner-e7z9cf5y.js → runner-q7d22d1w.js} +6 -6
  61. package/dist/cli/{scan-cursor-p7t5mb4x.js → scan-cursor-snx85enn.js} +7 -7
  62. package/dist/cli/{schema-t8bmshhj.js → schema-6rmxzg6g.js} +1 -1
  63. package/dist/cli/{scope-persistence-2kyrgsca.js → scope-persistence-23cqafkx.js} +7 -7
  64. package/dist/cli/{skill-generator-fwvmcv8m.js → skill-generator-980zjk9q.js} +13 -13
  65. package/dist/cli/{telemetry-t109dhv7.js → telemetry-xzy0qeap.js} +1 -1
  66. package/dist/cli/{worktree-collision-ownership-2jtg2my1.js → worktree-collision-ownership-xj0bsg92.js} +3 -3
  67. package/dist/cli/{worktree-isolation-87j70zg1.js → worktree-isolation-4d0nc04c.js} +27 -27
  68. package/dist/commands/close.d.ts +1 -0
  69. package/dist/commands/context-map-stats.d.ts +5 -0
  70. package/dist/config/schema.d.ts +16 -0
  71. package/dist/context-map/telemetry.d.ts +168 -26
  72. package/dist/council/council-evidence-writer.d.ts +2 -1
  73. package/dist/council/council-freshness.d.ts +49 -0
  74. package/dist/council/council-review-identity.d.ts +133 -0
  75. package/dist/council/council-round-state.d.ts +27 -3
  76. package/dist/council/disagreement-detector.d.ts +12 -2
  77. package/dist/council/general-council-types.d.ts +32 -0
  78. package/dist/council/types.d.ts +43 -7
  79. package/dist/hooks/pr-workflow-gate.d.ts +58 -5
  80. package/dist/index.js +177 -152
  81. package/dist/observability/catalog.d.ts +7 -5
  82. package/dist/telemetry.d.ts +22 -1
  83. package/dist/tools/dispatch-lanes.d.ts +30 -8
  84. package/dist/tools/phase-complete/gates/types.d.ts +7 -0
  85. package/dist/tools/write-final-council-evidence.d.ts +1 -7
  86. package/package.json +2 -1
@@ -2,8 +2,32 @@
2
2
  * Telemetry recording and persistence for the Context Capsule feature.
3
3
  *
4
4
  * Records capsule telemetry data (token estimates, cache hit/miss counts,
5
- * stale summary counts) per delegation. Aggregates telemetry written to
6
- * `.swarm/context-telemetry.jsonl` for user inspection.
5
+ * stale summary counts) per delegation.
6
+ *
7
+ * ISSUE #2037 (Observability PR 09/23): this store was previously an unbounded
8
+ * append-only JSONL (`.swarm/context-telemetry.jsonl`) full-read on every
9
+ * summary. It is now a BOUNDED single-file store:
10
+ *
11
+ * Line 1: `ctx-telemetry-manifest` header carrying a size-bounded FOLDED
12
+ * aggregate (records compacted away) + health counters.
13
+ * Line 2+: raw `TelemetryEntry` JSONL — the RECENT retained window, bounded
14
+ * to CONTEXT_TELEMETRY_LIMITS.activeMaxBytes / activeMaxEntries.
15
+ *
16
+ * Lifetime totals = folded aggregate (header) + retained window (file). Because
17
+ * every structural mutation (first write, compaction, cutover migration,
18
+ * finalize) is a synchronous, atomic single-file rewrite (write tmp + atomic
19
+ * rename), a reader sees either the old complete file or the new complete file
20
+ * — there is no partial-apply state, so double-count and loss cannot arise from
21
+ * a crash mid-mutation.
22
+ *
23
+ * Concurrency: in production the ONLY writer is the plugin's
24
+ * `context-capsule-inject` hook, and every mutating function here is
25
+ * synchronous, so within a single plugin process there is no interleaving at
26
+ * all. A second plugin instance on the same project root is guarded by an
27
+ * exclusive `.swarm/context-telemetry.lock` (`wx` create, stale-broken)
28
+ * acquired for the compaction/cutover read→rewrite window. Cross-process
29
+ * CONCURRENT WRITERS to this store are documented as unsupported (it is a
30
+ * single-writer store). All disk failures are fail-open.
7
31
  *
8
32
  * All functions are synchronous for simplicity and reliability. The module
9
33
  * uses the `_internals` DI seam pattern so tests can override filesystem
@@ -15,9 +39,32 @@
15
39
  *
16
40
  * No `bun:` imports — this module is Node-ESM-loadable (Invariant 2).
17
41
  *
18
- * See issue #1104, FR-007.
42
+ * See issue #1104, FR-007, and issue #2037.
19
43
  */
20
44
  import * as fs from 'node:fs';
45
+ export interface ContextTelemetryLimits {
46
+ /** Hard ceiling on the retained raw window (bytes). */
47
+ activeMaxBytes: number;
48
+ /** Hard ceiling on the retained raw window (entries). */
49
+ activeMaxEntries: number;
50
+ /** Raw retention age: records older than this are pruned from the raw
51
+ * window (the folded aggregate is lifetime and unaffected by pruning). */
52
+ ageMaxMs: number;
53
+ /** Bounded work per legacy-migration / compaction pass (bytes folded). */
54
+ compactMaxBytes: number;
55
+ /** Hard documented read bound: getTelemetrySummary never reads more than
56
+ * this, even when legacy history is arbitrarily large. Must satisfy
57
+ * readMaxBytes >= activeMaxBytes + headerMaxBytes + 1 KiB slack. */
58
+ readMaxBytes: number;
59
+ /** Writes between throttled maintenance checks (mirrors core telemetry's
60
+ * ROTATION_CHECK_INTERVAL). */
61
+ checkInterval: number;
62
+ /** Disk-pressure/failure warning cooldown. */
63
+ warnCooldownMs: number;
64
+ /** Upper bound for a serialized manifest header (single line). */
65
+ headerMaxBytes: number;
66
+ }
67
+ export declare const CONTEXT_TELEMETRY_LIMITS: ContextTelemetryLimits;
21
68
  /**
22
69
  * A single telemetry record for one capsule delegation.
23
70
  * Written as one JSON line to `.swarm/context-telemetry.jsonl`.
@@ -46,12 +93,23 @@ export interface TelemetryEntry {
46
93
  /** Whether capsule generation succeeded */
47
94
  success: boolean;
48
95
  }
96
+ /**
97
+ * Coverage disclosure for a computed summary (issue #2037). `'partial-unmigrated'`
98
+ * is produced whenever the on-disk store exceeds the read bound (a pre-cutover
99
+ * header-less file, or a header'd store mid-migration with an unmigrated legacy
100
+ * tail) — in both cases the totals are INCOMPLETE. `'truncated'` is reserved for
101
+ * the physical "read exceeded the bound" state but is never surfaced to callers:
102
+ * a truncated header'd read is re-labeled `'partial-unmigrated'`.
103
+ */
104
+ export type TelemetryCoverage = 'complete' | 'partial-unmigrated' | 'truncated' | 'empty';
49
105
  /**
50
106
  * Aggregate statistics computed from all telemetry entries.
51
- * Returned by {@link getTelemetrySummary}.
107
+ * Returned by {@link getTelemetrySummary}. The eight leading fields preserve the
108
+ * pre-issue-#2037 public surface (semantically compatible); the trailing fields
109
+ * are additive issue-#2037 disclosure (partial coverage / drops / retention).
52
110
  */
53
111
  export interface TelemetrySummary {
54
- /** Total number of capsule delegations recorded */
112
+ /** Total number of capsule delegations recorded (lifetime) */
55
113
  total_delegations: number;
56
114
  /** Sum of all cache hits across entries */
57
115
  total_cache_hits: number;
@@ -67,53 +125,137 @@ export interface TelemetrySummary {
67
125
  total_skipped_reads: number;
68
126
  /** Percentage of successful capsule generations (0–100) */
69
127
  success_rate: number;
128
+ /** Coverage disclosure (issue #2037). 'complete' = full lifetime history
129
+ * accounted; 'partial-unmigrated' = the on-disk store exceeds the read bound
130
+ * (legacy history not yet fully migrated) so only the accounted part is
131
+ * reflected — never presented as a complete-looking number. */
132
+ coverage: TelemetryCoverage;
133
+ /** Newest - oldest timestamp across all accounted records (ms, or null when
134
+ * there is no accounted history). */
135
+ tracked_period_ms: number | null;
136
+ /** Records currently retained in the on-disk raw window. */
137
+ retained_entries: number;
138
+ /** Records folded into the durable aggregate. */
139
+ folded_entries: number;
140
+ /** Corrupt/partial lines encountered (maintenance + current window). */
141
+ corrupt_entries: number;
142
+ /** Records folded into the aggregate due to age retention — included in
143
+ * lifetime totals (not an additive counter on top of them), just not
144
+ * retained in the raw window. */
145
+ dropped_entries: number;
146
+ /** Approximate on-disk bytes of the store. */
147
+ on_disk_bytes: number;
70
148
  }
71
149
  /**
72
150
  * Test-only dependency-injection seam. Production code calls through this
73
151
  * object so tests can replace the underlying implementations without
74
152
  * `mock.module` (which leaks across files in Bun's shared test-runner process).
75
153
  * Mutating this local object is file-scoped and trivially restorable
76
- * via `afterEach`.
154
+ * via `afterEach`. `limits` is also overridable so tests can exercise small
155
+ * budgets.
77
156
  */
78
157
  export declare const _internals: {
79
158
  readonly appendFileSync: typeof fs.appendFileSync;
80
159
  readonly readFileSync: typeof fs.readFileSync;
81
160
  readonly existsSync: typeof fs.existsSync;
82
161
  readonly mkdirSync: typeof fs.mkdirSync;
162
+ readonly statSync: fs.StatSyncFn;
163
+ readonly renameSync: typeof fs.renameSync;
164
+ readonly writeFileSync: typeof fs.writeFileSync;
165
+ readonly unlinkSync: typeof fs.unlinkSync;
166
+ readonly openSync: typeof fs.openSync;
167
+ readonly closeSync: typeof fs.closeSync;
168
+ readonly readSync: typeof fs.readSync;
169
+ readonly limits: ContextTelemetryLimits;
170
+ readonly emitHealth: typeof emitContextTelemetryHealth;
171
+ readonly withStoreLock: typeof withStoreLock;
83
172
  };
173
+ /** Size-bounded folded aggregate persisted in the manifest header. FOLDED-ONLY:
174
+ * records compacted/cut away from the raw window. Retained records are NOT in
175
+ * here. Lifetime total = folded + retained. */
176
+ export interface FoldedAggregate {
177
+ delegations: number;
178
+ successCount: number;
179
+ cacheHits: number;
180
+ cacheMisses: number;
181
+ staleEntries: number;
182
+ tokenSum: number;
183
+ recommendedReads: number;
184
+ skippedReads: number;
185
+ corrupt: number;
186
+ dropped: number;
187
+ oldestTimestamp: string | null;
188
+ newestTimestamp: string | null;
189
+ }
190
+ /** The manifest header — line 1 of `.swarm/context-telemetry.jsonl`. */
191
+ export interface TelemetryManifest {
192
+ v: 2;
193
+ type: 'ctx-telemetry-manifest';
194
+ schemaVersion: number;
195
+ folded: FoldedAggregate;
196
+ updatedAt: string;
197
+ }
198
+ declare function withStoreLock<T>(directory: string, fn: () => T): T | null;
84
199
  /**
85
- * Append a telemetry entry to `.swarm/context-telemetry.jsonl`.
86
- *
87
- * Creates the `.swarm/` directory if it does not exist. The entry is
88
- * serialized as a single JSON line and appended to the file.
89
- *
90
- * Returns `true` on success, `false` on any error. Never throws.
91
- *
92
- * @param entry - The telemetry record to append
93
- * @param directory - Project root directory (must contain `.swarm/`)
200
+ * Test seam (AGENTS.md invariant 7): resets the module-scoped maintenance
201
+ * counters so an unswept run in Bun's shared test-runner process cannot shift
202
+ * a later test's first maintenance pass. Restore is one call, always in
203
+ * `afterEach`.
94
204
  */
205
+ export declare function _resetMaintenanceCounters(): void;
95
206
  export declare function recordTelemetry(entry: TelemetryEntry, directory: string): boolean;
96
207
  /**
97
- * Read all telemetry entries from `.swarm/context-telemetry.jsonl`.
208
+ * Read the telemetry entries currently retained in `.swarm/context-telemetry.jsonl`.
98
209
  *
99
- * Parses each line as JSON and returns the array of valid entries.
100
- * Malformed lines are silently skipped. Returns an empty array if the
101
- * file does not exist. Never throws.
210
+ * ISSUE #2037: this now returns ONLY the bounded recent window (the retained
211
+ * records after the manifest header) NOT the entire lifetime history. Full
212
+ * lifetime history is represented by {@link getTelemetrySummary}, which folds
213
+ * the durable aggregate + retained window. This bounded read is the intended
214
+ * contract change: no whole-history file read on any command/runtime path.
215
+ *
216
+ * Malformed/partial lines are skipped. Returns an empty array if the file does
217
+ * not exist or cannot be parsed. Never throws.
102
218
  *
103
219
  * @param directory - Project root directory
104
- * @returns Array of parsed telemetry entries (empty if none or on error)
220
+ * @returns Array of parsed telemetry entries in the retained window
105
221
  */
106
222
  export declare function readTelemetry(directory: string): TelemetryEntry[];
107
223
  /**
108
- * Compute aggregate statistics from all telemetry entries.
109
- *
110
- * Reads all entries via {@link readTelemetry}, then computes totals
111
- * and averages across the dataset.
224
+ * Compute aggregate statistics from all telemetry entries (lifetime).
112
225
  *
113
- * Returns a summary with zeroed fields if no entries exist or on error.
114
- * Never throws.
226
+ * Lifetime totals = folded aggregate (durable header) + current retained
227
+ * window. This is a PURE READ: it never writes. Reads are always bounded to
228
+ * {@link ContextTelemetryLimits.readMaxBytes}; if a legacy (pre-issue-#2037)
229
+ * header-less file is larger than the read bound, only part of it is folded
230
+ * in-memory and {@link TelemetrySummary.coverage} is disclosed as
231
+ * 'partial-unmigrated' rather than silently presenting a complete-looking
232
+ * number. Returns a zeroed summary if no entries exist or on error. Never
233
+ * throws.
115
234
  *
116
235
  * @param directory - Project root directory
117
236
  * @returns Aggregate telemetry summary
118
237
  */
119
238
  export declare function getTelemetrySummary(directory: string): TelemetrySummary;
239
+ /**
240
+ * Fold the entire remaining retained window into the aggregate and atomically
241
+ * publish a defined, validated cut. Called by `/swarm close` before archiving
242
+ * (issue #2037: "close archives a defined, validated cut and leaves active
243
+ * state usable"). Also migrates any header-less legacy file. Fail-open: never
244
+ * throws to the close pipeline.
245
+ *
246
+ * @param directory - Project root directory
247
+ */
248
+ export declare function finalizeContextTelemetry(directory: string): void;
249
+ declare function emitContextTelemetryHealth(_directory: string, payload: {
250
+ trigger: 'compaction' | 'close';
251
+ accepted: number;
252
+ compacted: number;
253
+ retained: number;
254
+ dropped: number;
255
+ corrupt: number;
256
+ oldest: string | null;
257
+ newest: string | null;
258
+ bytes: number;
259
+ limitBytes: number;
260
+ }): void;
261
+ export {};
@@ -15,6 +15,7 @@
15
15
  * any filesystem op.
16
16
  */
17
17
  import { withTaskEvidenceLock } from '../evidence/task-file.js';
18
+ import type { CouncilReviewIdentity } from './council-review-identity';
18
19
  import type { CouncilSynthesis } from './types';
19
20
  /**
20
21
  * Dependency-injection seam for testing. Tests can temporarily replace
@@ -24,5 +25,5 @@ import type { CouncilSynthesis } from './types';
24
25
  export declare const _internals: {
25
26
  withTaskEvidenceLock: typeof withTaskEvidenceLock;
26
27
  };
27
- export declare function writeCouncilEvidence(workingDir: string, synthesis: CouncilSynthesis, attemptId?: string, expectedGeneration?: number): Promise<void>;
28
+ export declare function writeCouncilEvidence(workingDir: string, synthesis: CouncilSynthesis, attemptId?: string, expectedGeneration?: number, identity?: CouncilReviewIdentity): Promise<void>;
28
29
  export declare function hasCouncilEvidenceAttempt(workingDir: string, taskId: string, attemptId: string, roundNumber: number): boolean;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Centralized council evidence freshness evaluation (issue #2102 contract D).
3
+ *
4
+ * ONE evaluator + ONE bounded config field (`council.freshnessMaxAgeHours`,
5
+ * default 24 hours to preserve prior behavior) govern the phase council,
6
+ * architecture supervisor, and final council gates. Callers pass a single
7
+ * captured `nowMs` (one clock per aggregate phase preflight) so gates cannot
8
+ * disagree across an age boundary mid-run.
9
+ *
10
+ * Wall-clock age is never the sole identity check: generation binding is the
11
+ * identity-digest comparison in the gates. Freshness only decides whether
12
+ * otherwise-correctly-bound evidence is recent enough, not in the future,
13
+ * and not older than a later required input (e.g. the phase retrospective).
14
+ */
15
+ import type { CouncilConfig } from './types';
16
+ export declare const DEFAULT_FRESHNESS_MAX_AGE_MS: number;
17
+ export type CouncilFreshnessFailureReason = 'invalid_timestamp' | 'future_timestamp' | 'stale_evidence' | 'predates_required_input' | 'invalid_required_input';
18
+ export interface CouncilFreshnessOutcome {
19
+ ok: boolean;
20
+ reason?: CouncilFreshnessFailureReason;
21
+ /** Human-facing explanation (gate message body). */
22
+ message: string;
23
+ /** Concrete recovery action. */
24
+ recovery: string;
25
+ }
26
+ export declare function resolveCouncilFreshnessMaxAgeMs(config?: CouncilConfig): number;
27
+ /**
28
+ * Evaluate one evidence timestamp against the shared policy.
29
+ *
30
+ * - `timestampMs: null` → invalid_timestamp (missing/unparseable).
31
+ * - `timestampMs > nowMs` → future_timestamp (fail closed).
32
+ * - age > maxAgeMs → stale_evidence.
33
+ * - `mustPostdateMs` set (e.g. latest retrospective timestamp) and
34
+ * `timestampMs < mustPostdateMs` → predates_required_input.
35
+ */
36
+ export declare function evaluateCouncilFreshness(input: {
37
+ nowMs: number;
38
+ timestampMs: number | null;
39
+ maxAgeMs: number;
40
+ mustPostdateMs?: number | null;
41
+ }): CouncilFreshnessOutcome;
42
+ export declare function parseTimestampMs(value: unknown): number | null;
43
+ /**
44
+ * Latest retrospective-relevant timestamp from an already-loaded retro bundle
45
+ * (created_at/updated_at + this phase's retrospective entries). Shared by the
46
+ * phase-council and final-council gates so both prefer the bundle captured by
47
+ * the aggregate preflight over re-reading the disk (PRR-021(g)).
48
+ */
49
+ export declare function latestRetroTimestampMsFromBundle(bundle: unknown, phase: number): number | null;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Canonical council review identity (issue #2102 contract A).
3
+ *
4
+ * ONE shared implementation used by the task/phase/final council producers,
5
+ * the #2085 authoritative round store (as the scope/generation key), the
6
+ * evidence writers, and the completion gates. Writers and gates compute the
7
+ * identity from the same plan + config through this module, so the digests
8
+ * match byte-for-byte by construction.
9
+ *
10
+ * Identity components:
11
+ * - `planId` / `planIdentityHash` — collision-resistant raw plan identity
12
+ * (swarm/title), delegated to `plan/utils`.
13
+ * - `reviewHash` — a purpose-built, status-stable hash of every
14
+ * review-relevant plan field. Pure execution progress (statuses, the
15
+ * current-phase pointer, transient blocked reasons, timestamps) is
16
+ * excluded, so normal progress never invalidates a completed review.
17
+ * - `policyDigest` — canonical digest of the council policy that shaped the
18
+ * review (quorum/veto/concerns/maxRounds/freshness). Any policy change
19
+ * opens a new generation and invalidates prior evidence.
20
+ * - `identityDigest` — sha256 over the canonical identity object; this is
21
+ * the round-store scope/generation key and the evidence binding.
22
+ *
23
+ * `computePlanHash` (ledger integrity) and `computePlanStructureHash`
24
+ * (plan-critic execution gate) are intentionally NOT reused or modified:
25
+ * the first is status-sensitive by design, and the second still hashes
26
+ * progress-like fields (`blocked_reason`, `current_phase`) while dropping
27
+ * the review-relevant `fr_refs`. See `src/plan/ledger.ts` doc comments.
28
+ */
29
+ import type { RuntimePlan } from '../config/plan-schema';
30
+ import type { CouncilAgent, CouncilConfig } from './types';
31
+ /** Schema/cutover version of the council review identity itself. */
32
+ export declare const COUNCIL_REVIEW_IDENTITY_VERSION: 2;
33
+ export type CouncilLevel = 'task' | 'phase' | 'final';
34
+ export type CouncilReviewScope = {
35
+ kind: 'task';
36
+ taskId: string;
37
+ } | {
38
+ kind: 'phase';
39
+ phaseNumber: number;
40
+ } | {
41
+ kind: 'final';
42
+ final: true;
43
+ };
44
+ /** Normalized final-council completion policy (issue #2102 contract C). */
45
+ export type FinalCompletionPolicy = {
46
+ mode: 'all_required';
47
+ } | {
48
+ mode: 'quorum';
49
+ minimumMembers: number;
50
+ };
51
+ export interface CouncilReviewIdentity {
52
+ version: number;
53
+ level: CouncilLevel;
54
+ scope: CouncilReviewScope;
55
+ planId: string | null;
56
+ planIdentityHash: string | null;
57
+ reviewHash: string | null;
58
+ policyDigest: string;
59
+ identityDigest: string;
60
+ }
61
+ export declare function isIdentityDigest(value: unknown): value is string;
62
+ /**
63
+ * Resolve a submitted council member name to its canonical role (issue
64
+ * #2102 contract C). Accepts the exact canonical role (`critic`) and
65
+ * multi-swarm prefixed names (`local_critic`) via suffix resolution.
66
+ * Returns null for unknown names — they never count toward quorum.
67
+ *
68
+ * Swarm-membership itself cannot be verified for architect-submitted JSON
69
+ * verdicts (there is no per-agent authentication at the tool boundary), so
70
+ * the enforceable cross-swarm defense is canonical-role deduplication: two
71
+ * prefixed names that resolve to the same canonical role (`local_critic` +
72
+ * `mega_critic`) count as ONE member. Writers, gates, and tests must all
73
+ * use this shared resolver so the role policy never drifts.
74
+ */
75
+ export declare function resolveCouncilMemberRole(agentName: string): CouncilAgent | null;
76
+ /**
77
+ * Status-stable hash of every review-relevant plan field.
78
+ *
79
+ * INCLUDED (the council judges these): schema_version, title, swarm,
80
+ * execution_profile (execution requirements), migration_status (durable plan
81
+ * property), phase id/name/type/required_agents, task id/phase/size/
82
+ * description/depends/acceptance/files_touched/evidence_path/fr_refs
83
+ * (FR/spec references, #1687).
84
+ *
85
+ * EXCLUDED (pure execution progress — the issue's explicit list):
86
+ * phase.status, task.status, current_phase (progress pointer), blocked_reason
87
+ * (transient), specMtime/specHash (timestamps + spec content, tracked
88
+ * separately by the ledger's spec_updated path per ledger.ts's established
89
+ * rationale).
90
+ */
91
+ export declare function computeCouncilReviewHash(plan: RuntimePlan): string;
92
+ /**
93
+ * Canonical per-level council policy digest.
94
+ *
95
+ * Every field that shapes what the council required at this level is hashed,
96
+ * so changing the mode, a minimum, the role/quorum knobs, veto/concerns
97
+ * behavior, max rounds, or the freshness window opens a new generation.
98
+ * Inert fields (`parallelTimeoutMs`, `escalateOnMaxRounds`) deliberately do
99
+ * NOT contribute — changing them must not invalidate evidence.
100
+ */
101
+ export declare function computeCouncilPolicyDigest(level: CouncilLevel, config?: CouncilConfig): string;
102
+ /**
103
+ * Resolve the final-council completion policy from config.
104
+ *
105
+ * Missing/absent config, a missing `finalCompletionPolicy`, or an invalid
106
+ * quorum declaration all fail closed to the strict legacy `all_required`
107
+ * requirement (all five canonical roles, zero absentees). Explicit
108
+ * `quorum` requires a bounded `minimumMembers` of 3..5; only distinct
109
+ * members of the canonical five-role set ever count toward it.
110
+ */
111
+ export declare function resolveFinalCompletionPolicy(config?: CouncilConfig): FinalCompletionPolicy;
112
+ /**
113
+ * Compute the canonical council review identity. `plan` may be null (no plan
114
+ * on disk): the identity then carries null plan fields, and any consumer
115
+ * holding a plan will fail closed against it — evidence without plan binding
116
+ * can never satisfy a gate that has a plan.
117
+ */
118
+ export declare function computeCouncilReviewIdentity(input: {
119
+ level: CouncilLevel;
120
+ scope: CouncilReviewScope;
121
+ plan: RuntimePlan | null;
122
+ config?: CouncilConfig;
123
+ }): CouncilReviewIdentity;
124
+ /**
125
+ * The evidence-entry projection of an identity. Writers embed these fields in
126
+ * council evidence; gates recompute the identity and compare byte-for-byte.
127
+ */
128
+ export declare function councilIdentityEvidenceFields(identity: CouncilReviewIdentity): {
129
+ identity_version: number;
130
+ review_hash: string | null;
131
+ policy_digest: string;
132
+ identity_digest: string;
133
+ };
@@ -1,29 +1,45 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { withEvidenceLock } from '../evidence/lock.js';
3
3
  import { atomicWriteFile } from '../evidence/task-file.js';
4
+ /**
5
+ * Council round scope, keyed by the canonical council review identity
6
+ * (issue #2102 contract B). `identityDigest` binds every round to the exact
7
+ * review-relevant plan content and council policy it was convened under:
8
+ * a status-only progress change keeps the identity (and therefore the
9
+ * accepted round), while any review-relevant plan or policy change opens a
10
+ * fresh authoritative generation under a new token. Legacy v1 files (whose
11
+ * tokens predate identity binding) are never read — they remain on disk,
12
+ * auditable, and are never rewritten as if they carried identity proof.
13
+ */
4
14
  export type CouncilRoundScope = {
5
15
  kind: 'task';
6
16
  taskId: string;
17
+ identityDigest: string;
7
18
  } | {
8
19
  kind: 'phase';
9
20
  phaseNumber: number;
21
+ identityDigest: string;
10
22
  } | {
11
23
  kind: 'final';
12
- generation: string;
24
+ identityDigest: string;
13
25
  };
14
26
  export type CouncilRoundTransition = 'stay' | 'advance' | 'close';
15
27
  type AuditScope = {
16
28
  kind: 'task';
17
29
  scopeHash: string;
30
+ identityDigest: string;
18
31
  } | {
19
32
  kind: 'phase';
20
33
  phaseNumber: number;
34
+ identityDigest: string;
21
35
  } | {
22
36
  kind: 'final';
23
37
  scopeHash: string;
38
+ identityDigest: string;
24
39
  };
25
40
  interface StateSnapshot {
26
- version: 1;
41
+ version: 2;
42
+ identityDigest: string;
27
43
  currentRound: number;
28
44
  status: 'open' | 'closed';
29
45
  maxRoundsExhausted: boolean;
@@ -31,7 +47,7 @@ interface StateSnapshot {
31
47
  lastDigest?: string;
32
48
  }
33
49
  interface AttemptRecord {
34
- version: 1;
50
+ version: 2;
35
51
  event: 'received' | 'finalized' | 'recovered';
36
52
  attemptId: string;
37
53
  timestamp: string;
@@ -76,6 +92,14 @@ export interface CouncilAttemptInput {
76
92
  request: unknown;
77
93
  verdictCount: number;
78
94
  members: string[];
95
+ /**
96
+ * True when the user explicitly configured `council.escalateOnMaxRounds`.
97
+ * Only this boolean reaches the durable max-rounds exhaustion event —
98
+ * the configured handler/webhook string itself is never persisted or
99
+ * logged (URL/query redaction, issue #2102 contract F). No outbound
100
+ * execution ever happens.
101
+ */
102
+ escalationConfigured?: boolean;
79
103
  probePendingEvidence?: (attemptId: string, round: number, evidenceRef?: string) => Promise<boolean>;
80
104
  evaluate: (authoritativeRound: number) => Promise<CouncilAttemptEvaluation>;
81
105
  }
@@ -5,9 +5,19 @@
5
5
  * responses, returns the set of factual disagreements that should be routed
6
6
  * back to disputing members for Round 2 reconciliation.
7
7
  *
8
- * Two-pass detection:
8
+ * Three-pass detection:
9
9
  * Pass 1 — Explicit linguistic markers ("I disagree with", "unlike", etc.)
10
- * Pass 2 — Claim divergence heuristic (mutually exclusive recommendations)
10
+ * Pass 2 — Structured claims (issue #2102 contract G): contrary typed
11
+ * stances on the same subject, detectable without any marker
12
+ * phrase. Claims are optional and bounded; when absent, invalid,
13
+ * or malformed this pass contributes nothing and the fallback
14
+ * passes below still run.
15
+ * Pass 3 — Claim divergence heuristic (mutually exclusive recommendations)
16
+ *
17
+ * Marker-based disagreements are always kept: the combined list orders the
18
+ * explicit-marker pass FIRST, so structured data can never push a marker
19
+ * detection out of the MAX_DISAGREEMENTS cap, and dedupe merges positions
20
+ * instead of removing them.
11
21
  *
12
22
  * NSED design note (arXiv:2601.16863): only the disagreement delta is fed
13
23
  * forward to Round 2, not full Round 1 context — mirrors the "semantic forget
@@ -18,6 +18,31 @@ export interface WebSearchResult {
18
18
  snippet: string;
19
19
  query: string;
20
20
  }
21
+ /**
22
+ * Bounded structured claim on a council question (issue #2102 contract G).
23
+ * Claims are OPTIONAL: smaller models may omit them entirely and the
24
+ * disagreement detector falls back to its phrase/heuristic passes. A claim
25
+ * by itself is never proof of consensus or correctness — claims only make
26
+ * positions comparable so a phrase-free contrary stance is detectable.
27
+ */
28
+ export interface GeneralCouncilClaim {
29
+ /** Bounded subject/topic the claim is about (≤120 chars). */
30
+ subject: string;
31
+ /** Bounded statement of the member's position (≤600 chars). */
32
+ statement: string;
33
+ /** Typed stance; 'oppose'/'alternative' mark contrary positions. */
34
+ stance: 'support' | 'oppose' | 'neutral' | 'concern' | 'alternative';
35
+ /** Self-reported confidence, same validated range as the response. */
36
+ confidence: number;
37
+ /**
38
+ * Optional bounded evidence/source references (≤4 items, each ≤240 chars).
39
+ * The bound is enforced at the Zod tool boundary (`MemberClaimsSchema` in
40
+ * src/tools/convene-general-council.ts) and independently re-checked by
41
+ * `isWellFormedClaim` in the disagreement detector — the TypeScript type
42
+ * alone cannot express it.
43
+ */
44
+ evidence?: string[];
45
+ }
21
46
  export interface GeneralCouncilMemberConfig {
22
47
  memberId: string;
23
48
  model: string;
@@ -35,6 +60,13 @@ export interface GeneralCouncilMemberResponse {
35
60
  confidence: number;
36
61
  areasOfUncertainty: string[];
37
62
  durationMs: number;
63
+ /**
64
+ * Optional bounded structured claims (issue #2102 contract G). Absent,
65
+ * malformed, or oversized claims degrade to the phrase/Jaccard fallback —
66
+ * they never reject the submission and never suppress explicit
67
+ * disagreement evidence.
68
+ */
69
+ claims?: GeneralCouncilClaim[];
38
70
  }
39
71
  export interface GeneralCouncilDisagreementPosition {
40
72
  memberId: string;