opencode-swarm 7.148.0 → 7.148.1

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 (71) hide show
  1. package/dist/background/pr-review-collection-receipt.d.ts +2 -2
  2. package/dist/cli/{config-doctor-z2wam5wn.js → config-doctor-w6zphy4w.js} +2 -2
  3. package/dist/cli/{core-k7m76ce0.js → core-65qyqw2f.js} +1 -1
  4. package/dist/cli/{curation-policy-kd8j8cne.js → curation-policy-166wdjs4.js} +6 -6
  5. package/dist/cli/{curator-jwpm52kg.js → curator-2jhdfjqg.js} +27 -27
  6. package/dist/cli/{curator-llm-factory-4qt8b15s.js → curator-llm-factory-y43mx297.js} +27 -27
  7. package/dist/cli/{evidence-summary-service-90z295j3.js → evidence-summary-service-7qpganmn.js} +11 -11
  8. package/dist/cli/{gate-evidence-3q6q71fw.js → gate-evidence-p79xv25a.js} +6 -6
  9. package/dist/cli/{guardrail-explain-44xt373y.js → guardrail-explain-76sy950j.js} +28 -28
  10. package/dist/cli/{guardrail-log-kbp5jztb.js → guardrail-log-pm0w6ney.js} +3 -3
  11. package/dist/cli/{guardrail-reset-vpazhh8k.js → guardrail-reset-tkkg0bfz.js} +27 -27
  12. package/dist/cli/{hive-promoter-tdrkz9nh.js → hive-promoter-w4batwr6.js} +27 -27
  13. package/dist/cli/{index-jpa03jcc.js → index-2bsnwcmp.js} +1 -1
  14. package/dist/cli/{index-pgvx3rkq.js → index-2n9rwvcc.js} +1 -1
  15. package/dist/cli/{index-2y14mjfn.js → index-3yh2e5ca.js} +4 -2
  16. package/dist/cli/{index-9pmmexsd.js → index-5wypz451.js} +1 -1
  17. package/dist/cli/{index-x4rxh8j9.js → index-627mz27c.js} +3 -3
  18. package/dist/cli/{index-016708th.js → index-81n4ky69.js} +2 -2
  19. package/dist/cli/{index-52gnz782.js → index-c23yxr1s.js} +4 -4
  20. package/dist/cli/{index-d3bdr78h.js → index-ch6sjm8q.js} +1 -1
  21. package/dist/cli/{index-n96wwf56.js → index-dwg0z10h.js} +4 -4
  22. package/dist/cli/{index-sycndcwd.js → index-e9d2829t.js} +1 -1
  23. package/dist/cli/{index-h879g4r4.js → index-f115c8y6.js} +3 -3
  24. package/dist/cli/{index-y586zet4.js → index-fv5cc1rd.js} +2 -2
  25. package/dist/cli/{index-q81s0m8k.js → index-ghbkknjz.js} +79 -38
  26. package/dist/cli/{index-dmbaaz6d.js → index-hxqjf4me.js} +4 -4
  27. package/dist/cli/{index-zwpttdgn.js → index-jtnk7k6w.js} +1 -1
  28. package/dist/cli/{index-ffevkn7k.js → index-k5ze5v89.js} +1 -1
  29. package/dist/cli/{index-p62we2ar.js → index-kpgj4ha6.js} +1 -1
  30. package/dist/cli/{index-crpb0qke.js → index-kztxn1ww.js} +4034 -2834
  31. package/dist/cli/{index-qwj7qjjp.js → index-m97qwwrk.js} +2 -2
  32. package/dist/cli/{index-7ac5z627.js → index-mfhpk4c6.js} +1 -1
  33. package/dist/cli/{index-n0fpefqx.js → index-py2rsw1a.js} +4 -4
  34. package/dist/cli/{index-k6h5yc1j.js → index-q0gfmyzx.js} +5 -5
  35. package/dist/cli/{index-702zx3rk.js → index-qwzpsh3w.js} +1 -1
  36. package/dist/cli/{index-qcm97jfh.js → index-qzstn430.js} +1 -1
  37. package/dist/cli/{index-6q35swn2.js → index-rsn4jm90.js} +1 -1
  38. package/dist/cli/{index-dfvh1r8g.js → index-s3s2egzn.js} +29 -29
  39. package/dist/cli/{index-7hctrdk8.js → index-shazb5b6.js} +2 -2
  40. package/dist/cli/{index-rcjq8dm4.js → index-shm69en5.js} +2 -2
  41. package/dist/cli/{index-ykjjqg79.js → index-t6ys1386.js} +6 -6
  42. package/dist/cli/{index-gwrv6c56.js → index-w49v4xd6.js} +7 -7
  43. package/dist/cli/{index-07zgs6y0.js → index-x46mmaep.js} +14 -10
  44. package/dist/cli/index.js +27 -27
  45. package/dist/cli/{knowledge-escalator-0qnm48g8.js → knowledge-escalator-21n4ztqx.js} +11 -11
  46. package/dist/cli/{knowledge-events-3zv81nzp.js → knowledge-events-h5d5eayv.js} +9 -9
  47. package/dist/cli/{knowledge-link-w91bcyx2.js → knowledge-link-vgxe7gz9.js} +5 -5
  48. package/dist/cli/{knowledge-store-njtx6wj1.js → knowledge-store-y10m7x4f.js} +8 -6
  49. package/dist/cli/{knowledge-validator-7eqj5afv.js → knowledge-validator-dzm9sg0f.js} +8 -8
  50. package/dist/cli/{pending-delegations-k4x00mqc.js → pending-delegations-78zaaxz4.js} +3 -3
  51. package/dist/cli/{pr-subscriptions-x2mqc3f1.js → pr-subscriptions-9mg6mwc0.js} +3 -3
  52. package/dist/cli/{runner-gfzkp9nv.js → runner-rarzeth8.js} +6 -6
  53. package/dist/cli/{scan-cursor-a6dhyf55.js → scan-cursor-zn2qf06a.js} +7 -7
  54. package/dist/cli/{schema-p0vn1zgh.js → schema-ynwy7abm.js} +1 -1
  55. package/dist/cli/{scope-persistence-zs97zkt7.js → scope-persistence-sh30937s.js} +7 -7
  56. package/dist/cli/{skill-generator-5qg15q1j.js → skill-generator-r3knfwrp.js} +13 -13
  57. package/dist/cli/{telemetry-xzy0qeap.js → telemetry-1pnsftp0.js} +1 -1
  58. package/dist/cli/{worktree-collision-ownership-q2gswg16.js → worktree-collision-ownership-29md3k1n.js} +3 -3
  59. package/dist/cli/{worktree-isolation-ndb5fwys.js → worktree-isolation-yj7ssh24.js} +27 -27
  60. package/dist/consensus/contracts.d.ts +5 -1
  61. package/dist/consensus/corpus.d.ts +26 -3
  62. package/dist/consensus/miner.d.ts +5 -1
  63. package/dist/hooks/curator.d.ts +41 -1
  64. package/dist/hooks/knowledge-store.d.ts +26 -2
  65. package/dist/hooks/skill-scoring.d.ts +13 -3
  66. package/dist/hooks/skill-usage-log.d.ts +140 -17
  67. package/dist/hooks/skill-usage-pending.d.ts +392 -0
  68. package/dist/index.js +500 -499
  69. package/dist/observability/catalog.d.ts +6 -4
  70. package/dist/telemetry.d.ts +35 -1
  71. package/package.json +1 -1
