opencode-swarm 7.159.0 → 7.159.2

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 (83) hide show
  1. package/.opencode/skills/engineering-conventions/SKILL.md +20 -0
  2. package/.opencode/skills/execute/SKILL.md +2 -2
  3. package/dist/cli/{coder-settlement-66mqw0xt.js → coder-settlement-hny3vh3e.js} +9 -9
  4. package/dist/cli/{config-doctor-nkx61b9e.js → config-doctor-00ex0s3k.js} +2 -2
  5. package/dist/cli/{core-b3db7gf6.js → core-03tedwzg.js} +1 -1
  6. package/dist/cli/{curation-policy-vn3m5nq2.js → curation-policy-z0a5akng.js} +6 -6
  7. package/dist/cli/{curator-llm-factory-c4y1mrr5.js → curator-llm-factory-24bfn7h1.js} +31 -31
  8. package/dist/cli/{curator-w4ntvkz5.js → curator-n81nzdw8.js} +31 -31
  9. package/dist/cli/{evidence-summary-service-vadgnzk6.js → evidence-summary-service-nz7y1w2a.js} +13 -13
  10. package/dist/cli/{gate-evidence-0chatz5e.js → gate-evidence-dxafmzrn.js} +6 -6
  11. package/dist/cli/guardrail-explain-b3k9xrx9.js +67 -0
  12. package/dist/cli/{guardrail-log-t3c9g8tc.js → guardrail-log-t8c7acxg.js} +5 -5
  13. package/dist/cli/{guardrail-reset-wvdb5sv5.js → guardrail-reset-0ft2gf03.js} +31 -31
  14. package/dist/cli/{hive-promoter-mfq3ee0n.js → hive-promoter-brq9cx7c.js} +31 -31
  15. package/dist/cli/{index-nwkr6tvv.js → index-0zerbq1v.js} +1 -1
  16. package/dist/cli/{index-h52hs4e9.js → index-1e9rzhmr.js} +2 -2
  17. package/dist/cli/{index-b1tenv2s.js → index-1xcve0gc.js} +7 -1
  18. package/dist/cli/{index-8jnzp5ct.js → index-200myvdx.js} +2 -2
  19. package/dist/cli/{index-mc9qgkwd.js → index-6sheh3yp.js} +3 -3
  20. package/dist/cli/{index-k4arrwrz.js → index-7tjamatf.js} +8 -8
  21. package/dist/cli/{index-vywbn766.js → index-8ahjx7vt.js} +4 -4
  22. package/dist/cli/{index-pvzaryhr.js → index-8gh97gkz.js} +1 -1
  23. package/dist/cli/{index-3e8600a7.js → index-9ev0bbqh.js} +883 -252
  24. package/dist/cli/{index-xagq35tf.js → index-av3gnekk.js} +1 -1
  25. package/dist/cli/{index-k48kgm6z.js → index-b2dh37cf.js} +5 -5
  26. package/dist/cli/{index-ynjy98vv.js → index-b5s1n149.js} +5 -5
  27. package/dist/cli/{index-erdsawqz.js → index-bmst1sje.js} +6 -6
  28. package/dist/cli/{index-e2s08v0q.js → index-caqjwnak.js} +2 -2
  29. package/dist/cli/{index-tcbjjppb.js → index-dgt8z19x.js} +1 -1
  30. package/dist/cli/{index-6e6qxf5q.js → index-e1jcdpdg.js} +1 -1
  31. package/dist/cli/{index-gprxjmm7.js → index-ebz97mks.js} +3 -1
  32. package/dist/cli/{index-pfnj5pvs.js → index-egc8d995.js} +1 -1
  33. package/dist/cli/{index-we3nf5as.js → index-eh1gh043.js} +4 -4
  34. package/dist/cli/{index-z54qw70y.js → index-jd7mz4yg.js} +1 -1
  35. package/dist/cli/{index-8kfbdrxa.js → index-k7kvrwbj.js} +33 -33
  36. package/dist/cli/{index-7s1hmrfh.js → index-ky2r092m.js} +2 -2
  37. package/dist/cli/{index-xmz69g65.js → index-m0gfzxw1.js} +2 -2
  38. package/dist/cli/{index-ghjs1n7k.js → index-mcan50mx.js} +4 -4
  39. package/dist/cli/{index-04yrxg0z.js → index-nmp47dmm.js} +2 -2
  40. package/dist/cli/{index-9vt2hknx.js → index-p2m1skjz.js} +1 -1
  41. package/dist/cli/{index-h1g8w4v0.js → index-prknc0xn.js} +129 -44
  42. package/dist/cli/{index-zcmwwp1x.js → index-q8yma345.js} +2 -2
  43. package/dist/cli/{index-35grv4jg.js → index-qypnqmcj.js} +7 -7
  44. package/dist/cli/{index-1sdb57jv.js → index-rq461prn.js} +6 -6
  45. package/dist/cli/{index-b82tq895.js → index-v7wm4mw8.js} +3 -3
  46. package/dist/cli/{index-5zgf140g.js → index-vq4d1h7t.js} +1 -1
  47. package/dist/cli/{index-8fqb0ynp.js → index-w1rhyqw2.js} +1 -1
  48. package/dist/cli/{index-hte1e31d.js → index-wzc6vzq6.js} +2 -2
  49. package/dist/cli/{index-fcp41snp.js → index-y55hqgcj.js} +4 -4
  50. package/dist/cli/index.js +31 -31
  51. package/dist/cli/{knowledge-escalator-x85t1e66.js → knowledge-escalator-rxagfxnk.js} +12 -12
  52. package/dist/cli/{knowledge-events-bwx99mxc.js → knowledge-events-ymwdv799.js} +10 -10
  53. package/dist/cli/{knowledge-link-cj2abbae.js → knowledge-link-n1825n09.js} +5 -5
  54. package/dist/cli/{knowledge-store-21m81zsa.js → knowledge-store-nxm4xvvq.js} +6 -6
  55. package/dist/cli/{knowledge-validator-yxcadg1p.js → knowledge-validator-gtmq35k8.js} +8 -8
  56. package/dist/cli/{pending-delegations-b8qka5nf.js → pending-delegations-zdgazhdf.js} +3 -3
  57. package/dist/cli/{pr-review-reentry-authorization-wv3ck5kh.js → pr-review-reentry-authorization-jgzb0pma.js} +31 -31
  58. package/dist/cli/{pr-subscriptions-j9j70bmd.js → pr-subscriptions-pxv24d5k.js} +4 -4
  59. package/dist/cli/{runner-sj5sf36c.js → runner-fzgkhx6a.js} +6 -6
  60. package/dist/cli/{scan-cursor-m3w0tapb.js → scan-cursor-fasdj7kj.js} +7 -7
  61. package/dist/cli/{schema-0b0dqn28.js → schema-3x28z3rp.js} +1 -1
  62. package/dist/cli/{scope-persistence-199f7yqc.js → scope-persistence-p8dnxfhx.js} +8 -8
  63. package/dist/cli/{skill-generator-a2ekcbe2.js → skill-generator-6tjyffgd.js} +14 -14
  64. package/dist/cli/{telemetry-r8e8zey7.js → telemetry-3rh94wv3.js} +1 -1
  65. package/dist/cli/{worktree-collision-ownership-5qaab3q7.js → worktree-collision-ownership-3bbn1n2r.js} +5 -5
  66. package/dist/cli/{worktree-isolation-esesc842.js → worktree-isolation-1470tnxc.js} +31 -31
  67. package/dist/config/evidence-schema.d.ts +51 -0
  68. package/dist/config/schema.d.ts +20 -0
  69. package/dist/evaluation/contracts.d.ts +5 -5
  70. package/dist/evaluation/ephemeral-agent-dispatcher.d.ts +2 -0
  71. package/dist/evaluation/model-dispatcher.d.ts +2 -2
  72. package/dist/index.js +191 -191
  73. package/dist/observability/catalog.d.ts +5 -3
  74. package/dist/review/contracts.d.ts +2 -1
  75. package/dist/review/evidence.d.ts +12 -0
  76. package/dist/services/cost-accounting.d.ts +100 -0
  77. package/dist/services/status-service.d.ts +12 -0
  78. package/dist/telemetry.d.ts +1 -1
  79. package/dist/tools/sast-baseline.d.ts +120 -3
  80. package/dist/tools/sast-scan.d.ts +25 -1
  81. package/opencode-swarm.schema.json +40 -0
  82. package/package.json +1 -1
  83. package/dist/cli/guardrail-explain-ax2qmsp4.js +0 -67
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The event catalog (issue #2029).
3
3
  *
4
- * Exactly 51 entries, matching the `TelemetryEvent` union at
4
+ * Exactly 54 entries, matching the `TelemetryEvent` union at
5
5
  * `src/telemetry.ts:15-131`. Thirty-eight predate the #2029 contract; the 39th is
6
6
  * `agent_conflict_detected` (previously emitted through a force-cast past the
7
7
  * type system), the 40th is `close_archive_result` (issue #2030 — the
@@ -22,7 +22,9 @@
22
22
  * the bounded `trajectory_health` storage audit for the issue-#2041
23
23
  * `.swarm/trajectories/` PRM session store, and the 51st is the bounded
24
24
  * `pr_subscription_health` storage audit for the issue-#2042
25
- * `.swarm/pr-monitor/` PR-monitor subscription checkpoint store. These late
25
+ * `.swarm/pr-monitor/` subscription checkpoint store. The 52nd is
26
+ * `delegation_cost_correction`, the 53rd is `delegation_cost_binding`, and the
27
+ * 54th is `delegation_cost_join`. These late
26
28
  * additions are instances
27
29
  * of the defect class this contract exists to close: an event kind entering
28
30
  * the stream with no registration.
@@ -78,7 +80,7 @@ export interface CatalogEntry {
78
80
  /**
79
81
  * Whether an event of this kind must carry `trace.parentSpanId`.
80
82
  *
81
- * `false` for all 51 entries today, and that is a truthful statement about
83
+ * `false` for all 54 entries today, and that is a truthful statement about
82
84
  * the current system rather than a placeholder: no producer supplies a
83
85
  * parent span, so `createObservation` never sets one. Setting this to `true`
84
86
  * for a kind whose producer cannot supply a parent would make every
@@ -1,5 +1,6 @@
1
1
  import type { OpencodeClient } from '@opencode-ai/sdk';
2
2
  import { type EphemeralAgentDispatchResult } from '../evaluation/ephemeral-agent-dispatcher.js';
3
+ import type { PricingConfig } from '../services/cost-accounting.js';
3
4
  import type { ModelOverride } from '../utils/model-dispatch-fallback.js';
4
5
  export type ReviewDispatchRequest = {
5
6
  directory: string;
@@ -26,4 +27,4 @@ export interface ReviewModelDispatcher {
26
27
  * The returned dispatcher is instance-local and safe to inject into hooks and
27
28
  * tools; it does not read or mutate module-level client state.
28
29
  */
29
- export declare function createReviewModelDispatcher(client: OpencodeClient): ReviewModelDispatcher;
30
+ export declare function createReviewModelDispatcher(client: OpencodeClient, pricing?: PricingConfig): ReviewModelDispatcher;
@@ -1,7 +1,10 @@
1
+ import * as fs from 'node:fs';
1
2
  import { type FindingValidation, type ReviewFinding } from '../agents/agent-output-schema.js';
2
3
  import type { Plan } from '../config/plan-schema.js';
3
4
  import type { AutoReviewConfig } from '../config/schema.js';
4
5
  import { type ReviewFindingSeverity, type ReviewReceipt } from '../hooks/review-receipt.js';
6
+ import type { CostEvidence, CostEvidenceReason, CostEvidenceStatus } from '../services/cost-accounting.js';
7
+ import { bunWrite } from '../utils/bun-compat.js';
5
8
  import type { ReviewDiffCompleteness, ReviewDiffManifest, ReviewDiffSelector } from './diff-source.js';
6
9
  export interface AutoReviewEvidenceFinding extends ReviewFinding {
7
10
  finding_id: string;
@@ -64,6 +67,10 @@ export interface AutoReviewEvidence {
64
67
  tokens_cache: number;
65
68
  cost_usd: number | null;
66
69
  cost_source: 'reported' | 'estimated' | 'unavailable';
70
+ /** Bounded per-dispatch provenance; scalar totals remain compatibility projections. */
71
+ cost_evidence: CostEvidence[];
72
+ evidence_status: CostEvidenceStatus;
73
+ evidence_reason?: CostEvidenceReason;
67
74
  };
68
75
  }
69
76
  export declare function computeAutoReviewManifestHash(manifest: Omit<AutoReviewScopeManifest, 'hash'>): string;
@@ -72,6 +79,11 @@ export interface PersistAutoReviewEvidenceOptions {
72
79
  /** Rebuild the exact reviewed scope immediately before the atomic rename. */
73
80
  verifyCurrent?: () => Promise<boolean>;
74
81
  }
82
+ /** Dependency seam for failure-path tests; production defaults stay unchanged. */
83
+ export declare const _internals: {
84
+ bunWrite: typeof bunWrite;
85
+ renameSync: typeof fs.renameSync;
86
+ };
75
87
  export declare function persistAutoReviewEvidence(directory: string, evidence: AutoReviewEvidence, options?: PersistAutoReviewEvidenceOptions): Promise<string>;
76
88
  export declare function readAutoReviewEvidenceForPhase(directory: string, phase: number): AutoReviewEvidence | null;
77
89
  export declare function isAutoReviewEvidence(value: unknown): value is AutoReviewEvidence;
@@ -1,4 +1,33 @@
1
1
  export type CostSource = 'reported' | 'estimated' | 'unavailable';
2
+ export type CostEvidenceKind = 'provider_reported' | 'normalized_estimate';
3
+ export type CostEvidenceStatus = 'complete' | 'inconclusive';
4
+ export type CostCurrency = 'USD' | 'unknown' | (string & {});
5
+ export type CostEvidenceReason = 'authoritative' | 'missing_cost' | 'invalid_number' | 'pricing_missing' | 'unsupported_currency' | 'conflict' | 'unbilled' | 'join_miss' | 'legacy' | 'unreadable' | 'partial';
6
+ /** Fixed, non-provider-controlled paths accepted by the evidence adapter. */
7
+ export type CostEvidenceSourcePath = 'assistant.cost' | 'step-finish.cost' | 'response.info.cost' | 'response.cost' | 'legacy.cost' | 'pricing.model';
8
+ export type CostEvidence = {
9
+ kind: CostEvidenceKind;
10
+ amount_usd: number | null;
11
+ currency: CostCurrency;
12
+ source_path: CostEvidenceSourcePath;
13
+ reason: CostEvidenceReason;
14
+ usage: TokenUsage;
15
+ model?: string;
16
+ pricing_version?: string;
17
+ pricing_effective_at?: string;
18
+ billing_basis?: string;
19
+ /** A bounded canonical digest is useful to correction producers. */
20
+ digest?: string;
21
+ };
22
+ export type CostCandidate = {
23
+ kind: CostEvidenceKind;
24
+ amount: number | null;
25
+ currency: CostCurrency;
26
+ source_path: CostEvidenceSourcePath;
27
+ usage: TokenUsage;
28
+ model?: string;
29
+ reason?: CostEvidenceReason;
30
+ };
2
31
  export type ModelPricing = {
3
32
  input_per_million: number;
4
33
  output_per_million: number;
@@ -7,6 +36,14 @@ export type ModelPricing = {
7
36
  };
8
37
  export type PricingConfig = {
9
38
  models?: Record<string, ModelPricing>;
39
+ /** Currency of normalized table rows. Defaults to USD for compatibility. */
40
+ currency?: string;
41
+ version?: string;
42
+ effective_at?: string;
43
+ billing_basis?: string;
44
+ /** Explicit provider declarations; reported currency is never inferred. */
45
+ reported_cost_currency?: Record<string, string>;
46
+ subscription_unbilled?: boolean;
10
47
  };
11
48
  export type TokenUsage = {
12
49
  tokens_input: number;
@@ -17,6 +54,15 @@ export type TokenUsage = {
17
54
  export type DelegationCostFields = TokenUsage & {
18
55
  cost_usd: number | null;
19
56
  cost_source: CostSource;
57
+ cost_evidence?: CostEvidence[];
58
+ evidence_status?: CostEvidenceStatus;
59
+ evidence_reason?: CostEvidenceReason;
60
+ record_id?: string;
61
+ identity_fingerprint?: string;
62
+ child_session_digest?: string;
63
+ parent_session_digest?: string;
64
+ version?: number;
65
+ currency?: CostCurrency;
20
66
  model?: string;
21
67
  gate?: string;
22
68
  retry_index?: number;
@@ -27,6 +73,7 @@ export type DelegationCostInput = {
27
73
  gate?: string;
28
74
  retry_index?: number;
29
75
  pricing?: PricingConfig;
76
+ reported_cost_currency?: string;
30
77
  };
31
78
  export type CostSummary = {
32
79
  total_cost_usd: number;
@@ -46,6 +93,16 @@ export type CostSummary = {
46
93
  delegations: number;
47
94
  cost_usd: number;
48
95
  }>;
96
+ total_legacy_usd: number;
97
+ conflict_count: number;
98
+ legacy_count: number;
99
+ join_miss_count: number;
100
+ telemetry_error_count: number;
101
+ accepted_corrections: number;
102
+ rejected_corrections: number;
103
+ duplicate_corrections: number;
104
+ evidence_status: CostEvidenceStatus;
105
+ currencies: string[];
49
106
  };
50
107
  export type CostSummaryRow = {
51
108
  name: string;
@@ -59,7 +116,50 @@ export type CostSummaryRow = {
59
116
  };
60
117
  export declare const BUNDLED_MODEL_PRICING: Record<string, ModelPricing>;
61
118
  export declare function buildDelegationCostFields(input?: DelegationCostInput): DelegationCostFields;
119
+ /**
120
+ * Extract only the pinned SDK shapes. The old generic traversal was
121
+ * intentionally removed: a wrapper's default cost must not shadow a child
122
+ * AssistantMessage/StepFinishPart report.
123
+ */
124
+ export declare function extractCostEvidence(raw: unknown, input?: Pick<DelegationCostInput, 'model' | 'pricing' | 'reported_cost_currency'>): CostEvidence[];
125
+ export type CostProjection = {
126
+ cost_usd: number | null;
127
+ cost_source: CostSource;
128
+ evidence_status: CostEvidenceStatus;
129
+ reason: CostEvidenceReason;
130
+ currency: CostCurrency;
131
+ };
132
+ /** Project additive evidence to the legacy scalar fields. Unknown is never zero. */
133
+ export declare function projectCostEvidence(evidence: readonly CostEvidence[]): CostProjection;
134
+ export declare function isCostEvidenceComplete(evidence: readonly CostEvidence[] | undefined): boolean;
62
135
  export declare function summarizeTelemetryCosts(directory: string): CostSummary;
136
+ export type CostFoldStats = {
137
+ conflict_count: number;
138
+ join_miss_count: number;
139
+ telemetry_error_count: number;
140
+ accepted_corrections: number;
141
+ rejected_corrections: number;
142
+ duplicate_corrections: number;
143
+ };
144
+ export type FoldedCostTelemetry = {
145
+ events: Record<string, unknown>[];
146
+ stats: CostFoldStats;
147
+ versions: Record<string, number>;
148
+ };
149
+ /**
150
+ * Fold append-only cost corrections into one effective delegation snapshot.
151
+ * Corrections are buffered and replayed in version order so telemetry arrival
152
+ * order cannot permanently discard a valid correction. Invalid/orphan
153
+ * corrections remain diagnostics and can never create a row.
154
+ */
155
+ export declare function foldTelemetryEvents(events: readonly Record<string, unknown>[]): FoldedCostTelemetry;
156
+ export declare function isCostUpgrade(current: readonly CostEvidence[], next: readonly CostEvidence[]): boolean;
63
157
  export declare function readTelemetryEvents(directory: string): Record<string, unknown>[];
158
+ /**
159
+ * Bounded asynchronous telemetry reader for live recovery paths. The status
160
+ * service and offline summaries retain the atomic synchronous snapshot above;
161
+ * recovery must not block event delivery on a full-file synchronous read.
162
+ */
163
+ export declare function readTelemetryEventsAsync(directory: string): Promise<Record<string, unknown>[]>;
64
164
  export declare function estimateCostUsd(usage: TokenUsage, model?: string, pricing?: PricingConfig): number | null;
65
165
  export declare function roundUsd(value: number): number;
@@ -4,6 +4,7 @@ import { loadFullAutoRunState } from '../full-auto/state';
4
4
  import { type RecentEscalation } from '../hooks/knowledge-escalator';
5
5
  import { getActiveFullAutoSessionID, hasActiveFullAuto, hasActiveLeanTurbo } from '../state';
6
6
  import { loadLeanTurboRunState } from '../turbo/lean/state';
7
+ import { summarizeTelemetryCosts } from './cost-accounting.js';
7
8
  /**
8
9
  * Dependency-injection seam for status-service.
9
10
  * Allows tests to intercept Lean Turbo state queries without mock.module leakage.
@@ -14,6 +15,7 @@ export declare const _internals: {
14
15
  hasActiveFullAuto: typeof hasActiveFullAuto;
15
16
  getActiveFullAutoSessionID: typeof getActiveFullAutoSessionID;
16
17
  loadFullAutoRunState: typeof loadFullAutoRunState;
18
+ summarizeTelemetryCosts: typeof summarizeTelemetryCosts;
17
19
  };
18
20
  /**
19
21
  * Structured status data returned by the status service.
@@ -175,6 +177,16 @@ export interface StatusData {
175
177
  * stores surface as typed uncertainty, never partially-trusted counts.
176
178
  */
177
179
  backgroundWork?: BackgroundWorkStatus;
180
+ /** Issue #2043: compatibility total plus provenance completeness. */
181
+ costs?: {
182
+ totalCostUsd: number;
183
+ delegations: number;
184
+ unavailableDelegations: number;
185
+ evidenceStatus: 'complete' | 'inconclusive';
186
+ conflictCount: number;
187
+ joinMissCount: number;
188
+ telemetryErrorCount: number;
189
+ };
178
190
  }
179
191
  /** Issue #2104: opt-in background-work status snapshot for /swarm status. */
180
192
  export interface BackgroundWorkStatus {
@@ -1,6 +1,6 @@
1
1
  import { createObservation, toLegacyTelemetryLine } from './observability/index.js';
2
2
  import type { DelegationCostFields } from './services/cost-accounting.js';
3
- export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'reviewer_gate_decision' | 'phase_changed' | 'budget_updated' | 'context_pruned' | 'model_fallback'
3
+ export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'delegation_cost_correction' | 'delegation_cost_binding' | 'delegation_cost_join' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'reviewer_gate_decision' | 'phase_changed' | 'budget_updated' | 'context_pruned' | 'model_fallback'
4
4
  /**
5
5
  * Issue #2271 bug 4 — a configured agent model id was POSITIVELY confirmed
6
6
  * unresolvable against the live provider catalog (distinct from a runtime
@@ -16,17 +16,44 @@
16
16
  * `${relFile}|${rule_id}|L${line}|UNSTABLE|#${occurrenceIndex}`
17
17
  * Unstable fingerprints are ALWAYS treated as NEW findings (fail-closed).
18
18
  *
19
+ * Reflow identity (schema 1.1.0, issue #2302):
20
+ * The 3-line window hash is sensitive to adjacent-line edits and the
21
+ * occurrence index shifts when an identical same-rule line is inserted
22
+ * above a baselined one — both reclassify an unchanged pre-existing
23
+ * finding as NEW. Every stable entry therefore also records a
24
+ * position-independent reflow key
25
+ * `${relFile}|${rule_id}|${sha256(trimmed flagged line).slice(0,16)}`
26
+ * (aligned 1:1 with fingerprints[] in reflow_keys[]). Diff scans match
27
+ * current findings against the baseline as a multiset of reflow keys
28
+ * BEFORE classifying NEW; matches are reported as `moved` (never gating).
29
+ * A 1.0.0 baseline has no reflow keys (they cannot be reconstructed after
30
+ * the fact — findings_snapshot stores no line content) and degrades to
31
+ * exact matching only until its next capture rewrites it as 1.1.0.
32
+ *
33
+ * Absorption triage (schema 1.1.0, issue #2302):
34
+ * Merging a finding that matches neither the exact fingerprints nor the
35
+ * reflow multiset of the prior baseline is a NOVEL ABSORPTION. EVERY novel
36
+ * absorption — in already-indexed OR first-time-indexed files — requires an
37
+ * explicit refreshRationale; without it the capture is BLOCKED and the
38
+ * baseline is left untouched (fail-closed: a bare failure-response
39
+ * recapture can never silently accept a coder-introduced vulnerability).
40
+ * With a rationale, every absorbed finding records who/when/rationale in
41
+ * triage_log[]. First writes (`status:'written'`) are snapshots, not
42
+ * absorptions, and stay free.
43
+ *
19
44
  * Merge semantics:
20
45
  * On every capture for a set of files, ALL prior fingerprints for those files
21
46
  * are removed (full prune, engine-agnostic) before inserting current findings.
22
47
  * This prevents stale cross-engine fingerprints from causing false-pass verdicts.
23
48
  */
24
49
  import type { SastScanFinding } from './sast-scan';
25
- export declare const BASELINE_SCHEMA_VERSION: "1.0.0";
50
+ export declare const BASELINE_SCHEMA_VERSION: "1.1.0";
51
+ /** Baselines written before issue #2302 — loaded read-only, exact matching only. */
52
+ export declare const LEGACY_BASELINE_SCHEMA_VERSION: "1.0.0";
26
53
  /** Maximum findings to store in baseline (heuristic — open for tuning). */
27
54
  export declare const MAX_BASELINE_FINDINGS = 2000;
28
55
  export interface SastBaselineFile {
29
- schema_version: '1.0.0';
56
+ schema_version: '1.0.0' | '1.1.0';
30
57
  phase: number;
31
58
  created_at: string;
32
59
  updated_at: string;
@@ -35,14 +62,42 @@ export interface SastBaselineFile {
35
62
  files_indexed: string[];
36
63
  /** Fingerprint strings for all indexed findings. */
37
64
  fingerprints: string[];
65
+ /**
66
+ * Position-independent reflow identities aligned 1:1 with fingerprints[]
67
+ * (`${relFile}|${rule_id}|${sha256(trimmed flagged line).slice(0,16)}`,
68
+ * '' for unstable entries). Absent in 1.0.0 files — exact matching only.
69
+ */
70
+ reflow_keys?: string[];
38
71
  /** Full findings snapshot (for auditing / debugging). */
39
72
  findings_snapshot: SastScanFinding[];
73
+ /**
74
+ * Audit trail for every novel absorption into an existing baseline
75
+ * (issue #2302). First writes record no entries — a snapshot is not an
76
+ * acceptance. Entries are dropped when their fingerprint no longer
77
+ * survives in fingerprints[].
78
+ */
79
+ triage_log?: BaselineTriageEntry[];
40
80
  /** True if the snapshot was truncated at MAX_BASELINE_FINDINGS. */
41
81
  truncated: boolean;
42
82
  }
83
+ export interface BaselineTriageEntry {
84
+ /** Fingerprint of the absorbed finding (post-capture form). */
85
+ fingerprint: string;
86
+ /** Canonical relative path of the file containing the absorbed finding. */
87
+ rel_file: string;
88
+ rule_id: string;
89
+ /** Caller-supplied refresh rationale, or the first-time-index auto-rationale. */
90
+ rationale: string;
91
+ /** Session id of the capturing caller, or 'unknown-session'. */
92
+ actor: string;
93
+ /** ISO timestamp of the absorption. */
94
+ absorbed_at: string;
95
+ }
43
96
  export type LoadBaselineResult = {
44
97
  status: 'found';
45
98
  fingerprints: Set<string>;
99
+ /** Reflow keys aligned with the loaded fingerprints (empty for 1.0.0 files). */
100
+ reflowKeys: string[];
46
101
  bundle: SastBaselineFile;
47
102
  } | {
48
103
  status: 'not_found';
@@ -60,6 +115,18 @@ export interface IndexedFinding {
60
115
  index: number;
61
116
  stable: boolean;
62
117
  fingerprint: string;
118
+ /** Position-independent reflow identity ('' when unstable — never reflow-matched). */
119
+ reflowKey: string;
120
+ }
121
+ export interface BaselinePartition {
122
+ preExisting: SastScanFinding[];
123
+ moved: SastScanFinding[];
124
+ newFindings: SastScanFinding[];
125
+ }
126
+ export interface BlockedAbsorptionFinding {
127
+ fingerprint: string;
128
+ rel_file: string;
129
+ rule_id: string;
63
130
  }
64
131
  export type CaptureResult = {
65
132
  status: 'written';
@@ -69,6 +136,14 @@ export type CaptureResult = {
69
136
  status: 'merged';
70
137
  path: string;
71
138
  fingerprint_count: number;
139
+ /** Novel absorptions recorded in triage_log by this capture. */
140
+ absorbed_finding_count: number;
141
+ } | {
142
+ status: 'absorption_blocked';
143
+ path: string;
144
+ /** Novel findings in already-indexed files (bounded to MAX_BLOCKED_LIST). */
145
+ blocked: BlockedAbsorptionFinding[];
146
+ message: string;
72
147
  } | {
73
148
  status: 'error';
74
149
  message: string;
@@ -89,13 +164,34 @@ export declare function normalizeFindingPath(directory: string, file: string): s
89
164
  */
90
165
  export declare function fingerprintFinding(finding: SastScanFinding, directory: string, occurrenceIndex: number): FingerprintResult;
91
166
  /**
92
- * Assign occurrence indices to a batch of findings.
167
+ * Assign occurrence indices and reflow identities to a batch of findings.
93
168
  *
94
169
  * Two findings that produce the same (relFile, rule_id, contentHash) tuple
95
170
  * — e.g., copy-pasted vulnerable lines — receive different indices so they
96
171
  * get distinct fingerprints and can be individually classified.
172
+ *
173
+ * The reflow key hashes only the flagged line's own (trimmed) content, so it
174
+ * survives adjacent-line edits, pure line moves, and occurrence-index shifts
175
+ * — the three fingerprint instabilities fixed by issue #2302. It is '' when
176
+ * the fingerprint is unstable; unstable findings never reflow-match.
97
177
  */
98
178
  export declare function assignOccurrenceIndices(findings: SastScanFinding[], directory: string): IndexedFinding[];
179
+ /**
180
+ * Partition current findings against a loaded baseline (issue #2302).
181
+ *
182
+ * 1. Exact: stable fingerprint present in baselineFingerprints → pre-existing.
183
+ * Each exact match consumes one count of its reflow key — an exact
184
+ * fingerprint match implies the same flagged-line content, so a baseline
185
+ * entry can never absorb both an exact and a reflow match.
186
+ * 2. Reflow: stable finding whose reflow key still has unconsumed baseline
187
+ * counts → moved (same finding, new position/window; never gating).
188
+ * 3. Anything else — including every unstable finding — is NEW (fail-closed).
189
+ *
190
+ * Multiset counting keeps duplicate content honest: a baseline with one
191
+ * `exec(cmd)` line absorbs exactly one current `exec(cmd)` line; a second
192
+ * identical line stays NEW.
193
+ */
194
+ export declare function partitionAgainstBaseline(indexed: IndexedFinding[], baselineFingerprints: Set<string>, baselineReflowKeys: readonly string[]): BaselinePartition;
99
195
  /**
100
196
  * Capture or merge SAST findings into the phase-scoped baseline.
101
197
  *
@@ -105,6 +201,15 @@ export declare function assignOccurrenceIndices(findings: SastScanFinding[], dir
105
201
  * This full-prune (engine-agnostic) prevents stale cross-engine entries from
106
202
  * causing false-pass verdicts on later full-engine diff scans.
107
203
  *
204
+ * Absorption triage (issue #2302):
205
+ * Current findings that match the prior baseline (exact fingerprint or
206
+ * reflow key) are re-fingerprinted mechanically. A finding that matches
207
+ * neither is a NOVEL ABSORPTION: the capture is BLOCKED unless
208
+ * `refreshRationale` is supplied (the baseline is left untouched —
209
+ * fail-closed), for already-indexed AND first-time-indexed files alike.
210
+ * With a rationale, every absorbed finding records who (`actor`)/when/
211
+ * rationale in `triage_log`.
212
+ *
108
213
  * Severity threshold:
109
214
  * Callers MUST pass ALL findings regardless of severity threshold so the
110
215
  * baseline captures the full pre-existing surface. Threshold filtering is
@@ -117,12 +222,23 @@ export declare function assignOccurrenceIndices(findings: SastScanFinding[], dir
117
222
  export declare function captureOrMergeBaseline(directory: string, phase: number, findings: SastScanFinding[], engine: 'tier_a' | 'tier_a+tier_b', scannedFiles: string[], opts?: {
118
223
  force?: boolean;
119
224
  abortSignal?: AbortSignal;
225
+ /** Audited rationale required to absorb novel findings in already-indexed files. */
226
+ refreshRationale?: string;
227
+ /** Identity recorded in triage_log entries (session id of the caller). */
228
+ actor?: string;
120
229
  }): Promise<CaptureResult>;
121
230
  /**
122
231
  * Load the SAST baseline for a given phase.
123
232
  *
124
233
  * Returns 'not_found' when no baseline file exists (first run for phase).
125
234
  * Returns 'invalid_schema' when the file is present but unparseable.
235
+ *
236
+ * Schema versions: 1.1.0 files carry reflow_keys (reflow matching active);
237
+ * legacy 1.0.0 files load read-only with an empty reflowKeys array — exact
238
+ * matching only until the next capture rewrites them as 1.1.0 (reflow keys
239
+ * cannot be reconstructed retroactively; findings_snapshot stores no line
240
+ * content). A present-but-misaligned reflow_keys array is corruption and
241
+ * fails closed to invalid_schema (legacy gating; recoverable by recapture).
126
242
  */
127
243
  export declare function loadBaseline(directory: string, phase: number): LoadBaselineResult;
128
244
  /**
@@ -132,6 +248,7 @@ export declare function loadBaseline(directory: string, phase: number): LoadBase
132
248
  export declare const _internals: {
133
249
  fingerprintFinding: typeof fingerprintFinding;
134
250
  assignOccurrenceIndices: typeof assignOccurrenceIndices;
251
+ partitionAgainstBaseline: typeof partitionAgainstBaseline;
135
252
  captureOrMergeBaseline: typeof captureOrMergeBaseline;
136
253
  loadBaseline: typeof loadBaseline;
137
254
  };
@@ -32,6 +32,16 @@ export interface SastScanInput {
32
32
  * baseline diff mode: only findings absent from the phase baseline fail.
33
33
  */
34
34
  phase?: number;
35
+ /**
36
+ * Audited refresh rationale (issue #2302). Required when a capture would
37
+ * absorb findings that match neither the exact fingerprints nor the
38
+ * reflow identities of the prior baseline for already-indexed files —
39
+ * without it that capture is BLOCKED and the baseline is left untouched.
40
+ * Only meaningful together with capture_baseline: true.
41
+ */
42
+ baseline_refresh_rationale?: string;
43
+ /** Session id of the calling agent, recorded as the triage actor. */
44
+ session_id?: string;
35
45
  /** Host/tool cancellation propagated to optional external scanners. */
36
46
  abort_signal?: AbortSignal;
37
47
  }
@@ -67,17 +77,31 @@ export interface SastScanResult {
67
77
  files_skipped_non_code?: number;
68
78
  };
69
79
  /** 'baseline_captured' when capture_baseline:true succeeded */
70
- status?: 'baseline_captured' | 'baseline_merged';
80
+ status?: 'baseline_captured' | 'baseline_merged' | 'baseline_absorption_blocked';
71
81
  /** Number of findings recorded in the baseline (capture mode only) */
72
82
  finding_count?: number;
83
+ /** Novel findings absorbed into the baseline under a refresh rationale (capture mode only) */
84
+ absorbed_finding_count?: number;
73
85
  /** Findings NOT present in the baseline (diff mode only) */
74
86
  new_findings?: SastScanFinding[];
75
87
  /** Findings that match the baseline (diff mode only) */
76
88
  pre_existing_findings?: SastScanFinding[];
89
+ /**
90
+ * Findings that match the baseline via reflow identity — same finding,
91
+ * new position/window (diff mode only, issue #2302). Reported separately;
92
+ * never gate the verdict.
93
+ */
94
+ moved_findings?: SastScanFinding[];
77
95
  /** True when a baseline was loaded and diff mode was active */
78
96
  baseline_used?: boolean;
79
97
  /** True when pre_existing_findings were truncated to fit result limits */
80
98
  truncated_pre_existing?: boolean;
99
+ /**
100
+ * True when moved_findings were truncated to fit result limits. Advisory
101
+ * telemetry only (no in-tree consumer) — kept for parity with
102
+ * truncated_pre_existing and for future audit consumers.
103
+ */
104
+ truncated_moved_findings?: boolean;
81
105
  }
82
106
  export type SastFailureKind = 'semgrep_process_exit' | 'semgrep_timeout' | 'semgrep_cancelled' | 'semgrep_output_limit' | 'semgrep_spawn_error' | 'semgrep_invalid_output' | 'semgrep_scan_error' | 'semgrep_unexpected' | 'semgrep_unclassified' | 'coverage' | 'cancelled' | 'invalid_input';
83
107
  export interface SastScanFinding {
@@ -746,6 +746,46 @@
746
746
  "description": "Token/cost estimation fallback table. Provider-reported cost wins when present; entries only estimate from usage tokens when reports omit cost.",
747
747
  "type": "object",
748
748
  "properties": {
749
+ "currency": {
750
+ "default": "USD",
751
+ "type": "string",
752
+ "const": "USD"
753
+ },
754
+ "version": {
755
+ "type": "string",
756
+ "minLength": 1,
757
+ "maxLength": 128
758
+ },
759
+ "effective_at": {
760
+ "type": "string",
761
+ "format": "date-time",
762
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
763
+ },
764
+ "billing_basis": {
765
+ "type": "string",
766
+ "enum": [
767
+ "token",
768
+ "request",
769
+ "subscription"
770
+ ]
771
+ },
772
+ "reported_cost_currency": {
773
+ "default": {},
774
+ "type": "object",
775
+ "propertyNames": {
776
+ "type": "string",
777
+ "minLength": 1,
778
+ "maxLength": 128
779
+ },
780
+ "additionalProperties": {
781
+ "type": "string",
782
+ "const": "USD"
783
+ }
784
+ },
785
+ "subscription_unbilled": {
786
+ "default": false,
787
+ "type": "boolean"
788
+ },
749
789
  "models": {
750
790
  "default": {},
751
791
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.159.0",
3
+ "version": "7.159.2",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,67 +0,0 @@
1
- // @bun
2
- import {
3
- handleGuardrailExplain
4
- } from "./index-8jnzp5ct.js";
5
- import"./index-3e8600a7.js";
6
- import"./index-z54qw70y.js";
7
- import"./index-04yrxg0z.js";
8
- import"./index-6e6qxf5q.js";
9
- import"./index-k4arrwrz.js";
10
- import"./index-7s1hmrfh.js";
11
- import"./index-txdagbh7.js";
12
- import"./index-134d35c1.js";
13
- import"./index-nj6rmyeh.js";
14
- import"./index-fcp41snp.js";
15
- import"./index-k48kgm6z.js";
16
- import"./index-xmz69g65.js";
17
- import"./index-70ha9e09.js";
18
- import"./index-c8s9a3zh.js";
19
- import"./index-9ss2m4rs.js";
20
- import"./index-mc9qgkwd.js";
21
- import"./index-zsrtt42s.js";
22
- import"./index-1sdb57jv.js";
23
- import"./index-3f7hqtgg.js";
24
- import"./index-8fqb0ynp.js";
25
- import"./index-vywbn766.js";
26
- import"./index-pfnj5pvs.js";
27
- import"./index-rtry5xyf.js";
28
- import"./index-we3nf5as.js";
29
- import"./index-pvzaryhr.js";
30
- import"./index-e2s08v0q.js";
31
- import"./index-ddszzne4.js";
32
- import"./index-ynjy98vv.js";
33
- import"./index-b82tq895.js";
34
- import"./index-nwkr6tvv.js";
35
- import"./index-556ja8an.js";
36
- import"./index-4fwbbad3.js";
37
- import"./index-b43rt5ph.js";
38
- import"./index-q7sjgr1y.js";
39
- import"./index-erdsawqz.js";
40
- import"./index-35grv4jg.js";
41
- import"./index-hte1e31d.js";
42
- import"./index-gprxjmm7.js";
43
- import"./index-n832052r.js";
44
- import"./index-dtn0pne5.js";
45
- import"./index-z6xqpmqg.js";
46
- import"./index-r3mvsbtq.js";
47
- import"./index-tcbjjppb.js";
48
- import"./index-b1tenv2s.js";
49
- import"./index-9e6fdz2h.js";
50
- import"./index-4rhhvd1a.js";
51
- import"./index-yt75r1er.js";
52
- import"./index-zjygnfay.js";
53
- import"./index-ae75rja9.js";
54
- import"./index-5zgf140g.js";
55
- import"./index-h52hs4e9.js";
56
- import"./index-bk5tah7q.js";
57
- import"./index-zcmwwp1x.js";
58
- import"./index-xagq35tf.js";
59
- import"./index-ed5ykebp.js";
60
- import"./index-h1g8w4v0.js";
61
- import"./index-7g4c7s5r.js";
62
- import"./index-bpmtbmy9.js";
63
- import"./index-zgwm4ryv.js";
64
- import"./index-a76rekgs.js";
65
- export {
66
- handleGuardrailExplain
67
- };