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,86 @@
1
+ /**
2
+ * Learning provenance records (issue #1821, Lane 0a).
3
+ *
4
+ * Every durable learning write — a curator sweep decision, a micro-reflection,
5
+ * a PRM pattern, a consensus-mined lesson, a skill-improver edit — must be able
6
+ * to say *where it came from*. `LearningProvenanceV1` is the shared envelope:
7
+ * which mechanism produced it, which knowledge/task/evidence/run/model ids fed
8
+ * it, and which session/agent physically wrote it.
9
+ *
10
+ * Every reference class is deduplicated, sorted, and hard-capped at
11
+ * `MAX_REFS_PER_CLASS` so a long-running session cannot grow a provenance record
12
+ * without bound (AGENTS.md invariant 8 — global/persisted state must be
13
+ * bounded).
14
+ *
15
+ * This module is pure: no filesystem, no network, no module-level mutable state.
16
+ * The only ambient input is the wall clock, and only when the caller does not
17
+ * supply `producedAt` — callers that need determinism pass it explicitly.
18
+ */
19
+ import { z } from 'zod';
20
+ /**
21
+ * Hard cap on entries retained per reference class. Sorting happens before the
22
+ * cap so truncation is deterministic rather than insertion-order dependent.
23
+ */
24
+ export declare const MAX_REFS_PER_CLASS = 50;
25
+ /** The learning mechanism that produced a record. */
26
+ export type LearningMechanism = 'curator_sweep' | 'micro_reflection' | 'prm_pattern' | 'consensus_mine' | 'skill_improver';
27
+ export declare const LearningProvenanceV1Schema: z.ZodObject<{
28
+ v: z.ZodLiteral<1>;
29
+ mechanism: z.ZodEnum<{
30
+ skill_improver: "skill_improver";
31
+ consensus_mine: "consensus_mine";
32
+ curator_sweep: "curator_sweep";
33
+ micro_reflection: "micro_reflection";
34
+ prm_pattern: "prm_pattern";
35
+ }>;
36
+ sourceKnowledgeIds: z.ZodArray<z.ZodString>;
37
+ sourceTaskIds: z.ZodArray<z.ZodString>;
38
+ sourceEvidenceRefs: z.ZodArray<z.ZodString>;
39
+ sourceRunIds: z.ZodArray<z.ZodString>;
40
+ sourceModelIds: z.ZodArray<z.ZodString>;
41
+ writeOrigin: z.ZodObject<{
42
+ sessionId: z.ZodOptional<z.ZodString>;
43
+ agentRole: z.ZodOptional<z.ZodString>;
44
+ producedAt: z.ZodISODateTime;
45
+ }, z.core.$strict>;
46
+ }, z.core.$strict>;
47
+ export interface LearningProvenanceV1 {
48
+ v: 1;
49
+ mechanism: LearningMechanism;
50
+ sourceKnowledgeIds: string[];
51
+ sourceTaskIds: string[];
52
+ sourceEvidenceRefs: string[];
53
+ sourceRunIds: string[];
54
+ sourceModelIds: string[];
55
+ /** See `LearningWriteOriginSchema` for why there is no `agentId` here. */
56
+ writeOrigin: {
57
+ sessionId?: string;
58
+ agentRole?: string;
59
+ producedAt: string;
60
+ };
61
+ }
62
+ /** Caller-supplied provenance body. Every reference class is optional. */
63
+ export interface LearningProvenanceInput {
64
+ mechanism: LearningMechanism;
65
+ sourceKnowledgeIds?: string[];
66
+ sourceTaskIds?: string[];
67
+ sourceEvidenceRefs?: string[];
68
+ sourceRunIds?: string[];
69
+ sourceModelIds?: string[];
70
+ }
71
+ /** Caller-supplied write origin. `producedAt` defaults to now when omitted. */
72
+ export interface LearningWriteOriginInput {
73
+ sessionId?: string;
74
+ agentRole?: string;
75
+ producedAt?: string;
76
+ }
77
+ /**
78
+ * Build a validated `LearningProvenanceV1` from a partial body and a write
79
+ * origin. Fills `v`, missing reference classes, and `producedAt`; deduplicates,
80
+ * sorts, and caps every reference class; then parses through
81
+ * `LearningProvenanceV1Schema` so the returned value is guaranteed valid.
82
+ *
83
+ * Throws (via Zod) on an unknown mechanism or an unparseable `producedAt` —
84
+ * a malformed provenance record must never be persisted silently.
85
+ */
86
+ export declare function stampLearningProvenance(partial: LearningProvenanceInput, origin?: LearningWriteOriginInput): LearningProvenanceV1;
@@ -1,5 +1,3 @@
1
1
  export { EvidenceLockTimeoutError, withEvidenceLock, } from '../evidence/lock.js';