@@ -3,9 +3,19 @@
3
3
  *
4
4
  * Writes one JSONL line per skill-usage event to `.swarm/skill-usage.jsonl`.
5
5
  * Follows the same append-only JSONL pattern as knowledge-application.jsonl.
6
+ *
7
+ * Issue #2038 — the JSONL is the **operational** stream and is bounded by a
8
+ * hard global byte/age/count budget (`SKILL_USAGE_LIMITS`). The
9
+ * **authoritative** record of un-consumed feedback lives in the sidecar
10
+ * `.swarm/skill-usage-pending.json` (see `skill-usage-pending.ts`), so
11
+ * evicting from this stream can lose no correctness signal: every actionable
12
+ * verdict is enqueued in the sidecar *before* it is appended here.
6
13
  */
7
14
  import * as fs from 'node:fs';
8
15
  import type { ConfidenceFloorOptions } from './knowledge-store.js';
16
+ import { bumpKnowledgeConfidenceBatchResult } from './knowledge-store.js';
17
+ import { enqueueSkillUsageFeedback, isSkillUsageQueueUnderPressure, readPendingManifest, type SkillUsageCoverage } from './skill-usage-pending.js';
18
+ export { isSkillWindowTrustworthy, SKILL_USAGE_LIMITS, type SkillUsageCoverage, type SkillUsagePendingRecord, } from './skill-usage-pending.js';
9
19
  /** Single entry in the skill-usage audit log. */
