opencode-swarm 7.116.1 → 7.118.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 (50) hide show
  1. package/dist/cli/{config-doctor-htzxe394.js → config-doctor-p73yfdz0.js} +2 -2
  2. package/dist/cli/{curator-yxmtp8py.js → curator-920swpnk.js} +10 -10
  3. package/dist/cli/{curator-llm-factory-9qn52fnv.js → curator-llm-factory-kqt1f4sw.js} +10 -10
  4. package/dist/cli/{guardrail-explain-xvyz2eag.js → guardrail-explain-bycz51b0.js} +11 -11
  5. package/dist/cli/{guardrail-log-dzbqcgz9.js → guardrail-log-qjqgg7pz.js} +3 -3
  6. package/dist/cli/{hive-promoter-ykqc60w1.js → hive-promoter-w9pgfs6w.js} +15 -10
  7. package/dist/cli/{index-m7hc7nn7.js → index-11q1t6g0.js} +1 -1
  8. package/dist/cli/{index-7kcpkm4y.js → index-34tsqvqb.js} +2527 -1354
  9. package/dist/cli/{index-efdyjgeh.js → index-470tykwx.js} +2 -2
  10. package/dist/cli/{index-g4p13g8t.js → index-5hze8ztj.js} +12 -22
  11. package/dist/cli/{index-m43zgtjn.js → index-bcg89pt8.js} +1 -1
  12. package/dist/cli/{index-8j5d3ytd.js → index-g2wrs7jy.js} +1 -1
  13. package/dist/cli/{index-wegc6xwr.js → index-jbdgpqpj.js} +3 -3
  14. package/dist/cli/{index-p9jf8gt8.js → index-k83hebh1.js} +12 -12
  15. package/dist/cli/{index-nvc0nsvg.js → index-pdk1n8yh.js} +2 -0
  16. package/dist/cli/{index-t5f01hnz.js → index-pjhfe7c3.js} +2 -2
  17. package/dist/cli/{index-5h7d37nf.js → index-qx5z58a7.js} +19 -31
  18. package/dist/cli/{index-wxkf3089.js → index-sb9x6xjj.js} +3 -3
  19. package/dist/cli/{index-34g1mv3c.js → index-wpgv6hht.js} +52 -6
  20. package/dist/cli/index.js +10 -10
  21. package/dist/cli/{knowledge-escalator-tthw6c9q.js → knowledge-escalator-gt1q632m.js} +4 -4
  22. package/dist/cli/{knowledge-events-hrbmwxxj.js → knowledge-events-0bjdtdsb.js} +2 -2
  23. package/dist/cli/{knowledge-store-5cns5sns.js → knowledge-store-fm2300xf.js} +7 -2
  24. package/dist/cli/{knowledge-validator-xre0s774.js → knowledge-validator-91kx3eqv.js} +3 -3
  25. package/dist/cli/{schema-bqn7g3ez.js → schema-7c484wjz.js} +1 -1
  26. package/dist/cli/{skill-generator-22nk6c6a.js → skill-generator-t47xyfps.js} +5 -5
  27. package/dist/commands/link.d.ts +7 -4
  28. package/dist/commands/promote.d.ts +3 -0
  29. package/dist/commands/registry.d.ts +2 -2
  30. package/dist/commands/unlink.d.ts +12 -5
  31. package/dist/config/schema.d.ts +8 -4
  32. package/dist/hooks/curator-postmortem.d.ts +1 -1
  33. package/dist/hooks/hive-policy.d.ts +81 -0
  34. package/dist/hooks/hive-promoter.d.ts +147 -28
  35. package/dist/hooks/hive-transaction.d.ts +113 -0
  36. package/dist/hooks/knowledge-events.d.ts +4 -1
  37. package/dist/hooks/knowledge-link.d.ts +31 -4
  38. package/dist/hooks/knowledge-store.d.ts +6 -5
  39. package/dist/hooks/knowledge-types.d.ts +87 -0
  40. package/dist/index.js +343 -325
  41. package/dist/knowledge/cohort-identity.d.ts +103 -0
  42. package/dist/knowledge/config-fingerprint.d.ts +57 -0
  43. package/dist/knowledge/family-manifest.d.ts +50 -0
  44. package/dist/knowledge/family-migration.d.ts +109 -0
  45. package/dist/knowledge/hive-paths.d.ts +51 -0
  46. package/dist/knowledge/identity.d.ts +12 -2
  47. package/dist/knowledge/index.d.ts +1 -0
  48. package/dist/services/knowledge-diagnostics.d.ts +45 -0
  49. package/dist/services/status-service.d.ts +13 -0
  50. package/package.json +1 -1
@@ -2,12 +2,19 @@
2
2
  * Handles the `/swarm unlink` command.
3
3
  *
4
4
  * Stops sharing this worktree's swarm knowledge with its link store and returns