2
- export { type DependencyGraph, getDependencyChain, getExecutionOrder, getRunnableTasks, isTaskBlocked, parseDependencyGraph, type TaskNode, } from './dependency-graph.js';
3
2
  export { cleanupExpiredLocks, type FileLock, isLocked, listActiveLocks, releaseLock, tryAcquireLock, } from './file-locks.js';
4
- export { extractMetaSummaries, getLatestTaskSummary, indexMetaSummaries, type MetaSummaryEntry, querySummaries, } from './meta-indexer.js';
5
3
  export { type ComplexityMetrics, computeComplexity, type ReviewDepth, type ReviewRouting, routeReview, routeReviewForChanges, shouldParallelizeReview, } from './review-router.js';
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Plan-time parallel-execution verdict helper (#1656 / #1674 v8 flagship).
3
+ *
4
+ * Pure, synchronous, side-effect-free pairwise conflict analysis for N proposed
5
+ * parallel task groups. Used by BOTH:
6
+ * - the `plan_conflict_check` tool (architect-facing advisory — see
7
+ * `src/tools/plan-conflict-check.ts`), and
8
+ * - the delegation gate (`src/hooks/delegation-gate.ts`) which recomputes the
9
+ * verdict INLINE at coder-dispatch time to enforce the v8 "serial fallback
10
+ * when scopes overlap or are unknown" contract (acceptance criterion 4).
11
+ *
12
+ * Single source of truth: one helper, two call sites, so the architect's
13
+ * advisory and the gate's enforcement can never disagree on what "disjoint"
14
+ * means.
15
+ *
16
+ * Design notes:
17
+ * - Sync by design: it reads only `.swarm/scopes/scope-<taskId>.json` via
18
+ * the hardened persisted-scope reader (sync + fail-closed). The gate runs in
19
+ * `toolBefore` on every tool call and must stay bounded; an async/gitten
20
+ * helper would violate the bounded-gate spirit.
21
+ * - The helper itself NEVER calls `getCoChangePairs` (async + `git log`).
22
+ * Co-change signal is opt-in and supplied by the caller (the tool) via
23
+ * `options.cochangePairs`. The gate never supplies it, keeping the
24
+ * enforcement path git-free and fast.
25
+ * - Fail-closed: a missing/malformed scope → `unknown_scope`, which conflicts
26
+ * with everything, so `verdict` can never be `all_disjoint` while any task
27
+ * lacks a declared scope. This is the v8 safety guarantee.
28
+ * - Writes nothing. Honors issue #1656's "read-only (writes nothing)" tool
29
+ * acceptance criterion.
30
+ */
31
+ import type { CoChangeEntry } from '../tools/co-change-analyzer.js';
32
+ import { type CoChangeThreshold } from '../turbo/epic/cochange-conflict.js';
33
+ import { normalizePath, pathsConflict, readTaskScopes } from '../turbo/lean/conflicts.js';
34
+ /**
35
+ * Per-pair conflict classification. Mirrors `EpicPairVerdict`'s signal
36
+ * decomposition but flattens `none` into `disjoint` for the plan-level view.
37
+ */
38
+ export interface ParallelVerdictPair {
39
+ /** First task id (input order). */
40
+ a: string;
41
+ /** Second task id (input order). */
42
+ b: string;
43
+ /** `conflict` = path or co-change overlap; `disjoint` = provably no overlap; `unknown` = ≥1 task has no usable scope. */
44
+ verdict: 'conflict' | 'disjoint' | 'unknown';
45
+ /** Human-readable evidence lines (path pairs, co-change pairs). Empty for `disjoint`/`unknown`. */
46
+ evidence: string[];
47
+ }
48
+ /**
49
+ * Whole-plan verdict. The gate keys off `verdict === 'all_disjoint'` to allow
50
+ * parallel execution; anything else forces serial.
51
+ */
52
+ export interface ParallelVerdict {
53
+ /** `all_disjoint` iff every pair is `disjoint` (no conflicts, no unknowns). */
54
+ verdict: 'all_disjoint' | 'conflicts_present' | 'unknown_scopes';
55
+ /** Pairwise results, one per input task pair (i < j). */
56
+ pairs: ParallelVerdictPair[];
57
+ /** Suggested serialization order (topological sort over the conflict graph). Input order preserved when no conflicts. */
58
+ suggestedSerialOrder: string[];
59
+ /** Task ids whose scope could not be resolved (missing/malformed). */
60
+ unknownScopeTasks: string[];
61
+ }
62
+ /** Default co-change threshold when the caller opts into co-change but omits one. */
63
+ export declare const DEFAULT_PARALLEL_COCHANGE_THRESHOLD: CoChangeThreshold;
64
+ /** Hard cap for the synchronous O(N²) verdict path (F-005). */
65
+ export declare const MAX_PARALLEL_VERDICT_TASKS = 64;
66
+ export interface ComputeParallelVerdictOptions {
67
+ /** When true AND `cochangePairs` is supplied, fold co-change signal into each pair. Off by default. */
68
+ useCochange?: boolean;
69
+ /** Caller-supplied co-change data (e.g. from `getCoChangePairs`). The helper never fetches it. */
70
+ cochangePairs?: CoChangeEntry[];
71
+ /** Override the co-change threshold. Defaults to `DEFAULT_PARALLEL_COCHANGE_THRESHOLD`. */
72
+ cochangeThreshold?: CoChangeThreshold;
73
+ }
74
+ /**
75
+ * Compute a pairwise conflict verdict for the given task ids.
76
+ *
77
+ * Pure + synchronous. Reads only `.swarm/scopes/`. Writes nothing. Fail-closed
78
+ * on any read/parse error (treats the task as `unknown`).
79
+ *
80
+ * @param directory Project root (for `.swarm/scopes/` reads).
81
+ * @param taskIds Task ids to analyze. Caller is responsible for min-length
82
+ * validation (the tool requires ≥2; the gate only calls this
83
+ * with ≥2 pending tasks).
84
+ * @param options Optional co-change signal + threshold.
85
+ */
86
+ export declare function computeParallelVerdict(directory: string, taskIds: string[], options?: ComputeParallelVerdictOptions): ParallelVerdict;
87
+ /**
88
+ * Quick pairwise check used by the gate: are these task ids provably disjoint?
89
+ *
90
+ * Equivalent to `computeParallelVerdict(...).verdict === 'all_disjoint'` but
91
+ * exposed as a named predicate so the gate reads as intent.
92
+ */
93
+ export declare function isProvablyDisjoint(directory: string, taskIds: string[]): boolean;
94
+ export { normalizePath, pathsConflict, readTaskScopes };
@@ -16,6 +16,10 @@ export { formatCourseCorrectionForInjection, generateCourseCorrection, } from '.
16
16
  export { createDefaultEscalationState, EscalationTracker } from './escalation';
17
17
  export { detectContextThrash, detectExpansionDrift, detectPatterns, detectPingPong, detectRepetitionLoop, detectStuckOnTest, } from './pattern-detector';
18
18
  export type { CourseCorrection, EscalationState, PatternDetectionResult, PatternMatch, PatternSeverity, PatternType, PrmConfig, TaxonomyCategory, TrajectoryEntry, } from './types';
19
+ import type { LearningConfig } from '../config/schema.js';
20
+ import { appendInsightCandidates } from '../hooks/micro-reflector.js';
21
+ import { enqueueCandidate } from '../learning/candidate-queue.js';
22
+ import { recordPatternObservation } from '../learning/prm-pattern-support.js';
19
23
  import { getAgentSession } from '../state';
20
24
  import { telemetry } from '../telemetry';
21
25
  import { formatCourseCorrectionForInjection, generateCourseCorrection } from './course-correction';
@@ -44,6 +48,9 @@ export declare const _internals: {
44
48
  recordReplayEntry: typeof recordReplayEntry;
45
49
  startReplayRecording: typeof startReplayRecording;
46
50
  telemetry: typeof telemetry;
51
+ recordPatternObservation: typeof recordPatternObservation;
52
+ enqueueCandidate: typeof enqueueCandidate;
53
+ appendInsightCandidates: typeof appendInsightCandidates;
47
54
  };
48
55
  /**
49
56
  * Context passed to toolAfter handler
@@ -94,4 +101,42 @@ export declare function resetPrmSessionState(session: ResettablePrmSessionState,
94
101
  * // Wire prmHook.toolAfter into your tool.execute.after hook
95
102
  * ```
96
103
  */
97
- export declare function createPrmHook(config: PrmConfig, directory: string): PrmHook;
104
+ /**
105
+ * Knobs the PRM hook needs to hand supported patterns to the real-time
106
+ * admission queue (issue #1821, AC10). Optional so existing callers and tests
107
+ * keep working unchanged; when absent, pattern persistence is simply off.
108
+ */
109
+ export interface PrmPatternPersistenceOptions {
110
+ /**
111
+ * `learning.prm_persistence.enabled`. Governs the WHOLE producer, durable
112
+ * append included.
113
+ */
114
+ enabled: boolean;
115
+ min_support: number;
116
+ cooldown_ms: number;
117
+ /**
118
+ * `learning.realtime_admission.enabled`. Governs ONLY the in-memory enqueue.
119
+ *
120
+ * It must never gate the durable append (issue #1821 F3): AC8 says disabled
121
+ * or crashed real-time work loses nothing, and the phase-boundary backstop
122
+ * can only see candidates that reached `.swarm/insight-candidates.jsonl`.
123
+ * ANDing the two flags at the call site made `realtime_admission.enabled=false`
124
+ * silently discard every PRM candidate. Mirrors `micro-reflector.ts`, which
125
+ * appends unconditionally and gates only its enqueue.
126
+ */
127
+ admission_enabled: boolean;
128
+ /** `learning.realtime_admission.max_queue_size` — bounds the shared queue. */
129
+ max_queue_size: number;
130
+ }
131
+ /**
132
+ * Map a parsed `learning` config onto the PRM producer's knobs (issue #1821 F3).
133
+ *
134
+ * The mapping lives here, beside the interface that documents which flag governs
135
+ * what, and `src/index.ts` is its only production caller — so the AC8 coupling
136
+ * ("durable persistence is gated by `prm_persistence.enabled` ALONE") is
137
+ * expressed once and can be asserted directly by a test instead of being
138
+ * re-derived from an object literal at the plugin's wiring site, where the two
139
+ * flags were previously ANDed together.
140
+ */
141
+ export declare function resolvePrmPatternPersistenceOptions(learning: LearningConfig): PrmPatternPersistenceOptions;
142
+ export declare function createPrmHook(config: PrmConfig, directory: string, patternPersistence?: PrmPatternPersistenceOptions): PrmHook;
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Cross-producer recommendation dedup ledger (issue #1821, AC21).
3
+ *
4
+ * Three mechanisms propose learning recommendations: the curator sweep
5
+ * (`src/hooks/curator.ts`), the skill improver's macro-reflector
6
+ * (`src/services/trajectory-cluster.ts`), and the consensus miner
7
+ * (`src/consensus/miner.ts`). `src/learning/fingerprint.ts` gave them a shared
8
+ * identity function, but a fingerprint alone dedups nothing: somebody has to
9
+ * remember what was already emitted. This module is that memory.
10
+ *
11
+ * ## Why a second key
12
+ *
13
+ * `computeRecommendationFingerprint` deliberately folds `kind` into the digest —
14
+ * its own doc says "dedup across mechanisms is a policy decision made by the
15
+ * caller, not baked into the identity". This module *is* that caller, and the
16
+ * policy it implements is: **two recommendations are the same recommendation
17
+ * when they say the same thing about the same scope, regardless of which
18
+ * mechanism noticed it first.** So every ledger entry carries two ids:
19
+ *
20
+ * - `fingerprint` — the producer-scoped `lrec_…` id, kept verbatim for audit.
21
+ * - `crossKey` — an `lxk_…` id over `{ normalizedStatement, sortedScopeKeys }`.
22
+ *
23
+ * `crossKey` drops both `kind` and `target`. Dropping `kind` is the whole point.
24
+ * Dropping `target` is a redundancy call, not a necessity: each producer already
25
+ * interpolates its target into the statement it emits (the miner's `intent`
26
+ * names its `proposedTarget`; the improver's statement names the tool and
27
+ * failure kind its slug is built from; the curator's `entry_id` travels in
28
+ * `scopeKeys`), so keeping `target` would add no discriminating power while
29
+ * splitting identity on producer-local vocabulary.
30
+ *
31
+ * Disambiguation that genuinely must survive therefore travels in `scopeKeys`,
32
+ * which is order- and duplicate-insensitive (`normalizeScopeKeys`). The curator
33
+ * uses that to keep `archive entry-X` and `rewrite entry-X` distinct even when
34
+ * both carry the same lesson text; recommendations that mint *new* knowledge
35
+ * carry no scope keys, because content alone is their identity.
36
+ *
37
+ * ## What this does and does not achieve today
38
+ *
39
+ * The key is an EXACT hash of normalized text. Two producers dedup against each
40
+ * other only when they emit the same sentence. In the current codebase the
41
+ * improver and the miner build their statements from fixed templates
42
+ * (`Avoid the recurring … failure in …`, `Investigate the smallest … change …`)
43
+ * while the curator's statement is a free-form LLM lesson, so a cross-producer
44
+ * collision is possible but uncommon. The concrete, everyday win is therefore:
45
+ * (a) WITHIN-producer dedup, which the curator and the improver had none of, and
46
+ * (b) one shared, provenance-stamped record of every emitted recommendation that
47
+ * any producer can consult. Making cross-producer suppression routine would need
48
+ * near-duplicate matching (see `findNearDuplicate` / the knowledge dedup sweep)
49
+ * rather than an exact fingerprint, which is a different mechanism than the one
50
+ * `src/learning/fingerprint.ts` defines.
51
+ *
52
+ * ## Check, emit, then record — never "reserve"
53
+ *
54
+ * The two phases are deliberately separate:
55
+ *
56
+ * - `checkRecommendations` is read-only. It answers "has this been emitted
57
+ * before?" and writes nothing.
58
+ * - `recordEmittedRecommendations` is the locked write, and callers run it only
59
+ * for recommendations that **actually took effect**.
60
+ *
61
+ * An earlier design claimed the key up-front, before the caller emitted. That is
62
+ * wrong here: the curator legitimately *defers* recommendations (cohort-safe
63
+ * authorization not yet granted, target entry temporarily inactive, CAS revision
64
+ * drift, fair-scan generation already curated, actionability quarantine pending
65
+ * the hardening loop). Every one of those paths expects a later sweep to retry,
66
+ * and a key claimed before the emit would suppress that retry permanently.
67
+ * Recording after the fact costs a small race — two producers can both pass
68
+ * `check` and both emit — whose worst case is one duplicate, exactly the
69
+ * pre-#1821 behaviour. A burned key, by contrast, loses a lesson forever. The
70
+ * record step re-checks under the lock, so the ledger itself never grows a
71
+ * duplicate.
72
+ *
73
+ * ## Boundedness and containment
74
+ *
75
+ * The ledger is a JSONL file at `.swarm/learning/recommendation-ledger.jsonl`,
76
+ * hard-capped at `MAX_RECOMMENDATION_LEDGER_ENTRIES` entries with oldest-first
77
+ * FIFO eviction, and every entry is capped at `MAX_ENTRY_BYTES` (provenance is
78
+ * dropped rather than allowed to blow the bound), so the file has a hard ceiling
79
+ * of roughly 2 MB (AGENTS.md invariant 8). It never touches `process.cwd()`
80
+ * (invariant 4). Its own subdirectory is deliberate: `transactFile` locks the
81
+ * *containing directory*, so living under `learning/` keeps this lock disjoint
82
+ * from the `.swarm/` root lock that `transactKnowledge` takes.
83
+ *
84
+ * The root is `resolveKnowledgeStoreDir`, NOT a hardcoded `<directory>/.swarm`.
85
+ * That is the same link-aware resolution `resolveSwarmKnowledgePath` uses, so in
86
+ * a linked cohort the ledger lives beside the knowledge store it guards instead
87
+ * of being stranded per-worktree — a lane worktree would otherwise discard its
88
+ * ledger while the lessons it recorded persisted in the shared store.
89
+ *
90
+ * Eviction means a recommendation older than the last 500 emissions can surface
91
+ * again. That is the intended trade: bounded state beats perfect recall. Note
92
+ * this cap is independent of `knowledge.swarm_max_entries` (default 100): a
93
+ * lesson the knowledge store has already FIFO-evicted can still be suppressed
94
+ * here until its own entry ages out.
95
+ *
96
+ * ## Fail-open
97
+ *
98
+ * Every failure mode — unreadable ledger, lock timeout, corrupt lines, a
99
+ * provenance record the schema rejects — resolves to "emit everything", i.e.
100
+ * exactly the behaviour that existed before this module. A broken dedup ledger
101
+ * must never silence the learning loops.
102
+ */
103
+ import { transactFile } from '../hooks/knowledge-store.js';
104
+ import { type RecommendationKind } from '../learning/fingerprint.js';
105
+ import { type LearningProvenanceInput, type LearningProvenanceV1, type LearningWriteOriginInput } from '../learning/provenance.js';
106
+ /**
107
+ * Hard cap on retained ledger entries (AGENTS.md invariant 8). Sized to match
108
+ * `INSIGHT_CANDIDATES_MAX_ENTRIES` in `src/hooks/micro-reflector.ts`, the
109
+ * closest sibling queue: the same order of magnitude of learning artifacts flows
110
+ * through both, and one shared number is easier to reason about than two.
111
+ */
112
+ export declare const MAX_RECOMMENDATION_LEDGER_ENTRIES = 500;
113
+ /**
114
+ * Hard cap on one serialized entry. Provenance reference lists are individually
115
+ * bounded by `stampLearningProvenance`, but 5 classes x 50 refs x 512 chars is
116
+ * still ~128 KB, which would put the file's ceiling in the tens of megabytes.
117
+ * An entry over this budget keeps its identity fields and drops provenance —
118
+ * dedup is the contract, provenance is the bonus.
119
+ */
120
+ export declare const MAX_ENTRY_BYTES = 4096;
121
+ /** A recommendation a producer is about to emit, or has just emitted. */
122
+ export interface RecommendationCandidate {
123
+ /** Producing mechanism. Recorded on the entry; excluded from `crossKey`. */
124
+ kind: RecommendationKind;
125
+ /** Producer-local target (knowledge id, proposal slug, subsystem name). */
126
+ target: string;
127
+ /** The recommendation body. Normalized before hashing. */
128
+ statement: string;
129
+ /** Scope disambiguators. Order- and duplicate-insensitive. */
130
+ scopeKeys?: string[];
131
+ /** Learning provenance stamped onto the ledger entry when recorded. */
132
+ provenance?: LearningProvenanceInput;
133
+ /** Write origin for the provenance stamp. */
134
+ origin?: LearningWriteOriginInput;
135
+ }
136
+ /** The two ids a candidate resolves to. */
137
+ export interface RecommendationIdentity {
138
+ /** Producer-scoped `lrec_…` id from `src/learning/fingerprint.ts`. */
139
+ fingerprint: string;
140
+ /** Kind- and target-independent `lxk_…` id used for dedup. */
141
+ crossKey: string;
142
+ }
143
+ /** One durable record of an emitted recommendation. */
144
+ export interface RecommendationLedgerEntry extends RecommendationIdentity {
145
+ v: 1;
146
+ kind: RecommendationKind;
147
+ target: string;
148
+ /** ISO-8601 timestamp of the record call that appended this entry. */
149
+ emittedAt: string;
150
+ provenance?: LearningProvenanceV1;
151
+ }
152
+ /**
153
+ * Why a candidate was not accepted for emission.
154
+ *
155
+ * Module-private: consumers reach this set through
156
+ * `RecommendationDecision['suppressedBy']`, which is exported and narrows to the
157
+ * same two literals. Exporting the alias as well gave it a name no importer ever
158
+ * used.
159
+ */
160
+ type RecommendationSuppressionSource = 'ledger' | 'batch';
161
+ /** Per-candidate outcome, in input order. */
162
+ export interface RecommendationDecision extends RecommendationIdentity {
163
+ /** Index into the candidate array this decision belongs to. */
164
+ index: number;
165
+ /** True when the caller should emit this recommendation. */
166
+ emit: boolean;
167
+ /**
168
+ * `'ledger'` — a previous emission (possibly by another producer) already
169
+ * claimed this `crossKey`. `'batch'` — an earlier candidate in this same call
170
+ * claimed it. Absent when `emit` is true.
171
+ */
172
+ suppressedBy?: RecommendationSuppressionSource;
173
+ }
174
+ export interface CheckRecommendationsResult {
175
+ decisions: RecommendationDecision[];
176
+ /** Count of `emit === true` decisions. */
177
+ accepted: number;
178
+ /** Count of `emit === false` decisions. */
179
+ suppressed: number;
180
+ /**
181
+ * True when the ledger could not be read and every candidate was accepted by
182
+ * fail-open default rather than by an actual dedup check.
183
+ */
184
+ degraded: boolean;
185
+ }
186
+ export interface RecordRecommendationsResult {
187
+ /** Entries actually appended. */
188
+ recorded: number;
189
+ /** Candidates skipped because the ledger already carried their cross key. */
190
+ suppressed: number;
191
+ /** Entries dropped by the FIFO cap during this record. */
192
+ evicted: number;
193
+ /** True when the ledger could not be written. */
194
+ degraded: boolean;
195
+ }
196
+ /**
197
+ * Absolute path of the ledger, link-aware.
198
+ *
199
+ * Byte-identical to `<directory>/.swarm/learning/recommendation-ledger.jsonl`
200
+ * for an unlinked worktree; redirects to the shared cohort store when
201
+ * `.swarm/link.json` is active, exactly as `resolveSwarmKnowledgePath` does for
202
+ * the knowledge file this ledger shadows.
203
+ */
204
+ export declare function resolveRecommendationLedgerPath(directory: string): string;
205
+ /** Shape check for a value that claims to be a cross-producer key. */
206
+ export declare function isRecommendationCrossKey(value: string): boolean;
207
+ /**
208
+ * Compute the kind- and target-independent dedup key.
209
+ *
210
+ * Built from the same normalizers the `lrec_` fingerprint uses, so whitespace
211
+ * runs, surrounding space, letter case, trailing sentence punctuation, scope-key
212
+ * order, and scope-key duplicates all collapse to one identity.
213
+ */
214
+ export declare function computeCrossProducerKey(input: {
215
+ statement: string;
216
+ scopeKeys?: string[];
217
+ }): string;
218
+ /** Compute both ids for a candidate. Pure. */
219
+ export declare function computeRecommendationIdentity(candidate: RecommendationCandidate): RecommendationIdentity;
220
+ /**
221
+ * Read every retained ledger entry, propagating a real read failure.
222
+ *
223
+ * A missing ledger is the first-run case and yields `[]`; anything else throws
224
+ * so `checkRecommendations` can report `degraded` truthfully instead of treating
225
+ * an unreadable ledger as an empty one and silently claiming it deduped.
226
+ */
227
+ declare function readLedgerStrict(directory: string): Promise<RecommendationLedgerEntry[]>;
228
+ /**
229
+ * Read every retained ledger entry. Fail-open: returns `[]` when the ledger is
230
+ * absent or unreadable. This is the inspection entry point — the dedup decision
231
+ * itself goes through `readLedgerStrict` so a failure is visible rather than
232
+ * disguised as an empty ledger.
233
+ */
234
+ export declare function readRecommendationLedger(directory: string): Promise<RecommendationLedgerEntry[]>;
235
+ /**
236
+ * Answer "has this been emitted before?" without writing anything.
237
+ *
238
+ * Suppression is decided against the ledger's existing cross keys plus the keys
239
+ * claimed by earlier candidates in this same batch. Fail-open: an unreadable
240
+ * ledger yields `degraded: true` and accepts everything.
241
+ */
242
+ export declare function checkRecommendations(directory: string, candidates: readonly RecommendationCandidate[]): Promise<CheckRecommendationsResult>;
243
+ /**
244
+ * Append the recommendations a producer actually emitted.
245
+ *
246
+ * Runs one locked read-modify-write and re-checks every cross key under that
247
+ * lock, so a candidate another producer recorded between `checkRecommendations`
248
+ * and here is counted as `suppressed` rather than duplicated into the ledger.
249
+ * Candidates whose statement normalizes to nothing are never recorded.
250
+ */
251
+ export declare function recordEmittedRecommendations(directory: string, candidates: readonly RecommendationCandidate[], options?: {
252
+ producedAt?: string;
253
+ }): Promise<RecordRecommendationsResult>;
254
+ /**
255
+ * DI seam (AGENTS.md invariant 7). `now` lets tests pin `emittedAt`;
256
+ * `transactFile`, `readRecommendationLedger`, and
257
+ * `resolveRecommendationLedgerPath` let them exercise the fail-open paths
258
+ * without `mock.module`. Restore each entry in `afterEach`.
259
+ */
260
+ export declare const _internals: {
261
+ now: () => Date;
262
+ transactFile: typeof transactFile;
263
+ readLedgerStrict: typeof readLedgerStrict;
264
+ resolveRecommendationLedgerPath: typeof resolveRecommendationLedgerPath;
265
+ };
266
+ export {};
@@ -83,11 +83,15 @@ export interface SkillImproveResult {
83
83
  macroMotifs?: {
84
84
  motifs: number;
85
85
  proposalsWritten: number;
86
+ /** #1821 AC21: motifs suppressed by the cross-producer dedup ledger. */
87
+ duplicatesSuppressed?: number;
86
88
  };
87
89
  /** #1234 Part 4: success workflow-motif proposals written this run. */
88
90
  successMotifs?: {
89
91
  motifs: number;
90
92
  proposalsWritten: number;
93
+ /** #1821 AC21: motifs suppressed by the cross-producer dedup ledger. */
94
+ duplicatesSuppressed?: number;
91
95
  };
92
96
  /** Issue #1477: self-healing reconciliation of stale active generated skills
93
97
  * (regenerate-or-retire). In proposal-only runs this is a dry run (the
@@ -40,6 +40,20 @@ export interface StatusData {
40
40
  leanDegradedTasks?: number;
41
41
  /** Human-readable degradation summary */
42
42
  leanDegradationSummary?: string;
43
+ /**
44
+ * #1657: merge-back conflict recovery worktrees preserved for manual
45
+ * recovery (durable records under `.swarm/recovery/`). Surfaced so the
46
+ * architect/operator can see pending recovery work in `/swarm status`
47
+ * rather than having to re-read a prior tool result. `undefined`/empty when
48
+ * no lanes are preserved.
49
+ */
50
+ leanPreservedRecoveryWorktrees?: Array<{
51
+ laneId: string;
52
+ status: string;
53
+ worktreePath: string;
54
+ reason: string;
55
+ replayHint: string;
56
+ }>;
43
57
  /** Whether Full-Auto mode is currently active */
44
58
  fullAutoActive?: boolean;
45
59
  /**
@@ -78,6 +92,19 @@ export interface StatusData {
78
92
  unactionableQueueDepth?: number;
79
93
  /** #1234 Part 3: pending insight candidates awaiting phase boundary consumption */
80
94
  insightCandidatesPending?: number;
95
+ /**
96
+ * #1821 AC22: consensus report FILES stored under
97
+ * `.swarm/evolution/consensus/`.
98
+ *
99
+ * The reader half of the consensus store. Without it the miner's reports were
100
+ * a write-only directory — nothing in the product enumerated them, so a user
101
+ * had no way to learn they existed. A file count, deliberately: it is one
102
+ * `readdir` with no JSON parse and no integrity recomputation, so it costs the
103
+ * same whether the store holds one report or fifty. It therefore includes any
104
+ * corrupt report, because what it counts is files whose name is a well-formed
105
+ * report id.
106
+ */
107
+ consensusReports?: number;
81
108
  /**
82
109
  * Cohort/link status (issue #1846). Makes the linked knowledge store and its
83
110
  * health obvious in `/swarm status`. `undefined` when link state is absent.