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
+ * Shared write pipeline for content-addressed, write-once artifacts under
3
+ * `.swarm/` (issue #1821).
4
+ *
5
+ * Several subsystems persist artifacts that must never be silently rewritten:
6
+ * the evaluation store (`.swarm/evolution/{task-sets,runs,decisions,...}`) and
7
+ * the consensus report store (`.swarm/evolution/consensus/`). They all need the
8
+ * identical sequence — create the parent directory, take the evidence lock,
9
+ * re-read whatever is already on disk, treat a byte-identical (or
10
+ * caller-defined equivalent) payload as idempotent, reject a divergent payload,
11
+ * and otherwise commit through the atomic temp-file+rename write.
12
+ *
13
+ * That sequence lives here exactly once. Duplicating it per store is how the
14
+ * two copies drift: one gains a conflict check the other lacks, or one takes
15
+ * the lock under the wrong actor and corrupts lock attribution.
16
+ *
17
+ * Deliberate design points:
18
+ * - `agent` is a **parameter**, not a constant. The lock actor identifies the
19
+ * subsystem doing the write; hard-coding one store's actor would mislabel
20
+ * every other store's lock acquisitions in telemetry and diagnostics.
21
+ * - `serialize` is a **parameter**. The canonical-JSON encoder that the
22
+ * evaluation store uses lives in `src/evaluation/`, and importing it here
23
+ * would tie this module to a sibling subsystem it has no business knowing
24
+ * about. Callers supply their own canonical form.
25
+ * - `conflictError` is a **factory parameter** rather than a hard-coded error
26
+ * class, so each store throws its own conflict type (and keeps `instanceof`
27
+ * identity with its own tests) without this module importing any of them.
28
+ */
29
+ /**
30
+ * Read a UTF-8 file, returning `undefined` when it does not exist.
31
+ *
32
+ * A missing immutable artifact is the normal first-write case, not an error, so
33
+ * this collapses `ENOENT` into `undefined` while letting every other filesystem
34
+ * error (EACCES, EISDIR, EIO, ...) propagate. Exported because the same
35
+ * "absent means not-yet-written" semantics apply to the stores' own reads.
36
+ */
37
+ export declare function readOptionalFile(filePath: string): Promise<string | undefined>;
38
+ /**
39
+ * Why a write to an already-populated path could not proceed.
40
+ *
41
+ * - `corrupt` — a file exists but does not parse/validate as the artifact
42
+ * type, so it cannot be compared against the desired payload.
43
+ * - `divergent` — a valid artifact exists and differs from the desired payload.
44
+ */
45
+ export type ImmutableArtifactConflict = {
46
+ kind: 'corrupt';
47
+ filePath: string;
48
+ cause: unknown;
49
+ } | {
50
+ kind: 'divergent';
51
+ filePath: string;
52
+ };
53
+ export type WriteImmutableArtifactOptions<T> = {
54
+ /** Project root; the evidence lock is scoped to it. */
55
+ directory: string;
56
+ /** Lock key — the artifact path relative to `<directory>/.swarm/`. */
57
+ relativeLockPath: string;
58
+ /** Absolute path of the artifact file to write. */
59
+ filePath: string;
60
+ /** Lock actor, e.g. the owning store's name. */
61
+ agent: string;
62
+ /** Lock task identifier, for diagnostics. */
63
+ taskId: string;
64
+ /** The artifact to persist. */
65
+ value: T;
66
+ /** Canonical encoder; its output is what lands on disk verbatim. */
67
+ serialize: (value: unknown) => string;
68
+ /** Schema parse/validate for the artifact already on disk. */
69
+ parse: (value: unknown) => T;
70
+ /**
71
+ * Optional equivalence escape hatch for artifacts whose canonical form may
72
+ * legitimately differ (e.g. a decision that carries a decision timestamp).
73
+ */
74
+ isEquivalent?: (existing: T, desired: T) => boolean;
75
+ /** Builds the store-specific error thrown on a conflict. */
76
+ conflictError: (conflict: ImmutableArtifactConflict) => Error;
77
+ };
78
+ /**
79
+ * Write `value` to `filePath` exactly once.
80
+ *
81
+ * Idempotent: re-writing an equivalent payload returns the artifact already on
82
+ * disk without touching the file. Writing a different payload to the same path
83
+ * throws the caller-supplied conflict error. The whole read-compare-write runs
84
+ * under the evidence lock so concurrent writers cannot interleave.
85
+ */
86
+ export declare function writeImmutableArtifact<T>(options: WriteImmutableArtifactOptions<T>): Promise<T>;
@@ -16,6 +16,32 @@ export type LoadEvidenceResult = {
16
16
  status: 'invalid_schema';
17
17
  errors: string[];
18
18
  };
19
+ /**
20
+ * Options for {@link loadEvidence}.
21
+ *
22
+ * Additive and backward-compatible: omitting the argument entirely — which every
23
+ * pre-existing caller does — keeps the historical behaviour exactly, including
24
+ * the lazy in-place upgrade of a legacy flat retrospective.
25
+ */
26
+ export interface LoadEvidenceOptions {
27
+ /**
28
+ * Whether a legacy flat retrospective may be upgraded **in place** on read.
29
+ *
30
+ * `true` (the default, and the historical behaviour) persists the wrapped
31
+ * bundle back to `.swarm/evidence/<taskId>/evidence.json` under the evidence
32
+ * lock, so the repair happens once instead of on every read.
33
+ *
34
+ * `false` makes the call a pure read: the caller still receives the wrapped,
35
+ * validated bundle, but nothing is written, no lock is taken, and no
36
+ * `evidence-loader` actor appears in lock telemetry. Callers that advertise a
37
+ * read-only contract — the consensus corpus (`src/consensus/corpus.ts`) is the
38
+ * first — MUST pass `false`. A mining run that silently rewrote the evidence
39
+ * it was merely reading would falsify that contract and, because
40
+ * `LEGACY_TASK_COMPLEXITY_MAP` remaps values on the way through, would also
41
+ * change the stored data.
42
+ */
43
+ migrate?: boolean;
44
+ }
19
45
  /**
20
46
  * All valid evidence types (13 total)
21
47
  */
@@ -67,8 +93,12 @@ declare function wrapFlatRetrospective(flatEntry: Record<string, unknown>, taskI
67
93
  /**
68
94
  * Load evidence bundle for a task.
69
95
  * Returns a LoadEvidenceResult discriminated union.
96
+ *
97
+ * By default this may perform a one-time in-place upgrade of a legacy flat
98
+ * retrospective (see {@link LoadEvidenceOptions.migrate}). Pass
99
+ * `{ migrate: false }` for a guaranteed pure read.
70
100
  */
71
- export declare function loadEvidence(directory: string, taskId: string): Promise<LoadEvidenceResult>;
101
+ export declare function loadEvidence(directory: string, taskId: string, options?: LoadEvidenceOptions): Promise<LoadEvidenceResult>;
72
102
  /**
73
103
  * List all task IDs that have evidence bundles.
74
104
  * Returns sorted array of valid task IDs.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The pure actionability predicate (knowledge Layer 5), extracted as a LEAF
3
+ * module (issue #1821 Workstream A3).
4
+ *
5
+ * Why this file exists separately from `knowledge-validator.ts`: the predicate
6
+ * itself has always been pure, but its host module imports `node:fs/promises`
7
+ * and `proper-lockfile` at the top level. `hive-policy.ts` documents that it
8
+ * "performs NO I/O and holds NO module-level mutable state" (invariant 8), so
9
+ * importing the predicate from the validator would have quietly made that
10
+ * comment false. Extracting the predicate keeps the documented purity TRUE
11
+ * rather than weakening the comment.
12
+ *
13
+ * CONTRACT FOR THIS MODULE: no I/O imports, no module-level mutable state, no
14
+ * plugin-init-path import. It may only import types. `knowledge-validator.ts`
15
+ * re-exports everything here, so every existing consumer is unaffected.
16
+ */
17
+ import type { KnowledgeEntryBase } from './knowledge-types.js';
18
+ export interface ActionabilityResult {
19
+ actionable: boolean;
20
+ /** Present only when not actionable. */
21
+ reason?: 'missing_predicate' | 'missing_scope' | 'missing_predicate_and_scope';
22
+ }
23
+ /** The exact field subset the predicate reads. */
24
+ export type ActionabilityInput = Pick<KnowledgeEntryBase, 'forbidden_actions' | 'required_actions' | 'verification_checks' | 'verification_predicate' | 'applies_to_tools' | 'applies_to_agents'>;
25
+ /**
26
+ * Layer 5: an entry is actionable only when it carries at least one
27
+ * machine-checkable predicate AND at least one scope tag.
28
+ *
29
+ * predicate := forbidden_actions | required_actions | verification_checks
30
+ * | verification_predicate
31
+ * scope := applies_to_tools | applies_to_agents
32
+ *
33
+ * Plain-prose lessons (no predicate, no scope) are NOT actionable and must be
34
+ * quarantined rather than activated.
35
+ */
36
+ export declare function validateActionability(entry: ActionabilityInput): ActionabilityResult;
@@ -24,6 +24,7 @@
24
24
  * This dual dispatch means agent lists are incomplete — they capture factory-dispatched
25
25
  * curators but omit hook-dispatched ones. This is by design for hook-internal operations.
26
26
  */
27
+ import { checkRecommendations, recordEmittedRecommendations } from '../services/recommendation-ledger.js';
27
28
  import { listSkills, parseDraftFrontmatter, retireOrMarkStale, retireSkill } from '../services/skill-generator.js';
28
29
  import { getSkillVersion, reviseSkill } from '../services/skill-reviser.js';
29
30
  import type { ComplianceObservation, CuratorConfig, CuratorInitResult, CuratorPhaseResult, CuratorSummary, KnowledgeRecommendation, PhaseDigestEntry } from './curator-types.js';
@@ -61,6 +62,8 @@ export declare const _internals: {
61
62
  reviseSkill: typeof reviseSkill;
62
63
  getSkillVersion: typeof getSkillVersion;
63
64
  readLatestPostMortemDigest: (directory: string) => string | null;
65
+ checkRecommendations: typeof checkRecommendations;
66
+ recordEmittedRecommendations: typeof recordEmittedRecommendations;
64
67
  };
65
68
  export interface RecommendationParseDiagnostic {
66
69
  section: 'OBSERVATIONS' | 'RECOMMENDATION_ID';
@@ -21,8 +21,19 @@
21
21
  * (it neither credits nor blocks) and current behavior is preserved. Operators
22
22
  * raise the thresholds to activate application-evidence gating.
23
23
  *
24
+ * Actionability floor (#1821 A3): the `actionability_floor` gate refuses to
25
+ * promote a plain-prose lesson — a promotion candidate must carry at least one
26
+ * machine-checkable predicate AND at least one scope tag. It is ON by default
27
+ * (`knowledge.promotion_require_actionable`, schema default `true`); the
28
+ * hand-written `KnowledgeConfig` interface declares the field OPTIONAL, so this
29
+ * module reads it as `?? true`.
30
+ *
24
31
  * This module performs NO I/O and holds NO module-level mutable state
25
32
  * (invariant 8). It is NOT imported on the plugin-init path (invariant 1).
33
+ * `validateActionability` is imported from the LEAF module
34
+ * `./actionability-predicate.js` rather than from `./knowledge-validator.js`,
35
+ * which imports `node:fs/promises` + `proper-lockfile` — importing the
36
+ * validator here would have made the no-I/O statement above false.
26
37
  */
27
38
  import type { KnowledgeConfig, PromotionEvidenceRecord, SwarmKnowledgeEntry } from './knowledge-types.js';
28
39
  /** A single named policy gate and whether it passed. */
@@ -126,6 +126,7 @@ export declare const _internals: {
126
126
  promoted_demotion_signal_threshold: number;
127
127
  promotion_min_terminal_applications: number;
128
128
  promotion_min_distinct_cohorts: number;
129
+ promotion_require_actionable: boolean;
129
130
  realtime_learning_nudge: {
130
131
  enabled: boolean;
131
132
  first_after_tool_calls: number;
@@ -156,6 +157,26 @@ export interface ManualPromotionOptions {
156
157
  force?: boolean;
157
158
  reason?: string;
158
159
  }
160
+ /**
161
+ * Actionability fields supplied on the DIRECT-TEXT promote path (#1821 A3).
162
+ *
163
+ * `/swarm promote "<text>"` has no swarm entry to inherit predicates/scope
164
+ * from, so the command passes them explicitly. Without this parameter the
165
+ * default-on `actionability_floor` gate would be permanently unsatisfiable on
166
+ * that path: every direct-text promotion would return "Promotion blocked by
167
+ * policy" and operators would be pushed into `--force`, poisoning the
168
+ * override-audit signal that #1847 exists to keep honest.
169
+ *
170
+ * `promoteFromSwarm` does NOT take these — it inherits the source swarm entry's
171
+ * own actionable-directive fields via `carryActionableFields`.
172
+ */
173
+ export interface ManualActionabilityFields {
174
+ applies_to_tools?: string[];
175
+ applies_to_agents?: string[];
176
+ required_actions?: string[];
177
+ forbidden_actions?: string[];
178
+ verification_checks?: string[];
179
+ }
159
180
  /**
160
181
  * Promote a lesson directly to the hive (manual promotion).
161
182
  *
@@ -168,8 +189,13 @@ export interface ManualPromotionOptions {
168
189
  * On a policy PASS → promotes with actor='manual'.
169
190
  *
170
191
  * An exact entry id / direct text alone is NEVER authorization to bypass policy.
192
+ *
193
+ * `actionable` (#1821 A3) carries the predicate/scope fields the direct-text
194
+ * path has no swarm entry to inherit from. They land on the synthetic stand-in
195
+ * (so the `actionability_floor` gate can see them) AND on the written hive
196
+ * entry (so a correctly-supplied field is not dropped on write).
171
197
  */
172
- export declare function promoteToHive(directory: string, lesson: string, category?: string, options?: ManualPromotionOptions, config?: KnowledgeConfig): Promise<string>;
198
+ export declare function promoteToHive(directory: string, lesson: string, category?: string, options?: ManualPromotionOptions, config?: KnowledgeConfig, actionable?: ManualActionabilityFields): Promise<string>;
173
199
  /**
174
200
  * Promote a lesson from swarm knowledge to hive.
175
201
  *
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Active-store near-duplicate dedup sweep (issue #1821 Lane A).
3
+ *
4
+ * The swarm knowledge store accretes near-duplicate lessons: the same insight
5
+ * arrives from a curator pass, a retrospective, and a micro-reflection, each
6
+ * with its own id. The `/swarm link` family migration already merges
7
+ * near-duplicates when two cohort stores are unified, but nothing ever did it
8
+ * for the ACTIVE store of a single worktree. This sweep closes that gap using
9
+ * the SAME merge helpers (`knowledge/entry-merge.ts`), so a lesson merged by a
10
+ * sweep and a lesson merged by a link end up in the same shape.
11
+ *
12
+ * ## Contract
13
+ *
14
+ * - ACTIVE entries only (`candidate` / `established` / `promoted`). Archived and
15
+ * quarantined entries are neither merge targets nor merge sources.
16
+ * - Bucketed by `category` so the pairwise comparison count is bounded by the
17
+ * sum of per-category squares rather than the whole store squared, and hard
18
+ * capped by `learning.dedup_sweep.max_comparisons`.
19
+ * - Deterministic: entries are compared in `id` order and clusters are resolved
20
+ * in `id` order, so two concurrent sweeps converge on the same winner rather
21
+ * than racing to different survivors.
22
+ * - All merges plus all loser archivals commit inside ONE `transactKnowledge`
23
+ * transaction, so a crash cannot leave a loser archived with its evidence
24
+ * never carried to the winner.
25
+ * - IDEMPOTENT. Losers are archived and only active entries are considered, so
26
+ * a second sweep over an unchanged store is a no-op. See the CLUSTERING note
27
+ * below for why that holds transitively, and
28
+ * `tests/unit/hooks/knowledge-dedup-sweep.test.ts` for the assertion — the
29
+ * underlying `mergeStoreEntries` is NOT idempotent (it re-merges and doubles
30
+ * counters on a repeat), so this property is earned here, not inherited.
31
+ *
32
+ * ## CLUSTERING (why a second sweep is a no-op)
33
+ *
34
+ * Near-duplication is not transitive: A~B and B~C does not imply A~C. A greedy
35
+ * "compare against the surviving representative" pass would therefore leave C
36
+ * active next to a winner it is a near-duplicate of, and the NEXT sweep would
37
+ * merge them — the sweep would never reach a fixed point.
38
+ *
39
+ * Instead every in-bucket pair is compared and near-duplicate pairs are unioned
40
+ * into connected components (union-find). Each component elects ONE winner that
41
+ * absorbs every other member. Because the winner's surviving lesson is always
42
+ * one of its own component's member lessons, and no member of one component is
43
+ * a near-duplicate of any member of another (that is exactly what the
44
+ * transitive closure guarantees), the surviving winners are pairwise non-
45
+ * duplicate. The next sweep finds nothing.
46
+ *
47
+ * The only ways a follow-up sweep does more work are the explicit budgets
48
+ * (`max_comparisons`, `max_merges_per_sweep`) — that is bounded progress, not
49
+ * non-convergence.
50
+ *
51
+ * ## Configuration
52
+ *
53
+ * The sweep reads its OWN config via `loadPluginConfigWithMeta(directory)`.
54
+ * `runCuratorPhase`'s 5th parameter is `knowledgeConfig: { directory?: string }`
55
+ * and cannot carry thresholds, and widening that signature would ripple through
56
+ * all three curator entry points. The dynamic `import('../config/index.js')`
57
+ * matches the established hook pattern (`knowledge-curator.ts`,
58
+ * `knowledge-escalator.ts`, `phase-monitor.ts`) and keeps `hooks → config` off
59
+ * the static import graph.
60
+ */
61
+ import type { KnowledgeEntryBase } from './knowledge-types.js';
62
+ export interface DedupSweepOptions {
63
+ /**
64
+ * Knowledge-store directory override — the curator's
65
+ * `knowledgeConfig.directory`. Defaults to `directory`. Audit surfaces
66
+ * (tombstone events, rewrite history) always use `directory`, matching the
67
+ * curator's own archive-invalidation call.
68
+ */
69
+ knowledgeDirectory?: string;
70
+ }
71
+ /** One applied merge: `loserId` was absorbed into `winnerId` and archived. */
72
+ export interface DedupSweepMerge {
73
+ winnerId: string;
74
+ loserId: string;
75
+ category: string;
76
+ }
77
+ export interface DedupSweepResult {
78
+ /** False when `learning.dedup_sweep.enabled` is off (nothing was read). */
79
+ enabled: boolean;
80
+ /** Number of ACTIVE entries considered. */
81
+ scanned: number;
82
+ /** Pairwise near-duplicate comparisons actually performed. */
83
+ comparisons: number;
84
+ /** True when `max_comparisons` cut the scan short. */
85
+ comparisonBudgetExhausted: boolean;
86
+ /** True when `max_merges_per_sweep` cut the merge set short. */
87
+ mergeBudgetExhausted: boolean;
88
+ /** Merges that actually committed. */
89
+ merges: DedupSweepMerge[];
90
+ }
91
+ declare function isActive(entry: KnowledgeEntryBase): boolean;
92
+ /**
93
+ * Evidence mass used as the third winner-selection tiebreak. Counts the
94
+ * outcome signals that represent a real interaction with the entry plus the
95
+ * confirmation records.
96
+ */
97
+ declare function evidenceWeight(entry: KnowledgeEntryBase): number;
98
+ /**
99
+ * Winner-selection order (negative result ⇒ `a` wins):
100
+ * 1. actionable beats non-actionable — an actionable directive is enforceable
101
+ * and a non-actionable near-duplicate is not; losing the actionable one
102
+ * would silently downgrade the store even though `mergeEntryFields` now
103
+ * carries the predicates across.
104
+ * 2. higher confidence
105
+ * 3. more evidence
106
+ * 4. older `created_at` (the original, not the restatement)
107
+ * 5. lexicographic `id` — a total order, so concurrent sweeps converge.
108
+ */
109
+ declare function compareCandidates(a: KnowledgeEntryBase, b: KnowledgeEntryBase): number;
110
+ interface PlannedMerge {
111
+ winner: KnowledgeEntryBase;
112
+ loser: KnowledgeEntryBase;
113
+ category: string;
114
+ }
115
+ /**
116
+ * Build the merge plan from a read-only snapshot. Pure (no I/O), so it is
117
+ * cheap to reason about and the transaction below only has to re-validate.
118
+ */
119
+ declare function planMerges(active: KnowledgeEntryBase[], threshold: number, maxComparisons: number, maxMerges: number): {
120
+ plan: PlannedMerge[];
121
+ comparisons: number;
122
+ comparisonBudgetExhausted: boolean;
123
+ mergeBudgetExhausted: boolean;
124
+ };
125
+ /**
126
+ * Merge active near-duplicate knowledge entries in `directory`'s swarm store.
127
+ *
128
+ * Never throws: every failure path is logged and returns a result describing
129
+ * what did happen. The caller (`runCuratorPhase`) must never be blocked by a
130
+ * background learning loop.
131
+ */
132
+ export declare function sweepActiveNearDuplicates(directory: string, options?: DedupSweepOptions): Promise<DedupSweepResult>;
133
+ /**
134
+ * Tier-0 pure-function test seam (see `.opencode/skills/writing-tests`). These
135
+ * are deterministic and dependency-free at the I/O boundary, so their tests need
136
+ * no mocks at all — planning, winner selection, and evidence weighting are
137
+ * exercised directly instead of through a temp-directory round trip.
138
+ */
139
+ export declare const _test_exports: {
140
+ compareCandidates: typeof compareCandidates;
141
+ evidenceWeight: typeof evidenceWeight;
142
+ isActive: typeof isActive;
143
+ planMerges: typeof planMerges;
144
+ };
145
+ export {};
@@ -63,6 +63,10 @@ export interface InjectForDelegateResult {
63
63
  entries: RankedEntry[];
64
64
  trace_id: string;
65
65
  }
66
+ /** Invalidate memoized injections. Called by the admission path after a write. */
67
+ export declare function bumpKnowledgeGeneration(): number;
68
+ /** Current corpus generation. Part of the architect injection cache key. */
69
+ export declare function getKnowledgeGeneration(): number;
66
70
  /**
67
71
  * Retrieve the subset of active knowledge directives scoped to a delegated
68
72
  * subagent's role + expected tools (Change 1, Task 1.2). Emits a single
@@ -42,6 +42,32 @@ export declare function appendRewriteHistory(directory: string, record: RewriteH
42
42
  * Read rewrite/merge history records (issue #1848 §3). Used for audit/recovery.
43
43
  */
44
44
  export declare function readRewriteHistory(directory: string): Promise<RewriteHistoryRecord[]>;
45
+ /**
46
+ * Normalize a loosely-typed knowledge array field: keep only strings,
47
+ * optionally truncate each item, deduplicate case-insensitively, then cap.
48
+ *
49
+ * The steps run in EXACTLY this order and the order is observable:
50
+ *
51
+ * 1. non-array input → `[]`
52
+ * 2. drop non-string items
53
+ * 3. truncate each item to `itemMaxChars` (when provided)
54
+ * 4. deduplicate on a case-insensitive key, PRESERVING the first
55
+ * occurrence's original casing
56
+ * 5. cap to `opts.cap`, keeping the first N survivors
57
+ *
58
+ * Truncation happens BEFORE deduplication, so two items that differ only past
59
+ * `itemMaxChars` collapse into one. Deduplication happens BEFORE the cap, so a
60
+ * run of duplicates can no longer evict distinct values off the end — that
61
+ * eviction was the defect this helper eradicates (a bare positional cap of 20
62
+ * with no dedup, repeated at six call sites — issue #1821 Lane 0b).
63
+ *
64
+ * `values` is typed `unknown` because every call site receives untrusted or
65
+ * loosely-typed input (LLM output, tool arguments, on-disk records).
66
+ */
67
+ export declare function dedupeCapped(values: unknown, opts: {
68
+ cap: number;
69
+ itemMaxChars?: number;
70
+ }): string[];
45
71
  export declare function appendKnowledge<T>(filePath: string, entry: T): Promise<void>;
46
72
  export declare function rewriteKnowledge<T>(filePath: string, entries: T[]): Promise<void>;
47
73
  export declare function transactFile<T>(filePath: string, read: (filePath: string) => Promise<T>, write: (filePath: string, data: T) => Promise<void>, mutate: (data: T) => T | null): Promise<boolean>;
@@ -490,6 +490,12 @@ export interface KnowledgeConfig {
490
490
  * `validated_terminal_applications` gate. Default 0 (conservative; see
491
491
  * `promotion_min_terminal_applications`). */
492
492
  promotion_min_distinct_cohorts: number;
493
+ /** #1821: require a promotion candidate to carry an actionable directive
494
+ * before it may be promoted. Default true (see `KnowledgeConfigSchema`).
495
+ * Declared OPTIONAL here on purpose: this hand-written interface is what the
496
+ * hooks layer imports, and hundreds of full config literals in tests would
497
+ * otherwise need updating. The Zod inference keeps it required. */
498
+ promotion_require_actionable?: boolean;
493
499
  /** Change 5: retrieval-upgrade tuning (MMR / cold-start / synonyms). */
494
500
  retrieval?: {
495
501
  mmr_lambda?: number;
@@ -79,23 +79,19 @@ export declare function validateSkillCandidatePath(p: unknown): boolean;
79
79
  /** Validate the optional ActionableDirectiveFields block on a knowledge entry. */
80
80
  export declare function validateActionableFields(fields: ActionableDirectiveFields | undefined): ActionableValidationResult;
81
81
  export type { ActionableDirectiveFields, DirectivePriority };
82
- export interface ActionabilityResult {
83
- actionable: boolean;
84
- /** Present only when not actionable. */
85
- reason?: 'missing_predicate' | 'missing_scope' | 'missing_predicate_and_scope';
86
- }
87
82
  /**
88
- * Layer 5: an entry is actionable only when it carries at least one
89
- * machine-checkable predicate AND at least one scope tag.
90
- *
91
- * predicate := forbidden_actions | required_actions | verification_checks
92
- * | verification_predicate
93
- * scope := applies_to_tools | applies_to_agents
83
+ * The predicate itself now lives in the leaf module
84
+ * `./actionability-predicate.js` (issue #1821 Workstream A3). It is re-exported
85
+ * here so every historical consumer of `knowledge-validator` is unaffected.
94
86
  *
95
- * Plain-prose lessons (no predicate, no scope) are NOT actionable and must be
96
- * quarantined rather than activated.
87
+ * Why it moved: this module imports `node:fs/promises` and `proper-lockfile` at
88
+ * the top level, while `hive-policy.ts` documents that it performs NO I/O
89
+ * (invariant 8). The promotion `actionability_floor` gate needs the predicate,
90
+ * so the predicate moved to a leaf with no I/O imports instead of the purity
91
+ * comment being weakened.
97
92
  */
98
- export declare function validateActionability(entry: Pick<KnowledgeEntryBase, 'forbidden_actions' | 'required_actions' | 'verification_checks' | 'verification_predicate' | 'applies_to_tools' | 'applies_to_agents'>): ActionabilityResult;
93
+ export type { ActionabilityInput, ActionabilityResult, } from './actionability-predicate.js';
94
+ export { validateActionability } from './actionability-predicate.js';
99
95
  /** Returns the knowledge-unactionable.jsonl path for the given directory (link-aware). */
100
96
  export declare function resolveUnactionablePath(directory: string): string;
101
97
  /** One quarantined-unactionable record. */
@@ -24,11 +24,18 @@ export type MicroOutcome = 'success' | 'failure_test' | 'failure_lint' | 'failur
24
24
  export declare const MICRO_PROMPT_INPUT_CAP = 1800;
25
25
  /** One v3-schema candidate insight written to the queue. */
26
26
  export interface InsightCandidate extends ActionableDirectiveFields {
27
+ /**
28
+ * Deterministic candidate identity (`ic_<16hex>`), issue #1821 Workstream B
29
+ * D1. OPTIONAL on purpose: legacy id-less lines already on disk recompute
30
+ * the identical value through `resolveInsightCandidateId`, so no migration
31
+ * of `.swarm/insight-candidates.jsonl` is required.
32
+ */
33
+ id?: string;
27
34
  lesson: string;
28
35
  category: string;
29
36
  tags: string[];
30
37
  source: {
31
- kind: 'micro_reflection';
38
+ kind: 'micro_reflection' | 'prm_pattern';
32
39
  task_id?: string;
33
40
  agent: string;
34
41
  outcome: MicroOutcome;
@@ -36,6 +43,75 @@ export interface InsightCandidate extends ActionableDirectiveFields {
36
43
  };
37
44
  created_at: string;
38
45
  }
46
+ /**
47
+ * Compute the deterministic identity of an insight candidate.
48
+ *
49
+ * Hashes exactly `{lesson, taskId, createdAt}`. `canonicalJson` DROPS
50
+ * `undefined` keys, so a candidate with no `source.task_id` hashes
51
+ * consistently whether the key is absent or explicitly undefined — which is
52
+ * what makes the value reproducible from a legacy durable line that never
53
+ * carried an `id`.
54
+ */
55
+ export declare function computeInsightCandidateId(cand: {
56
+ lesson: string;
57
+ source?: {
58
+ task_id?: string;
59
+ };
60
+ created_at: string;
61
+ }): string;
62
+ /**
63
+ * Resolve a candidate's identity by ALWAYS deriving it from content.
64
+ *
65
+ * The stamped `id` is deliberately NOT trusted. `.swarm/insight-candidates.jsonl`
66
+ * is read back through a bare `JSON.parse(...) as InsightCandidate`, and the
67
+ * curator explicitly treats that file as tamper-suspect (it re-applies both
68
+ * write-time gates on read). A trusted `id` would be an unvalidated field on
69
+ * that same untrusted line: setting it to an already-admitted candidate's id
70
+ * would make the fold-in silently DROP a distinct new lesson, and rotating it
71
+ * every run would let the same lesson be re-confirmed every phase, inflating
72
+ * confidence toward hive auto-promotion.
73
+ *
74
+ * Recomputation is a single sha256 over a three-field object, bounded by the
75
+ * meso batch limit — far cheaper than the failure it prevents. The stamped `id`
76
+ * survives as a diagnostic and round-trip aid only.
77
+ */
78
+ export declare function resolveInsightCandidateId(cand: InsightCandidate): string;
79
+ /**
80
+ * The marker recorded in an admitted entry's EXISTING `source_knowledge_ids`
81
+ * field so the phase-boundary fold-in can recognise a candidate real-time
82
+ * admission already handled. 27 chars, matching the 64-char per-id ceiling
83
+ * `validateActionableFields` applies wherever that field IS validated (the
84
+ * admission/fold-in paths pass an explicit field subset that omits it, so the
85
+ * length is a construction guarantee here rather than a checked one).
86
+ */
87
+ export declare function insightAdmissionMarker(candidateId: string): string;
88
+ /**
89
+ * Cap on `source_knowledge_ids` for an entry stamped by real-time admission.
90
+ *
91
+ * The knowledge store deliberately EXCLUDES `source_knowledge_ids` from its
92
+ * 20-item write normalization (see `WRITE_NORMALIZED_ARRAY_FIELDS` in
93
+ * `knowledge-store.ts`) because `skill-invalidator.ts` walks the full list.
94
+ * That exclusion means nothing else bounds the array, so an entry reinforced by
95
+ * many distinct candidates would grow it forever (AGENTS.md invariant 8). 50
96
+ * matches the cap the curator's own producer already applies.
97
+ */
98
+ export declare const MAX_SOURCE_KNOWLEDGE_IDS = 50;
99
+ /**
100
+ * Union an insight marker into an entry's `source_knowledge_ids`, bounded.
101
+ *
102
+ * Eviction drops the OLDEST `insight:` markers only. Non-insight ids (`task:`
103
+ * ids and real knowledge ids) are NEVER dropped: `skill-invalidator.ts` walks
104
+ * them to retire skills whose source entry was archived, so silently losing one
105
+ * would leave a stale skill live. If the array is over cap purely because of
106
+ * non-insight ids it is left over cap — correctness beats the bound here, and
107
+ * that case is already bounded by the producers of those ids.
108
+ *
109
+ * Losing a very old insight marker can at worst allow ONE extra confirmation
110
+ * for a long-superseded candidate; unbounded growth has no such ceiling.
111
+ */
112
+ export declare function unionInsightMarker(existing: string[] | undefined, marker: string): string[];
113
+ /** Extract the insight marker carried by an entry, if any. */
114
+ export declare function findInsightAdmissionMarker(sourceKnowledgeIds: string[] | undefined): string | undefined;
39
115
  /** Returns `.swarm/insight-candidates.jsonl` for a project directory. */
40
116
  export declare function resolveInsightCandidatesPath(directory: string): string;
41
117
  /**
@@ -47,6 +123,15 @@ export declare function classifyOutcome(transcript: string, trajectory: Trajecto
47
123
  /** Read a task's trajectory slice. Fail-open: [] when absent/corrupt. */
48
124
  export declare function readTaskTrajectory(directory: string, taskId: string): Promise<TrajectoryEntry[]>;
49
125
  export declare const INSIGHT_CANDIDATES_MAX_ENTRIES = 500;
126
+ /** Append validated candidates to the insight queue (best-effort, fail-open).
127
+ * Uses transactFile for consistency with consumeInsightCandidates and enforces
128
+ * a FIFO cap to prevent unbounded growth between phase completions.
129
+ *
130
+ * EXPORTED (#1821) so the PRM pattern producer can write the SAME durable
131
+ * crash backstop the micro-reflector writes. Any producer that only enqueues
132
+ * in memory loses its candidate on process death, on queue overflow, and on a
133
+ * drain failure — with no phase-boundary fold-in to recover it. */
134
+ export declare function appendInsightCandidates(directory: string, candidates: InsightCandidate[]): Promise<void>;
50
135
  /** Build the bounded micro-reflection prompt (≤ MICRO_PROMPT_INPUT_CAP chars). */
51
136
  export declare function buildMicroPrompt(params: {
52
137
  agent: string;
@@ -75,6 +160,13 @@ export declare function runMicroReflection(params: {
75
160
  trajectory: TrajectoryEntry[];
76
161
  llmDelegate?: CuratorLLMDelegate;
77
162
  quota?: EnrichmentQuotaOptions;
163
+ /** Session that produced the reflection — required for real-time admission. */
164
+ sessionID?: string;
165
+ /** `learning.realtime_admission` knobs the enqueue side needs. */
166
+ admission?: {
167
+ enabled?: boolean;
168
+ maxQueueSize?: number;
169
+ };
78
170
  }): Promise<MicroReflectionResult>;
79
171
  export interface MicroReflectorInput {
80
172
  tool: unknown;
@@ -87,10 +179,22 @@ export interface MicroReflectorInput {
87
179
  export interface MicroReflectorOutput {
88
180
  output?: unknown;
89
181
  }
182
+ /**
183
+ * True for the delegation tool under either casing.
184
+ *
185
+ * EXPORTED (issue #1821 Workstream B) so the real-time admission drain adapter
186
+ * can self-gate on exactly the same predicate this hook uses. Six private
187
+ * copies of this two-line check already exist across the hook layer; the drain
188
+ * reuses this one rather than adding a seventh.
189
+ */
190
+ export declare function isTaskTool(tool: unknown): boolean;
90
191
  /**
91
192
  * `tool.execute.after` adapter for the `Task` tool. Resolves the delegate, the
92
193
  * transcript, the task id, and the trajectory slice, then runs micro-reflection.
93
194
  * The LLM delegate is provided by the caller (so tests can inject one); when
94
195
  * absent, classification still runs but no LLM call is made.
95
196
  */
96
- export declare function microReflectorAfter(directory: string, input: MicroReflectorInput, output: MicroReflectorOutput, llmDelegate?: CuratorLLMDelegate, quota?: EnrichmentQuotaOptions): Promise<void>;
197
+ export declare function microReflectorAfter(directory: string, input: MicroReflectorInput, output: MicroReflectorOutput, llmDelegate?: CuratorLLMDelegate, quota?: EnrichmentQuotaOptions, admission?: {
198
+ enabled?: boolean;
199
+ maxQueueSize?: number;
200
+ }): Promise<void>;