5
- * it to a local `.swarm/knowledge.jsonl`. By default the shared lessons are
6
- * copied back into the local store (deduplicated) so the worktree keeps the
7
- * pooled knowledge it had access to; pass `--no-copy` to skip the copy-back.
5
+ * it to a local `.swarm/knowledge.jsonl`. By default the shared knowledge
6
+ * *family* (store, events, rejected, retractions, counters, quarantine,
7
+ * unactionable, application-legacy) is copied back into the local store per the
8
+ * family manifest so the worktree keeps the pooled knowledge it had access to;
9
+ * pass `--no-copy` to skip the copy-back.
10
+ *
11
+ * The copy-back is transactional (issue #1846): it reads the shared family
12
+ * under the shared-store lock and writes the local family atomically, so a
13
+ * concurrent append in a peer worktree cannot be lost. The shared cohort is
14
+ * never deleted or truncated.
8
15
  *
9
16
  * Usage:
10
- * - /swarm unlink — unlink and copy shared lessons back to local.
11
- * - /swarm unlink --no-copy — unlink without copying shared lessons back.
17
+ * - /swarm unlink — unlink and copy the shared family back to local.
18
+ * - /swarm unlink --no-copy — unlink without copying the shared family back.
12
19
  */
13
20
  export declare function handleUnlinkCommand(directory: string, args: string[]): Promise<string>;
@@ -420,9 +420,9 @@ export declare const LintConfigSchema: z.ZodObject<{
420
420
  fix: "fix";
421
421
  }>>;
422
422
  linter: z.ZodDefault<z.ZodEnum<{
423
+ auto: "auto";
423
424
  biome: "biome";
424
425
  eslint: "eslint";
425
- auto: "auto";
426
426
  }>>;
427
427
  patterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
428
428
  exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -630,6 +630,8 @@ export declare const KnowledgeConfigSchema: z.ZodObject<{
630
630
  contradiction_quarantine_window_days: z.ZodDefault<z.ZodNumber>;
631
631
  promoted_demotion_min_negative_phases: z.ZodDefault<z.ZodNumber>;
632
632
  promoted_demotion_signal_threshold: z.ZodDefault<z.ZodNumber>;
633
+ promotion_min_terminal_applications: z.ZodDefault<z.ZodNumber>;
634
+ promotion_min_distinct_cohorts: z.ZodDefault<z.ZodNumber>;
633
635
  realtime_learning_nudge: z.ZodDefault<z.ZodObject<{
634
636
  enabled: z.ZodDefault<z.ZodBoolean>;
635
637
  first_after_tool_calls: z.ZodDefault<z.ZodNumber>;
@@ -1068,8 +1070,8 @@ export declare const ParallelizationConfigSchema: z.ZodObject<{
1068
1070
  export type ParallelizationConfig = z.infer<typeof ParallelizationConfigSchema>;
1069
1071
  export declare const WorktreeIsolationConfigSchema: z.ZodObject<{
1070
1072
  policy: z.ZodDefault<z.ZodEnum<{
1071
- disabled: "disabled";
1072
1073
  auto: "auto";
1074
+ disabled: "disabled";
1073
1075
  required: "required";
1074
1076
  }>>;
1075
1077
  merge_strategy: z.ZodDefault<z.ZodEnum<{
@@ -1852,9 +1854,9 @@ export declare const PluginConfigSchema: z.ZodObject<{
1852
1854
  fix: "fix";
1853
1855
  }>>;
1854
1856
  linter: z.ZodDefault<z.ZodEnum<{
1857
+ auto: "auto";
1855
1858
  biome: "biome";
1856
1859
  eslint: "eslint";
1857
- auto: "auto";
1858
1860
  }>>;
1859
1861
  patterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
1860
1862
  exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -1940,6 +1942,8 @@ export declare const PluginConfigSchema: z.ZodObject<{
1940
1942
  contradiction_quarantine_window_days: z.ZodDefault<z.ZodNumber>;
1941
1943
  promoted_demotion_min_negative_phases: z.ZodDefault<z.ZodNumber>;
1942
1944
  promoted_demotion_signal_threshold: z.ZodDefault<z.ZodNumber>;
1945
+ promotion_min_terminal_applications: z.ZodDefault<z.ZodNumber>;
1946
+ promotion_min_distinct_cohorts: z.ZodDefault<z.ZodNumber>;
1943
1947
  realtime_learning_nudge: z.ZodDefault<z.ZodObject<{
1944
1948
  enabled: z.ZodDefault<z.ZodBoolean>;
1945
1949
  first_after_tool_calls: z.ZodDefault<z.ZodNumber>;
@@ -2255,8 +2259,8 @@ export declare const PluginConfigSchema: z.ZodObject<{
2255
2259
  }, z.core.$strip>>;
2256
2260
  worktree: z.ZodOptional<z.ZodObject<{
2257
2261
  policy: z.ZodDefault<z.ZodEnum<{
2258
- disabled: "disabled";
2259
2262
  auto: "auto";
2263
+ disabled: "disabled";
2260
2264
  required: "required";
2261
2265
  }>>;
2262
2266
  merge_strategy: z.ZodDefault<z.ZodEnum<{
@@ -136,7 +136,7 @@ export declare const _internals: {
136
136
  applied: number;
137
137
  skipped: number;
138
138
  }>;
139
- checkHivePromotions: (entries: SwarmKnowledgeEntry[], knowledgeConfig: KnowledgeConfig) => Promise<import("./hive-promoter.js").HivePromotionSummary>;
139
+ checkHivePromotions: (entries: SwarmKnowledgeEntry[], knowledgeConfig: KnowledgeConfig, directory: string) => Promise<import("./hive-promoter.js").HivePromotionSummary>;
140
140
  applyProposalTriage: (directory: string, triage: ParsedPostMortemActions["queueTriage"]) => Promise<{
141
141
  approved: string[];
142
142
  rejected: string[];
@@ -0,0 +1,81 @@
1
+ /**
2
+ * One promotion policy evaluator used by automatic promotion AND the manual
3
+ * `/swarm promote` command (issue #1847 §4).
4
+ *
5
+ * Previously, automatic promotion went through `isHiveEligible` (3 routes:
6
+ * hive_eligible+3 phases / hive-fast-track tag / age) while manual promotion
7
+ * (`promoteToHive` / `promoteFromSwarm`) bypassed policy entirely — an exact
8
+ * entry id was effectively authorization to skip the gate, with no durable
9
+ * override record. This module is the single policy function both paths share.
10
+ *
11
+ * Manual promotion that fails the policy MUST either be blocked, or proceed
12
+ * only with an explicit `--force` override that records a durable, audited
13
+ * override (actor, reason, source revision, failed gates). An exact entry id
14
+ * alone is NEVER authorization to bypass policy.
15
+ *
16
+ * Conservative application evidence (#1847 §2, AC5/AC6): the
17
+ * `validated_terminal_applications` gate counts ONLY validated terminal receipts
18
+ * tied to a real retrieval trace + result membership. Legacy records carry no
19
+ * evidence and receive NO synthetic credit. Until #1849 produces real receipts,
20
+ * the configured thresholds default to 0, so the gate is satisfied by absence
21
+ * (it neither credits nor blocks) and current behavior is preserved. Operators
22
+ * raise the thresholds to activate application-evidence gating.
23
+ *
24
+ * This module performs NO I/O and holds NO module-level mutable state
25
+ * (invariant 8). It is NOT imported on the plugin-init path (invariant 1).
26
+ */
27
+ import type { KnowledgeConfig, PromotionEvidenceRecord, SwarmKnowledgeEntry } from './knowledge-types.js';
28
+ /** A single named policy gate and whether it passed. */
29
+ export interface PromotionPolicyGate {
30
+ name: string;
31
+ passed: boolean;
32
+ detail: string;
33
+ }
34
+ export interface PromotionPolicyInput {
35
+ entry: SwarmKnowledgeEntry;
36
+ config: KnowledgeConfig;
37
+ /**
38
+ * Validated terminal-application evidence for this entry's lesson
39
+ * (near-duplicate-clustered). Empty for legacy/no-evidence records. The
40
+ * promoter loads this OUTSIDE the hive transaction and passes it in.
41
+ */
42
+ evidence: PromotionEvidenceRecord[];
43
+ }
44
+ export interface PromotionPolicyDecision {
45
+ eligible: boolean;
46
+ /** Every gate and whether it passed — surfaced in diagnostics + override audit. */
47
+ gates: PromotionPolicyGate[];
48
+ /** Human-readable summary of the deciding gate. */
49
+ reason: string;
50
+ }
51
+ /**
52
+ * Evaluate the one promotion policy. Used by auto promotion and manual promote.
53
+ * Returns the full gates list so diagnostics can explain each failed condition
54
+ * (AC: "Promotion diagnostics explain eligibility failures and lineage").
55
+ */
56
+ export declare function evaluatePromotionPolicy(input: PromotionPolicyInput): PromotionPolicyDecision;
57
+ /**
58
+ * The three historical eligibility routes, preserved verbatim from the
59
+ * pre-#1847 `isHiveEligible`. Exposed so `isHiveEligible` can delegate to it
60
+ * (M1 fix — keeps the public export + its test consumers working) and so the
61
+ * policy gate above can report which route satisfied it.
62
+ */
63
+ export declare function describeEligibilityRoute(entry: SwarmKnowledgeEntry, autoPromoteDays: number): {
64
+ passed: boolean;
65
+ detail: string;
66
+ };
67
+ /** Names of override-audit fields the promoter fills when a manual --force fires. */
68
+ export interface OverrideAuditDetail {
69
+ actor: 'manual-override';
70
+ reason: string;
71
+ failedGates: PromotionPolicyGate[];
72
+ entryId: string;
73
+ lesson: string;
74
+ }
75
+ /**
76
+ * Build the failed-gates summary for a manual override, given a decision that
77
+ * was NOT eligible. Used by the promoter to populate `lineage.override_failed_gates`.
78
+ */
79
+ export declare function failedGateNames(decision: PromotionPolicyDecision): string[];
80
+ /** Count distinct canonical cohorts among promotion evidence (for diagnostics). */
81
+ export declare function countDistinctEvidenceCohorts(evidence: PromotionEvidenceRecord[]): number;
@@ -1,59 +1,178 @@
1
- /** Hive promoter hook for opencode-swarm v6.17 two-tier knowledge system. */
1
+ /**
2
+ * Hive promoter for opencode-swarm v6.17 two-tier knowledge system.
3
+ *
4
+ * #1847 (transactional hive promotion): every hive write is routed through ONE
5
+ * global cross-process transaction (`transactHiveStore` in `./hive-transaction`).
6
+ * The transaction holds the hive directory lock across read → normalize →
7
+ * eligibility → canonical-cohort counting → dedup/merge → append/update →
8
+ * source confirmation → cap → staged audit → atomic persist. No caller reads
9
+ * the hive, makes a promotion decision, and later calls a separate unlocked
10
+ * write API.
11
+ *
12
+ * Canonical project identity (#1846): cross-project confirmations key on the
13
+ * canonical `cohort_id` from `resolveCohortId`, not the worktree `project_name`.
14
+ * Sibling worktrees and remote aliases of one repository count as one project.
15
+ *
16
+ * Lineage (#1847 §3): promoted entries retain source entry id, source cohort,
17
+ * promotion event id, and actor. Manual promotion that fails policy proceeds
18
+ * only with an explicit `--force` override that records a durable audit entry;
19
+ * an exact entry id alone is never authorization to bypass policy.
20
+ *
21
+ * This module is NOT imported on the plugin-init path (invariant 1): promotion
22
+ * runs only on the lazy `/swarm promote`, close, curate, and postmortem paths.
23
+ */
24
+ import { resolveCohortId } from '../knowledge/cohort-identity.js';
2
25
  import { appendCuratorRecommendation, readCuratorSummary } from './curator.js';
3
- import type { KnowledgeConfig, SwarmKnowledgeEntry } from './knowledge-types.js';
4
- /** Hive promotion summary for curator state */
26
+ import { transactHiveStore } from './hive-transaction.js';
27
+ import type { KnowledgeConfig, ProjectConfirmationRecord, PromotionEvidenceRecord, SwarmKnowledgeEntry } from './knowledge-types.js';
28
+ import { validateLesson } from './knowledge-validator.js';
29
+ /** Hive promotion summary for curator state. */
5
30
  export interface HivePromotionSummary {
6
31
  timestamp: string;
7
32
  new_promotions: number;
8
33
  encounters_incremented: number;
9
34
  advancements: number;
10
35
  total_hive_entries: number;
36
+ /** #1847: per-entry policy diagnostics for operator visibility. */
37
+ diagnostics?: string[];
11
38
  }
12
39
  /**
13
- * Check whether a swarm knowledge entry is eligible for hive promotion.
14
- * Three routes to eligibility:
15
- * Route 1: hive_eligible flag + 3+ distinct phases
16
- * Route 2: 'hive-fast-track' tag
17
- * Route 3: age exceeds auto_promote_days threshold
18
- *
19
- * @param entry - The swarm knowledge entry to check
20
- * @param autoPromoteDays - Number of days before age-based promotion kicks in
21
- * @returns true if the entry is eligible for hive promotion
40
+ * Check whether a swarm entry is eligible for hive promotion via the historical
41
+ * 3 routes. Kept as a thin wrapper delegating to the canonical policy route
42
+ * description (M1 fix, #1847) so existing test consumers
43
+ * (`hive-promoter-inactive.test.ts`, `close.test.ts` mock) keep working.
44
+ *
45
+ * This is ONE of the gates inside {@link evaluatePromotionPolicy}; it is not
46
+ * the whole policy. New callers should use `evaluatePromotionPolicy`.
22
47
  */
23
48
  export declare function isHiveEligible(entry: SwarmKnowledgeEntry, autoPromoteDays: number): boolean;
24
49
  /**
25
- * Main promotion logic: checks swarm entries and promotes eligible ones to hive.
26
- * Also updates existing hive entries with new project confirmations.
27
- * Returns a summary of the promotion activity for curator state.
50
+ * Count distinct projects/cohort identities in a hive entry's confirmed_by.
28
51
  *
29
- * @note The 'hive-fast-track' tag is treated as privileged it bypasses the
30
- * 3-phase confirmation requirement. It should only be set by authorized tooling
31
- * (inferTags() never produces it automatically).
52
+ * #1847: dedup by canonical `cohort_id` (sibling worktrees + remote aliases of
53
+ * one repo count as one project). Legacy records written before #1847 lack
54
+ * `cohort_id`; for those, fall back to `project_name` — but two records sharing
55
+ * a `cohort_id` are NEVER counted as distinct. This means AC3 ("sibling
56
+ * worktrees count as one project") is enforced for confirmations written from
57
+ * this PR forward; legacy confirmations remain `project_name`-keyed and are NOT
58
+ * retroactively re-counted (consistent with the no-broad-rewrite non-goal).
32
59
  */
33
- export declare function checkHivePromotions(swarmEntries: SwarmKnowledgeEntry[], config: KnowledgeConfig): Promise<HivePromotionSummary>;
60
+ export declare function countDistinctProjects(confirmedBy: ProjectConfirmationRecord[]): number;
61
+ /**
62
+ * Main promotion logic: checks swarm entries and promotes eligible ones to hive
63
+ * inside ONE cross-process transaction. Also updates existing hive entries with
64
+ * new canonical-cohort confirmations. Returns a summary for curator state.
65
+ *
66
+ * `directory` is required (#1847) to resolve the canonical cohort identity
67
+ * (#1846) for cross-project distinctness.
68
+ *
69
+ * @note The 'hive-fast-track' tag is privileged — it bypasses the 3-phase
70
+ * confirmation requirement inside the eligibility_route gate. It should only
71
+ * be set by authorized tooling (inferTags() never produces it automatically).
72
+ */
73
+ export declare function checkHivePromotions(swarmEntries: SwarmKnowledgeEntry[], config: KnowledgeConfig, directory: string): Promise<HivePromotionSummary>;
34
74
  export declare const _internals: {
35
75
  readSwarmEntries: (directory: string) => Promise<SwarmKnowledgeEntry[]>;
36
76
  checkHivePromotions: typeof checkHivePromotions;
37
77
  readCuratorSummary: typeof readCuratorSummary;
38
78
  appendCuratorRecommendation: typeof appendCuratorRecommendation;
79
+ resolveCohortId: typeof resolveCohortId;
80
+ transactHiveStore: typeof transactHiveStore;
81
+ validateLesson: typeof validateLesson;
82
+ /** Loads validated terminal-application evidence per swarm entry id. Empty
83
+ * until #1849 produces real receipts (conservative: no synthetic credit). */
84
+ loadPromotionEvidence: (_swarmEntries: SwarmKnowledgeEntry[]) => Promise<Record<string, PromotionEvidenceRecord[]>>;
85
+ /** Loads the default KnowledgeConfig (schema defaults) for manual promotion
86
+ * paths when the command did not load one. */
87
+ loadDefaultKnowledgeConfig: () => {
88
+ enabled: boolean;
89
+ swarm_max_entries: number;
90
+ hive_max_entries: number;
91
+ auto_promote_days: number;
92
+ max_inject_count: number;
93
+ delegate_max_inject_count: number;
94
+ inject_char_budget: number;
95
+ max_lesson_display_chars: number;
96
+ dedup_threshold: number;
97
+ scope_filter: string[];
98
+ hive_enabled: boolean;
99
+ rejected_max_entries: number;
100
+ validation_enabled: boolean;
101
+ evergreen_confidence: number;
102
+ evergreen_utility: number;
103
+ low_utility_threshold: number;
104
+ min_retrievals_for_utility: number;
105
+ schema_version: number;
106
+ directive_min_confidence: number;
107
+ same_project_weight: number;
108
+ cross_project_weight: number;
109
+ min_encounter_score: number;
110
+ initial_encounter_score: number;
111
+ encounter_increment: number;
112
+ max_encounter_score: number;
113
+ default_max_phases: number;
114
+ todo_max_phases: number;
115
+ sweep_enabled: boolean;
116
+ confidence_floor_action: "none" | "demote" | "quarantine";
117
+ confidence_floor_min_outcomes: number;
118
+ confidence_floor_signal_threshold: number;
119
+ contradiction_threshold_action: "quarantine" | "tag_only";
120
+ contradiction_quarantine_threshold: number;
121
+ contradiction_quarantine_window_days: number;
122
+ promoted_demotion_min_negative_phases: number;
123
+ promoted_demotion_signal_threshold: number;
124
+ promotion_min_terminal_applications: number;
125
+ promotion_min_distinct_cohorts: number;
126
+ realtime_learning_nudge: {
127
+ enabled: boolean;
128
+ first_after_tool_calls: number;
129
+ repeat_after_tool_calls: number;
130
+ };
131
+ enrichment: {
132
+ max_calls_per_day: number;
133
+ quota_window: "utc" | "local";
134
+ batch_size?: number | undefined;
135
+ };
136
+ context_budget_threshold?: number | undefined;
137
+ retrieval?: {
138
+ mmr_lambda: number;
139
+ cold_start_bonus: number;
140
+ cold_start_max_age_phases: number;
141
+ synonym_min_cooccurrence: number;
142
+ synonym_map_max_pairs: number;
143
+ } | undefined;
144
+ };
39
145
  };
40
146
  /**
41
147
  * Create a hook that promotes swarm entries to the hive.
42
148
  * The hook fires unconditionally - the caller decides when to invoke it.
43
149
  */
44
150
  export declare function createHivePromoterHook(directory: string, config: KnowledgeConfig): (input: unknown, output: unknown) => Promise<void>;
151
+ /** Options for manual promotion (override semantics, #1847 §4). */
152
+ export interface ManualPromotionOptions {
153
+ force?: boolean;
154
+ reason?: string;
155
+ }
45
156
  /**
46
157
  * Promote a lesson directly to the hive (manual promotion).
47
- * @param directory - Project directory
48
- * @param lesson - The lesson text to promote
49
- * @param category - Optional category (defaults to 'process')
50
- * @returns Confirmation message
158
+ *
159
+ * Runs the one policy evaluator. On a policy FAIL:
160
+ * - without `force` returns a diagnostic string listing the failed gates
161
+ * (does NOT promote);
162
+ * - with `force` + `reason` → promotes and writes a durable override audit
163
+ * record (actor='manual-override', failed gates recorded) inside the
164
+ * transaction.
165
+ * On a policy PASS → promotes with actor='manual'.
166
+ *
167
+ * An exact entry id / direct text alone is NEVER authorization to bypass policy.
51
168
  */
52
- export declare function promoteToHive(directory: string, lesson: string, category?: string): Promise<string>;
169
+ export declare function promoteToHive(directory: string, lesson: string, category?: string, options?: ManualPromotionOptions, config?: KnowledgeConfig): Promise<string>;
53
170
  /**
54
171
  * Promote a lesson from swarm knowledge to hive.
55
- * @param directory - Project directory
56
- * @param lessonId - The ID of the lesson to promote from swarm
57
- * @returns Confirmation message
172
+ *
173
+ * Snapshots the swarm read OUTSIDE the hive transaction (the swarm store is
174
+ * read-only here — no two-phase locking needed). Then runs the one policy
175
+ * evaluator + override semantics inside the hive transaction.
58
176
  */
59
- export declare function promoteFromSwarm(directory: string, lessonId: string): Promise<string>;
177
+ export declare function promoteFromSwarm(directory: string, lessonId: string, options?: ManualPromotionOptions, config?: KnowledgeConfig): Promise<string>;
178
+ export { resolveHiveKnowledgePath } from './knowledge-store.js';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * One global, cross-process transaction primitive for hive storage (issue #1847 §1).
3
+ *
4
+ * Problem: hive promotion previously performed read → N× append → batch rewrite
5
+ * → cap enforcement as FOUR separate directory-lock acquisitions, with unlocked
6
+ * read/validate/dedup windows between them. Because the hive store
7
+ * (`shared-learnings.jsonl`) is a shared, cross-project, cross-process file,
8
+ * two opencode-swarm sessions could each read the same snapshot and one's
9
+ * rewrite silently dropped the other's entries (TOCTOU lost update — #1604).
10
+ *
11
+ * This module routes every hive writer through ONE critical section that spans,
12
+ * under a single directory lock:
13
+ * 1. read + mixed-schema normalize;
14
+ * 2. the caller's mutate (eligibility, canonical project counting, dedup,
15
+ * merge decision, append/update, source confirmation);
16
+ * 3. validate-before-commit;
17
+ * 4. priority-aware cap enforcement (in the same closure, not a separate call);
18
+ * 5. staged audit/reject appends (raw, under the held lock — never via
19
+ * `appendHiveKnowledgeEvent`, which would re-enter the directory lock and
20
+ * deadlock — see the curator precedent at `curator.ts:1966-1969`);
21
+ * 6. atomic persistence (temp + rename).
22
+ *
23
+ * Lock contract (AGENTS.md invariants 3, 8): `stale: 5000` to MATCH every other
24
+ * hive writer (`appendKnowledge`, `rewriteKnowledge`, `transactKnowledge`,
25
+ * `knowledge-application.ts`, `knowledge-escalator.ts`). proper-lockfile's stale
26
+ * mechanism is preemptive — a process that cannot acquire the lock and sees its
27
+ * mtime older than its OWN stale threshold force-breaks it. Using a longer stale
28
+ * here would let a concurrent 5s writer break this transaction mid-flight and
29
+ * tear the file, which is exactly the bug this PR fixes. The held closure is
30
+ * kept fast (in-memory mutate + one atomic write + raw appends) so 5s is never
31
+ * exceeded; all heavy work (git cohort resolution, evidence rollup, near-
32
+ * duplicate precompute) runs OUTSIDE the lock and is passed in via HiveTxContext.
33
+ *
34
+ * This module holds NO module-level mutable state (invariant 8) and is NOT
35
+ * imported on the plugin-init path (invariant 1).
36
+ */
37
+ import * as path from 'node:path';
38
+ import lockfile from 'proper-lockfile';
39
+ import { atomicWriteFile } from '../evidence/task-file.js';
40
+ import { resolveHiveDataDir, resolveHiveEventsPath, resolveHiveKnowledgePath, resolveHiveRejectedPath } from '../knowledge/hive-paths.js';
41
+ import { readKnowledge, selectKnowledgeCapSurvivors } from './knowledge-store.js';
42
+ import type { HiveKnowledgeEntry, RejectedLesson } from './knowledge-types.js';
43
+ /**
44
+ * Context handed to a hive mutation closure. All expensive pre-work (cohort
45
+ * resolution, evidence loading, near-duplicate indexing) is performed by the
46
+ * caller OUTSIDE the lock and injected here so the held closure stays fast.
47
+ */
48
+ export interface HiveTxContext {
49
+ /** Current hive entries (read + normalized under the lock). */
50
+ entries: HiveKnowledgeEntry[];
51
+ }
52
+ /** A pre-serialized knowledge-event line to append to the hive audit log. */
53
+ export interface HiveAuditEntry {
54
+ line: string;
55
+ }
56
+ /** The result a mutation closure returns. */
57
+ export type HiveMutationOutcome<T> = {
58
+ kind: 'commit';
59
+ entries: HiveKnowledgeEntry[];
60
+ /** Hive cap to enforce inside the same closure (omit for no cap). */
61
+ maxEntries?: number;
62
+ /** Rejected lessons to append to the hive rejected log under the lock. */
63
+ rejects?: RejectedLesson[];
64
+ /** Audit lines to append to the hive events log under the lock. */
65
+ audit?: HiveAuditEntry[];
66
+ /** Caller return value surfaced back through HiveTransactionResult. */
67
+ return: T;
68
+ } | {
69
+ kind: 'noop';
70
+ return: T;
71
+ };
72
+ export interface HiveTransactionResult<T> {
73
+ /** True iff the hive file was rewritten within the transaction. */
74
+ committed: boolean;
75
+ /**
76
+ * The caller's return value. Present when `committed` is true OR the mutate
77
+ * closure returned a `noop` outcome. Undefined on lock-acquire / mkdir /
78
+ * validation-before-commit failure (F-004/PRR-1): callers MUST check
79
+ * `committed` (or `return !== undefined`) before dereferencing it.
80
+ */
81
+ return: T | undefined;
82
+ /** Human-readable diagnostics (lock timeout, validation failure, etc.). */
83
+ diagnostics: string[];
84
+ }
85
+ /**
86
+ * Run `mutate` against the hive store inside one cross-process transaction.
87
+ *
88
+ * On lock-acquire failure or validation failure the prior hive file is left
89
+ * intact (the atomic write is not performed) and `committed` is false; the
90
+ * function never hangs. `mutate` receives the current (normalized) entries and
91
+ * returns either a `commit` (new entries + optional cap/rejects/audit) or a
92
+ * `noop`.
93
+ */
94
+ export declare function transactHiveStore<T>(mutate: (ctx: HiveTxContext) => Promise<HiveMutationOutcome<T>> | HiveMutationOutcome<T>): Promise<HiveTransactionResult<T>>;
95
+ /** Path to the hive events log (re-exported for callers building audit lines). */
96
+ export { resolveHiveEventsPath };
97
+ export declare const HIVE_TXN_LOCK_STALE_MS = 5000;
98
+ /**
99
+ * Test-only DI seam (AGENTS.md invariant 7). Tests inject a fake lockfile /
100
+ * readers / writers rather than `mock.module`-ing the consumers, which leaks
101
+ * across test files in Bun's shared runner.
102
+ */
103
+ export declare const _internals: {
104
+ lockfile: typeof lockfile;
105
+ readKnowledge: typeof readKnowledge;
106
+ atomicWriteFile: typeof atomicWriteFile;
107
+ selectKnowledgeCapSurvivors: typeof selectKnowledgeCapSurvivors;
108
+ resolveHiveDataDir: typeof resolveHiveDataDir;
109
+ resolveHiveKnowledgePath: typeof resolveHiveKnowledgePath;
110
+ resolveHiveRejectedPath: typeof resolveHiveRejectedPath;
111
+ resolveHiveEventsPath: typeof resolveHiveEventsPath;
112
+ path: typeof path;
113
+ };
@@ -19,6 +19,7 @@
19
19
  * `directory` argument injected by `createSwarmTool` / hook constructors — never
20
20
  * from the process working directory.
21
21
  */
22
+ import { resolveHiveEventsPath as resolveHiveEventsPathImpl } from '../knowledge/hive-paths.js';
22
23
  import type { ConfidenceFloorOptions } from './knowledge-store.js';
23
24
  import type { KnowledgeApplicationRecord, RetrievalOutcome } from './knowledge-types.js';
24
25
  /** Current event-log record schema version. Bump when the on-disk shape changes. */
@@ -182,7 +183,7 @@ export declare const RECEIPT_EVENT_TYPES: ReadonlySet<string>;
182
183
  export declare function resolveKnowledgeEventsPath(directory: string): string;
183
184
  /** Returns the knowledge-counter-baseline.json path (link-aware). */
184
185
  export declare function resolveKnowledgeCounterBaselinePath(directory: string): string;
185
- export declare function resolveHiveEventsPath(): string;
186
+ export declare const resolveHiveEventsPath: typeof resolveHiveEventsPathImpl;
186
187
  /** Returns the knowledge-application.jsonl path for legacy v2 audit records (link-aware). */
187
188
  export declare function resolveLegacyApplicationLogPath(directory: string): string;
188
189
  /** Generate a fresh trace id. One per retrieval; receipts reference it. */
@@ -267,6 +268,7 @@ export interface CounterRollup {
267
268
  }
268
269
  /** Cap on retained per-entry violation timestamps. */
269
270
  export declare const MAX_VIOLATION_TIMESTAMPS = 10;
271
+ declare function mergeRollupInto(target: CounterRollup, source: CounterRollup): void;
270
272
  declare function readCounterBaseline(directory: string): Promise<Map<string, CounterRollup>>;
271
273
  /**
272
274
  * Recompute per-entry counters deterministically from the immutable event log,
@@ -362,5 +364,6 @@ export declare const _internals: {
362
364
  appendHiveKnowledgeEvent: typeof appendHiveKnowledgeEvent;
363
365
  recordHiveKnowledgeEvent: typeof recordHiveKnowledgeEvent;
364
366
  readHiveKnowledgeEvents: typeof readHiveKnowledgeEvents;
367
+ mergeRollupInto: typeof mergeRollupInto;
365
368
  };
366
369
  export {};
@@ -27,7 +27,8 @@
27
27
  */
28
28
  /** On-disk pointer at `<directory>/.swarm/link.json`. */
29
29
  export interface LinkPointer {
30
- version: 1;
30
+ /** Pointer schema version. v1 pointers stay valid; v2 carries cohort metadata. */
31
+ version: 1 | 2;
31
32
  /** Path-safe identifier of the shared store (projectHash or sanitized name). */
32
33
  linkId: string;
33
34
  /** Human-friendly name when the link was created from an explicit name. */
@@ -36,6 +37,16 @@ export interface LinkPointer {
36
37
  createdAt: string;
37
38
  /** How the link was established. */
38
39
  source: 'manual' | 'auto';
40
+ /** Canonical cohort id from `resolveCohortId` (issue #1846). */
41
+ cohortId?: string;
42
+ /** How the cohort id was derived. */
43
+ identitySource?: 'remote' | 'git-common-dir' | 'path';
44
+ /** True when the cohort id is machine-local (git-common-dir/path fallback). */
45
+ degraded?: boolean;
46
+ /** Deterministic hash of cohort-relevant config (issue #1846 §4). */
47
+ configFingerprint?: string;
48
+ /** Monotonic generation counter, bumped on each link/unlink. Aids cache invalidation. */
49
+ generation?: number;
39
50
  }
40
51
  export interface LinkedLocalKnowledgeStatus {
41
52
  linked: boolean;
@@ -55,7 +66,13 @@ export declare function resolveLinkDir(linkId: string): string;
55
66
  * project hash). Lowercased for case-insensitive stability across worktrees.
56
67
  */
57
68
  export declare function sanitizeLinkId(name: string): string | null;
58
- /** Read and validate the link pointer for a worktree. Null if absent/invalid. */
69
+ /**
70
+ * Read and validate the link pointer for a worktree. Null if absent/invalid.
71
+ *
72
+ * Schema-version aware (issue #1846): reads `version` from disk rather than
73
+ * hard-stamping `1`, validates it is a known version, and preserves v2 cohort
74
+ * metadata fields when present. Unknown fields are ignored (forward-compat).
75
+ */
59
76
  export declare function readLinkPointer(directory: string): LinkPointer | null;
60
77
  export declare function getLinkedLocalKnowledgeStatus(directory: string): LinkedLocalKnowledgeStatus;
61
78
  /** Write the link pointer atomically and invalidate the resolution cache. */
@@ -67,8 +84,18 @@ export declare function removeLinkPointer(directory: string): Promise<void>;
67
84
  *
68
85
  * Returns the shared link directory when an active pointer is present, otherwise
69
86
  * the local `<directory>/.swarm`. Fail-open: any read/parse error degrades to the
70
- * local directory, so a corrupt pointer never strands knowledge. Synchronous and
71
- * cached so the hot retrieval path pays at most one tiny file read per TTL window.
87
+ * local directory, so a corrupt pointer never strands knowledge. Synchronous
88
+ * and cached; on a cache hit a cheap `stat` of the pointer file revalidates
89
+ * cross-process changes so the hot retrieval path stays fast while still
90
+ * observing link/unlink without waiting for the TTL to expire.
91
+ *
92
+ * Cross-process revalidation (issue #1846 §2): on a cache hit we additionally
93
+ * `stat` the pointer file and compare `mtimeMs:ctimeMs:size` to the cached
94
+ * fingerprint. A change (another process wrote/removed the pointer) invalidates
95
+ * the entry, so a stale process observes link/unlink without waiting for the
96
+ * TTL to expire. The TTL remains a backstop. The per-call `stat` is negligible
97
+ * (the pointer is tiny and almost never changes) and is the cost of correct
98
+ * cross-process link/unlink observation.
72
99
  *
73
100
  * NOTE: when unlinked, the return value is byte-identical to the legacy
74
101
  * `path.join(directory, '.swarm')`, so existing callers/tests are unaffected.
@@ -1,12 +1,14 @@
1
1
  /** Core storage layer for the opencode-swarm v6.17 two-tier knowledge system. */
2
+ import { resolveHiveDataDir, resolveHiveEventsPath as resolveHiveEventsPathImpl, resolveHiveKnowledgePath as resolveHiveKnowledgePathImpl, resolveHiveRejectedPath as resolveHiveRejectedPathImpl } from '../knowledge/hive-paths.js';
2
3
  import type { KnowledgeEntryBase, RejectedLesson, RetrievalOutcome } from './knowledge-types.js';
3
4
  export declare function getPlatformConfigDir(): string;
4
5
  export declare function resolveSwarmKnowledgePath(directory: string): string;
5
6
  export declare function resolveSwarmRejectedPath(directory: string): string;
6
7
  export declare function resolveSwarmRetractionsPath(directory: string): string;
7
- export declare function resolveHiveKnowledgePath(): string;
8
- export declare function resolveHiveRejectedPath(): string;
9
- export declare function resolveHiveEventsPath(): string;
8
+ export declare const resolveHiveKnowledgePath: typeof resolveHiveKnowledgePathImpl;
9
+ export declare const resolveHiveRejectedPath: typeof resolveHiveRejectedPathImpl;
10
+ export declare const resolveHiveEventsPath: typeof resolveHiveEventsPathImpl;
11
+ export { resolveHiveDataDir };
10
12
  declare function parseKnowledgeContent<T>(content: string, max: number): T[];
11
13
  export declare function readKnowledge<T>(filePath: string, maxEntries?: number): Promise<T[]>;
12
14
  export declare function normalizeEntry<T>(raw: T): T;
@@ -29,7 +31,7 @@ export declare function transactKnowledge<T>(filePath: string, mutate: (entries:
29
31
  export declare function getArchivedKnowledgeIds(directory: string): Promise<Set<string>>;
30
32
  export declare function appendKnowledgeWithCapEnforcement<T>(filePath: string, entry: T, maxEntries: number): Promise<boolean>;
31
33
  export declare function enforceKnowledgeCap<T>(filePath: string, maxEntries: number): Promise<void>;
32
- declare function selectKnowledgeCapSurvivors<T>(entries: T[], maxEntries: number): T[];
34
+ export declare function selectKnowledgeCapSurvivors<T>(entries: T[], maxEntries: number): T[];
33
35
  export interface SweepResult {
34
36
  scanned: number;
35
37
  aged: number;
@@ -136,4 +138,3 @@ export declare const _internals: {
136
138
  bumpKnowledgeConfidenceBatch: typeof bumpKnowledgeConfidenceBatch;
137
139
  getArchivedKnowledgeIds: typeof getArchivedKnowledgeIds;
138
140
  };
139
- export {};