harnery 0.36.0 → 0.37.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.
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +87 -46
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +26 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +11 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.d.ts.map +1 -0
- package/dist/commands/diagnostics.js +229 -0
- package/dist/commands/files.d.ts +12 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -1
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +4 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +71 -2
- package/dist/commands/qa-run.d.ts +4 -0
- package/dist/commands/qa-run.d.ts.map +1 -0
- package/dist/commands/qa-run.js +141 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +29 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +101 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -0
- package/dist/core/agents/cli.js +7 -3
- package/dist/core/agents/health.d.ts +17 -0
- package/dist/core/agents/health.d.ts.map +1 -0
- package/dist/core/agents/health.js +109 -0
- package/dist/core/agents/mailbox.d.ts +92 -0
- package/dist/core/agents/mailbox.d.ts.map +1 -0
- package/dist/core/agents/mailbox.js +325 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
- package/dist/core/agents/reconcile-coordination-v3.js +58 -0
- package/dist/core/agents/render/prompt-context.d.ts +5 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +34 -13
- package/dist/core/agents/render/session-context.d.ts +8 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +34 -3
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +50 -1
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +6 -10
- package/dist/core/agents/session-finalizer-v3.js +4 -1
- package/dist/core/artifacts/index.d.ts +26 -1
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +146 -6
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +43 -0
- package/dist/core/diagnostics/advice.d.ts +9 -0
- package/dist/core/diagnostics/advice.d.ts.map +1 -0
- package/dist/core/diagnostics/advice.js +112 -0
- package/dist/core/diagnostics/bundle.d.ts +35 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -0
- package/dist/core/diagnostics/bundle.js +874 -0
- package/dist/core/diagnostics/comparison.d.ts +5 -0
- package/dist/core/diagnostics/comparison.d.ts.map +1 -0
- package/dist/core/diagnostics/comparison.js +444 -0
- package/dist/core/diagnostics/contract.d.ts +245 -0
- package/dist/core/diagnostics/contract.d.ts.map +1 -0
- package/dist/core/diagnostics/contract.js +18 -0
- package/dist/core/diagnostics/identity.d.ts +3 -0
- package/dist/core/diagnostics/identity.d.ts.map +1 -0
- package/dist/core/diagnostics/identity.js +10 -0
- package/dist/core/diagnostics/index.d.ts +8 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -0
- package/dist/core/diagnostics/index.js +7 -0
- package/dist/core/diagnostics/replay.d.ts +8 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -0
- package/dist/core/diagnostics/replay.js +213 -0
- package/dist/core/diagnostics/sanitize.d.ts +9 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
- package/dist/core/diagnostics/sanitize.js +84 -0
- package/dist/core/events/legacy-storage/compression.d.ts +13 -0
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
- package/dist/core/events/legacy-storage/compression.js +107 -0
- package/dist/core/events/legacy-storage/index.d.ts +1 -0
- package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/index.js +1 -0
- package/dist/core/events/v3/archive-retention.d.ts +33 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
- package/dist/core/events/v3/archive-retention.js +195 -0
- package/dist/core/events/v3/capabilities.d.ts +1 -1
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +1 -0
- package/dist/core/events/v3/index.d.ts +1 -0
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +13 -2
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +1 -0
- package/dist/core/events/v3/producers/recorder.d.ts +16 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +88 -1
- package/dist/core/hooks/adapter/output.d.ts +8 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +11 -1
- package/dist/core/hooks/cli.js +183 -192
- package/dist/core/hooks/health.d.ts +59 -0
- package/dist/core/hooks/health.d.ts.map +1 -0
- package/dist/core/hooks/health.js +84 -0
- package/dist/core/resources/contract.d.ts +90 -0
- package/dist/core/resources/contract.d.ts.map +1 -0
- package/dist/core/resources/contract.js +2 -0
- package/dist/core/resources/index.d.ts +6 -0
- package/dist/core/resources/index.d.ts.map +1 -0
- package/dist/core/resources/index.js +5 -0
- package/dist/core/resources/sampler.d.ts +27 -0
- package/dist/core/resources/sampler.d.ts.map +1 -0
- package/dist/core/resources/sampler.js +357 -0
- package/dist/core/resources/service-status.d.ts +3 -0
- package/dist/core/resources/service-status.d.ts.map +1 -0
- package/dist/core/resources/service-status.js +48 -0
- package/dist/core/resources/service.d.ts +26 -0
- package/dist/core/resources/service.d.ts.map +1 -0
- package/dist/core/resources/service.js +292 -0
- package/dist/core/resources/storage.d.ts +13 -0
- package/dist/core/resources/storage.d.ts.map +1 -0
- package/dist/core/resources/storage.js +30 -0
- package/dist/core/storage/atomic-json.d.ts +3 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -0
- package/dist/core/storage/atomic-json.js +15 -0
- package/dist/core/storage/builtins.d.ts.map +1 -1
- package/dist/core/storage/builtins.js +45 -6
- package/dist/core/storage/logger.d.ts +2 -0
- package/dist/core/storage/logger.d.ts.map +1 -1
- package/dist/core/storage/logger.js +2 -0
- package/dist/core/storage/query.d.ts +14 -0
- package/dist/core/storage/query.d.ts.map +1 -1
- package/dist/core/storage/query.js +51 -0
- package/dist/core/supervisor/activity.d.ts +4 -0
- package/dist/core/supervisor/activity.d.ts.map +1 -0
- package/dist/core/supervisor/activity.js +141 -0
- package/dist/core/supervisor/contract.d.ts +268 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -0
- package/dist/core/supervisor/contract.js +35 -0
- package/dist/core/supervisor/explanations.d.ts +4 -0
- package/dist/core/supervisor/explanations.d.ts.map +1 -0
- package/dist/core/supervisor/explanations.js +71 -0
- package/dist/core/supervisor/findings.d.ts +20 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -0
- package/dist/core/supervisor/findings.js +399 -0
- package/dist/core/supervisor/history.d.ts +9 -0
- package/dist/core/supervisor/history.d.ts.map +1 -0
- package/dist/core/supervisor/history.js +46 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-alerts.js +70 -0
- package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
- package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-storage.js +24 -0
- package/dist/core/supervisor/hook-health.d.ts +72 -0
- package/dist/core/supervisor/hook-health.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health.js +203 -0
- package/dist/core/supervisor/hooks.d.ts +10 -0
- package/dist/core/supervisor/hooks.d.ts.map +1 -0
- package/dist/core/supervisor/hooks.js +28 -0
- package/dist/core/supervisor/index.d.ts +15 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +14 -0
- package/dist/core/supervisor/log-feed.d.ts +9 -0
- package/dist/core/supervisor/log-feed.d.ts.map +1 -0
- package/dist/core/supervisor/log-feed.js +103 -0
- package/dist/core/supervisor/service.d.ts +29 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +450 -0
- package/dist/core/supervisor/services.d.ts +11 -0
- package/dist/core/supervisor/services.d.ts.map +1 -0
- package/dist/core/supervisor/services.js +155 -0
- package/dist/core/supervisor/status.d.ts +3 -0
- package/dist/core/supervisor/status.d.ts.map +1 -0
- package/dist/core/supervisor/status.js +48 -0
- package/dist/core/supervisor/storage.d.ts +30 -0
- package/dist/core/supervisor/storage.d.ts.map +1 -0
- package/dist/core/supervisor/storage.js +73 -0
- package/dist/core/supervisor/timeline.d.ts +3 -0
- package/dist/core/supervisor/timeline.d.ts.map +1 -0
- package/dist/core/supervisor/timeline.js +87 -0
- package/dist/core/workflow/admission.d.ts +23 -0
- package/dist/core/workflow/admission.d.ts.map +1 -0
- package/dist/core/workflow/admission.js +136 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +77 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +39 -1
- package/dist/core/workflow/run-state.d.ts +3 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +8 -1
- package/dist/core/workflow/types.d.ts +33 -1
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +2 -1
- package/dist/lib/browser/client.d.ts +5 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +1 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
- package/dist/lib/browser/qa-run-contracts.js +226 -0
- package/dist/lib/browser/qa-run.d.ts +51 -0
- package/dist/lib/browser/qa-run.d.ts.map +1 -0
- package/dist/lib/browser/qa-run.js +684 -0
- package/dist/lib/coord-root-id.d.ts +9 -0
- package/dist/lib/coord-root-id.d.ts.map +1 -0
- package/dist/lib/coord-root-id.js +17 -0
- package/dist/lib/instructions/apply.d.ts +3 -1
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +74 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +6 -0
- package/package.json +12 -1
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +102 -49
- package/src/commands/artifacts.ts +40 -2
- package/src/commands/browse.ts +15 -0
- package/src/commands/diagnostics.ts +271 -0
- package/src/commands/files.ts +102 -2
- package/src/commands/governor.ts +8 -0
- package/src/commands/ledger-v3.ts +82 -0
- package/src/commands/qa-run.ts +184 -0
- package/src/commands/resources.ts +38 -0
- package/src/commands/supervisor.ts +141 -0
- package/src/commands/web.ts +19 -0
- package/src/commands/workflow.ts +8 -0
- package/src/core/agents/cli.ts +7 -3
- package/src/core/agents/health.ts +142 -0
- package/src/core/agents/mailbox.ts +373 -0
- package/src/core/agents/reconcile-coordination-v3.ts +77 -0
- package/src/core/agents/render/prompt-context.ts +42 -12
- package/src/core/agents/render/session-context.ts +37 -3
- package/src/core/agents/rules/commit-conflict.ts +58 -1
- package/src/core/agents/rules/stop-hook.ts +6 -12
- package/src/core/agents/session-finalizer-v3.ts +4 -1
- package/src/core/artifacts/index.ts +196 -10
- package/src/core/config.ts +111 -2
- package/src/core/diagnostics/advice.ts +147 -0
- package/src/core/diagnostics/bundle.ts +1142 -0
- package/src/core/diagnostics/comparison.ts +565 -0
- package/src/core/diagnostics/contract.ts +311 -0
- package/src/core/diagnostics/identity.ts +11 -0
- package/src/core/diagnostics/index.ts +7 -0
- package/src/core/diagnostics/replay.ts +314 -0
- package/src/core/diagnostics/sanitize.ts +97 -0
- package/src/core/events/legacy-storage/compression.ts +130 -0
- package/src/core/events/legacy-storage/index.ts +1 -0
- package/src/core/events/v3/archive-retention.ts +251 -0
- package/src/core/events/v3/capabilities.ts +2 -0
- package/src/core/events/v3/index.ts +9 -0
- package/src/core/events/v3/live-routing.ts +1 -0
- package/src/core/events/v3/producers/hook-base.ts +14 -2
- package/src/core/events/v3/producers/intake.ts +1 -0
- package/src/core/events/v3/producers/recorder.ts +110 -1
- package/src/core/hooks/adapter/output.ts +12 -1
- package/src/core/hooks/cli.ts +192 -215
- package/src/core/hooks/health.ts +146 -0
- package/src/core/resources/contract.ts +98 -0
- package/src/core/resources/index.ts +5 -0
- package/src/core/resources/sampler.ts +458 -0
- package/src/core/resources/service-status.ts +62 -0
- package/src/core/resources/service.ts +337 -0
- package/src/core/resources/storage.ts +44 -0
- package/src/core/storage/atomic-json.ts +15 -0
- package/src/core/storage/builtins.ts +62 -6
- package/src/core/storage/logger.ts +2 -0
- package/src/core/storage/query.ts +58 -0
- package/src/core/supervisor/activity.ts +193 -0
- package/src/core/supervisor/contract.ts +312 -0
- package/src/core/supervisor/explanations.ts +104 -0
- package/src/core/supervisor/findings.ts +788 -0
- package/src/core/supervisor/history.ts +61 -0
- package/src/core/supervisor/hook-health-alerts.ts +108 -0
- package/src/core/supervisor/hook-health-storage.ts +25 -0
- package/src/core/supervisor/hook-health.ts +301 -0
- package/src/core/supervisor/hooks.ts +35 -0
- package/src/core/supervisor/index.ts +14 -0
- package/src/core/supervisor/log-feed.ts +131 -0
- package/src/core/supervisor/service.ts +511 -0
- package/src/core/supervisor/services.ts +203 -0
- package/src/core/supervisor/status.ts +59 -0
- package/src/core/supervisor/storage.ts +126 -0
- package/src/core/supervisor/timeline.ts +133 -0
- package/src/core/workflow/admission.ts +215 -0
- package/src/core/workflow/engine.ts +93 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +54 -1
- package/src/core/workflow/run-state.ts +13 -1
- package/src/core/workflow/types.ts +39 -1
- package/src/lib/browser/client.ts +6 -0
- package/src/lib/browser/index.ts +29 -0
- package/src/lib/browser/qa-run-contracts.ts +380 -0
- package/src/lib/browser/qa-run.ts +784 -0
- package/src/lib/coord-root-id.ts +23 -0
- package/src/lib/instructions/apply.ts +81 -2
- package/src/lib/instructions/templates.ts +6 -0
|
@@ -25,6 +25,7 @@ import { appendFileSync, existsSync } from "node:fs";
|
|
|
25
25
|
import { basename, dirname, join } from "node:path";
|
|
26
26
|
import { coordFreshnessSeconds } from "../../config.ts";
|
|
27
27
|
import { resolveOwner } from "../../hooks/resolve/owner.ts";
|
|
28
|
+
import { recordLiveClaimChangeV3 } from "../live-authority-v3.ts";
|
|
28
29
|
import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
|
|
29
30
|
import { instanceHasLivePid } from "../state/pidmap.ts";
|
|
30
31
|
|
|
@@ -34,6 +35,7 @@ interface PeerHeartbeat {
|
|
|
34
35
|
name?: string;
|
|
35
36
|
files_touched?: string[];
|
|
36
37
|
last_heartbeat?: string;
|
|
38
|
+
platform?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
interface Conflict {
|
|
@@ -190,6 +192,15 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
190
192
|
};
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
// A blocked commit proves this session has work in every staged path, even
|
|
196
|
+
// when the edit hook never acquired a claim (for example, a shell edit made
|
|
197
|
+
// just before another session claimed the same file). Keep those paths on
|
|
198
|
+
// the blocked session's durable claim set. The original holder's later
|
|
199
|
+
// commit will then see the overlap instead of silently sweeping this
|
|
200
|
+
// session's working-tree hunks. A successful later commit releases clean
|
|
201
|
+
// paths through the existing post-commit hook.
|
|
202
|
+
retainBlockedStagedPaths(coordRoot, me, mySession, req.staged_paths, peers);
|
|
203
|
+
|
|
193
204
|
return {
|
|
194
205
|
allow: false,
|
|
195
206
|
exit_code: 1,
|
|
@@ -204,6 +215,41 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
204
215
|
};
|
|
205
216
|
}
|
|
206
217
|
|
|
218
|
+
function retainBlockedStagedPaths(
|
|
219
|
+
coordRoot: string,
|
|
220
|
+
instanceId: string,
|
|
221
|
+
sessionId: string,
|
|
222
|
+
stagedPaths: readonly string[],
|
|
223
|
+
peers: readonly PeerHeartbeat[],
|
|
224
|
+
): void {
|
|
225
|
+
if (instanceId === "__unattributed__") return;
|
|
226
|
+
const self = peers.find((peer) => peer.instance_id === instanceId);
|
|
227
|
+
if (!self) return;
|
|
228
|
+
|
|
229
|
+
for (const path of stagedPaths) {
|
|
230
|
+
try {
|
|
231
|
+
recordLiveClaimChangeV3({
|
|
232
|
+
coordRoot,
|
|
233
|
+
owner: instanceId,
|
|
234
|
+
nativeSessionId: self.session_id ?? sessionId,
|
|
235
|
+
adapter: adapterFromPlatform(self.platform),
|
|
236
|
+
operation: "acquired",
|
|
237
|
+
path,
|
|
238
|
+
access: "write",
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
// The commit is already blocked. Coordination remains fail-open for
|
|
242
|
+
// authority-write failures so a damaged cache cannot brick Git.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function adapterFromPlatform(platform: unknown): "claude-code" | "cursor" | "codex" {
|
|
248
|
+
if (platform === "cursor") return "cursor";
|
|
249
|
+
if (platform === "codex") return "codex";
|
|
250
|
+
return "claude-code";
|
|
251
|
+
}
|
|
252
|
+
|
|
207
253
|
function findOverlap(
|
|
208
254
|
staged: string,
|
|
209
255
|
files: readonly string[],
|
|
@@ -269,6 +315,18 @@ function isHolderSelfAttributed(
|
|
|
269
315
|
stagedSet: Set<string>,
|
|
270
316
|
peers: readonly PeerHeartbeat[],
|
|
271
317
|
): boolean {
|
|
318
|
+
const holder = peers.find((p) => p.instance_id === holderId);
|
|
319
|
+
|
|
320
|
+
// Windows-hosted Codex sessions cross a process boundary into WSL, so they
|
|
321
|
+
// deliberately have no usable PID anchor. Their canonical V3 session row is
|
|
322
|
+
// the foreign-identity evidence instead. Treating that row as a transient
|
|
323
|
+
// self identity would suppress the symmetric claim retained after a blocked
|
|
324
|
+
// commit and reopen the working-tree sweep this guard is meant to stop.
|
|
325
|
+
if (holder?.platform === "codex" && holder.session_id) {
|
|
326
|
+
debugGate(coordRoot, { holder: holderId, gate: "B", reason: "canonical_codex_session" });
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
272
330
|
// Gate B (cheaper): live foreign pid-map entry blocks self-attribution.
|
|
273
331
|
if (holderHasLiveForeignPid(coordRoot, holderId)) {
|
|
274
332
|
debugGate(coordRoot, { holder: holderId, gate: "B", reason: "live_pid_anchor" });
|
|
@@ -276,7 +334,6 @@ function isHolderSelfAttributed(
|
|
|
276
334
|
}
|
|
277
335
|
|
|
278
336
|
// Gate A: every held path is either in the staged set or already clean in HEAD.
|
|
279
|
-
const holder = peers.find((p) => p.instance_id === holderId);
|
|
280
337
|
if (!holder) {
|
|
281
338
|
debugGate(coordRoot, { holder: holderId, gate: "A", reason: "holder_not_found" });
|
|
282
339
|
return false;
|
|
@@ -154,22 +154,16 @@ export function evaluateStopHookV3Events(
|
|
|
154
154
|
.map((event) => observedBoolean(event.payload.ritual?.status_box_present_strict))
|
|
155
155
|
.filter((value): value is boolean => value !== undefined);
|
|
156
156
|
const currentStrictBox = claudeContinuation ? req.status_box_present_strict : undefined;
|
|
157
|
-
if (
|
|
157
|
+
if (latestBox === undefined && currentStrictBox === undefined) {
|
|
158
158
|
return evidenceUnavailable(
|
|
159
159
|
"turn.completed has no observed V3 assistant-text status-box result; Stop remains fail-open",
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const ackPresent =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
? strictBoxObservations.includes(true) || currentStrictBox === true
|
|
168
|
-
: latestBox === true;
|
|
169
|
-
const ackBlock =
|
|
170
|
-
req.adapter === "cursor"
|
|
171
|
-
? () => rule13Block(coordRoot, req.session_id)
|
|
172
|
-
: () => rule23Block(coordRoot, req.session_id);
|
|
163
|
+
const ackPresent = claudeContinuation
|
|
164
|
+
? strictBoxObservations.includes(true) || currentStrictBox === true
|
|
165
|
+
: latestBox === true;
|
|
166
|
+
const ackBlock = () => rule23Block(coordRoot, req.session_id);
|
|
173
167
|
|
|
174
168
|
// Pure prose turns keep the human-visible acknowledgement but do not require
|
|
175
169
|
// task or final-status mutations. This preserves the pre-V3 policy exactly.
|
|
@@ -354,7 +348,7 @@ function sessionIdSuffix(sessionId?: string): string {
|
|
|
354
348
|
* The verdict keeps the first failure's rule id, so the `blocked_rule`
|
|
355
349
|
* telemetry and the adapter's `rule=` stderr contract are unchanged; the
|
|
356
350
|
* reason enumerates the rest so a single continuation can repair all of them.
|
|
357
|
-
*
|
|
351
|
+
* Duplicate rules are folded so one continuation repairs every missing signal.
|
|
358
352
|
*/
|
|
359
353
|
function mergeStopBlocks(blocks: VerdictResult[]): VerdictResult | undefined {
|
|
360
354
|
let primary: VerdictResult | undefined;
|
|
@@ -584,7 +584,10 @@ function observeCascades(
|
|
|
584
584
|
}
|
|
585
585
|
if (
|
|
586
586
|
event.event_type === "lifecycle.sweep_observed" &&
|
|
587
|
-
|
|
587
|
+
// stale-sweep.ts emits the observation as `stale_heartbeat`; the
|
|
588
|
+
// finalization request it produces is the one named `stale_sweep`.
|
|
589
|
+
// Matching the request name here made this branch unreachable.
|
|
590
|
+
event.payload.observation === "stale_heartbeat"
|
|
588
591
|
) {
|
|
589
592
|
const record = records.find(
|
|
590
593
|
({ state }) => !state.terminal && state.instance_id === event.payload.subject_instance_id,
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
artifactAutoCleanEnabled,
|
|
28
28
|
artifactAutoCleanIntervalHours,
|
|
29
29
|
artifactDefaultRetentionDays,
|
|
30
|
+
artifactMaxBytes,
|
|
31
|
+
artifactMaxUnitBytes,
|
|
30
32
|
} from "../config.ts";
|
|
31
33
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
|
|
32
34
|
|
|
@@ -52,12 +54,15 @@ export interface ArtifactManifestV1 {
|
|
|
52
54
|
};
|
|
53
55
|
released_at?: string;
|
|
54
56
|
released_by?: ArtifactActor;
|
|
57
|
+
oversize_acknowledged?: boolean;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export type ArtifactClassification =
|
|
58
61
|
| "managed-active"
|
|
59
62
|
| "managed-current"
|
|
60
63
|
| "managed-expired"
|
|
64
|
+
| "managed-oversize"
|
|
65
|
+
| "managed-over-budget"
|
|
61
66
|
| "managed-tracked"
|
|
62
67
|
| "invalid-manifest"
|
|
63
68
|
| "unmanaged"
|
|
@@ -78,6 +83,7 @@ export interface ArtifactInventoryEntry {
|
|
|
78
83
|
last_modified_at: string | null;
|
|
79
84
|
expires_at: string | null;
|
|
80
85
|
owner_instance_id: string | null;
|
|
86
|
+
oversize_acknowledged: boolean;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
export interface ArtifactCreateInput {
|
|
@@ -87,6 +93,7 @@ export interface ArtifactCreateInput {
|
|
|
87
93
|
actor?: ArtifactActor;
|
|
88
94
|
now?: Date;
|
|
89
95
|
id?: string;
|
|
96
|
+
big?: boolean;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export interface ArtifactMutationInput {
|
|
@@ -94,6 +101,15 @@ export interface ArtifactMutationInput {
|
|
|
94
101
|
now?: Date;
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
export interface ArtifactAdoptionResult {
|
|
105
|
+
candidates: Array<{ path: string; name: string; bytes: number; kind: "file" | "directory" }>;
|
|
106
|
+
candidate_bytes: number;
|
|
107
|
+
requires_big: boolean;
|
|
108
|
+
adopted_artifact_id: string | null;
|
|
109
|
+
adopted_path: string | null;
|
|
110
|
+
adopted_directories: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
interface ParsedManifest {
|
|
98
114
|
ok: true;
|
|
99
115
|
manifest: ArtifactManifestV1;
|
|
@@ -145,6 +161,7 @@ export function createArtifact(
|
|
|
145
161
|
retention: {
|
|
146
162
|
expires_at: addDays(now, retentionDays).toISOString(),
|
|
147
163
|
},
|
|
164
|
+
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
148
165
|
};
|
|
149
166
|
atomicWriteManifest(path, manifest);
|
|
150
167
|
return { path, manifest };
|
|
@@ -177,7 +194,7 @@ export function inventoryArtifacts(
|
|
|
177
194
|
for (const name of names) {
|
|
178
195
|
rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
|
|
179
196
|
}
|
|
180
|
-
return rows;
|
|
197
|
+
return applyArtifactBudgets(repoRoot, rows);
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
export function showArtifact(
|
|
@@ -186,12 +203,8 @@ export function showArtifact(
|
|
|
186
203
|
opts: { now?: Date; freshnessSeconds?: number } = {},
|
|
187
204
|
): { entry: ArtifactInventoryEntry; manifest: ArtifactManifestV1 } {
|
|
188
205
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
189
|
-
const entry =
|
|
190
|
-
|
|
191
|
-
path,
|
|
192
|
-
opts.now ?? new Date(),
|
|
193
|
-
opts.freshnessSeconds ?? 600,
|
|
194
|
-
);
|
|
206
|
+
const entry = inventoryArtifacts(repoRoot, opts).find((row) => row.path === path);
|
|
207
|
+
if (!entry) throw new Error(`artifact "${ref}" was not found`);
|
|
195
208
|
const parsed = readManifest(path);
|
|
196
209
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
197
210
|
return { entry, manifest: parsed.manifest };
|
|
@@ -243,6 +256,108 @@ export function releaseArtifact(
|
|
|
243
256
|
return manifest;
|
|
244
257
|
}
|
|
245
258
|
|
|
259
|
+
/** Adopt untracked loose files and legacy directories without changing directory paths. */
|
|
260
|
+
export function adoptUnmanagedArtifactFiles(
|
|
261
|
+
repoRoot: string,
|
|
262
|
+
input: {
|
|
263
|
+
yes?: boolean;
|
|
264
|
+
big?: boolean;
|
|
265
|
+
purpose: string;
|
|
266
|
+
retentionDays: number;
|
|
267
|
+
actor?: ArtifactActor;
|
|
268
|
+
now?: Date;
|
|
269
|
+
},
|
|
270
|
+
): ArtifactAdoptionResult {
|
|
271
|
+
const now = input.now ?? new Date();
|
|
272
|
+
const candidates: ArtifactAdoptionResult["candidates"] = [];
|
|
273
|
+
for (const row of inventoryArtifacts(repoRoot, { now })) {
|
|
274
|
+
if (row.classification !== "unmanaged") continue;
|
|
275
|
+
try {
|
|
276
|
+
const stat = lstatSync(row.path);
|
|
277
|
+
if (stat.isSymbolicLink() || containsTrackedPath(repoRoot, row.path)) continue;
|
|
278
|
+
if (stat.isFile()) {
|
|
279
|
+
candidates.push({ path: row.path, name: row.name, bytes: stat.size, kind: "file" });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (stat.isDirectory() && !existsSync(join(row.path, ARTIFACT_MANIFEST))) {
|
|
283
|
+
const bytes = safeTreeSize(row.path);
|
|
284
|
+
if (bytes !== null && normalizeSlug(row.name)) {
|
|
285
|
+
candidates.push({ path: row.path, name: row.name, bytes, kind: "directory" });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// A racing or unreadable entry stays unmanaged.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const candidateBytes = candidates.reduce((sum, row) => sum + row.bytes, 0);
|
|
293
|
+
const fileBytes = candidates.reduce((sum, row) => sum + (row.kind === "file" ? row.bytes : 0), 0);
|
|
294
|
+
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
295
|
+
const requiresBig =
|
|
296
|
+
fileBytes > maxUnitBytes ||
|
|
297
|
+
candidates.some((row) => row.kind === "directory" && row.bytes > maxUnitBytes);
|
|
298
|
+
const preview: ArtifactAdoptionResult = {
|
|
299
|
+
candidates,
|
|
300
|
+
candidate_bytes: candidateBytes,
|
|
301
|
+
requires_big: requiresBig,
|
|
302
|
+
adopted_artifact_id: null,
|
|
303
|
+
adopted_path: null,
|
|
304
|
+
adopted_directories: 0,
|
|
305
|
+
};
|
|
306
|
+
if (!input.yes || candidates.length === 0) return preview;
|
|
307
|
+
if (requiresBig && !input.big) {
|
|
308
|
+
throw new Error("unmanaged adoption exceeds the per-bundle ceiling; repeat with --big");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Revalidate every exact source before creating a destination. A changed,
|
|
312
|
+
// tracked, linked, or non-regular entry aborts the whole adoption.
|
|
313
|
+
for (const candidate of candidates) {
|
|
314
|
+
const stat = lstatSync(candidate.path);
|
|
315
|
+
const bytes = stat.isDirectory() ? safeTreeSize(candidate.path) : stat.size;
|
|
316
|
+
if (
|
|
317
|
+
stat.isSymbolicLink() ||
|
|
318
|
+
bytes !== candidate.bytes ||
|
|
319
|
+
containsTrackedPath(repoRoot, candidate.path)
|
|
320
|
+
) {
|
|
321
|
+
throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
|
|
322
|
+
}
|
|
323
|
+
if (candidate.kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
|
|
324
|
+
throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const files = candidates.filter((candidate) => candidate.kind === "file");
|
|
328
|
+
const created = files.length
|
|
329
|
+
? createArtifact(repoRoot, {
|
|
330
|
+
slug: "adopted-unmanaged",
|
|
331
|
+
purpose: input.purpose,
|
|
332
|
+
retentionDays: input.retentionDays,
|
|
333
|
+
actor: input.actor,
|
|
334
|
+
now,
|
|
335
|
+
big: input.big,
|
|
336
|
+
})
|
|
337
|
+
: null;
|
|
338
|
+
for (const candidate of files) renameSync(candidate.path, join(created!.path, candidate.name));
|
|
339
|
+
const retentionDays = positiveDays(input.retentionDays);
|
|
340
|
+
const directories = candidates.filter((candidate) => candidate.kind === "directory");
|
|
341
|
+
for (const candidate of directories) {
|
|
342
|
+
atomicWriteManifest(candidate.path, {
|
|
343
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
344
|
+
artifact_id: randomUUID(),
|
|
345
|
+
slug: normalizeSlug(candidate.name),
|
|
346
|
+
purpose: `${input.purpose}: ${candidate.name}`,
|
|
347
|
+
created_at: now.toISOString(),
|
|
348
|
+
created_by: input.actor,
|
|
349
|
+
retention: { expires_at: addDays(now, retentionDays).toISOString() },
|
|
350
|
+
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
...preview,
|
|
355
|
+
adopted_artifact_id: created?.manifest.artifact_id ?? null,
|
|
356
|
+
adopted_path: created?.path ?? null,
|
|
357
|
+
adopted_directories: directories.length,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
246
361
|
export function cleanArtifacts(
|
|
247
362
|
repoRoot: string,
|
|
248
363
|
opts: { yes?: boolean; now?: Date; freshnessSeconds?: number } = {},
|
|
@@ -253,11 +368,24 @@ export function cleanArtifacts(
|
|
|
253
368
|
if (!opts.yes) return rows;
|
|
254
369
|
|
|
255
370
|
return rows.map((entry) => {
|
|
256
|
-
if (entry.
|
|
371
|
+
if (entry.action !== "would-delete") return entry;
|
|
257
372
|
// Reclassify immediately before removal. A renewal, release-state change,
|
|
258
373
|
// heartbeat, symlink swap, or tracked file added since inventory must win.
|
|
259
|
-
const current =
|
|
260
|
-
|
|
374
|
+
const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
|
|
375
|
+
(row) => row.path === entry.path,
|
|
376
|
+
);
|
|
377
|
+
if (!current) {
|
|
378
|
+
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
current.action !== "would-delete" ||
|
|
382
|
+
current.artifact_id !== entry.artifact_id ||
|
|
383
|
+
current.bytes !== entry.bytes ||
|
|
384
|
+
current.last_modified_at !== entry.last_modified_at ||
|
|
385
|
+
current.expires_at !== entry.expires_at
|
|
386
|
+
) {
|
|
387
|
+
return current;
|
|
388
|
+
}
|
|
261
389
|
try {
|
|
262
390
|
const top = lstatSync(current.path);
|
|
263
391
|
if (!top.isDirectory() || top.isSymbolicLink()) {
|
|
@@ -404,6 +532,7 @@ function classifyArtifactPath(
|
|
|
404
532
|
last_modified_at: new Date(effectiveLastModifiedMs).toISOString(),
|
|
405
533
|
expires_at: effectiveExpiresAt,
|
|
406
534
|
owner_instance_id: manifest.created_by?.instance_id ?? null,
|
|
535
|
+
oversize_acknowledged: manifest.oversize_acknowledged === true,
|
|
407
536
|
});
|
|
408
537
|
|
|
409
538
|
if (base.bytes === null || lastModifiedMs === null) {
|
|
@@ -450,6 +579,59 @@ function classifyArtifactPath(
|
|
|
450
579
|
};
|
|
451
580
|
}
|
|
452
581
|
|
|
582
|
+
function applyArtifactBudgets(
|
|
583
|
+
repoRoot: string,
|
|
584
|
+
inputRows: ArtifactInventoryEntry[],
|
|
585
|
+
): ArtifactInventoryEntry[] {
|
|
586
|
+
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
587
|
+
const maxBytes = artifactMaxBytes(repoRoot);
|
|
588
|
+
const rows = inputRows.map((row) => ({ ...row }));
|
|
589
|
+
|
|
590
|
+
for (const row of rows) {
|
|
591
|
+
if (
|
|
592
|
+
row.classification === "managed-current" &&
|
|
593
|
+
row.bytes !== null &&
|
|
594
|
+
row.bytes > maxUnitBytes &&
|
|
595
|
+
!row.oversize_acknowledged
|
|
596
|
+
) {
|
|
597
|
+
row.classification = "managed-oversize";
|
|
598
|
+
row.reason = `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`;
|
|
599
|
+
row.action = "would-delete";
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const managedBytes = rows.reduce(
|
|
604
|
+
(sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
|
|
605
|
+
0,
|
|
606
|
+
);
|
|
607
|
+
let retainedBytes =
|
|
608
|
+
managedBytes -
|
|
609
|
+
rows.reduce(
|
|
610
|
+
(sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0),
|
|
611
|
+
0,
|
|
612
|
+
);
|
|
613
|
+
if (retainedBytes <= maxBytes) return rows;
|
|
614
|
+
|
|
615
|
+
const candidates = rows
|
|
616
|
+
.filter(
|
|
617
|
+
(row) =>
|
|
618
|
+
row.classification === "managed-current" && row.action === "keep" && row.bytes !== null,
|
|
619
|
+
)
|
|
620
|
+
.sort((left, right) =>
|
|
621
|
+
`${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(
|
|
622
|
+
`${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`,
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
for (const row of candidates) {
|
|
626
|
+
if (retainedBytes <= maxBytes) break;
|
|
627
|
+
row.classification = "managed-over-budget";
|
|
628
|
+
row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring inactive bundles are removed first`;
|
|
629
|
+
row.action = "would-delete";
|
|
630
|
+
retainedBytes -= row.bytes ?? 0;
|
|
631
|
+
}
|
|
632
|
+
return rows;
|
|
633
|
+
}
|
|
634
|
+
|
|
453
635
|
function readManifest(path: string): ParsedManifest | ManifestError {
|
|
454
636
|
const manifestPath = join(path, ARTIFACT_MANIFEST);
|
|
455
637
|
if (!existsSync(manifestPath)) {
|
|
@@ -495,6 +677,9 @@ function readManifest(path: string): ParsedManifest | ManifestError {
|
|
|
495
677
|
if (m.released_by !== undefined && !validActor(m.released_by)) {
|
|
496
678
|
return { ok: false, reason: "invalid released_by" };
|
|
497
679
|
}
|
|
680
|
+
if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
|
|
681
|
+
return { ok: false, reason: "invalid oversize_acknowledged" };
|
|
682
|
+
}
|
|
498
683
|
return { ok: true, manifest: m as ArtifactManifestV1 };
|
|
499
684
|
}
|
|
500
685
|
|
|
@@ -604,6 +789,7 @@ function rowFor(
|
|
|
604
789
|
last_modified_at: null,
|
|
605
790
|
expires_at: null,
|
|
606
791
|
owner_instance_id: null,
|
|
792
|
+
oversize_acknowledged: false,
|
|
607
793
|
};
|
|
608
794
|
}
|
|
609
795
|
|
package/src/core/config.ts
CHANGED
|
@@ -137,12 +137,23 @@ interface HarneryConfig {
|
|
|
137
137
|
* (default 32 MiB; `0` disables rotation). Read via
|
|
138
138
|
* `eventLedgerRotateActiveBytes()`.
|
|
139
139
|
*/
|
|
140
|
-
events?: {
|
|
140
|
+
events?: {
|
|
141
|
+
rotate_active_bytes?: number;
|
|
142
|
+
archive_max_bytes?: number;
|
|
143
|
+
archive_max_age_days?: number;
|
|
144
|
+
archive_keep_min?: number;
|
|
145
|
+
archive_auto_clean?: boolean;
|
|
146
|
+
};
|
|
141
147
|
/**
|
|
142
148
|
* Managed working-artifact defaults. `default_retention_days` is the
|
|
143
149
|
* create-time TTL when the caller does not pass `artifacts create --days`.
|
|
144
150
|
*/
|
|
145
|
-
artifacts?: {
|
|
151
|
+
artifacts?: {
|
|
152
|
+
default_retention_days?: number;
|
|
153
|
+
auto_clean?: boolean;
|
|
154
|
+
max_bytes?: number;
|
|
155
|
+
max_unit_bytes?: number;
|
|
156
|
+
};
|
|
146
157
|
/**
|
|
147
158
|
* `harn backup` (restic) defaults: `repo` path/URL, `password_file`, and the
|
|
148
159
|
* `keep_daily`/`keep_weekly`/`keep_monthly` prune policy. Read via `backupConfig()`.
|
|
@@ -622,6 +633,51 @@ export function eventLedgerRotateActiveBytes(coordRoot?: string | null): number
|
|
|
622
633
|
return resolveEventLedgerRotateActiveBytesV3(root);
|
|
623
634
|
}
|
|
624
635
|
|
|
636
|
+
const DEFAULT_EVENT_ARCHIVE_MAX_BYTES = 1024 * 1024 * 1024;
|
|
637
|
+
const DEFAULT_EVENT_ARCHIVE_MAX_AGE_DAYS = 7;
|
|
638
|
+
const DEFAULT_EVENT_ARCHIVE_KEEP_MIN = 2;
|
|
639
|
+
|
|
640
|
+
export interface EventLedgerArchivePolicy {
|
|
641
|
+
maxBytes: number;
|
|
642
|
+
maxAgeDays: number;
|
|
643
|
+
keepMin: number;
|
|
644
|
+
autoClean: boolean;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Bounded retention for closed V3 epochs under `ledgers/v3-archives`. */
|
|
648
|
+
export function eventLedgerArchivePolicy(coordRoot?: string | null): EventLedgerArchivePolicy {
|
|
649
|
+
const root = coordRoot ?? findCoordRoot();
|
|
650
|
+
const configured = root ? readConfig(root).events : undefined;
|
|
651
|
+
return {
|
|
652
|
+
maxBytes: integerSetting(
|
|
653
|
+
coordEnv("EVENT_V3_ARCHIVE_MAX_BYTES"),
|
|
654
|
+
configured?.archive_max_bytes,
|
|
655
|
+
64 * 1024 * 1024,
|
|
656
|
+
1024 * 1024 * 1024 * 1024,
|
|
657
|
+
DEFAULT_EVENT_ARCHIVE_MAX_BYTES,
|
|
658
|
+
),
|
|
659
|
+
maxAgeDays: integerSetting(
|
|
660
|
+
coordEnv("EVENT_V3_ARCHIVE_MAX_AGE_DAYS"),
|
|
661
|
+
configured?.archive_max_age_days,
|
|
662
|
+
1,
|
|
663
|
+
3650,
|
|
664
|
+
DEFAULT_EVENT_ARCHIVE_MAX_AGE_DAYS,
|
|
665
|
+
),
|
|
666
|
+
keepMin: integerSetting(
|
|
667
|
+
coordEnv("EVENT_V3_ARCHIVE_KEEP_MIN"),
|
|
668
|
+
configured?.archive_keep_min,
|
|
669
|
+
1,
|
|
670
|
+
1000,
|
|
671
|
+
DEFAULT_EVENT_ARCHIVE_KEEP_MIN,
|
|
672
|
+
),
|
|
673
|
+
autoClean: booleanSetting(
|
|
674
|
+
coordEnv("EVENT_V3_ARCHIVE_AUTO_CLEAN"),
|
|
675
|
+
configured?.archive_auto_clean,
|
|
676
|
+
true,
|
|
677
|
+
),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
625
681
|
function parseWebPort(value: unknown, source: string): number {
|
|
626
682
|
const port = typeof value === "number" ? value : Number(value);
|
|
627
683
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
@@ -703,6 +759,33 @@ export function artifactDefaultRetentionDays(coordRoot?: string | null): number
|
|
|
703
759
|
: 3;
|
|
704
760
|
}
|
|
705
761
|
|
|
762
|
+
const DEFAULT_ARTIFACT_MAX_BYTES = 20 * 1024 * 1024 * 1024;
|
|
763
|
+
const DEFAULT_ARTIFACT_MAX_UNIT_BYTES = 1024 * 1024 * 1024;
|
|
764
|
+
|
|
765
|
+
/** Soft repository budget for managed working artifacts. */
|
|
766
|
+
export function artifactMaxBytes(coordRoot?: string | null): number {
|
|
767
|
+
const root = coordRoot ?? findCoordRoot();
|
|
768
|
+
return integerSetting(
|
|
769
|
+
coordEnv("ARTIFACT_MAX_BYTES"),
|
|
770
|
+
root ? readConfig(root).artifacts?.max_bytes : undefined,
|
|
771
|
+
64 * 1024 * 1024,
|
|
772
|
+
1024 * 1024 * 1024 * 1024,
|
|
773
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Size at which one bundle requires an explicit `artifacts create --big` acknowledgement. */
|
|
778
|
+
export function artifactMaxUnitBytes(coordRoot?: string | null): number {
|
|
779
|
+
const root = coordRoot ?? findCoordRoot();
|
|
780
|
+
return integerSetting(
|
|
781
|
+
coordEnv("ARTIFACT_MAX_UNIT_BYTES"),
|
|
782
|
+
root ? readConfig(root).artifacts?.max_unit_bytes : undefined,
|
|
783
|
+
16 * 1024 * 1024,
|
|
784
|
+
1024 * 1024 * 1024 * 1024,
|
|
785
|
+
DEFAULT_ARTIFACT_MAX_UNIT_BYTES,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
706
789
|
/**
|
|
707
790
|
* Whether the daily SessionStart sweep of expired artifact workspaces runs.
|
|
708
791
|
* Precedence: `HARNERY_ARTIFACT_AUTO_CLEAN` (0/false disables) ->
|
|
@@ -733,6 +816,32 @@ export function artifactAutoCleanIntervalHours(): number {
|
|
|
733
816
|
return 24;
|
|
734
817
|
}
|
|
735
818
|
|
|
819
|
+
function integerSetting(
|
|
820
|
+
envValue: string | undefined,
|
|
821
|
+
configured: unknown,
|
|
822
|
+
minimum: number,
|
|
823
|
+
maximum: number,
|
|
824
|
+
fallback: number,
|
|
825
|
+
): number {
|
|
826
|
+
for (const value of [envValue, configured]) {
|
|
827
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
828
|
+
if (Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum) return parsed;
|
|
829
|
+
}
|
|
830
|
+
return fallback;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function booleanSetting(
|
|
834
|
+
envValue: string | undefined,
|
|
835
|
+
configured: unknown,
|
|
836
|
+
fallback: boolean,
|
|
837
|
+
): boolean {
|
|
838
|
+
if (envValue !== undefined) {
|
|
839
|
+
if (envValue === "1" || envValue.toLowerCase() === "true") return true;
|
|
840
|
+
if (envValue === "0" || envValue.toLowerCase() === "false") return false;
|
|
841
|
+
}
|
|
842
|
+
return typeof configured === "boolean" ? configured : fallback;
|
|
843
|
+
}
|
|
844
|
+
|
|
736
845
|
/** Resolved `harn backup` defaults (restic repo/password + prune policy). */
|
|
737
846
|
export interface BackupConfig {
|
|
738
847
|
repo: string;
|