opencode-agent-skill 14.4.0 → 15.0.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 (44) hide show
  1. package/README.md +33 -15
  2. package/docs/PI-COMPAT.md +151 -2
  3. package/docs/V15-MANAGED-RUNTIME.md +128 -0
  4. package/global-config/agents/architect.md +63 -1
  5. package/global-config/agents/executor.md +7 -0
  6. package/global-config/agents/plan-checker.md +8 -1
  7. package/global-config/agents/verifier.md +3 -3
  8. package/global-config/skills/database-engineering/references/workflow.md +15 -0
  9. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
  10. package/lib/activity-deadline.mjs +70 -0
  11. package/lib/adaptive-context-budget.mjs +25 -5
  12. package/lib/affected-tests.mjs +1 -1
  13. package/lib/capability-fabric.mjs +17 -0
  14. package/lib/completion-auditor.mjs +25 -5
  15. package/lib/evidence-store.mjs +92 -8
  16. package/lib/execution-contract.mjs +579 -0
  17. package/lib/leaf-runtime-optimizer.mjs +76 -0
  18. package/lib/pi-child-invocation.mjs +65 -0
  19. package/lib/pi-rpc-pool.mjs +61 -9
  20. package/lib/plan-salvage.mjs +124 -0
  21. package/lib/planning-speed-policy.mjs +131 -0
  22. package/lib/repo-graph.mjs +1 -1
  23. package/lib/repo-inspect.mjs +1 -0
  24. package/lib/runtime-artifacts.mjs +42 -0
  25. package/lib/runtime-events.mjs +33 -1
  26. package/lib/safety.mjs +1 -0
  27. package/lib/semantic-index.mjs +1 -1
  28. package/lib/service-manager.mjs +526 -0
  29. package/lib/session-display.mjs +49 -0
  30. package/lib/task-graph.mjs +87 -0
  31. package/lib/task-policy.mjs +150 -4
  32. package/lib/trajectory.mjs +111 -1
  33. package/lib/turbo-fast-path.mjs +44 -0
  34. package/lib/workspace-fingerprint.mjs +2 -11
  35. package/lib/workspace-root.mjs +49 -0
  36. package/lib/worktree-sandbox.mjs +234 -11
  37. package/package.json +8 -5
  38. package/pi/extensions/ues-child-runtime.ts +145 -2
  39. package/pi/extensions/ues.ts +1760 -160
  40. package/scripts/check-release-consistency.mjs +25 -2
  41. package/scripts/check-source-integrity.mjs +471 -4
  42. package/scripts/eval-pi.mjs +76 -36
  43. package/scripts/smoke-pi-extension.mjs +1 -1
  44. package/pi/prompts/ues-run.md +0 -17