10
20
  export interface SkillUsageEntry {
11
21
  /** Auto-generated unique identifier (UUID v4). */
@@ -51,9 +61,24 @@ export interface PruneResult {
51
61
  pruned: number;
52
62
  /** Number of entries remaining in the log. */
53
63
  remaining: number;
54
- /** Error message when the write/rename step fails; absent on success. */
64
+ /**
65
+ * Error message when the compaction could not be published; absent on
66
+ * success. Set both when the stream write/rename fails and when the
67
+ * manifest save that now precedes the rewrite fails (issue #2038 residual
68
+ * R1) — in the latter case nothing was dropped, so `pruned` is 0.
69
+ */
55
70
  error?: string;
56
71
  }
72
+ /**
73
+ * What the window returned by a read can and cannot answer (issue #2038,
74
+ * requirement 4 / BLK-5). `complete === false` means entries the caller might
75
+ * have expected are not in `entries` — either this read was byte-truncated, or
76
+ * compaction has evicted history.
77
+ */
78
+ export interface SkillUsageReadCoverage extends SkillUsageCoverage {
79
+ /** True when THIS read was bounded by `readMaxBytes` and saw only a suffix. */
80
+ truncatedRead: boolean;
81
+ }
57
82
  /**
58
83
  * Normalize a compliance verdict to the canonical spelling.
59
84
  * The sole producer (`skill-propagation-gate.ts`) lowercases the regex
@@ -67,7 +92,8 @@ export declare function normalizeComplianceVerdict(verdict: string): string;
67
92
  /**
68
93
  * Test-only dependency-injection seam. Tests override these without
69
94
  * `mock.module` (which leaks across files in Bun's shared test-runner).
70
- * Restore in `afterEach`.
95
+ * Restore in `afterEach`, and call `_resetSkillUsageMaintenanceState()` to
96
+ * clear module-scoped maintenance counters.
71
97
  */
72
98
  export declare const _internals: {
73
99
  generateId: () => string;
@@ -81,33 +107,112 @@ export declare const _internals: {
81
107
  openSync: typeof fs.openSync;
82
108
  readSync: typeof fs.readSync;
83
109
  closeSync: typeof fs.closeSync;
110
+ unlinkSync: typeof fs.unlinkSync;
84
111
  pruneSkillUsageLog: typeof pruneSkillUsageLog;
85
112
  resolveSourceKnowledgeIds: typeof resolveSourceKnowledgeIds;
86
113
  applySkillUsageFeedback: typeof applySkillUsageFeedback;
114
+ /** Test seam: lets FB-008-style tests drive `failed:true` / partial-apply through the real consumption path without `mock.module`. */
115
+ bumpKnowledgeConfidenceBatchResult: typeof bumpKnowledgeConfidenceBatchResult;
87
116
  parseGeneratedFromKnowledge: typeof parseGeneratedFromKnowledge;
88
117
  computeComplianceByVersion: typeof computeComplianceByVersion;
89
118
  normalizeComplianceVerdict: typeof normalizeComplianceVerdict;
90
- readFeedbackAppliedEntryIds: typeof readFeedbackAppliedEntryIds;
91
119
  appendFeedbackAppliedMarker: typeof appendFeedbackAppliedMarker;
120
+ /**
121
+ * Streaming read seam (issue #2038, BLK-11). The one-time migration and the
122
+ * compaction pass must see EVERY line of a legacy file — they are bounded in
123
+ * peak memory and per-read chunk size, never byte-truncated — so they cannot
124
+ * use `readFileSync` and cannot use the byte-bounded steady-state funnel.
125
+ */
126
+ streamLogLines: typeof streamLogLines;
127
+ enqueueSkillUsageFeedback: typeof enqueueSkillUsageFeedback;
128
+ isQueueUnderPressure: typeof isSkillUsageQueueUnderPressure;
129
+ readPendingManifest: typeof readPendingManifest;
92
130
  };
93
- declare function readFeedbackAppliedEntryIds(directory: string): Set<string>;
131
+ /**
132
+ * Reset the throttled-maintenance counters so an unswept run in Bun's shared
133
+ * test-runner process cannot shift a later test's first maintenance pass.
134
+ */
135
+ export declare function _resetSkillUsageMaintenanceState(): void;
136
+ /**
137
+ * Legacy acknowledgment writer.
138
+ *
139
+ * Post-migration, acknowledgment is the sidecar's job: consumption dequeues
140
+ * the record instead of appending a marker line, which is what removes the
141
+ * unbounded marker accumulation described in issue #2038. This function is
142
+ * retained so that a pre-migration on-disk log written by an older build
143
+ * still round-trips through the reader above.
144
+ */
94
145
  declare function appendFeedbackAppliedMarker(directory: string, processedEntryIds: string[]): void;
146
+ /**
147
+ * Read `filePath` in `chunkBytes` slices and hand each complete, non-empty
148
+ * line to `onLine`. Peak resident memory is O(chunkBytes), independent of the
149
+ * file size — the metric requirement 4 actually cares about for the one-time
150
+ * migration and the compaction pass (BLK-11).
151
+ *
152
+ * A single line longer than 4x the chunk bound cannot be assembled without
153
+ * unbounded buffering; it is dropped and reported through `onOverlongLine` so
154
+ * the caller can fold it into the durable `corrupt` counter rather than
155
+ * losing it silently.
156
+ */
157
+ declare function streamLogLines(filePath: string, chunkBytes: number, onLine: (line: string) => void, onOverlongLine?: () => void): void;
95
158
  /**
96
159
  * Validate and append a single skill-usage entry to the JSONL log.
97
160
  *
98
161
  * The `id` field is auto-generated; callers provide all other fields.
99
162
  * Uses synchronous I/O for consistency with the JSONL append pattern.
163
+ *
164
+ * Two behaviors matter for issue #2038:
165
+ *
166
+ * 1. **Actionable verdicts are enqueued FIRST, then appended** (approved plan
167
+ * §2.2). A crash between the two leaves at worst an orphan queue record
168
+ * with no stats row — harmless, the record is self-sufficient. The reverse
169
+ * order leaves an authoritative gap. A failed enqueue **aborts the append
170
+ * and propagates**; the sole actionable caller already handles a throw.
171
+ * 2. **The `not_checked` path is a hard no-op for the queue** — no lock, no
172
+ * queue read-modify-write. That keeps O(paths x queue) synchronous I/O out
173
+ * of the hot delegation loop, and it is safe because `not_checked` carries
174
+ * no correctness signal. Under queue pressure those optional appends stop
175
+ * entirely (requirement 5).
100
176
  */
101
177
  export declare function appendSkillUsageEntry(directory: string, entry: Omit<SkillUsageEntry, 'id'>): void;
178
+ /**
179
+ * Read and parse skill-usage entries together with the coverage of the window
180
+ * they came from (issue #2038, BLK-5).
181
+ *
182
+ * Additive: `readSkillUsageEntries` remains a wrapper returning `entries`
183
+ * only, so the DI seams in `corpus.ts` / `curator.ts` that are typed as
184
+ * `typeof readSkillUsageEntries` keep working unchanged.
185
+ *
186
+ * Takes no lock and never migrates — reads must stay cheap and must never
187
+ * mutate the store.
188
+ */
189
+ export declare function readSkillUsageEntriesWithCoverage(directory: string, options?: SkillUsageFilterOptions): {
190
+ entries: SkillUsageEntry[];
191
+ coverage: SkillUsageReadCoverage;
192
+ };
102
193
  /**
103
194
  * Read and parse skill-usage entries from the JSONL log, optionally filtered.
104
195
  *
105
196
  * Malformed lines are silently skipped (no throw). Returns an empty array
106
- * if the log file does not exist.
197
+ * if the log file does not exist. Bounded by `SKILL_USAGE_LIMITS.readMaxBytes`;
198
+ * use {@link readSkillUsageEntriesWithCoverage} when the caller needs to know
199
+ * whether the window it received is complete.
107
200
  */
108
201
  export declare function readSkillUsageEntries(directory: string, options?: SkillUsageFilterOptions): SkillUsageEntry[];
109
202
  /** Default maximum bytes to read from the end of the log file. */
110
203
  export declare const TAIL_BYTES_DEFAULT: number;
204
+ /**
205
+ * Ceiling for an explicitly requested tail size.
206
+ *
207
+ * Deliberately equal to `TAIL_BYTES_DEFAULT`: the tail reader exists to serve
208
+ * the delegation dedup/scoring window, which is specified as 64 KiB, so an
209
+ * oversized request is clamped rather than honored. Issue #2038 §5 required
210
+ * that this ceiling and the global read budget must not disagree *in silence* —
211
+ * they no longer do, because the bounded read funnel
212
+ * ({@link readSkillUsageEntriesWithCoverage}) is routed around this constant
213
+ * and bounds itself with `SKILL_USAGE_LIMITS.readMaxBytes`, reporting
214
+ * `coverage.truncatedRead` when it cuts.
215
+ */
111
216
  export declare const MAX_TAIL_BYTES: number;
112
217
  /**
113
218
  * Read the last `maxBytes` of the skill-usage JSONL log and parse matching
@@ -131,11 +236,24 @@ export interface VersionComplianceStats {
131
236
  }
132
237
  export declare function computeComplianceByVersion(entries: SkillUsageEntry[], skillPath: string): Map<number | undefined, VersionComplianceStats>;
133
238
  /**
134
- * Prune the skill-usage log, keeping at most `maxEntriesPerSkill` entries
135
- * per unique skillPath. Oldest entries beyond the limit are removed.
239
+ * Prune the skill-usage log.
136
240
  *
137
- * Writes atomically (temp file + rename). No-op if the log file doesn't
138
- * exist or all skills are within their limits.
241
+ * Two budgets are enforced, in this order:
242
+ * 1. the legacy per-skill FIFO (`maxEntriesPerSkill`, default 500), and
243
+ * 2. the **hard global** byte / age / count ceiling from `SKILL_USAGE_LIMITS`.
244
+ *
245
+ * **BLK-3 — the highest-risk detail of issue #2038.** The old body returned
246
+ * early with `if (pruned === 0) return ...` immediately after per-skill
247
+ * pruning, so in the reported scenario — thousands of distinct skills, every
248
+ * one of them under 500 entries — nothing was ever pruned, the file was never
249
+ * rewritten, and the 1 MiB trigger simply re-fired on every append. The global
250
+ * budget is now evaluated by `applyRetention` **before** any early return, and
251
+ * the rewrite decision below is taken on the union of both budgets plus the
252
+ * marker set. There is deliberately no `pruned === 0` short-circuit ahead of it.
253
+ *
254
+ * Writes atomically (temp file + rename) in **global timestamp order**
255
+ * (BLK-12) so a rewrite can never push a live session's recent entries out of
256
+ * the 64 KiB tail window that the delegation dedup preload depends on.
139
257
  *
140
258
  * @returns Stats about how many entries were pruned and how many remain.
141
259
  */
@@ -165,25 +283,30 @@ export declare function resolveSourceKnowledgeIds(directory: string, skillPath:
165
283
  */
166
284
  declare function parseGeneratedFromKnowledge(content: string): string[];
167
285
  /**
168
- * Read skill-usage entries, resolve source knowledge IDs for each skill,
169
- * and apply confidence bumps/decays to the originating knowledge entries.
286
+ * Consume pending skill-usage feedback and apply confidence bumps/decays to
287
+ * the originating knowledge entries.
288
+ *
289
+ * Reads the **authoritative sidecar queue**, never the JSONL stream: the
290
+ * compliant/violated counts and the per-skill delta are computed from queue
291
+ * records only, so an entry evicted from the operational stream can never flip
292
+ * a pending record's delta sign (approved plan §2 corollary).
170
293
  *
171
- * For each unique skillPath with at least one compliance or violated entry:
294
+ * For each unique skillPath with at least one queued actionable record:
172
295
  * 1. Resolve source knowledge UUIDs from the skill's SKILL.md frontmatter.
173
- * 2. Count compliant and violated events for that skill.
296
+ * 2. Count compliant and violated records for that skill.
174
297
  * 3. Compute net delta: if compliant count > violation count → +0.05; else → -0.1.
175
- * 4. Call `bumpKnowledgeConfidenceBatch` with the aggregated deltas.
298
+ * 4. Call `bumpKnowledgeConfidenceBatchResult` with the aggregated deltas.
299
+ * 5. Dequeue, retain-with-retry, or terminal-dequeue each record per §3.
176
300
  *
177
301
  * @param directory - Project root directory.
178
- * @param options.sinceTimestamp - Optional ISO 8601 cutoff; only process entries after this time.
302
+ * @param options.sinceTimestamp - Optional ISO 8601 cutoff; only process records after this time.
179
303
  * @returns Count of processed skills and total confidence bumps/decays applied.
180
304
  */
181
305
  export declare function applySkillUsageFeedback(directory: string, options?: {
182
306
  sinceTimestamp?: string;
183
- /** G2: forwarded to bumpKnowledgeConfidenceBatch. */
307
+ /** G2: forwarded to bumpKnowledgeConfidenceBatchResult. */
184
308
  floorOptions?: ConfidenceFloorOptions;
185
309
  }): Promise<{
186
310
  processed: number;
187
311
  bumps: number;
188
312
  }>;
189
- export {};
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Skill-usage pending sidecar — `.swarm/skill-usage-pending.json` (issue #2038).
3
+ *
4
+ * Two files back the skill-usage subsystem:
5
+ *
6
+ * 1. `.swarm/skill-usage.jsonl` — the **OPERATIONAL** stream. Pure JSONL usage
7
+ * entries, no manifest header line, freely evictable under the global
8
+ * byte/age/count budget.
9
+ * 2. `.swarm/skill-usage-pending.json` — this file, the **AUTHORITATIVE**
10
+ * sidecar. A single JSON document holding the pending-feedback queue *and*
11
+ * all manifest state: `{ version, migrated, records[], counters{}, coverage{} }`.
12
+ *
13
+ * The manifest lives here rather than in a JSONL header line so that
14
+ * `parseSkillUsageEntry` stays header-free and the tail reader and the full
15
+ * reader remain on one parser (approved plan §0).
16
+ *
17
+ * Evicting an entry from the JSONL can lose no correctness signal because
18
+ * every actionable verdict (`compliant` / `violated`) is enqueued here
19
+ * *before* it is appended to the stream (approved plan §2). The queue record
20
+ * is self-sufficient: consumption computes compliant/violated counts and the
21
+ * per-skill delta from queue records only, never from JSONL entries.
22
+ *
23
+ * All I/O is synchronous, matching `skill-usage-log.ts`. State lives only under
24
+ * `.swarm/` resolved from the injected `directory` (never `process.cwd()`).
25
+ */
26
+ import * as fs from 'node:fs';
27
+ /**
28
+ * The hard global budget for the skill-usage subsystem.
29
+ *
30
+ * | Key | Justification |
31
+ * |---|---|
32
+ * | `version` | requirement 1 "versioned"; stored in the sidecar |
33
+ * | `maxEntries` | must exceed 500 so the per-skill 500-entry fixture still prunes nothing |
34
+ * | `maxBytes` | must exceed the ~100 KB 600-entry fixture |
35
+ * | `maxAgeMs` | the age budget |
36
+ * | `floorPerSkill` | >= `curatorMinSample`, so the guaranteed window can still authorize a retirement |
37
+ * | `curatorMinSample` | minimum per-skill sample before the curator may retire/revise |
38
+ * | `readMaxBytes` | >= `maxBytes` + slack, so a bounded file always reads complete |
39
+ * | `migrationChunkBytes` | **chunk/buffer bound, NOT a total-bytes cap** — migration is single-pass streaming and is never byte-truncated |
40
+ * | `headerMaxBytes` | sidecar manifest-scalar bound |
41
+ * | `queueMaxRecords` | requirement 1 applies to marker types too; an upper guard, NOT the binding cap (see below) |
42
+ * | `queueMaxBytes` | **the binding cap** (see below) |
43
+ * | `maxAttempts` | bounds transient-failure retention |
44
+ * | `checkInterval` | throttled maintenance cadence, mirrors `telemetry.ts` |
45
+ *
46
+ * **Which queue cap binds, and why (issue #2038 review).** The approved plan
47
+ * justified `queueMaxBytes` as "~5,000 x ~100 B". That estimate is wrong by a
48
+ * factor of two. Measured `recordBytes` (`JSON.stringify(record).length + 1`,
49
+ * which is what {@link queueByteSize} sums):
50
+ *
51
+ * | `skillPath` | bytes/record | `queueMaxBytes` binds at |
52
+ * |---|---|---|
53
+ * | `skill-x` (a test-length path) | 200 | 2,621 records |
54
+ * | `.claude/skills/writing-tests/SKILL.md` | 230 | 2,279 records |
55
+ * | `.claude/skills/engineering-conventions/SKILL.md` | 240 | 2,184 records |
56
+ *
57
+ * A 36-char UUID id and two ISO-8601 timestamps (`timestamp`, `enqueuedAt`)
58
+ * are already 88 B of values before any key name. `queueMaxRecords` = 5,000
59
+ * would need <= 104.9 B/record to be reachable, and 5,000 realistic records
60
+ * would need a ~1,123 KiB byte budget.
61
+ *
62
+ * So `queueMaxBytes` (512 KiB) binds first, at roughly **2,200-2,600
63
+ * records**, and `queueMaxRecords` (5,000) is **not reachable** in practice.
64
+ * That is deliberate and left as-is: both numbers are published in
65
+ * `docs/observability-retention-registry.md` and
66
+ * `scripts/retention-registry.data.ts`, the byte budget is the one requirement
67
+ * 1 actually cares about (unbounded *growth*), and the eviction ladder in
68
+ * {@link enforceQueueBounds} is driven by `overBudget()`, which ORs the two —
69
+ * so the ladder and every counted discard behave identically whichever cap
70
+ * trips. `queueMaxRecords` remains as a cardinality guard for the degenerate
71
+ * case of pathologically short records. Do not "fix" the record count by
72
+ * shrinking the record: every field it carries has a consumer in
73
+ * `applySkillUsageFeedback`.
74
+ */
75
+ export declare const SKILL_USAGE_LIMITS: {
76
+ readonly version: 1;
77
+ readonly maxEntries: 5000;
78
+ readonly maxBytes: number;
79
+ readonly maxAgeMs: number;
80
+ readonly floorPerSkill: 20;
81
+ readonly curatorMinSample: 10;
82
+ readonly readMaxBytes: 1677722;
83
+ readonly migrationChunkBytes: number;
84
+ readonly headerMaxBytes: number;
85
+ readonly queueMaxRecords: 5000;
86
+ readonly queueMaxBytes: number;
87
+ readonly maxAttempts: 5;
88
+ readonly checkInterval: 50;
89
+ };
90
+ /**
91
+ * Stale-break window for `.swarm/skill-usage.lock`, mirroring
92
+ * `src/context-map/telemetry.ts`. The knowledge-store bump holds its own
93
+ * lock for at most 5 retries / 5 s, so it cannot push a consumption cycle
94
+ * past this window.
95
+ */
96
+ export declare const SKILL_USAGE_LOCK_STALE_MS: number;
97
+ /** Lifecycle state of a queued feedback record. */
98
+ export type SkillUsagePendingState = 'pending' | 'in_flight' | 'uncertain';
99
+ /**
100
+ * Terminal outcomes. Every one of these DEQUEUES the record and increments a
101
+ * health counter; none of them increments `processed` / `bumps` on the
102
+ * `applySkillUsageFeedback` return value (approved plan §3, E7).
103
+ */
104
+ export type SkillUsageTerminalOutcome = 'no_source_knowledge' | 'no_matching_knowledge' | 'bump_unrecoverable' | 'uncertain_expired';
105
+ /** A single queued actionable verdict awaiting a confidence bump. */
106
+ export interface SkillUsagePendingRecord {
107
+ /** Same id as the JSONL entry it mirrors — the dedupe key. */
108
+ id: string;
109
+ /**
110
+ * Canonical skill path — no `file:` prefix, forward slashes only — the
111
+ * same spelling written to the stream for this id (issue #2038 review,
112
+ * DEFECT 2). Both writers normalize through
113
+ * `skill-usage-log.ts` `canonicalSkillPath`, so a record and its stream row
114
+ * can never disagree; `applySkillUsageFeedback` groups on THIS field, so a
115
+ * disagreement would split one skill's feedback into two groups.
116
+ * Records migrated from a pre-fix sidecar may still carry a raw spelling
117
+ * until they drain; every consumer of this field strips `file:`
118
+ * idempotently, so both forms resolve.
119
+ */
120
+ skillPath: string;
121
+ /** Actionable verdict only. */
122
+ verdict: 'compliant' | 'violated';
123
+ /** ISO 8601 timestamp of the usage event. */
124
+ timestamp: string;
125
+ /** ISO 8601 mint time — the reference for queue age bounds. */
126
+ enqueuedAt: string;
127
+ state: SkillUsagePendingState;
128
+ /** Transient-failure retry counter, bounded by `maxAttempts`. */
129
+ attempts: number;
130
+ /** ISO 8601 time the record was claimed; only set while `in_flight`. */
131
+ inFlightAt?: string;
132
+ }
133
+ /** Durable lifetime counters. Fixed key set — bounded cardinality by construction. */
134
+ export declare const SKILL_USAGE_COUNTER_KEYS: readonly ["accepted", "compacted", "dropped", "skills_dropped", "corrupt", "uncertain_expired", "pending_evicted", "no_source_knowledge", "no_matching_knowledge", "bump_retry", "bump_unrecoverable", "bump_applied_zero", "pressure", "curator_skipped"];
135
+ export type SkillUsageCounterKey = (typeof SKILL_USAGE_COUNTER_KEYS)[number];
136
+ /**
137
+ * What the retained JSONL window can and cannot answer.
138
+ *
139
+ * Per-skill coverage is DERIVED from these global facts plus the retained
140
+ * count for the skill (see {@link isSkillWindowTrustworthy}) rather than
141
+ * stored per skill — a per-skill map would be an unbounded key set, which is
142
+ * exactly the failure mode issue #2038 is about.
143
+ */
144
+ export interface SkillUsageCoverage {
145
+ /** True when no entry has ever been evicted by compaction. */
146
+ complete: boolean;
147
+ /** Oldest retained entry timestamp at the last compaction, or null. */
148
+ oldestRetained: string | null;
149
+ /** Newest retained entry timestamp at the last compaction, or null. */
150
+ newestRetained: string | null;
151
+ /** Cumulative entries evicted by compaction. */
152
+ entriesDropped: number;
153
+ /** Cumulative skills dropped whole by the admit-by-most-recent-use step. */
154
+ skillsDropped: number;
155
+ /** The floor guarantee in force, so consumers do not hardcode it. */
156
+ floorPerSkill: number;
157
+ }
158
+ /** The whole sidecar document. */
159
+ export interface SkillUsagePendingDocument {
160
+ version: number;
161
+ /** False (or absent) means the legacy migration has not completed yet. */
162
+ migrated: boolean;
163
+ records: SkillUsagePendingRecord[];
164
+ counters: Record<SkillUsageCounterKey, number>;
165
+ coverage: SkillUsageCoverage;
166
+ }
167
+ /** An enqueue request — the caller supplies the id minted for the JSONL entry. */
168
+ export interface SkillUsageEnqueueInput {
169
+ id: string;
170
+ skillPath: string;
171
+ verdict: 'compliant' | 'violated';
172
+ timestamp: string;
173
+ }
174
+ /** Opaque handle for an acquired `.swarm/skill-usage.lock`. */
175
+ export interface SkillUsageLockHandle {
176
+ readonly lockPath: string;
177
+ }
178
+ /**
179
+ * Test-only dependency-injection seam. Tests override these without
180
+ * `mock.module` (which leaks across files in Bun's shared test-runner).
181
+ * Restore in `afterEach`, and call `_resetSkillUsagePendingState()` to clear
182
+ * the module-scoped pressure cache.
183
+ */
184
+ export declare const _internals: {
185
+ existsSync: typeof fs.existsSync;
186
+ readFileSync: typeof fs.readFileSync;
187
+ writeFileSync: typeof fs.writeFileSync;
188
+ renameSync: typeof fs.renameSync;
189
+ mkdirSync: typeof fs.mkdirSync;
190
+ statSync: any;
191
+ openSync: typeof fs.openSync;
192
+ closeSync: typeof fs.closeSync;
193
+ unlinkSync: typeof fs.unlinkSync;
194
+ now: () => number;
195
+ emitHealth: (payload: SkillUsageHealthPayload) => void;
196
+ };
197
+ /**
198
+ * Reset module-scoped caches so an unswept run in Bun's shared test-runner
199
+ * process cannot shift a later test's first read or pressure decision.
200
+ */
201
+ export declare function _resetSkillUsagePendingState(): void;
202
+ /** Absolute path to the authoritative sidecar. */
203
+ export declare function resolvePendingPath(directory: string): string;
204
+ /** Absolute path to the shared skill-usage lock file. */
205
+ export declare function resolveSkillUsageLockPath(directory: string): string;
206
+ /**
207
+ * Non-blocking lock acquisition. Returns `null` when the lock is held by a
208
+ * live holder — maintenance and consumption are then **skipped, never forced**.
209
+ * Injection still fails open because nothing here throws.
210
+ */
211
+ export declare function acquireSkillUsageLock(directory: string): SkillUsageLockHandle | null;
212
+ /**
213
+ * Lock acquisition for the **enqueue** path, which is exempt from the
214
+ * skip-not-force rule (approved plan §2.3, §9): a failed enqueue must abort
215
+ * the append and propagate rather than silently drop a correctness signal.
216
+ * Holders only perform a handful of synchronous file operations, so a short
217
+ * bounded retry absorbs ordinary contention.
218
+ */
219
+ export declare function acquireSkillUsageLockOrThrow(directory: string): SkillUsageLockHandle;
220
+ /** Release a lock acquired above. Never throws. */
221
+ export declare function releaseSkillUsageLock(handle: SkillUsageLockHandle): void;
222
+ /** A fresh, un-migrated document. */
223
+ export declare function createPendingDocument(): SkillUsagePendingDocument;
224
+ /** Result of loading the sidecar. */
225
+ export interface LoadPendingResult {
226
+ doc: SkillUsagePendingDocument;
227
+ /** True when a corrupt sidecar was renamed aside and a fresh one substituted. */
228
+ quarantined: boolean;
229
+ /** Absolute path the corrupt document was moved to, when quarantined. */
230
+ quarantinePath?: string;
231
+ }
232
+ /**
233
+ * Read the sidecar.
234
+ *
235
+ * A corrupt or oversized document is **quarantined** (renamed aside) and
236
+ * counted — never silently reset to `[]`. The replacement document carries
237
+ * `migrated: false`, which makes the next lock-taking touch rebuild the queue
238
+ * from whatever the JSONL stream still holds (approved plan §6, requirement 3).
239
+ */
240
+ export declare function loadPendingDocument(directory: string): LoadPendingResult;
241
+ /**
242
+ * Atomically replace the sidecar (temp file + rename). Throws on failure so
243
+ * the enqueue path can abort its append and propagate (approved plan §2.3).
244
+ */
245
+ export declare function savePendingDocument(directory: string, doc: SkillUsagePendingDocument): void;
246
+ /**
247
+ * Read just the manifest scalars a reader needs, using a `statSync`-keyed
248
+ * cache so the 8 steady-state read sites do not each re-parse the sidecar.
249
+ */
250
+ export declare function readPendingManifest(directory: string): {
251
+ coverage: SkillUsageCoverage;
252
+ migrated: boolean;
253
+ };
254
+ /**
255
+ * Per-skill coverage rule (approved plan §8 / BLK-8), derived rather than
256
+ * stored so the key set stays bounded.
257
+ *
258
+ * A skill's retained window may be used for a retire/revise decision when:
259
+ * (i) global coverage is COMPLETE — nothing has ever been evicted, so the
260
+ * retained window IS the whole history and there is nothing for this
261
+ * gate to protect against; OR
262
+ * (ii) coverage is incomplete AND the sample is at least `curatorMinSample`
263
+ * AND the retained window is at least the most-recent `floorPerSkill`
264
+ * entries — retention guarantees each surviving skill
265
+ * `min(count, floorPerSkill)` most-recent entries, so
266
+ * `retained >= floorPerSkill` establishes the window's shape by
267
+ * construction.
268
+ *
269
+ * **Why the minimum-sample floor sits behind `coverage.complete` (issue #2038
270
+ * implementation review, F2).** An earlier revision tested the floor FIRST, so a
271
+ * skill with 3 uses and 3 violations was never retired even on a fully complete,
272
+ * untruncated window. That silently narrowed shipped #1770/#1822 behavior, where
273
+ * the retire trigger was `violationRate > 0.3` with no sample floor, and it had
274
+ * nothing to do with compaction coverage — which is all this gate exists to
275
+ * judge. On complete coverage the sample IS the truth, so the pre-existing
276
+ * behavior applies unchanged; the floor is a statement about a TRUNCATED window
277
+ * and only applies to one.
278
+ *
279
+ * Note that under the shipped constants (`floorPerSkill` 20 >= `curatorMinSample`
280
+ * 10) the explicit `curatorMinSample` test below is subsumed by the floor
281
+ * comparison. It is kept because the constants are independently tunable and a
282
+ * sidecar may carry an older, smaller `coverage.floorPerSkill`; do not read it as
283
+ * load-bearing at today's values.
284
+ *
285
+ * Returns false when the decision must be skipped (and counted).
286
+ */
287
+ export declare function isSkillWindowTrustworthy(coverage: SkillUsageCoverage, retainedCount: number): boolean;
288
+ /** Total serialized size of the queue records. */
289
+ export declare function queueByteSize(doc: SkillUsagePendingDocument): number;
290
+ /**
291
+ * Apply the queue budget: age, record count, and byte size — to every record
292
+ * including `uncertain` ones. Every discard is counted; none is silent.
293
+ */
294
+ export declare function enforceQueueBounds(doc: SkillUsagePendingDocument, nowMs?: number): void;
295
+ /**
296
+ * Whether optional (`not_checked`) usage appends must stop.
297
+ *
298
+ * Deliberately cheap: the hot delegation loop appends one entry per skill
299
+ * path, so this must not take the lock or parse the sidecar. It reuses the
300
+ * value computed by the last write and otherwise refreshes from a `statSync`
301
+ * at most once per {@link PRESSURE_CACHE_MS}.
302
+ */
303
+ export declare function isSkillUsageQueueUnderPressure(directory: string): boolean;
304
+ /**
305
+ * Merge records into the document, deduped by `id` (approved plan BLK-10).
306
+ * Returns the number actually added.
307
+ */
308
+ export declare function mergePendingRecords(doc: SkillUsagePendingDocument, incoming: SkillUsageEnqueueInput[], nowIso: string): number;
309
+ /**
310
+ * Enqueue one actionable verdict.
311
+ *
312
+ * **Must be called BEFORE the JSONL append** (approved plan §2.2): a crash
313
+ * between the two then leaves at worst an orphan queue record with no stats
314
+ * row — harmless, because the record is self-sufficient. The reverse order
315
+ * leaves an authoritative gap.
316
+ *
317
+ * **Throws** on lock failure or write failure so the caller aborts the append
318
+ * and propagates (§2.3). This is the one path exempt from the skip-not-force
319
+ * rule; the sole actionable caller already handles a throw.
320
+ */
321
+ export declare function enqueueSkillUsageFeedback(directory: string, input: SkillUsageEnqueueInput): void;
322
+ /**
323
+ * Resolve claims abandoned by a crashed cycle.
324
+ *
325
+ * An `in_flight` record whose claim is older than the lock stale-break window
326
+ * becomes `uncertain`: it survives and stays visible, but is never replayed —
327
+ * satisfying both clauses of "survives ... and is consumed at most once".
328
+ * A claim younger than that window belongs to a cycle that may still be
329
+ * running in another process and is left alone.
330
+ */
331
+ export declare function resolveStaleInFlight(doc: SkillUsagePendingDocument, nowMs?: number): number;
332
+ /** Records eligible for consumption. `in_flight` and `uncertain` are excluded. */
333
+ export declare function selectConsumableRecords(doc: SkillUsagePendingDocument, sinceTimestamp?: string): SkillUsagePendingRecord[];
334
+ /** Mark records claimed. Persist before releasing the lock. */
335
+ export declare function markRecordsInFlight(doc: SkillUsagePendingDocument, ids: Iterable<string>, nowIso?: string): void;
336
+ /** Normal-path dequeue after a bump that actually applied. No terminal counter. */
337
+ export declare function dequeueRecords(doc: SkillUsagePendingDocument, ids: Iterable<string>): number;
338
+ /**
339
+ * Terminal dequeue. Counted in `skill_usage_health` only — terminal outcomes
340
+ * never increment `processed` / `bumps` (approved plan §3, E7).
341
+ */
342
+ export declare function applyTerminalOutcome(doc: SkillUsagePendingDocument, ids: Iterable<string>, outcome: SkillUsageTerminalOutcome): number;
343
+ /**
344
+ * Transient-failure handling: the record stays `pending` and its attempt
345
+ * counter advances. At `maxAttempts` it goes terminal `bump_unrecoverable`,
346
+ * so a permanently-locked directory cannot retain records forever.
347
+ */
348
+ export declare function retainWithRetry(doc: SkillUsagePendingDocument, ids: Iterable<string>): {
349
+ retried: string[];
350
+ unrecoverable: string[];
351
+ };
352
+ /**
353
+ * Payload shape of `telemetry.skillUsageHealth`. Counts only — **no
354
+ * `skillPath` and no per-skill identifier**: the adversarial case in issue
355
+ * #2038 is thousands of one-off skill IDs, so a per-skill label would be an
356
+ * unbounded label set and nothing in `check-event-contract.ts` would catch it.
357
+ */
358
+ export interface SkillUsageHealthPayload {
359
+ trigger: 'compaction' | 'migration' | 'consumption' | 'pressure';
360
+ accepted: number;
361
+ compacted: number;
362
+ dropped: number;
363
+ skills_dropped: number;
364
+ corrupt: number;
365
+ pending_retained: number;
366
+ uncertain_retained: number;
367
+ uncertain_expired: number;
368
+ /** Actionable records discarded by the queue budget (issue #2038 F3). */
369
+ pending_evicted: number;
370
+ no_source_knowledge: number;
371
+ no_matching_knowledge: number;
372
+ bump_retry: number;
373
+ bump_unrecoverable: number;
374
+ bump_applied_zero: number;
375
+ pressure: number;
376
+ curator_skipped: number;
377
+ bytes: number;
378
+ limit_bytes: number;
379
+ oldest_timestamp: string | null;
380
+ newest_timestamp: string | null;
381
+ coverage: boolean;
382
+ }
383
+ /** Build the health payload from durable counters plus live gauges. */
384
+ export declare function buildSkillUsageHealthPayload(doc: SkillUsagePendingDocument, trigger: SkillUsageHealthPayload['trigger'], gauges: {
385
+ bytes: number;
386
+ limitBytes: number;
387
+ }): SkillUsageHealthPayload;
388
+ /** Emit the health signal. Never throws — observability must not break a write. */
389
+ export declare function emitSkillUsageHealth(doc: SkillUsagePendingDocument, trigger: SkillUsageHealthPayload['trigger'], gauges: {
390
+ bytes: number;
391
+ limitBytes: number;
392
+ }): void;