opencode-swarm 7.130.2 → 7.132.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 (85) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/{config-doctor-489wej17.js → config-doctor-3rssa871.js} +2 -2
  3. package/dist/cli/{curation-policy-aaz0hap6.js → curation-policy-3914h3g9.js} +2 -2
  4. package/dist/cli/{curator-s2ynewsr.js → curator-1e77dq6x.js} +13 -13
  5. package/dist/cli/{curator-llm-factory-mxjcctn8.js → curator-llm-factory-41c17ybs.js} +13 -13
  6. package/dist/cli/{evidence-summary-service-hf7qq777.js → evidence-summary-service-jfagmvrh.js} +1 -1
  7. package/dist/cli/{guardrail-explain-s85kv81r.js → guardrail-explain-624brj1y.js} +14 -14
  8. package/dist/cli/{guardrail-log-2cejc33f.js → guardrail-log-vfka4tcn.js} +3 -3
  9. package/dist/cli/{hive-promoter-qdgxajht.js → hive-promoter-mw5prfy2.js} +13 -13
  10. package/dist/cli/{index-kzvwb2se.js → index-03zyn94g.js} +110 -85
  11. package/dist/cli/{index-xa29259t.js → index-2dtwjx39.js} +1 -1
  12. package/dist/cli/{index-5xxgeksy.js → index-5x3g1q91.js} +50 -2
  13. package/dist/cli/{index-zft4hp35.js → index-64z5dvam.js} +1 -1
  14. package/dist/cli/{index-ts9wh1ba.js → index-6mcx76j9.js} +60 -8
  15. package/dist/cli/{index-etdgtk3m.js → index-8w3sp149.js} +3502 -1947
  16. package/dist/cli/{index-6fqj8fcp.js → index-99tq1zd5.js} +2 -2
  17. package/dist/cli/{index-jvyn76xe.js → index-e630fw43.js} +3 -3
  18. package/dist/cli/{index-nr5pr54q.js → index-exs5bt14.js} +1 -1
  19. package/dist/cli/{index-hjar4crm.js → index-fegng5pv.js} +1 -1
  20. package/dist/cli/{index-jxha73tm.js → index-nsxchdx0.js} +1 -1
  21. package/dist/cli/{index-3rnkyb60.js → index-ppkp20fp.js} +5 -5
  22. package/dist/cli/{index-p6qgjkbt.js → index-q0ysv66x.js} +9 -1
  23. package/dist/cli/{index-jvwkxz8q.js → index-qg1cj1ev.js} +15 -15
  24. package/dist/cli/{index-s8h2cedv.js → index-sbq3gbjs.js} +1 -1
  25. package/dist/cli/{index-wy3q73j2.js → index-zah4qws8.js} +18 -14
  26. package/dist/cli/{index-agte9w1e.js → index-zrh07ykv.js} +2 -2
  27. package/dist/cli/index.js +13 -13
  28. package/dist/cli/{knowledge-escalator-1ypz0266.js → knowledge-escalator-5hv2dwhz.js} +3 -3
  29. package/dist/cli/{knowledge-events-yvcczmja.js → knowledge-events-434663dp.js} +1 -1
  30. package/dist/cli/{knowledge-store-h8bp9eje.js → knowledge-store-73ykq5j3.js} +3 -1
  31. package/dist/cli/{knowledge-validator-sz9v1fdq.js → knowledge-validator-tnadtt1h.js} +4 -4
  32. package/dist/cli/{scan-cursor-k9zyt374.js → scan-cursor-e33n9pej.js} +2 -2
  33. package/dist/cli/{schema-fkpn2r1s.js → schema-1kwmcayk.js} +5 -1
  34. package/dist/cli/{skill-generator-a6a9jxfd.js → skill-generator-k111qfdw.js} +5 -5
  35. package/dist/commands/consolidate.d.ts +14 -0
  36. package/dist/commands/curate.d.ts +2 -0
  37. package/dist/commands/promote.d.ts +25 -0
  38. package/dist/commands/registry.d.ts +2 -2
  39. package/dist/config/schema.d.ts +101 -0
  40. package/dist/consensus/contracts.d.ts +479 -0
  41. package/dist/consensus/corpus.d.ts +261 -0
  42. package/dist/consensus/miner.d.ts +200 -0
  43. package/dist/consensus/public-api.d.ts +42 -0
  44. package/dist/consensus/store.d.ts +119 -0
  45. package/dist/evidence/immutable-store.d.ts +86 -0
  46. package/dist/evidence/manager.d.ts +31 -1
  47. package/dist/hooks/actionability-predicate.d.ts +36 -0
  48. package/dist/hooks/curator.d.ts +3 -0
  49. package/dist/hooks/hive-policy.d.ts +11 -0
  50. package/dist/hooks/hive-promoter.d.ts +27 -1
  51. package/dist/hooks/knowledge-dedup-sweep.d.ts +145 -0
  52. package/dist/hooks/knowledge-injector.d.ts +4 -0
  53. package/dist/hooks/knowledge-store.d.ts +26 -0
  54. package/dist/hooks/knowledge-types.d.ts +6 -0
  55. package/dist/hooks/knowledge-validator.d.ts +10 -14
  56. package/dist/hooks/micro-reflector.d.ts +106 -2
  57. package/dist/hooks/realtime-learning-nudge.d.ts +19 -0
  58. package/dist/index.js +464 -456
  59. package/dist/knowledge/entry-merge.d.ts +112 -0
  60. package/dist/knowledge/family-migration.d.ts +6 -2
  61. package/dist/learning/admission.d.ts +154 -0
  62. package/dist/learning/candidate-queue.d.ts +178 -0
  63. package/dist/learning/fingerprint.d.ts +56 -0
  64. package/dist/learning/prm-pattern-support.d.ts +124 -0
  65. package/dist/learning/provenance.d.ts +86 -0
  66. package/dist/parallel/index.d.ts +0 -2
  67. package/dist/plan/parallel-verdict.d.ts +94 -0
  68. package/dist/prm/index.d.ts +46 -1
  69. package/dist/services/recommendation-ledger.d.ts +266 -0
  70. package/dist/services/skill-improver.d.ts +4 -0
  71. package/dist/services/status-service.d.ts +27 -0
  72. package/dist/services/trajectory-cluster.d.ts +65 -2
  73. package/dist/tools/consensus-mine.d.ts +172 -0
  74. package/dist/tools/index.d.ts +2 -0
  75. package/dist/tools/knowledge-add.d.ts +34 -0
  76. package/dist/tools/lean-turbo-run-phase.d.ts +7 -0
  77. package/dist/tools/manifest.d.ts +2 -0
  78. package/dist/tools/plan-conflict-check.d.ts +61 -0
  79. package/dist/tools/tool-metadata.d.ts +8 -0
  80. package/dist/turbo/lean/recovery.d.ts +90 -0
  81. package/dist/turbo/lean/runner.d.ts +16 -1
  82. package/dist/worktree/merge.d.ts +5 -0
  83. package/package.json +1 -1
  84. package/dist/parallel/dependency-graph.d.ts +0 -34
  85. package/dist/parallel/meta-indexer.d.ts +0 -32
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Read-only corpus assembly for the consensus miner (issue #1821, Workstream C).
3
+ *
4
+ * This module owns exactly one job: turn every already-existing evidence store
5
+ * in `.swarm/` into a single flat stream of `CorpusObservation` records. It
6
+ * introduces **no new store**. Every reader below is an existing exported
7
+ * function; the two enumerators added here are `listTrajectorySessions` (a
8
+ * `readdir` over `.swarm/trajectories`) and `listEvaluationRunIds` (a `readdir`
9
+ * over `.swarm/evolution/runs`), because both underlying stores read one
10
+ * artifact at a time and never needed a listing of their own. An enumerator is
11
+ * not a store: it writes nothing, caches nothing, and owns no schema. One
12
+ * source composes rather than enumerates: `.swarm/skills/rejected-edits.jsonl`
13
+ * has no bulk reader, so `defaultReaders` pairs that module's already-exported
14
+ * `rejectedEditsPath` with the shared JSONL `readKnowledge`.
15
+ *
16
+ * Hard rules this module upholds (AGENTS.md invariants 4 and 8):
17
+ * - Every reader receives an injected `directory`. There is no `process.cwd()`
18
+ * fallback anywhere in this file, and none may be added: a consensus report
19
+ * mined against the wrong root would silently attribute one project's
20
+ * evidence to another.
21
+ * - Nothing here writes to disk: no artifact, no lock, no cache file, no marker.
22
+ * That is not a property of *reading* — it has to be chosen. `loadEvidence`
23
+ * performs a lazy in-place upgrade of a legacy flat retrospective by default
24
+ * (rewriting the bundle under an `evidence-loader` lock and creating a lock
25
+ * sentinel under `.swarm/locks/`), so `defaultReaders` binds it with
26
+ * `{ migrate: false }`. A mining run must never mutate the evidence it is
27
+ * merely counting. Note that `migrate: false` skips only the PERSISTENCE: the
28
+ * returned bundle is still the wrapped, normalized view — including the
29
+ * `task_complexity` remap, which happens in `wrapFlatRetrospective` before the
30
+ * write branch — so the corpus reads normalized values while the file on disk
31
+ * keeps its legacy ones.
32
+ * Three upstream readers do populate PROCESS-LOCAL caches, which is worth
33
+ * stating rather than hiding behind the word "read-only": `readTrajectory`
34
+ * fills the PRM in-memory trajectory cache (`src/prm/trajectory-store.ts`),
35
+ * while `readKnowledge` and `loadEvidence` fill the two SEPARATE maps in
36
+ * `src/utils/swarm-artifact-cache.ts` — the parsed-artifact cache and the text
37
+ * cache, each independently bounded at `MAX_CACHE_ENTRIES` = 128 entries. All
38
+ * are bounded and FIFO-evicting, so mining a large `.swarm/` tree CAN evict
39
+ * another subsystem's cached entries and change what that subsystem sees next.
40
+ * The PRM one goes further: it is a plain `Map` with no revalidation, and
41
+ * `readTrajectory` REPLACES the entry for the session it read, re-bounded by
42
+ * its own `maxLines` default rather than by whatever bound the live writer was
43
+ * using — a session whose list exceeds 1000 entries is cut to the newest
44
+ * `floor(maxLines / 2)` = 500 ENTRIES. That 500 is unrelated to
45
+ * `MAX_TRACKED_TRAJECTORY_SESSIONS` = 500, which bounds how many SESSIONS the
46
+ * cache holds at once; the two constants merely happen to share a value.
47
+ * None of these caches is on disk: nothing about them writes a file, and none
48
+ * survives the process. (The readers themselves obviously do read files — the
49
+ * claim above is that nothing here WRITES, not that nothing here touches the
50
+ * filesystem. The artifact cache in particular `stat`s the file on every
51
+ * lookup to check freshness, and on a miss invokes the caller's own read
52
+ * function and `stat`s again before storing.)
53
+ * - Every free-text fragment that survives into a signal, a statement, or an
54
+ * evidence reference passes through `redactSecrets` and a hard length bound
55
+ * before it is retained. Prompts and reasoning traces are never read into an
56
+ * observation at all — only outcomes, verdicts, categories, and the bounded
57
+ * excerpts below.
58
+ * - The whole corpus is capped at `maxEvidenceItems` observations. The cap is
59
+ * applied against a deterministic source order and a deterministic per-source
60
+ * sort, so the same `.swarm/` tree always yields the same truncated corpus.
61
+ * Within a source the cut is *balanced between failing and succeeding
62
+ * observations* rather than lexicographic, because a lexicographic cut
63
+ * systematically drops whichever class sorts late and would let truncation
64
+ * erase counterexamples while confidence rose. That removes the systematic
65
+ * bias; it does not make truncation lossless. The balance is struck PER
66
+ * SOURCE, so one signal can still lose every counterexample it had, and once
67
+ * the budget is spent every later source is dropped WHOLE. `report.truncation`
68
+ * exists so a reader can tell a partial view from a complete one — see
69
+ * `docs/consensus-mining.md`.
70
+ */
71
+ import type { EvaluationRunV1 } from '../evaluation/contracts.js';
72
+ import { readGateGroundTruth } from '../evaluation/gate-ground-truth.js';
73
+ import { type GateAuditReadSummary } from '../evaluation/store.js';
74
+ import { type LoadEvidenceResult } from '../evidence/manager.js';
75
+ import type { RetrievalOutcome } from '../hooks/knowledge-types.js';
76
+ import { readSkillUsageEntries } from '../hooks/skill-usage-log.js';
77
+ import type { TrajectoryEntry } from '../prm/types.js';
78
+ import type { ConsensusCorpusHash, ConsensusSourceKind } from './contracts.js';
79
+ /**
80
+ * One normalized evidence record.
81
+ *
82
+ * `runId` is the *support unit*: the miner counts distinct `runId` values, so
83
+ * whatever a source uses as its independent-trial identity belongs here. For
84
+ * evaluation and gate-audit evidence that is the literal run id; for per-task
85
+ * evidence it is the task's own identity, and for session trajectories the
86
+ * session id. Each is namespaced so two sources can never collide.
87
+ */
88
+ export interface CorpusObservation {
89
+ runId: string;
90
+ taskId?: string;
91
+ taskCategory?: string;
92
+ agentRole?: string;
93
+ modelId?: string;
94
+ seed?: string;
95
+ success: boolean;
96
+ signals: string[];
97
+ evidenceRef: string;
98
+ }
99
+ export interface ConsensusCorpus {
100
+ observations: CorpusObservation[];
101
+ hashes: ConsensusCorpusHash[];
102
+ /** True when `maxEvidenceItems` truncated the stream. */
103
+ truncated: boolean;
104
+ /** Sources that threw while being read. Never fatal — the corpus degrades. */
105
+ unreadableSources: ConsensusSourceKind[];
106
+ }
107
+ /**
108
+ * Injectable readers. Dependency injection rather than `mock.module` (AGENTS.md
109
+ * invariant 7): the corpus pulls from eight subsystems, and mocking those module
110
+ * paths would leak across Bun's shared test-runner process.
111
+ */
112
+ export interface CorpusReaders {
113
+ listEvaluationRunIds: (directory: string) => Promise<string[]>;
114
+ readEvaluationRun: (directory: string, runId: string) => Promise<EvaluationRunV1 | undefined>;
115
+ listGateAuditResults: (directory: string) => Promise<GateAuditReadSummary>;
116
+ readGateGroundTruth: typeof readGateGroundTruth;
117
+ listEvidenceTaskIds: (directory: string) => Promise<string[]>;
118
+ readTaskTrajectory: (directory: string, taskId: string) => Promise<TrajectoryEntry[]>;
119
+ listTrajectorySessions: (directory: string) => Promise<string[]>;
120
+ readTrajectory: (sessionId: string, directory: string) => Promise<TrajectoryEntry[]>;
121
+ readSkillUsageEntries: typeof readSkillUsageEntries;
122
+ readKnowledgeEntries: (directory: string) => Promise<KnowledgeLike[]>;
123
+ loadEvidence: (directory: string, taskId: string) => Promise<LoadEvidenceResult>;
124
+ readRejectedLessons: (directory: string) => Promise<RejectedLessonLike[]>;
125
+ readRejectedSkillEdits: (directory: string) => Promise<RejectedSkillEditLike[]>;
126
+ }
127
+ /**
128
+ * Structural view of a knowledge entry. Deliberately minimal: the consensus
129
+ * miner reads four fields, and depending on the full `KnowledgeEntry` union
130
+ * would couple this module to a schema another lane actively edits.
131
+ */
132
+ export interface KnowledgeLike {
133
+ id?: unknown;
134
+ lesson?: unknown;
135
+ category?: unknown;
136
+ retrieval_outcomes?: RetrievalOutcome;
137
+ }
138
+ /**
139
+ * Structural view of a `RejectedLesson` (`src/hooks/knowledge-types.ts`). Same
140
+ * reason as `KnowledgeLike`: the corpus reads four fields and must not couple
141
+ * itself to a schema another lane edits.
142
+ */
143
+ export interface RejectedLessonLike {
144
+ id?: unknown;
145
+ lesson?: unknown;
146
+ rejection_reason?: unknown;
147
+ rejection_layer?: unknown;
148
+ }
149
+ /**
150
+ * Structural view of a `RejectedSkillEditRecord`
151
+ * (`src/services/skill-evaluator.ts`). `candidatePreview` is deliberately NOT
152
+ * read: it is up to 800 bytes of the rejected skill BODY, which is closer to a
153
+ * prompt than to an outcome, and the corpus reads outcomes only.
154
+ */
155
+ export interface RejectedSkillEditLike {
156
+ slug?: unknown;
157
+ operation?: unknown;
158
+ reason?: unknown;
159
+ candidateHash?: unknown;
160
+ }
161
+ export interface LoadCorpusOptions {
162
+ /** Hard cap on retained observations. Required — there is no default. */
163
+ maxEvidenceItems: number;
164
+ /** Hard cap on any single retained free-text fragment. */
165
+ maxExcerptChars: number;
166
+ /**
167
+ * Caller-supplied retention predicate, applied per source AFTER that source's
168
+ * hash and observation count are recorded and BEFORE the `maxEvidenceItems`
169
+ * budget is spent.
170
+ *
171
+ * That position is the whole point. The consensus miner's request filters used
172
+ * to run only on the already-truncated stream, so narrowing a request to the
173
+ * one task category that mattered removed observations from a corpus the cap
174
+ * had already shaped — 50 observations in, 0 out. Applying the predicate here
175
+ * spends the budget on observations that can survive the request instead, so
176
+ * narrowing genuinely widens what is available to it.
177
+ *
178
+ * Applied after the per-source hash so `corpusHashes` keeps meaning "what this
179
+ * source contained", independent of any one request; `truncation.observations`
180
+ * on the report is what declares how many were actually tallied.
181
+ *
182
+ * Omitted \u21D2 every observation is retained, which is the pre-existing behaviour.
183
+ */
184
+ filter?: (observation: CorpusObservation) => boolean;
185
+ /** Reader overrides for tests. Unspecified readers use the real store. */
186
+ readers?: Partial<CorpusReaders>;
187
+ }
188
+ /**
189
+ * Total, locale-INDEPENDENT string order.
190
+ *
191
+ * `String.prototype.localeCompare` without an explicit locale is ICU- and
192
+ * environment-sensitive: it orders `a-b, a:b, ab, aB` differently from code-unit
193
+ * order, and the collation can differ between hosts. That is fine for display
194
+ * and fatal here — this ordering decides which observations survive truncation
195
+ * and the order of the attribute array, both of which are hashed into
196
+ * `integrityHash`. A report whose id depends on the host's collation is not
197
+ * reproducible. Code-unit comparison is the same everywhere.
198
+ */
199
+ export declare function compareRefs(left: string, right: string): number;
200
+ /**
201
+ * Redact, collapse, and bound a free-text fragment before it is retained.
202
+ *
203
+ * Order matters: redaction runs BEFORE truncation, so a secret that straddles
204
+ * the length bound is still replaced rather than half-copied into the report.
205
+ * Newlines and control characters collapse to single spaces because signals are
206
+ * compared for equality — a fragment that differs only by line wrapping must
207
+ * not split one consensus attribute into two.
208
+ *
209
+ * Format characters (`\p{Cf}`) are collapsed alongside control characters — to a
210
+ * SPACE, like everything else here, not deleted — and that is a correctness rule
211
+ * rather than tidiness. U+202E (RIGHT-TO-LEFT OVERRIDE), U+2066–U+2069 (the
212
+ * isolates) and U+200E/U+200F reorder how the text RENDERS without changing the
213
+ * bytes stored, so a persisted signal, statement, evidence ref, or `llmSummary`
214
+ * could read as something other than the bytes the report actually holds.
215
+ * U+200B–U+200D and U+FEFF are the same class of invisible, and they also hide
216
+ * INSIDE a token: `[REDACTED\u200B:x]` is not the placeholder it renders as, and
217
+ * `sk\u200B-…` is not the secret shape `redactSecrets` matches.
218
+ *
219
+ * Replacing rather than deleting is a real choice, in both directions. What it
220
+ * BUYS: deletion would let `[REDACTED\u200B:x]` close up into a well-formed
221
+ * `[REDACTED:x]` after the forged-marker check in `extractRestatement` has
222
+ * already run on the raw text — manufacturing exactly the forged redaction
223
+ * marker that check exists to reject. A space breaks the token instead, and
224
+ * `MARKUP_RE` then rejects it on the bare `[`. What it does NOT buy: `a\u200Db`
225
+ * becomes `a b`, not `ab`, so a fragment differing from another only by a
226
+ * zero-width joiner still differs afterwards. Equality is canonicalized (every
227
+ * such fragment collapses the same way), not made to agree with the joiner-free
228
+ * spelling.
229
+ */
230
+ export declare function sanitizeExcerpt(value: string, maxChars: number): string;
231
+ /**
232
+ * READ-ONLY enumerator over `.swarm/trajectories`.
233
+ *
234
+ * `src/prm/trajectory-store.ts` reads exactly one session at a time and has no
235
+ * bulk enumerator, so the consensus corpus needs this to discover which sessions
236
+ * exist. It lives here rather than in the PRM store because it is the consensus
237
+ * miner's need, not the PRM subsystem's, and because adding it there would put a
238
+ * second writer-adjacent surface in a module another lane owns. It performs a
239
+ * single `readdir`, filters to `<sessionId>.jsonl` names that match the shared
240
+ * identifier shape, sorts, bounds, and returns. Missing directory \u21D2 `[]`.
241
+ */
242
+ export declare function listTrajectorySessions(directory: string): Promise<string[]>;
243
+ /**
244
+ * READ-ONLY enumerator over `.swarm/evolution/runs`.
245
+ *
246
+ * `src/evaluation/store.ts` exposes `readEvaluationRun(directory, runId)` but no
247
+ * listing (its own callers always know the run id). Mirrors the directory
248
+ * discipline of `listGateAuditResults`: validated names only, sorted, bounded.
249
+ */
250
+ export declare function listEvaluationRunIds(directory: string): Promise<string[]>;
251
+ /**
252
+ * Load and normalize every corpus source.
253
+ *
254
+ * Failure of any single source is non-fatal and recorded in `unreadableSources`:
255
+ * a corrupt trajectory file must not make the whole mining run unavailable, and
256
+ * a silently-empty corpus would be worse than a declared partial one. The
257
+ * returned observations are stable-sorted, reduced by the optional
258
+ * `options.filter`, and then truncated to `maxEvidenceItems` — filter first, so
259
+ * the budget is spent on observations the caller can actually use.
260
+ */
261
+ export declare function loadConsensusCorpus(directory: string, options: LoadCorpusOptions): Promise<ConsensusCorpus>;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * The consensus miner (issue #1821, Workstream C).
3
+ *
4
+ * Reads the read-only corpus, counts agreement deterministically, gates the
5
+ * result, and emits a report. It is a **proposals-only** boundary: it activates
6
+ * no skill, writes no knowledge, and mutates none of the evidence it reads.
7
+ * `mineConsensus` itself writes nothing at all; the subsystem's only disk
8
+ * mutations live in `./store.ts`, which persists a report and — under
9
+ * `pruneConsensusReports` — deletes the subsystem's own older ones.
10
+ *
11
+ * Ordering is a correctness property, not a style choice. The pipeline is:
12
+ *
13
+ * 1. deterministic filtering
14
+ * 2. deterministic co-occurrence + distinct-run support counting
15
+ * 3. deterministic gates (support, successful runs, task diversity)
16
+ * 4. deterministic retention of negative evidence
17
+ * 5. deterministic proposal + fingerprint construction
18
+ * 6. ONLY THEN, optional LLM restatement, into a SEPARATE field
19
+ *
20
+ * Step 6 last is what keeps a model from influencing whether something
21
+ * qualifies. A model may only rephrase a conclusion the arithmetic already
22
+ * reached. If no dispatcher is available, or the call times out, or
23
+ * summarization is disabled, or the response fails the restatement guard, the
24
+ * attribute simply has no `llmSummary` — graceful degradation, never a hard
25
+ * dependency.
26
+ *
27
+ * Two properties are easy to lose here and are enforced explicitly:
28
+ *
29
+ * - **Reproducibility.** Ordering alone does NOT make the report reproducible.
30
+ * Two fields had to be moved out of hashed content, and both were reachable in
31
+ * ordinary use. `llm_summarization_enabled` defaults to `true`, so a model's
32
+ * wording in `statement` gave a different `integrityHash` — and a different
33
+ * `reportId` — on every run over an identical corpus; the restatement now goes
34
+ * into `ConsensusAttributeV1.llmSummary`, which the hash excludes. And
35
+ * `provenance.writeOrigin.sessionId` comes from `ctx.sessionID`, so two
36
+ * sessions mining the same corpus forked the artifact too; the hash now
37
+ * excludes the whole `writeOrigin`. `statement` is always the deterministic
38
+ * rendering, and who ran the mine cannot change what the mine found.
39
+ * - **Model prose reaches disk only through a bounded whitelist.**
40
+ * `SUMMARIZATION_SYSTEM` is a request, not a filter, and `sanitizeExcerpt`
41
+ * only redacts secrets, collapses control and format characters, and
42
+ * truncates. `extractRestatement` is the actual guard, and what it enforces is
43
+ * precisely this: one `FINDING:` line per dispatch survives (the first),
44
+ * everything else in the response is discarded, and the captured text is
45
+ * admitted only if it carries no forged `[REDACTED:…]` marker, no bracket or
46
+ * angle-bracket markup, no listed reasoning marker, and — once decimal points
47
+ * and at most one lower-case-continued `e.g.`/`i.e.`/`etc.` are MASKED — no
48
+ * sentence terminator other than a single trailing run, and fits
49
+ * `MAX_CONSENSUS_STATEMENT_CHARS` without truncation (issue #1821 AC18). Note
50
+ * the masking clause: the persisted text can hold several literal `.`
51
+ * characters (`… on 0.8 of the runs, e.g. the refactor pair.` is admitted and
52
+ * contains four: the decimal, the two in `e.g.`, and the trailing one. Three
53
+ * of them are masked, leaving the single trailing run the rule allows). The
54
+ * bound is on UNMASKED terminators, not on periods.
55
+ *
56
+ * The limitation belongs in the same breath, because the absolute version was
57
+ * claimed here twice and is false: **a single grammatical sentence chained
58
+ * with semicolons, colons, dashes, tabs, or the one permitted abbreviation can
59
+ * still read as a multi-step narration, and this guard does not stop that.**
60
+ * It bounds how much model text, in what shape, can reach an attribute — one
61
+ * sentence-shaped fragment of at most `MAX_CONSENSUS_STATEMENT_CHARS`, in a
62
+ * field excluded from the integrity hash that never displaces `statement`. It
63
+ * does not classify meaning.
64
+ */
65
+ import type { ConsensusConfig } from '../config/schema.js';
66
+ import type { EvaluationModelDispatcher } from '../evaluation/model-dispatcher.js';
67
+ import type { ConsensusMineRequest, ConsensusReportV1 } from './contracts.js';
68
+ import type { ConsensusCorpus, CorpusObservation } from './corpus.js';
69
+ /**
70
+ * Minimum distinct task identities before an attribute may become a proposal.
71
+ *
72
+ * Two is not a tuning knob. One anecdote — one task, however many times it was
73
+ * observed — cannot distinguish "this is how the system behaves" from "this is
74
+ * how that one task behaves". Below this an attribute is still emitted, as an
75
+ * investigation note with `proposedTarget: 'none'`, because suppressing it
76
+ * entirely would hide the very evidence a human needs to decide whether to go
77
+ * looking for a second task.
78
+ */
79
+ export declare const MIN_TASK_DIVERSITY_FOR_PROPOSAL = 2;
80
+ /**
81
+ * Minimum distinct RUNS before an attribute may become a proposal.
82
+ *
83
+ * A second, independent gate from `MIN_TASK_DIVERSITY_FOR_PROPOSAL`, and it is
84
+ * NOT implied by the request's `minSupport`: `min_support: 1` is an accepted
85
+ * argument, so without this an attribute supported by a single run could clear
86
+ * every threshold the caller asked for and still be a one-run recommendation.
87
+ * Exported so `consensus_mine` can print the gate it actually applies rather
88
+ * than restating it — the printed `thresholds` block used to omit this one, and
89
+ * an attribute that cleared every printed number could still be forced to
90
+ * `proposedTarget: 'none'` with nothing in the output explaining why.
91
+ */
92
+ export declare const MIN_SUPPORT_FOR_PROPOSAL = 2;
93
+ /**
94
+ * Hard cap on LLM restatement dispatches per report.
95
+ *
96
+ * Exported so `consensus_mine` can state the real ceiling in the description the
97
+ * model reads. Each unit is one `session.create` + one `session.prompt`
98
+ * (`src/evaluation/model-dispatcher.ts`), so this is 20 sessions and 20 prompts
99
+ * in the worst case, not 20 cheap local calls.
100
+ */
101
+ export declare const MAX_LLM_SUMMARIES = 20;
102
+ export interface MineConsensusDeps {
103
+ /** Effective consensus configuration. Required — the miner reads no config. */
104
+ config: ConsensusConfig;
105
+ /** Corpus loader override. Defaults to the real read-only loader. */
106
+ loadCorpus?: (directory: string, options: {
107
+ maxEvidenceItems: number;
108
+ maxExcerptChars: number;
109
+ filter?: (observation: CorpusObservation) => boolean;
110
+ }) => Promise<ConsensusCorpus>;
111
+ /**
112
+ * Optional LLM dispatcher. Absent ⇒ deterministic statements are kept.
113
+ * Injected rather than constructed so the miner never reaches for a runtime
114
+ * client and stays unit-testable without one.
115
+ */
116
+ dispatcher?: EvaluationModelDispatcher;
117
+ /** Model id for summarization dispatches. */
118
+ summarizationModelId?: string;
119
+ /** Prefixed-swarm selector forwarded to the dispatcher. */
120
+ preferredSwarm?: string;
121
+ /** Parent session for dispatched child sessions. */
122
+ sessionId?: string;
123
+ /** Agent role recorded in proposal provenance. */
124
+ agentRole?: string;
125
+ /**
126
+ * Fingerprints already proposed by earlier reports. A proposal whose
127
+ * fingerprint appears here is suppressed so a standing recommendation is not
128
+ * re-proposed on every mining run.
129
+ */
130
+ priorFingerprints?: Iterable<string>;
131
+ /** Clock seam. Defaults to `Date.now`-backed ISO output. */
132
+ now?: () => Date;
133
+ }
134
+ export interface MineConsensusResult {
135
+ report: ConsensusReportV1;
136
+ /** True when `maxEvidenceItems` truncated the corpus. */
137
+ truncated: boolean;
138
+ /** Corpus sources that could not be read. */
139
+ unreadableSources: string[];
140
+ /** Attributes emitted as investigation notes rather than proposals. */
141
+ investigationNoteCount: number;
142
+ /** Proposals suppressed because a prior report already carries them. */
143
+ dedupedProposalCount: number;
144
+ /** How many statements the LLM actually restated. */
145
+ summarizedCount: number;
146
+ /** Why summarization did not run, when it did not. */
147
+ summarizationSkippedReason?: 'disabled_by_config' | 'no_dispatcher' | 'no_attributes';
148
+ }
149
+ /**
150
+ * A report body, with the three hash-excluded fields optional so both the
151
+ * pre-id construction path and the post-read verification path can call the
152
+ * same function.
153
+ */
154
+ export type ConsensusReportIntegrityInput = Omit<ConsensusReportV1, 'integrityHash' | 'reportId' | 'generatedAt'> & Partial<Pick<ConsensusReportV1, 'integrityHash' | 'reportId' | 'generatedAt'>>;
155
+ /**
156
+ * Recompute a report's integrity hash from its own content.
157
+ *
158
+ * Deliberately subtractive rather than additive: it hashes everything except
159
+ * the named exclusions, so a field added to `ConsensusReportV1` later is covered
160
+ * automatically. An additive allow-list would silently leave new fields
161
+ * unhashed — the failure mode where a report's content changes but its
162
+ * integrity hash does not.
163
+ *
164
+ * Exclusions fall into two classes, both failing the same test — "would an
165
+ * identical corpus produce an identical value?":
166
+ *
167
+ * 1. **Who and when, as opposed to what.** The report's own `generatedAt` (plus
168
+ * `reportId`, which is *derived* from this hash and would otherwise be
169
+ * circular), and every proposal's ENTIRE `provenance.writeOrigin` — the
170
+ * `producedAt` clock and the `sessionId` / `agentRole` that identify whoever
171
+ * physically ran the mine — which, since `ProposedSkillChangeProvenance`
172
+ * dropped the unreachable `agentId`, is the whole of that object. All are
173
+ * real provenance and stay in the artifact; none of them is content.
174
+ *
175
+ * Excluding the identity fields is not cosmetic. `sessionId` comes from
176
+ * `ctx.sessionID`, so with only `producedAt` excluded, two sessions mining a
177
+ * byte-identical corpus produced different `integrityHash` values and
178
+ * therefore different `reportId`s — the same user-visible symptom as hashing
179
+ * the model's wording, just from a different field. Who ran the mine cannot
180
+ * be allowed to change what the mine found.
181
+ * 2. **Non-reproducible model prose** — every attribute's `llmSummary`. This one
182
+ * is not a nicety: `llm_summarization_enabled` defaults to `true`, so hashing
183
+ * a model's wording made "same inputs ⇒ identical hash" FALSE in the default
184
+ * configuration. Two mining runs over a byte-identical corpus produced
185
+ * different `integrityHash` values and therefore different `reportId`s,
186
+ * which defeats content addressing, defeats `isEquivalent` in the store, and
187
+ * turns every re-mine into a new artifact. The deterministic `statement` that
188
+ * the summary paraphrases IS hashed, so nothing about the finding escapes
189
+ * coverage — only the paraphrase does.
190
+ */
191
+ export declare function computeConsensusIntegrityHash(report: ConsensusReportIntegrityInput): string;
192
+ /** Deterministic report id, derived from the integrity hash. */
193
+ export declare function deriveReportId(integrityHash: string): string;
194
+ /**
195
+ * Mine consensus attributes and proposals from `directory`'s `.swarm/` evidence.
196
+ *
197
+ * MUTATES NOTHING. The returned report is a value; persisting it is the
198
+ * caller's separate, explicit `writeConsensusReport` call.
199
+ */
200
+ export declare function mineConsensus(directory: string, request: ConsensusMineRequest, deps: MineConsensusDeps): Promise<MineConsensusResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stable package-level boundary for the consensus miner (issue #1821).
3
+ *
4
+ * This module exports exactly one thing — `mineAndStoreConsensusV1` — because
5
+ * exactly one caller exists: `src/tools/consensus-mine.ts`. It previously also
6
+ * exported a frozen callable `consensusV1` namespace mirroring
7
+ * `src/evaluation/public-api.ts`, together with a `src/consensus/index.ts`
8
+ * barrel. Both were dead RUNTIME VALUES — a public API surface with no importer
9
+ * anywhere in `src/`, `tests/`, or `scripts/` — and the evaluation precedent did
10
+ * not apply: that barrel IS consumed (`src/index.ts` re-exports `evaluationV1`),
11
+ * whereas nothing ever wired consensus into the plugin entry. Both were removed
12
+ * rather than left standing (see the "never ship unwired code" directive in
13
+ * `CLAUDE.md`). If a future consumer needs a versioned namespace, reintroduce it
14
+ * together with that consumer.
15
+ *
16
+ * The two type aliases below have no by-name importer either, and are kept for a
17
+ * different and declared reason: they are the parameter and return shapes of
18
+ * `mineAndStoreConsensusV1` itself, so they are already handed out by its
19
+ * signature. See the export carve-out note at the bottom of `./contracts.ts`.
20
+ */
21
+ import type { ConsensusMineRequest, ConsensusReportV1 } from './contracts.js';
22
+ import { type MineConsensusDeps } from './miner.js';
23
+ export type MineAndStoreConsensusOptions = {
24
+ directory: string;
25
+ request: ConsensusMineRequest;
26
+ deps: MineConsensusDeps;
27
+ };
28
+ export type MineAndStoreConsensusResult = {
29
+ report: ConsensusReportV1;
30
+ truncated: boolean;
31
+ unreadableSources: string[];
32
+ investigationNoteCount: number;
33
+ dedupedProposalCount: number;
34
+ summarizedCount: number;
35
+ summarizationSkippedReason?: string;
36
+ };
37
+ /**
38
+ * Mine, then persist. The two phases stay separable on purpose — `mineConsensus`
39
+ * mutates nothing and is safe to call speculatively; only this boundary writes,
40
+ * and it writes exactly one artifact: the report itself.
41
+ */
42
+ export declare function mineAndStoreConsensusV1(options: MineAndStoreConsensusOptions): Promise<MineAndStoreConsensusResult>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Immutable persistence for consensus reports (issue #1821, Workstream C).
3
+ *
4
+ * Reports live under `.swarm/evolution/consensus/<reportId>.json`, alongside the
5
+ * evaluation substrate's runs and decisions. The write goes through the shared
6
+ * `writeImmutableArtifact` pipeline in `src/evidence/immutable-store.ts` — the
7
+ * same one the evaluation store binds — so the lock/read/compare/atomic-rename
8
+ * sequence exists exactly once in the codebase. This module supplies only the
9
+ * three parameters that pipeline deliberately leaves to its callers: the lock
10
+ * actor, the canonical serializer, and the conflict-error factory.
11
+ *
12
+ * Idempotence has three wrinkles a report has and a run does not, all reachable
13
+ * under the DEFAULT configuration. Two mining runs over an identical corpus
14
+ * produce reports that differ in `generatedAt` (a wall clock), in each
15
+ * proposal's `provenance.writeOrigin` (the `producedAt` clock plus the
16
+ * `sessionId` / `agentRole` of whoever ran the mine), and — because
17
+ * `llm_summarization_enabled` defaults to `true` — in each attribute's
18
+ * `llmSummary`, which is model prose and is not reproducible. All three are
19
+ * excluded from `integrityHash`, so an `isEquivalent` escape hatch comparing
20
+ * recomputed hashes treats such reports as the same artifact, exactly how
21
+ * `savePromotionDecision` handles `decidedAt`. A report that differs in any
22
+ * *content* field still conflicts.
23
+ */
24
+ import type { ConsensusReportV1 } from './contracts.js';
25
+ export declare class ConsensusConflictError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ export declare class ConsensusIntegrityError extends Error {
29
+ constructor(message: string);
30
+ }
31
+ /**
32
+ * Persist a report exactly once.
33
+ *
34
+ * The integrity hash is **verified before persist**, not merely recorded: a
35
+ * report whose declared hash does not match its own content is rejected rather
36
+ * than written, so a corrupted or hand-edited report can never enter the store
37
+ * and later be trusted on read.
38
+ */
39
+ export declare function writeConsensusReport(directory: string, input: ConsensusReportV1): Promise<ConsensusReportV1>;
40
+ /**
41
+ * Read one report. Returns `undefined` when absent.
42
+ *
43
+ * Re-verifies the integrity hash on the way out: a report that was tampered with
44
+ * on disk after being written must not be handed to a caller as authoritative.
45
+ */
46
+ export declare function readConsensusReport(directory: string, reportId: string): Promise<ConsensusReportV1 | undefined>;
47
+ export type ConsensusListSummary = {
48
+ reports: ConsensusReportV1[];
49
+ /** Report ids present on disk that failed to parse or verify. */
50
+ corruptReportIds: string[];
51
+ };
52
+ /**
53
+ * Enumerate every stored report, newest first.
54
+ *
55
+ * Corrupt entries are reported rather than thrown so one bad file cannot make
56
+ * the whole history unreadable — the same posture `listGateAuditResults` takes.
57
+ * Sorted by `generatedAt` descending, ties broken by id, so the ordering is
58
+ * total and reproducible.
59
+ */
60
+ export declare function listConsensusReports(directory: string): Promise<ConsensusListSummary>;
61
+ export type ConsensusPruneResult = {
62
+ /** Report ids deleted, oldest-first. */
63
+ deleted: string[];
64
+ /** Report ids retained. */
65
+ retained: string[];
66
+ /** Ids that failed to delete, with the reason. Never fatal. */
67
+ failed: Array<{
68
+ reportId: string;
69
+ error: string;
70
+ }>;
71
+ /**
72
+ * Report ids on disk that failed to parse or verify.
73
+ *
74
+ * These are neither deleted nor counted in `retained`, so without this field
75
+ * `deleted + retained` silently under-counts what is actually stored and a
76
+ * caller printing those two numbers describes a store it cannot see all of.
77
+ * Empty when pruning is disabled, because that mode enumerates nothing.
78
+ */
79
+ corrupt: string[];
80
+ };
81
+ /**
82
+ * Count the report files present, without parsing or verifying any of them.
83
+ *
84
+ * This is the cheap listing primitive `/swarm status` needs: `readdir` plus a
85
+ * name filter, no JSON parse and no hash recomputation, so surfacing the store
86
+ * in an interactive command costs one syscall rather than a full re-verification
87
+ * of every stored report. It therefore counts CORRUPT reports too — it counts
88
+ * files whose name is a well-formed report id, which is exactly the claim the
89
+ * status line makes. Use `listConsensusReports` when you need content.
90
+ */
91
+ export declare function countConsensusReportFiles(directory: string): Promise<number>;
92
+ /**
93
+ * Enforce `consensus.report_retention`.
94
+ *
95
+ * Deliberate posture, mirroring the evaluation substrate's retention rules:
96
+ * - `retain === 0` **disables** pruning rather than deleting everything. The
97
+ * schema's `min(0)` bound reads as "the constraint is off", the same way
98
+ * `default_min_successful_runs: 0` turns that gate off. Interpreting it as
99
+ * "keep zero reports" would delete the report the caller just wrote.
100
+ * - Corrupt reports are **never** deleted. An unparseable artifact is data-
101
+ * quality evidence; silently discarding it destroys the only trace of the bug
102
+ * that produced it. They are reported as `corrupt` rather than dropped
103
+ * silently: they are excluded from `retained` as well as from `deleted`, so a
104
+ * caller that printed only those two numbers would describe a store smaller
105
+ * than the one on disk.
106
+ * - Newest-first by `generatedAt`, ties broken by id, so pruning is a total
107
+ * order and two runs over the same store delete the same files.
108
+ * - Only files directly under `.swarm/evolution/consensus/` with a validated
109
+ * `<id>.json` name are candidates; nothing recurses (AGENTS.md invariant 4).
110
+ */
111
+ export declare function pruneConsensusReports(directory: string, retain: number): Promise<ConsensusPruneResult>;
112
+ /**
113
+ * Every proposal fingerprint already present in the store.
114
+ *
115
+ * This is what the miner dedupes against so a standing recommendation is not
116
+ * re-proposed on every run. Corrupt reports are skipped: an unreadable prior
117
+ * report is a reason to re-propose, not to crash.
118
+ */
119
+ export declare function listConsensusProposalFingerprints(directory: string): Promise<Set<string>>;