@@ -13,6 +13,11 @@ When the user explicitly chooses the long-horizon workflow (for example with `/u
13
13
  STATE.json
14
14
  EVIDENCE.json
15
15
  EVENTS.jsonl
16
+ EXECUTION_CONTRACT.json
17
+ FINAL_VERDICTS.json
18
+ phases/
19
+ MANIFEST.json
20
+ phase-XX-<slug>.json
16
21
  tasks/
17
22
  reports/
18
23
  ```
@@ -21,11 +26,15 @@ The directory is git-ignored execution state, not hidden reasoning. Store requir
21
26
 
22
27
  ## Machine-enforced gates
23
28
 
24
- The V6 engine enforces three boundaries:
29
+ The runtime enforces seven boundaries:
25
30
 
26
- 1. **Plan gate** — `work plan` leaves the item in `awaiting-plan-approval`. Long/high-risk plans require a structured `plan-verification` receipt bound to the current plan hash before `work approve-plan` succeeds.
27
- 2. **Concurrent state gate** — all mutable `STATE.json` and `EVIDENCE.json` operations use a per-work-item lock plus atomic file replacement, preventing safe-wave executors from losing each other's state.
28
- 3. **Integration gate** — long/high-risk PASS requires a structured `integration-verification` receipt bound to the current workspace fingerprint. Finalization rejects any later workspace change.
31
+ 1. **Inherited-work gate** — snapshot source-facing dirty paths before planning. UES children may not discard them with restore/checkout/stash/clean/reset, and planned writers must declare every touched file.
32
+ 2. **Local-env gate** — local `.env*` runtime inputs are no-write by default; only explicit user authorization permits local env mutation. Repository templates remain writable.
33
+ 3. **Plan gate** — `work plan` leaves the item in `awaiting-plan-approval`. Long/high-risk plans require a structured `plan-verification` receipt bound to the current plan hash before `work approve-plan` succeeds.
34
+ 4. **Phase gate** — explicit execution phases become deterministic previous-phase barriers. Constraint-only phases stay global invariants.
35
+ 5. **Concurrent state gate** — all mutable `STATE.json` and `EVIDENCE.json` operations use a per-work-item lock plus atomic file replacement, preventing safe-wave executors from losing each other's state.
36
+ 6. **Integration gate** — long/high-risk source completion requires a structured `integration-verification` receipt bound to the current workspace fingerprint. Finalization rejects any later workspace change.
37
+ 7. **Verdict matrix gate** — source, runtime, database cleanup and real-device proof remain independent. Missing required evidence is reported as not verified rather than promoted to PASS.
29
38
 
30
39
  ## Pipeline
31
40
 
@@ -33,18 +42,19 @@ The V6 engine enforces three boundaries:
33
42
  2. Write observable acceptance criteria into `SPEC.md`.
34
43
  3. Initialize state with `ocskill work init`.
35
44
  4. Create `PLAN.json` following the plan schema and import it with `ocskill work plan`.
36
- 5. Run `ues-plan-checker`. If it returns PASS, create `work gate-receipt <slug> plan` and persist approval with `work approve-plan --receipt-file ...`.
37
- 6. Use `ocskill task-graph` to compute dependency-safe waves.
38
- 7. For each ready task:
45
+ 5. If the user supplied explicit `PHASE N` sections, preserve every execution phase in the plan and let UES add phase barriers; constraint-only phases remain invariants.
46
+ 6. Run `ues-plan-checker`. If it returns PASS, create `work gate-receipt <slug> plan` and persist approval with `work approve-plan --receipt-file ...`.
47
+ 7. Persist the execution contract/phase artifacts and use `ocskill task-graph` to compute dependency-safe waves.
48
+ 8. For each ready task:
39
49
  - on OpenCode V2, if two or more approved tasks are independent, prefer `ues.dispatch_parallel` so one shared model can execute them concurrently with isolated worktrees, leases, independent verifier sessions and serialized integration;
40
50
  - otherwise prefer `ues.dispatch_task`, which performs `work start`, creates a fresh `ues-executor` session, selects the configured attempt-based model tier, prompts it with a bounded context pack and waits for completion;
41
51
  - inspect the child diff and verification;
42
52
  - record at least one successful `work verify-command` receipt for long/high-risk work;
43
53
  - persist `work complete --evidence ...` or `work fail --reason ...`.
44
- 8. On failure, re-diagnose rather than stacking patches. A later `ues.dispatch_task` attempt can escalate from standard to heavy when configured.
45
- 9. After all tasks complete, run `ues-integration-verifier`.
46
- 10. Persist the verifier's actual result with a structured integration receipt plus `ocskill work verify-integration --receipt-file ...`.
47
- 11. Only a recorded PASS with an unchanged workspace can be finalized.
54
+ 9. On failure, re-diagnose rather than stacking patches. A later `ues.dispatch_task` attempt can escalate from standard to heavy when configured.
55
+ 10. After all tasks complete, run `ues-integration-verifier`.
56
+ 11. Persist the verifier's actual result with a structured integration receipt plus `ocskill work verify-integration --receipt-file ...`.
57
+ 12. Emit the independent final verdict matrix and persist `FINAL_VERDICTS.json`. Only a recorded PASS with an unchanged workspace can be finalized.
48
58
 
49
59
  ## Parallelism
50
60
 
@@ -0,0 +1,70 @@
1
+ function finite(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
2
+ const parsed = Number(value)
3
+ if (!Number.isFinite(parsed)) return fallback
4
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
5
+ }
6
+
7
+ export function createAdaptiveDeadline(options = {}, startedAt = Date.now()) {
8
+ const initialMs = finite(options.hardTimeoutMs, 60_000, 1_000)
9
+ const absoluteMs = Math.max(
10
+ initialMs,
11
+ finite(options.absoluteHardTimeoutMs, initialMs, initialMs),
12
+ )
13
+ const extensionMs = finite(options.activityExtensionMs, 0, 0, absoluteMs)
14
+ const activityWindowMs = finite(
15
+ options.activityWindowMs,
16
+ Math.max(5_000, Math.min(extensionMs || initialMs, 30_000)),
17
+ 1_000,
18
+ absoluteMs,
19
+ )
20
+
21
+ let deadlineAt = startedAt + initialMs
22
+ const absoluteAt = startedAt + absoluteMs
23
+ let extensions = 0
24
+
25
+ function maybeExtend(now = Date.now(), lastActivityAt = 0) {
26
+ if (extensionMs <= 0 || deadlineAt >= absoluteAt) {
27
+ return { extended: false, deadlineAt, absoluteAt, extensions }
28
+ }
29
+ if (now - Number(lastActivityAt || 0) > activityWindowMs) {
30
+ return { extended: false, deadlineAt, absoluteAt, extensions }
31
+ }
32
+ const next = Math.min(absoluteAt, Math.max(deadlineAt, now + extensionMs))
33
+ if (next <= deadlineAt) {
34
+ return { extended: false, deadlineAt, absoluteAt, extensions }
35
+ }
36
+ deadlineAt = next
37
+ extensions += 1
38
+ return { extended: true, deadlineAt, absoluteAt, extensions }
39
+ }
40
+
41
+ function shouldAbort(now = Date.now(), lastActivityAt = 0) {
42
+ if (now < deadlineAt) {
43
+ return { abort: false, deadlineAt, absoluteAt, extensions }
44
+ }
45
+ const extension = maybeExtend(now, lastActivityAt)
46
+ if (extension.extended) {
47
+ return { abort: false, ...extension }
48
+ }
49
+ return {
50
+ abort: true,
51
+ reason: now >= absoluteAt ? "absolute-hard-timeout" : "hard-timeout",
52
+ deadlineAt,
53
+ absoluteAt,
54
+ extensions,
55
+ }
56
+ }
57
+
58
+ return {
59
+ startedAt,
60
+ initialMs,
61
+ absoluteMs,
62
+ extensionMs,
63
+ activityWindowMs,
64
+ get deadlineAt() { return deadlineAt },
65
+ get absoluteAt() { return absoluteAt },
66
+ get extensions() { return extensions },
67
+ maybeExtend,
68
+ shouldAbort,
69
+ }
70
+ }
@@ -59,20 +59,35 @@ export function adaptiveContextBudget(taskPolicy = {}, role = "executor", attemp
59
59
  const profile = String(taskPolicy.executionProfile || taskPolicy.profile?.name || "standard")
60
60
  const highRisk = taskPolicy.risk === "high" || taskPolicy.risk === "critical"
61
61
 
62
- // High-risk work keeps the original evidence budget. Turbo mode must not trade
63
- // away security/payment/schema evidence for latency.
64
- if (highRisk || options.disabled === true) {
62
+ if (options.disabled === true) {
65
63
  return {
66
64
  schemaVersion: 1,
67
65
  budget: base,
68
66
  baseBudget: base,
69
67
  adaptive: false,
70
- reason: highRisk ? "high-risk-preserves-base-budget" : "disabled",
68
+ reason: "disabled",
71
69
  }
72
70
  }
73
71
 
74
72
  const table = ROLE_TARGETS[profile] || ROLE_TARGETS.standard
75
73
  const target = clamp(table?.[role], base, 4_000, base)
74
+ const plannerRole = role === "architect" || role === "plan-checker"
75
+
76
+ // High-risk execution/verification keeps the full evidence ceiling. Read-only
77
+ // planning roles are allowed a bounded context because they can use targeted
78
+ // repository tools and are independently plan-gated before any write begins.
79
+ if (highRisk && !plannerRole) {
80
+ return {
81
+ schemaVersion: 1,
82
+ budget: base,
83
+ baseBudget: base,
84
+ adaptive: false,
85
+ reason: "high-risk-execution-preserves-base-budget",
86
+ profile,
87
+ role,
88
+ attempt: Math.max(1, Math.trunc(Number(attempt || 1))),
89
+ }
90
+ }
76
91
  const normalizedAttempt = Math.max(1, Math.trunc(Number(attempt || 1)))
77
92
 
78
93
  // Failed attempts expand deterministically back toward the policy ceiling.
@@ -92,6 +107,11 @@ export function adaptiveContextBudget(taskPolicy = {}, role = "executor", attemp
92
107
  profile,
93
108
  role,
94
109
  attempt: normalizedAttempt,
95
- reason: budget < base ? "role-bounded-expand-on-failure" : "policy-ceiling",
110
+ reason:
111
+ highRisk && plannerRole
112
+ ? "high-risk-planner-role-bounded"
113
+ : budget < base
114
+ ? "role-bounded-expand-on-failure"
115
+ : "policy-ceiling",
96
116
  }
97
117
  }
@@ -6,7 +6,7 @@ import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
6
6
 
7
7
  const TEST_RE = /(?:^|[/.\\])(?:__tests__|tests?|spec)(?:[/.\\]|$)|\.(?:test|spec|e2e-spec)\.[cm]?[jt]sx?$/i
8
8
  const SOURCE_EXT = new Set([".js",".jsx",".mjs",".cjs",".ts",".tsx",".py",".java",".kt",".kts",".cs",".go",".rs",".dart",".swift"])
9
- const SKIP = new Set([".git","node_modules",".next","dist","build","coverage",".venv","venv","target","Pods","DerivedData",".gradle",".ues-cache",".ues-work",".ues-sandboxes",".ues-traces",".ues-learning",".ues-dashboard",".ues-memory",".ues-evals"])
9
+ const SKIP = new Set([".git","node_modules",".next","dist","build","coverage",".venv","venv","target","Pods","DerivedData",".gradle",".ues-cache",".ues-work",".ues-sandboxes",".ues-traces",".ues-learning",".ues-dashboard",".ues-memory",".ues-evals",".ues-services"])
10
10
  const AFFECTED_TEST_CACHE = new Map()
11
11
  const AFFECTED_TEST_INFLIGHT = new Map()
12
12
 
@@ -129,6 +129,23 @@ export function defaultCapabilityRegistry(root = process.cwd()) {
129
129
  "context.reversible": [
130
130
  { id: "ues-evidence-context", kind: "builtin", priority: 120, quality: 0.98, costClass: "low", latencyClass: "fast", metadata: { reversible: true, searchable: true, levels: ["T1", "T2", "T3", "raw"] } },
131
131
  ],
132
+ "runtime.service": [
133
+ {
134
+ id: "ues-managed-service",
135
+ kind: "builtin",
136
+ priority: 125,
137
+ quality: 0.98,
138
+ costClass: "low",
139
+ latencyClass: "fast",
140
+ metadata: {
141
+ shellFree: true,
142
+ readiness: ["port", "log", "process"],
143
+ actions: ["start", "wait-ready", "status", "logs", "stop", "restart"],
144
+ boundedLifetime: true,
145
+ evidenceLogs: true,
146
+ },
147
+ },
148
+ ],
132
149
  memory: [
133
150
  { id: "ues-memory", kind: "builtin", priority: 100, quality: 0.9, costClass: "low", latencyClass: "fast" },
134
151
  ],
@@ -11,13 +11,30 @@ function passRun(run) {
11
11
  return Boolean(run && Number(run.exitCode) === 0 && run.verdict === "PASS")
12
12
  }
13
13
 
14
- function benignEmptySection(value) {
15
- const normalized = text(value)
14
+ function normalizeSection(value) {
15
+ return text(value)
16
16
  .toLowerCase()
17
17
  .replace(/[`*_#]/g, "")
18
18
  .replace(/[.!:;]+$/g, "")
19
+ .replace(/\s+/g, " ")
19
20
  .trim()
20
- return !normalized || /^(?:none(?: observed| found| remaining)?|n\/a|na|nil|0|no failures?(?: observed| found| remaining)?|no unresolved gaps?(?: observed| found| remaining)?|no gaps?(?: observed| found| remaining)?|not applicable)$/.test(normalized)
21
+ }
22
+
23
+ function benignEmptySection(value) {
24
+ const normalized = normalizeSection(value)
25
+ return !normalized || /^(?:none(?: observed| found| remaining)?|n\/a|na|nil|0|no failures?(?: observed| found| remaining)?|no unresolved gaps?(?: observed| found| remaining)?|no gaps?(?: observed| found| remaining)?|not applicable|không có|không có gì|không có lỗi|không phát hiện lỗi|không còn khoảng trống|không có khoảng trống)$/.test(normalized)
26
+ }
27
+
28
+ function benignFailureSection(value) {
29
+ const normalized = normalizeSection(value)
30
+ return benignEmptySection(normalized) ||
31
+ /^(?:không có|không phát hiện) .{0,120}(?:lỗi|thất bại)(?: nào)?$/.test(normalized)
32
+ }
33
+
34
+ function outOfScopeGapOnly(value) {
35
+ const normalized = normalizeSection(value)
36
+ if (benignEmptySection(normalized)) return true
37
+ return /(?:not requested|outside (?:the )?scope|out of scope|not required|không có yêu cầu|không được yêu cầu|ngoài phạm vi|không thuộc phạm vi)/.test(normalized)
21
38
  }
22
39
 
23
40
  function receiptPassed(row = {}) {
@@ -43,8 +60,11 @@ export function auditCompletion(input = {}) {
43
60
  for (const key of ["checks-run", "acceptance-criteria-proven", "completion-evidence"]) {
44
61
  if (!text(sections[key])) failures.push(`missing-report-section:${key}`)
45
62
  }
46
- if (Object.hasOwn(sections, "failures") && !benignEmptySection(sections.failures)) failures.push("verification-reports-failures")
47
- if (Object.hasOwn(sections, "unresolved-gaps") && !benignEmptySection(sections["unresolved-gaps"])) failures.push("verification-reports-unresolved-gaps")
63
+ if (Object.hasOwn(sections, "failures") && !benignFailureSection(sections.failures)) failures.push("verification-reports-failures")
64
+ if (Object.hasOwn(sections, "unresolved-gaps") && !benignEmptySection(sections["unresolved-gaps"])) {
65
+ if (outOfScopeGapOnly(sections["unresolved-gaps"])) warnings.push("verification-reports-out-of-scope-gap")
66
+ else failures.push("verification-reports-unresolved-gaps")
67
+ }
48
68
  if (Object.hasOwn(sections, "checks-not-run") && !benignEmptySection(sections["checks-not-run"])) warnings.push("verification-reports-checks-not-run")
49
69
 
50
70
  const snapshot = input.workspaceSnapshot || null
@@ -5,6 +5,41 @@ import path from "node:path"
5
5
 
6
6
  const STORE_VERSION = 1
7
7
  const REF_PREFIX = "evidence:sha256:"
8
+ const DEFAULT_MAX_STORE_BYTES = 192 * 1024 * 1024
9
+ const DEFAULT_MAX_STORE_ENTRIES = 1200
10
+ const DEFAULT_MAX_STORE_AGE_DAYS = 14
11
+ const AUTO_GC_EVERY_WRITES = 8
12
+ const AUTO_GC_LARGE_ITEM_BYTES = 16 * 1024 * 1024
13
+ const PUT_COUNTS = new Map()
14
+
15
+ function boundedNumber(value, fallback, min, max) {
16
+ const parsed = Number(value)
17
+ if (!Number.isFinite(parsed)) return fallback
18
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
19
+ }
20
+
21
+ function defaultStoreLimits() {
22
+ return {
23
+ maxBytes: boundedNumber(
24
+ process.env.UES_EVIDENCE_MAX_BYTES,
25
+ DEFAULT_MAX_STORE_BYTES,
26
+ 16 * 1024 * 1024,
27
+ 2 * 1024 * 1024 * 1024,
28
+ ),
29
+ maxEntries: boundedNumber(
30
+ process.env.UES_EVIDENCE_MAX_ENTRIES,
31
+ DEFAULT_MAX_STORE_ENTRIES,
32
+ 100,
33
+ 20_000,
34
+ ),
35
+ maxAgeDays: boundedNumber(
36
+ process.env.UES_EVIDENCE_MAX_AGE_DAYS,
37
+ DEFAULT_MAX_STORE_AGE_DAYS,
38
+ 1,
39
+ 365,
40
+ ),
41
+ }
42
+ }
8
43
 
9
44
  function stableValue(value) {
10
45
  if (Array.isArray(value)) return value.map(stableValue)
@@ -21,6 +56,24 @@ function toBytes(value, options = {}) {
21
56
  return { bytes: Buffer.from(text, "utf8"), encoding: "utf8", mediaType: options.mediaType || "application/json" }
22
57
  }
23
58
 
59
+ async function protectedEvidenceHashes(root) {
60
+ const file = path.join(path.resolve(root), ".ues-memory", "MEMORY.json")
61
+ const parsed = await readFile(file, "utf8")
62
+ .then((raw) => JSON.parse(raw))
63
+ .catch(() => null)
64
+ const protectedHashes = new Set()
65
+ const now = Date.now()
66
+ for (const memory of parsed?.memories || []) {
67
+ if (memory?.status !== "verified" || memory?.supersededBy) continue
68
+ const expiresAt = Date.parse(String(memory?.expiresAt || ""))
69
+ if (expiresAt && expiresAt <= now) continue
70
+ for (const ref of memory?.evidenceRefs || []) {
71
+ try { protectedHashes.add(normalizeHash(ref)) } catch {}
72
+ }
73
+ }
74
+ return protectedHashes
75
+ }
76
+
24
77
  function normalizeHash(ref) {
25
78
  const value = String(ref || "").trim()
26
79
  const hash = value.startsWith(REF_PREFIX) ? value.slice(REF_PREFIX.length) : value.replace(/^sha256:/, "")
@@ -79,6 +132,13 @@ export async function putEvidence(root, value, options = {}) {
79
132
  preview,
80
133
  }
81
134
  await writeFile(target.meta, JSON.stringify(metadata, null, 2) + "\n", "utf8")
135
+
136
+ const writes = (PUT_COUNTS.get(root) || 0) + 1
137
+ PUT_COUNTS.set(root, writes)
138
+ if (writes % AUTO_GC_EVERY_WRITES === 0 || encoded.bytes.length >= AUTO_GC_LARGE_ITEM_BYTES) {
139
+ await gcEvidenceStore(root, defaultStoreLimits()).catch(() => null)
140
+ }
141
+
82
142
  return metadata
83
143
  }
84
144
 
@@ -216,25 +276,44 @@ export async function evidenceStoreStatus(root = process.cwd()) {
216
276
  }
217
277
 
218
278
  export async function gcEvidenceStore(root = process.cwd(), options = {}) {
279
+ root = path.resolve(root)
219
280
  const files = await metadataFiles(root)
220
- const maxEntries = Math.max(10, Number(options.maxEntries || 2_000))
221
- const maxAgeMs = Math.max(0, Number(options.maxAgeDays ?? 30)) * 86_400_000
281
+ const defaults = defaultStoreLimits()
282
+ const maxEntries = Math.max(10, Number(options.maxEntries ?? defaults.maxEntries))
283
+ const maxBytes = Math.max(1024 * 1024, Number(options.maxBytes ?? defaults.maxBytes))
284
+ const maxAgeMs = Math.max(0, Number(options.maxAgeDays ?? defaults.maxAgeDays)) * 86_400_000
222
285
  const now = Date.now()
286
+ const protectedHashes = options.protectMemoryRefs === false
287
+ ? new Set()
288
+ : await protectedEvidenceHashes(root)
223
289
  const rows = []
224
290
  for (const metaFile of files) {
225
291
  const meta = JSON.parse(await readFile(metaFile, "utf8").catch(() => "{}"))
226
292
  const time = Date.parse(meta.lastSeenAt || meta.createdAt || 0) || 0
227
- rows.push({ metaFile, meta, time })
293
+ rows.push({ metaFile, meta, time, bytes: Math.max(0, Number(meta.bytes || 0)) })
228
294
  }
229
295
  rows.sort((a, b) => b.time - a.time)
230
296
 
231
297
  const removed = []
232
- for (let index = 0; index < rows.length; index += 1) {
233
- const row = rows[index]
234
- const tooMany = index >= maxEntries
298
+ let retainedEntries = 0
299
+ let retainedBytes = 0
300
+ let protectedEntries = 0
301
+ let protectedBytes = 0
302
+ for (const row of rows) {
303
+ const hash = normalizeHash(row.meta.sha256 || path.basename(row.metaFile, ".json"))
304
+ const protectedByMemory = protectedHashes.has(hash)
305
+ const tooMany = retainedEntries >= maxEntries
235
306
  const tooOld = maxAgeMs > 0 && row.time > 0 && now - row.time > maxAgeMs
236
- if (!tooMany && !tooOld) continue
237
- const hash = row.meta.sha256 || path.basename(row.metaFile, ".json")
307
+ const tooLarge = retainedBytes + row.bytes > maxBytes
308
+ if (protectedByMemory || (!tooMany && !tooOld && !tooLarge)) {
309
+ retainedEntries += 1
310
+ retainedBytes += row.bytes
311
+ if (protectedByMemory) {
312
+ protectedEntries += 1
313
+ protectedBytes += row.bytes
314
+ }
315
+ continue
316
+ }
238
317
  const target = evidencePaths(root, hash)
239
318
  await rm(target.meta, { force: true })
240
319
  await rm(target.data, { force: true })
@@ -244,6 +323,11 @@ export async function gcEvidenceStore(root = process.cwd(), options = {}) {
244
323
  return {
245
324
  removed,
246
325
  removedCount: removed.length,
326
+ retainedEntries,
327
+ retainedBytes,
328
+ protectedEntries,
329
+ protectedBytes,
330
+ limits: { maxEntries, maxBytes, maxAgeMs },
247
331
  status: await evidenceStoreStatus(root),
248
332
  }
249
333
  }