harnery 0.40.0 → 0.42.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/README.md +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +34 -20
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +4 -0
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +388 -45
- package/dist/commands/checkpoint.js +8 -12
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/deinit.d.ts +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +8 -4
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +2 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +58 -3
- package/dist/commands/fetch.d.ts.map +1 -1
- package/dist/commands/fetch.js +10 -0
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +21 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +271 -109
- package/dist/commands/instructions.js +5 -4
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +2 -1
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/commands/webview.d.ts +4 -0
- package/dist/commands/webview.d.ts.map +1 -0
- package/dist/commands/webview.js +94 -0
- package/dist/core/adapter.d.ts +50 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +59 -1
- package/dist/core/adapters/attestation.d.ts.map +1 -1
- package/dist/core/adapters/attestation.js +3 -2
- package/dist/core/adapters/profiles.d.ts +29 -3
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +53 -1
- package/dist/core/adapters/registry.d.ts.map +1 -1
- package/dist/core/adapters/registry.js +33 -0
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -7
- package/dist/core/agents/cli.js +25 -16
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +7 -2
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +3 -9
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +2 -8
- package/dist/core/agents/rules/stop-hook.d.ts +2 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +1 -1
- package/dist/core/agents/session-events.d.ts +6 -2
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +105 -26
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalization-state-v3.js +2 -1
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalizer-v3.js +5 -4
- package/dist/core/agents/session-name-display.d.ts +28 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +43 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -2
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +61 -0
- package/dist/core/agents/state/live-coordination-writer.js +3 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +2 -10
- package/dist/core/agents/turn-changes.d.ts +17 -0
- package/dist/core/agents/turn-changes.d.ts.map +1 -0
- package/dist/core/agents/turn-changes.js +76 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +11 -3
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +526 -52
- package/dist/core/config.d.ts +73 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +142 -3
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +2 -1
- package/dist/core/conversations/archive.d.ts.map +1 -1
- package/dist/core/conversations/archive.js +4 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +5 -4
- package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/canary.js +5 -4
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/compression.js +2 -1
- package/dist/core/events/v3/adapter-id.d.ts +10 -2
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
- package/dist/core/events/v3/adapter-id.js +16 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +2 -1
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +5 -4
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +17 -0
- package/dist/core/events/v3/contract.d.ts +5 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -1
- package/dist/core/events/v3/contract.js +7 -1
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +3 -2
- package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
- package/dist/core/events/v3/fingerprint-keys.js +7 -6
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +5 -4
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +8 -7
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
- package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
- package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +10 -4
- package/dist/core/events/v3/producers/hook.d.ts +2 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +16 -1
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +16 -6
- package/dist/core/events/v3/producers/recorder.d.ts +2 -4
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +125 -101
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/events/v3/recovery.d.ts.map +1 -1
- package/dist/core/events/v3/recovery.js +4 -3
- package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/inventory.js +2 -1
- package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/maintenance.js +4 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
- package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
- package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
- package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
- package/dist/core/events/v3/tool-line-changes.js +135 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -1
- package/dist/core/events/v3/writer.js +8 -7
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +6 -5
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +12 -11
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +7 -6
- package/dist/core/guard/evaluator.js +1 -0
- package/dist/core/guard/storage.d.ts.map +1 -1
- package/dist/core/guard/storage.js +3 -2
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/index.js +20 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +1 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +2 -3
- package/dist/core/hooks/adapter/events.d.ts +46 -1
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +36 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
- package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
- package/dist/core/hooks/adapter/output.d.ts +7 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +8 -1
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +52 -8
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +133 -12
- package/dist/core/hooks/cli.js +676 -531
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +2 -1
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
- package/dist/core/hooks/prompt-context/contract.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +1 -1
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -4
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/hooks/session-name-presence.d.ts +3 -1
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +7 -2
- package/dist/core/hooks/shell-waiter.d.ts +2 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
- package/dist/core/hooks/shell-waiter.js +173 -0
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +5 -4
- package/dist/core/semantic/review.d.ts.map +1 -1
- package/dist/core/semantic/review.js +5 -4
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +15 -14
- package/dist/core/semantic/storage.d.ts.map +1 -1
- package/dist/core/semantic/storage.js +5 -4
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -1
- package/dist/core/storage/atomic-json.js +7 -3
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/storage/durable-history.d.ts.map +1 -1
- package/dist/core/storage/durable-history.js +6 -5
- package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
- package/dist/core/storage/maintenance-providers.js +2 -1
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +7 -6
- package/dist/core/storage/metrics.d.ts.map +1 -1
- package/dist/core/storage/metrics.js +5 -1
- package/dist/core/storage/modes.d.ts +17 -0
- package/dist/core/storage/modes.d.ts.map +1 -0
- package/dist/core/storage/modes.js +47 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +3 -2
- package/dist/core/storage/receipts.d.ts.map +1 -1
- package/dist/core/storage/receipts.js +4 -3
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +7 -6
- package/dist/core/supervisor/hook-health.d.ts +2 -1
- package/dist/core/supervisor/hook-health.d.ts.map +1 -1
- package/dist/core/supervisor/hook-health.js +4 -3
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +7 -6
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +8 -7
- package/dist/core/workflow/adapters.d.ts +7 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -1
- package/dist/core/workflow/adapters.js +9 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +6 -5
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +33 -4
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +4 -3
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +2 -3
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -2
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts +36 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
- package/dist/core/workflow/spawn-opencode.js +135 -0
- package/dist/core/workflow/transcript.d.ts.map +1 -1
- package/dist/core/workflow/transcript.js +3 -2
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +5 -4
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +3 -2
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/paths.js +2 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +10 -9
- package/dist/lazy-commands.js +8 -1
- package/dist/lib/agent-browser/client.d.ts +9 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +13 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/client.d.ts +35 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +117 -8
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/identity.d.ts +25 -0
- package/dist/lib/browser/identity.d.ts.map +1 -0
- package/dist/lib/browser/identity.js +66 -0
- package/dist/lib/browser/index.d.ts +3 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/launch-args.d.ts +26 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -1
- package/dist/lib/browser/launch-args.js +41 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/user-agent.d.ts +53 -0
- package/dist/lib/browser/user-agent.d.ts.map +1 -0
- package/dist/lib/browser/user-agent.js +79 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/docs-metadata-managed.d.ts +1 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +17 -1
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -9
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +2 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/pace/index.d.ts +2 -0
- package/dist/lib/pace/index.d.ts.map +1 -0
- package/dist/lib/pace/index.js +1 -0
- package/dist/lib/pace/pace.d.ts +95 -0
- package/dist/lib/pace/pace.d.ts.map +1 -0
- package/dist/lib/pace/pace.js +313 -0
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/forward-headers.d.ts +11 -0
- package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
- package/dist/lib/tunnel/forward-headers.js +17 -0
- package/dist/lib/tunnel/gate.js +34 -10
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/dist/lib/webview/index.d.ts +2 -0
- package/dist/lib/webview/index.d.ts.map +1 -0
- package/dist/lib/webview/index.js +1 -0
- package/dist/lib/webview/webview.d.ts +116 -0
- package/dist/lib/webview/webview.d.ts.map +1 -0
- package/dist/lib/webview/webview.js +289 -0
- package/opencode-plugin/package.json +19 -0
- package/opencode-plugin/src/index.ts +791 -0
- package/package.json +16 -4
- package/schemas/config.schema.json +104 -0
- package/schemas/event-v3.schema.json +164 -0
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +47 -18
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse-ai.ts +8 -0
- package/src/commands/browse.ts +570 -48
- package/src/commands/checkpoint.ts +7 -9
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/deinit.ts +9 -4
- package/src/commands/disk.ts +114 -0
- package/src/commands/docs.ts +2 -0
- package/src/commands/doctor.ts +63 -3
- package/src/commands/fetch.ts +14 -0
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +334 -122
- package/src/commands/instructions.ts +8 -4
- package/src/commands/ledger-v3.ts +2 -1
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/commands/webview.ts +159 -0
- package/src/core/adapter.ts +102 -1
- package/src/core/adapters/attestation.ts +3 -2
- package/src/core/adapters/profiles.ts +63 -1
- package/src/core/adapters/registry.ts +37 -0
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/canonical-emit.ts +2 -5
- package/src/core/agents/cli.ts +26 -17
- package/src/core/agents/coord-client.ts +9 -3
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/rules/claim-conflict.ts +3 -8
- package/src/core/agents/rules/commit-conflict.ts +2 -7
- package/src/core/agents/rules/stop-hook.ts +3 -1
- package/src/core/agents/session-events.ts +120 -26
- package/src/core/agents/session-finalization-state-v3.ts +2 -1
- package/src/core/agents/session-finalizer-v3.ts +5 -4
- package/src/core/agents/session-name-display.ts +48 -0
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +24 -2
- package/src/core/agents/state/live-coordination-view.ts +74 -0
- package/src/core/agents/state/live-coordination-writer.ts +3 -0
- package/src/core/agents/state/stale-sweep.ts +2 -9
- package/src/core/agents/turn-changes.ts +97 -0
- package/src/core/artifacts/delivery-card.ts +12 -3
- package/src/core/artifacts/index.ts +701 -47
- package/src/core/config.ts +206 -4
- package/src/core/context/index.ts +2 -1
- package/src/core/conversations/archive.ts +4 -3
- package/src/core/diagnostics/bundle.ts +5 -3
- package/src/core/events/legacy-storage/canary.ts +5 -4
- package/src/core/events/legacy-storage/compression.ts +2 -1
- package/src/core/events/v3/adapter-id.ts +29 -1
- package/src/core/events/v3/authority-outbox.ts +2 -1
- package/src/core/events/v3/bootstrap.ts +5 -4
- package/src/core/events/v3/capabilities.ts +17 -0
- package/src/core/events/v3/contract.ts +9 -1
- package/src/core/events/v3/control-witness.ts +3 -2
- package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
- package/src/core/events/v3/fingerprint-keys.ts +7 -6
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/live-feed.ts +5 -4
- package/src/core/events/v3/producers/command-recorder.ts +8 -6
- package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
- package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
- package/src/core/events/v3/producers/hook-base.ts +11 -4
- package/src/core/events/v3/producers/hook.ts +19 -1
- package/src/core/events/v3/producers/intake.ts +18 -6
- package/src/core/events/v3/producers/recorder.ts +139 -114
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/events/v3/recovery.ts +4 -3
- package/src/core/events/v3/support-storage/inventory.ts +2 -1
- package/src/core/events/v3/support-storage/maintenance.ts +8 -3
- package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
- package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
- package/src/core/events/v3/tool-line-changes.ts +139 -0
- package/src/core/events/v3/writer.ts +8 -7
- package/src/core/governor/planning.ts +6 -5
- package/src/core/governor/service.ts +12 -11
- package/src/core/governor/state.ts +7 -6
- package/src/core/guard/evaluator.ts +1 -0
- package/src/core/guard/storage.ts +3 -2
- package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
- package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
- package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
- package/src/core/hooks/adapter/behaviors/index.ts +31 -0
- package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
- package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
- package/src/core/hooks/adapter/behaviors/types.ts +110 -0
- package/src/core/hooks/adapter/detect.ts +2 -3
- package/src/core/hooks/adapter/events.ts +72 -1
- package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
- package/src/core/hooks/adapter/output.ts +8 -1
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +198 -16
- package/src/core/hooks/cli.ts +776 -539
- package/src/core/hooks/effects/image-capture.ts +2 -1
- package/src/core/hooks/prompt-context/contract.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +42 -5
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/hooks/session-name-presence.ts +10 -3
- package/src/core/hooks/shell-waiter.ts +166 -0
- package/src/core/resources/service.ts +5 -4
- package/src/core/semantic/review.ts +5 -4
- package/src/core/semantic/service.ts +15 -14
- package/src/core/semantic/storage.ts +5 -4
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/atomic-json.ts +7 -3
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/storage/durable-history.ts +6 -5
- package/src/core/storage/maintenance-providers.ts +2 -1
- package/src/core/storage/maintenance.ts +7 -6
- package/src/core/storage/metrics.ts +5 -1
- package/src/core/storage/modes.ts +53 -0
- package/src/core/storage/process-log.ts +3 -2
- package/src/core/storage/receipts.ts +4 -3
- package/src/core/storage/segments.ts +7 -6
- package/src/core/supervisor/hook-health.ts +4 -4
- package/src/core/supervisor/service.ts +7 -6
- package/src/core/work/state.ts +8 -7
- package/src/core/workflow/adapters.ts +15 -0
- package/src/core/workflow/approvals.ts +6 -5
- package/src/core/workflow/billing.ts +34 -4
- package/src/core/workflow/durable-record.ts +4 -3
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/live-session-v3.ts +2 -3
- package/src/core/workflow/run-state.ts +3 -2
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +153 -0
- package/src/core/workflow/transcript.ts +3 -2
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/core/workflow/workspaces/leases.ts +5 -4
- package/src/core/workflow/workspaces/local-git.ts +3 -2
- package/src/core/workflow/workspaces/paths.ts +2 -1
- package/src/core/workflow/workspaces/state.ts +10 -9
- package/src/lazy-commands.ts +11 -1
- package/src/lib/agent-browser/client.ts +18 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/client.ts +147 -9
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/identity.ts +64 -0
- package/src/lib/browser/index.ts +16 -1
- package/src/lib/browser/launch-args.ts +53 -1
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/user-agent.ts +130 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/docs-metadata-managed.ts +21 -0
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +2 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/pace/index.ts +14 -0
- package/src/lib/pace/pace.ts +370 -0
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/forward-headers.ts +16 -0
- package/src/lib/tunnel/gate.ts +39 -10
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
- package/src/lib/webview/index.ts +13 -0
- package/src/lib/webview/webview.ts +457 -0
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* become project records: screenshots, exports, audit dumps, rollback inputs,
|
|
6
6
|
* and similar material. Each direct child of `.harnery/artifacts/` is one
|
|
7
7
|
* managed unit with a small manifest. Cleanup fails closed: only a valid,
|
|
8
|
-
*
|
|
8
|
+
* unheld, inactive, untracked managed unit is deletable, and only when its
|
|
9
|
+
* retention expired or a size rule applies after its idle grace. Explicit
|
|
10
|
+
* removal lets a creating owner delete its reviewed, unheld workspace sooner.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { spawnSync } from "node:child_process";
|
|
@@ -17,8 +19,10 @@ import {
|
|
|
17
19
|
mkdirSync,
|
|
18
20
|
readdirSync,
|
|
19
21
|
readFileSync,
|
|
22
|
+
realpathSync,
|
|
20
23
|
renameSync,
|
|
21
24
|
rmSync,
|
|
25
|
+
statfsSync,
|
|
22
26
|
writeFileSync,
|
|
23
27
|
} from "node:fs";
|
|
24
28
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -28,11 +32,21 @@ import {
|
|
|
28
32
|
artifactAutoCleanEnabled,
|
|
29
33
|
artifactAutoCleanIntervalHours,
|
|
30
34
|
artifactDefaultRetentionDays,
|
|
35
|
+
artifactHoldDays,
|
|
36
|
+
artifactIdleGraceHours,
|
|
31
37
|
artifactMaxBytes,
|
|
38
|
+
artifactMaxHeldBytes,
|
|
32
39
|
artifactMaxUnitBytes,
|
|
40
|
+
artifactMinFreeBytes,
|
|
33
41
|
coordFreshnessSeconds,
|
|
34
42
|
resolveBinName,
|
|
35
43
|
} from "../config.ts";
|
|
44
|
+
import {
|
|
45
|
+
readCoordinationViewV3,
|
|
46
|
+
requireAuthoritySafeCoordinationViewV3,
|
|
47
|
+
} from "../events/v3/coordination-view.ts";
|
|
48
|
+
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.ts";
|
|
49
|
+
import { stateFileMode } from "../storage/modes.ts";
|
|
36
50
|
import {
|
|
37
51
|
type ArtifactActivity,
|
|
38
52
|
artifactRootEntries,
|
|
@@ -91,11 +105,23 @@ export interface ArtifactHold {
|
|
|
91
105
|
reason: string;
|
|
92
106
|
set_by: ArtifactActor;
|
|
93
107
|
set_at: string;
|
|
108
|
+
/** When the hold lapses unless renewed. Absent only on a persistent hold or
|
|
109
|
+
* a hold recorded before holds expired; the latter stays in force. */
|
|
110
|
+
expires_at?: string;
|
|
111
|
+
/** Set by embedding hosts whose hold mirrors an external lease. Never lapses. */
|
|
112
|
+
persistent?: true;
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
export interface ArtifactHoldInput {
|
|
97
116
|
id: string;
|
|
98
117
|
reason: string;
|
|
118
|
+
/** Hold lifetime in days (1 to 365); defaults to `artifacts.hold_days`. */
|
|
119
|
+
days?: number;
|
|
120
|
+
/** Hold lifetime in minutes (1 to 525,600); replaces `days`. */
|
|
121
|
+
minutes?: number;
|
|
122
|
+
/** For a hold that mirrors an external lease, such as an open checkout with
|
|
123
|
+
* unsynchronized work. The hold never lapses; its owner must remove it. */
|
|
124
|
+
persistent?: boolean;
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
export function artifactCapabilities() {
|
|
@@ -107,6 +133,12 @@ export function artifactCapabilities() {
|
|
|
107
133
|
explicit_v1_migration: true,
|
|
108
134
|
minute_retention: true,
|
|
109
135
|
discard_after_review: true,
|
|
136
|
+
owner_scoped_remove: true,
|
|
137
|
+
allow_big_after_create: true,
|
|
138
|
+
hold_expiry: true,
|
|
139
|
+
persistent_holds: true,
|
|
140
|
+
held_budget: true,
|
|
141
|
+
disk_free_report: true,
|
|
110
142
|
} as const;
|
|
111
143
|
}
|
|
112
144
|
|
|
@@ -130,7 +162,11 @@ export interface ArtifactInventoryEntry {
|
|
|
130
162
|
classification: ArtifactClassification;
|
|
131
163
|
reason: string;
|
|
132
164
|
action: "keep" | "would-delete" | "deleted";
|
|
165
|
+
/** Disk use: allocated blocks, each hard-linked file counted once. Every
|
|
166
|
+
* size rule reads this figure. */
|
|
133
167
|
bytes: number | null;
|
|
168
|
+
/** Sum of file lengths. A sparse file makes this larger than `bytes`. */
|
|
169
|
+
apparent_bytes: number | null;
|
|
134
170
|
artifact_id: string | null;
|
|
135
171
|
slug: string | null;
|
|
136
172
|
created_at: string | null;
|
|
@@ -138,6 +174,11 @@ export interface ArtifactInventoryEntry {
|
|
|
138
174
|
expires_at: string | null;
|
|
139
175
|
owner_instance_id: string | null;
|
|
140
176
|
oversize_acknowledged: boolean;
|
|
177
|
+
/** Latest file change, owner heartbeat, renewal, or release. Size rules wait
|
|
178
|
+
* `artifacts.idle_grace_hours` past this before they may delete. */
|
|
179
|
+
idle_since: string | null;
|
|
180
|
+
/** Advice for a unit that a size rule will delete once its grace ends. */
|
|
181
|
+
warning: string | null;
|
|
141
182
|
}
|
|
142
183
|
|
|
143
184
|
export interface ArtifactCreateInput {
|
|
@@ -213,7 +254,8 @@ function createArtifactUnlocked(
|
|
|
213
254
|
if (!isSafeId(artifactId)) {
|
|
214
255
|
throw new Error("artifact id must use ASCII letters, digits, hyphens, or underscores");
|
|
215
256
|
}
|
|
216
|
-
const
|
|
257
|
+
const holdDays = artifactHoldDays(repoRoot);
|
|
258
|
+
const holds = (input.holds ?? []).map((hold) => makeHold(hold, input.actor, now, holdDays));
|
|
217
259
|
if (new Set(holds.map((hold) => hold.id)).size !== holds.length) {
|
|
218
260
|
throw new Error("duplicate hold id");
|
|
219
261
|
}
|
|
@@ -268,7 +310,7 @@ export function inventoryArtifacts(
|
|
|
268
310
|
for (const name of names) {
|
|
269
311
|
rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
|
|
270
312
|
}
|
|
271
|
-
return applyArtifactBudgets(repoRoot, rows);
|
|
313
|
+
return applyArtifactBudgets(repoRoot, rows, now);
|
|
272
314
|
}
|
|
273
315
|
|
|
274
316
|
export function showArtifact(
|
|
@@ -326,6 +368,27 @@ function renewArtifactUnlocked(
|
|
|
326
368
|
return manifest;
|
|
327
369
|
}
|
|
328
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Record the `--big` acknowledgement on an existing workspace, for work that
|
|
373
|
+
* turns out larger than expected. Unlike a hold, it leaves expiry in force and
|
|
374
|
+
* exempts the unit only from the per-bundle ceiling. Idempotent.
|
|
375
|
+
*/
|
|
376
|
+
export function allowBigArtifact(
|
|
377
|
+
repoRoot: string,
|
|
378
|
+
ref: string,
|
|
379
|
+
input: ArtifactMutationInput = {},
|
|
380
|
+
): ArtifactManifestV2 {
|
|
381
|
+
return withArtifactLock(repoRoot, () => {
|
|
382
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
383
|
+
const parsed = readManifest(path);
|
|
384
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
385
|
+
if (parsed.manifest.oversize_acknowledged) return parsed.manifest;
|
|
386
|
+
const manifest: ArtifactManifestV2 = { ...parsed.manifest, oversize_acknowledged: true };
|
|
387
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
388
|
+
return manifest;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
329
392
|
export function releaseArtifact(
|
|
330
393
|
repoRoot: string,
|
|
331
394
|
ref: string,
|
|
@@ -384,6 +447,112 @@ export function discardArtifact(
|
|
|
384
447
|
});
|
|
385
448
|
}
|
|
386
449
|
|
|
450
|
+
export interface ArtifactRemovalResult {
|
|
451
|
+
entry: ArtifactInventoryEntry;
|
|
452
|
+
reason: string;
|
|
453
|
+
deleted: boolean;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Preview or immediately remove one reviewed workspace belonging to this actor. */
|
|
457
|
+
export function removeArtifact(
|
|
458
|
+
repoRoot: string,
|
|
459
|
+
ref: string,
|
|
460
|
+
reason: string,
|
|
461
|
+
input: ArtifactMutationInput & { yes?: boolean } = {},
|
|
462
|
+
): ArtifactRemovalResult {
|
|
463
|
+
const why = reason.trim();
|
|
464
|
+
if (!why) throw new Error("remove requires a reason confirming the files are no longer needed");
|
|
465
|
+
if (!validActor(input.actor))
|
|
466
|
+
throw new Error("remove requires a current artifact owner identity");
|
|
467
|
+
const actor = input.actor;
|
|
468
|
+
const now = input.now ?? new Date();
|
|
469
|
+
assertValidDate(now, "now");
|
|
470
|
+
const store = artifactsRoot(repoRoot);
|
|
471
|
+
const checkStore = () => {
|
|
472
|
+
for (const path of [resolve(repoRoot), dirname(store), store]) {
|
|
473
|
+
const stat = lstatSync(path);
|
|
474
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
475
|
+
throw new Error("artifact store must use real directories, not symlinks");
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
checkStore();
|
|
479
|
+
return withArtifactLock(repoRoot, () => {
|
|
480
|
+
checkStore();
|
|
481
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
482
|
+
const inspect = () => {
|
|
483
|
+
const target = realpathSync(path);
|
|
484
|
+
const cwd = realpathSync(process.cwd());
|
|
485
|
+
if (cwd === target || cwd.startsWith(`${target}${sep}`))
|
|
486
|
+
throw new Error("cannot remove the working directory or its ancestor");
|
|
487
|
+
if (lstatSync(path).dev !== lstatSync(store).dev)
|
|
488
|
+
throw new Error("artifact removal cannot cross a mount boundary");
|
|
489
|
+
const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
|
|
490
|
+
if (!["managed-current", "managed-expired", "managed-active"].includes(entry.classification))
|
|
491
|
+
throw new Error(`cannot remove artifact: ${entry.reason}`);
|
|
492
|
+
if (entry.owner_instance_id !== actor.instance_id)
|
|
493
|
+
throw new Error("only the creating artifact owner may remove this workspace");
|
|
494
|
+
const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(repoRoot));
|
|
495
|
+
for (const peer of Object.values(view.instances)) {
|
|
496
|
+
if (!peer.authority_eligible || peer.instance_id === liveInstanceIdV3(actor.instance_id))
|
|
497
|
+
continue;
|
|
498
|
+
for (const claim of peer.files_touched) {
|
|
499
|
+
const claimed = resolve(repoRoot, claim);
|
|
500
|
+
if (
|
|
501
|
+
path === claimed ||
|
|
502
|
+
path.startsWith(`${claimed}${sep}`) ||
|
|
503
|
+
claimed.startsWith(`${path}${sep}`)
|
|
504
|
+
)
|
|
505
|
+
throw new Error(`artifact overlaps another agent's claim: ${claim}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return entry;
|
|
509
|
+
};
|
|
510
|
+
const entry = inspect();
|
|
511
|
+
const before = artifactRemovalSnapshot(path);
|
|
512
|
+
if (!input.yes) return { entry, reason: why, deleted: false };
|
|
513
|
+
// Metadata mutations share this lock. Recheck payload, claims, and protections
|
|
514
|
+
// immediately before removal because ordinary file writers do not take it.
|
|
515
|
+
checkStore();
|
|
516
|
+
const current = inspect();
|
|
517
|
+
if (
|
|
518
|
+
!isDeepStrictEqual(entry, current) ||
|
|
519
|
+
!isDeepStrictEqual(before, artifactRemovalSnapshot(path))
|
|
520
|
+
)
|
|
521
|
+
throw new Error("artifact changed during removal inspection; preview it again");
|
|
522
|
+
rmSync(path, { recursive: true, force: false });
|
|
523
|
+
const removed = { ...current, action: "deleted" as const, reason: why };
|
|
524
|
+
recordArtifactDeletion(repoRoot, removed, now, actor);
|
|
525
|
+
return { entry: removed, reason: why, deleted: true };
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Refuse links, mounts, special files, and embedded stores; fingerprint every entry. */
|
|
530
|
+
function artifactRemovalSnapshot(path: string): Map<string, string> {
|
|
531
|
+
const device = lstatSync(path).dev;
|
|
532
|
+
const entries = new Map<string, string>();
|
|
533
|
+
const walk = (target: string) => {
|
|
534
|
+
const stat = lstatSync(target);
|
|
535
|
+
if (stat.isSymbolicLink() || stat.dev !== device || (!stat.isFile() && !stat.isDirectory()))
|
|
536
|
+
throw new Error(`unsafe artifact removal path: ${target}`);
|
|
537
|
+
if (target !== path && [".git", ".harnery"].includes(basename(target)))
|
|
538
|
+
throw new Error(`embedded repository or coordination state cannot be removed: ${target}`);
|
|
539
|
+
if (target !== join(path, ARTIFACT_MANIFEST) && basename(target) === ARTIFACT_MANIFEST)
|
|
540
|
+
throw new Error(`nested artifact must be managed separately: ${target}`);
|
|
541
|
+
entries.set(
|
|
542
|
+
target,
|
|
543
|
+
[stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":"),
|
|
544
|
+
);
|
|
545
|
+
if (stat.isDirectory()) {
|
|
546
|
+
const names = readdirSync(target).sort();
|
|
547
|
+
if (names.includes("HEAD") && names.includes("objects") && names.includes("config"))
|
|
548
|
+
throw new Error(`embedded Git metadata cannot be removed: ${target}`);
|
|
549
|
+
for (const name of names) walk(join(target, name));
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
walk(path);
|
|
553
|
+
return entries;
|
|
554
|
+
}
|
|
555
|
+
|
|
387
556
|
/** Advice only: a successful check does not establish that its evidence is disposable. */
|
|
388
557
|
export function artifactReviewGuidance(repoRoot: string, ref: string): string {
|
|
389
558
|
const target = isAbsolute(ref) ? basename(ref) : ref;
|
|
@@ -416,7 +585,7 @@ export function holdArtifact(
|
|
|
416
585
|
ref: string,
|
|
417
586
|
input: ArtifactHoldInput & { actor: ArtifactActor; now?: Date },
|
|
418
587
|
): ArtifactManifestV2 {
|
|
419
|
-
const hold = makeHold(input, input.actor, input.now ?? new Date());
|
|
588
|
+
const hold = makeHold(input, input.actor, input.now ?? new Date(), artifactHoldDays(repoRoot));
|
|
420
589
|
return withArtifactLock(repoRoot, () => {
|
|
421
590
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
422
591
|
const parsed = readManifest(path);
|
|
@@ -429,7 +598,19 @@ export function holdArtifact(
|
|
|
429
598
|
) {
|
|
430
599
|
throw new Error("hold id already exists with a different owner or reason");
|
|
431
600
|
}
|
|
432
|
-
|
|
601
|
+
// Repeating a hold renews it: the original owner and set_at stay, and the
|
|
602
|
+
// window restarts from now. A persistent hold stays persistent.
|
|
603
|
+
const { expires_at: _expires, persistent: _persistent, ...kept } = previous;
|
|
604
|
+
const renewed: ArtifactHold =
|
|
605
|
+
previous.persistent || hold.persistent
|
|
606
|
+
? { ...kept, persistent: true }
|
|
607
|
+
: { ...kept, expires_at: hold.expires_at! };
|
|
608
|
+
const manifest = {
|
|
609
|
+
...parsed.manifest,
|
|
610
|
+
holds: parsed.manifest.holds.map((item) => (item.id === hold.id ? renewed : item)),
|
|
611
|
+
};
|
|
612
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
613
|
+
return manifest;
|
|
433
614
|
}
|
|
434
615
|
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
435
616
|
atomicWriteManifest(path, manifest, input.now);
|
|
@@ -640,6 +821,7 @@ function cleanArtifactsUnlocked(
|
|
|
640
821
|
: applyArtifactUnitBudget(
|
|
641
822
|
repoRoot,
|
|
642
823
|
classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds),
|
|
824
|
+
now,
|
|
643
825
|
);
|
|
644
826
|
if (!current) {
|
|
645
827
|
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
@@ -664,6 +846,7 @@ function cleanArtifactsUnlocked(
|
|
|
664
846
|
};
|
|
665
847
|
}
|
|
666
848
|
rmSync(current.path, { recursive: true, force: false });
|
|
849
|
+
recordArtifactDeletion(repoRoot, current, now);
|
|
667
850
|
return { ...current, action: "deleted" };
|
|
668
851
|
} catch (error) {
|
|
669
852
|
return {
|
|
@@ -676,6 +859,314 @@ function cleanArtifactsUnlocked(
|
|
|
676
859
|
});
|
|
677
860
|
}
|
|
678
861
|
|
|
862
|
+
/** Sibling of the artifacts root, like the stamp, so it never enters the inventory. */
|
|
863
|
+
const DELETION_LOG = ".harnery/artifact-deletions.jsonl";
|
|
864
|
+
const DELETION_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
865
|
+
|
|
866
|
+
export interface ArtifactDeletionRecord {
|
|
867
|
+
deleted_at: string;
|
|
868
|
+
name: string;
|
|
869
|
+
relative_path: string;
|
|
870
|
+
artifact_id: string | null;
|
|
871
|
+
slug: string | null;
|
|
872
|
+
owner_instance_id: string | null;
|
|
873
|
+
classification: ArtifactClassification;
|
|
874
|
+
reason: string;
|
|
875
|
+
bytes: number | null;
|
|
876
|
+
apparent_bytes: number | null;
|
|
877
|
+
expires_at: string | null;
|
|
878
|
+
idle_since: string | null;
|
|
879
|
+
removed_by?: ArtifactActor;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Append one deletion to the log, keeping 30 days. Callers hold the artifact
|
|
884
|
+
* lock. Best-effort: the directory is already gone, so a failed write must not
|
|
885
|
+
* turn a completed deletion into a reported failure.
|
|
886
|
+
*/
|
|
887
|
+
function recordArtifactDeletion(
|
|
888
|
+
repoRoot: string,
|
|
889
|
+
row: ArtifactInventoryEntry,
|
|
890
|
+
now: Date,
|
|
891
|
+
removedBy?: ArtifactActor,
|
|
892
|
+
): void {
|
|
893
|
+
const record: ArtifactDeletionRecord = {
|
|
894
|
+
deleted_at: now.toISOString(),
|
|
895
|
+
name: row.name,
|
|
896
|
+
relative_path: row.relative_path,
|
|
897
|
+
artifact_id: row.artifact_id,
|
|
898
|
+
slug: row.slug,
|
|
899
|
+
owner_instance_id: row.owner_instance_id,
|
|
900
|
+
classification: row.classification,
|
|
901
|
+
reason: row.reason,
|
|
902
|
+
bytes: row.bytes,
|
|
903
|
+
apparent_bytes: row.apparent_bytes,
|
|
904
|
+
expires_at: row.expires_at,
|
|
905
|
+
idle_since: row.idle_since,
|
|
906
|
+
...(removedBy ? { removed_by: removedBy } : {}),
|
|
907
|
+
};
|
|
908
|
+
try {
|
|
909
|
+
const path = join(resolve(repoRoot), DELETION_LOG);
|
|
910
|
+
const kept = readArtifactDeletions(repoRoot, {
|
|
911
|
+
since: new Date(now.getTime() - DELETION_LOG_RETENTION_MS),
|
|
912
|
+
});
|
|
913
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
914
|
+
writeFileSync(temp, [...kept, record].map((item) => `${JSON.stringify(item)}\n`).join(""), {
|
|
915
|
+
mode: stateFileMode(),
|
|
916
|
+
});
|
|
917
|
+
renameSync(temp, path);
|
|
918
|
+
} catch {
|
|
919
|
+
// See the doc comment: the deletion stands even if its record cannot.
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Deletions the cleanup recorded, oldest first. Unreadable lines are skipped. */
|
|
924
|
+
export function readArtifactDeletions(
|
|
925
|
+
repoRoot: string,
|
|
926
|
+
opts: { since?: Date } = {},
|
|
927
|
+
): ArtifactDeletionRecord[] {
|
|
928
|
+
let text: string;
|
|
929
|
+
try {
|
|
930
|
+
text = readFileSync(join(resolve(repoRoot), DELETION_LOG), "utf8");
|
|
931
|
+
} catch {
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
const since = opts.since?.getTime() ?? -Infinity;
|
|
935
|
+
const records: ArtifactDeletionRecord[] = [];
|
|
936
|
+
for (const line of text.split("\n")) {
|
|
937
|
+
if (!line.trim()) continue;
|
|
938
|
+
try {
|
|
939
|
+
const record = JSON.parse(line) as ArtifactDeletionRecord;
|
|
940
|
+
if (typeof record?.name === "string" && Date.parse(record.deleted_at) >= since)
|
|
941
|
+
records.push(record);
|
|
942
|
+
} catch {
|
|
943
|
+
// A torn or foreign line does not hide the others.
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return records;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** Sibling of the artifacts root, like the deletion log, so it never enters the inventory. */
|
|
950
|
+
const USAGE_CACHE = ".harnery/artifact-usage.json";
|
|
951
|
+
const LARGEST_HOLDS = 5;
|
|
952
|
+
|
|
953
|
+
export interface ArtifactHeldSummary {
|
|
954
|
+
name: string;
|
|
955
|
+
bytes: number;
|
|
956
|
+
holds: Array<{
|
|
957
|
+
id: string;
|
|
958
|
+
/** The hold owner's name when recorded, otherwise its instance id. */
|
|
959
|
+
set_by: string;
|
|
960
|
+
expires_at: string | null;
|
|
961
|
+
persistent: boolean;
|
|
962
|
+
}>;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Last full-inventory measurement, read by commands that must not walk the store. */
|
|
966
|
+
export interface ArtifactUsageCache {
|
|
967
|
+
measured_at: string;
|
|
968
|
+
bytes: number;
|
|
969
|
+
held_bytes: number;
|
|
970
|
+
free_bytes: number | null;
|
|
971
|
+
largest_holds: ArtifactHeldSummary[];
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export interface ArtifactUsageReport {
|
|
975
|
+
held_bytes: number;
|
|
976
|
+
max_held_bytes: number;
|
|
977
|
+
held_over_budget: boolean;
|
|
978
|
+
largest_holds: ArtifactHeldSummary[];
|
|
979
|
+
free_bytes: number | null;
|
|
980
|
+
min_free_bytes: number;
|
|
981
|
+
low_disk: boolean;
|
|
982
|
+
/** Plain sentences for a human; empty when nothing needs attention. */
|
|
983
|
+
warnings: string[];
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Bytes available to this user on the artifact store's filesystem, or null. */
|
|
987
|
+
export function artifactFreeBytes(repoRoot: string): number | null {
|
|
988
|
+
try {
|
|
989
|
+
const root = artifactsRoot(repoRoot);
|
|
990
|
+
const stat = statfsSync(existsSync(root) ? root : resolve(repoRoot));
|
|
991
|
+
const free = Number(stat.bavail) * Number(stat.bsize);
|
|
992
|
+
return Number.isFinite(free) && free >= 0 ? free : null;
|
|
993
|
+
} catch {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Held bytes, free disk, and warnings for one full inventory. Held work is
|
|
1000
|
+
* never deleted: the held budget and the disk floor only report. Records the
|
|
1001
|
+
* measurement in `.harnery/artifact-usage.json` so `create` and `hold` can
|
|
1002
|
+
* warn without walking the store.
|
|
1003
|
+
*/
|
|
1004
|
+
export function artifactUsageReport(
|
|
1005
|
+
repoRoot: string,
|
|
1006
|
+
rows: ArtifactInventoryEntry[],
|
|
1007
|
+
opts: { now?: Date } = {},
|
|
1008
|
+
): ArtifactUsageReport {
|
|
1009
|
+
const now = opts.now ?? new Date();
|
|
1010
|
+
const kept = rows.filter((row) => row.action !== "deleted");
|
|
1011
|
+
const held = kept.filter((row) => row.classification === "managed-held");
|
|
1012
|
+
const heldBytes = held.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
|
|
1013
|
+
const largest = [...held]
|
|
1014
|
+
.sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0))
|
|
1015
|
+
.slice(0, LARGEST_HOLDS)
|
|
1016
|
+
.map((row) => heldSummary(row));
|
|
1017
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
1018
|
+
if (existsSync(artifactsRoot(repoRoot))) {
|
|
1019
|
+
writeUsageCache(repoRoot, {
|
|
1020
|
+
measured_at: now.toISOString(),
|
|
1021
|
+
bytes: kept.reduce((sum, row) => sum + (row.bytes ?? 0), 0),
|
|
1022
|
+
held_bytes: heldBytes,
|
|
1023
|
+
free_bytes: freeBytes,
|
|
1024
|
+
largest_holds: largest,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return usageReport(repoRoot, heldBytes, largest, freeBytes, null);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** The cached measurement, or null when none was recorded or it is unreadable. */
|
|
1031
|
+
export function readArtifactUsageCache(repoRoot: string): ArtifactUsageCache | null {
|
|
1032
|
+
try {
|
|
1033
|
+
const value = JSON.parse(readFileSync(join(resolve(repoRoot), USAGE_CACHE), "utf8"));
|
|
1034
|
+
if (
|
|
1035
|
+
!value ||
|
|
1036
|
+
!validIso(value.measured_at) ||
|
|
1037
|
+
typeof value.held_bytes !== "number" ||
|
|
1038
|
+
typeof value.bytes !== "number"
|
|
1039
|
+
)
|
|
1040
|
+
return null;
|
|
1041
|
+
return {
|
|
1042
|
+
measured_at: value.measured_at,
|
|
1043
|
+
bytes: value.bytes,
|
|
1044
|
+
held_bytes: value.held_bytes,
|
|
1045
|
+
free_bytes: typeof value.free_bytes === "number" ? value.free_bytes : null,
|
|
1046
|
+
largest_holds: Array.isArray(value.largest_holds) ? value.largest_holds : [],
|
|
1047
|
+
};
|
|
1048
|
+
} catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Usage for commands that must stay cheap (`create`, `hold`): held bytes from
|
|
1055
|
+
* the last full inventory, free disk measured now. Never walks the store.
|
|
1056
|
+
*/
|
|
1057
|
+
export function artifactQuickUsage(repoRoot: string): {
|
|
1058
|
+
usage: { held_bytes: number | null; held_measured_at: string | null; free_bytes: number | null };
|
|
1059
|
+
low_disk: boolean;
|
|
1060
|
+
min_free_bytes: number;
|
|
1061
|
+
warnings: string[];
|
|
1062
|
+
} {
|
|
1063
|
+
const cache = readArtifactUsageCache(repoRoot);
|
|
1064
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
1065
|
+
const report = usageReport(
|
|
1066
|
+
repoRoot,
|
|
1067
|
+
cache?.held_bytes ?? 0,
|
|
1068
|
+
cache?.largest_holds ?? [],
|
|
1069
|
+
freeBytes,
|
|
1070
|
+
cache?.measured_at ?? null,
|
|
1071
|
+
);
|
|
1072
|
+
return {
|
|
1073
|
+
usage: {
|
|
1074
|
+
held_bytes: cache?.held_bytes ?? null,
|
|
1075
|
+
held_measured_at: cache?.measured_at ?? null,
|
|
1076
|
+
free_bytes: freeBytes,
|
|
1077
|
+
},
|
|
1078
|
+
low_disk: report.low_disk,
|
|
1079
|
+
min_free_bytes: report.min_free_bytes,
|
|
1080
|
+
warnings: report.warnings,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Refuse new large work when the artifact store's disk is below its floor.
|
|
1086
|
+
* `create --big` calls this; `--allow-low-disk` is the deliberate override.
|
|
1087
|
+
*/
|
|
1088
|
+
export function assertArtifactDiskFloor(repoRoot: string): void {
|
|
1089
|
+
const minFree = artifactMinFreeBytes(repoRoot);
|
|
1090
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
1091
|
+
if (minFree === 0 || freeBytes === null || freeBytes >= minFree) return;
|
|
1092
|
+
const bin = resolveBinName(repoRoot);
|
|
1093
|
+
throw new Error(
|
|
1094
|
+
`only ${gib(freeBytes)} is free on the artifact store's disk, below the ${gib(minFree)} floor (artifacts.min_free_bytes), so a --big workspace was not created. Free space with \`${bin} artifacts clean --yes\`, \`${bin} artifacts discard <ref> --reason <text>\` for reviewed work, or \`${bin} artifacts unhold <ref> --id <id>\` for finished holds, then retry. Pass --allow-low-disk to create it anyway.`,
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function heldSummary(row: ArtifactInventoryEntry): ArtifactHeldSummary {
|
|
1099
|
+
const parsed = readManifest(row.path);
|
|
1100
|
+
return {
|
|
1101
|
+
name: row.name,
|
|
1102
|
+
bytes: row.bytes ?? 0,
|
|
1103
|
+
holds: parsed.ok
|
|
1104
|
+
? parsed.manifest.holds.map((hold) => ({
|
|
1105
|
+
id: hold.id,
|
|
1106
|
+
set_by: hold.set_by.name ?? hold.set_by.instance_id,
|
|
1107
|
+
expires_at: hold.expires_at ?? null,
|
|
1108
|
+
persistent: hold.persistent === true,
|
|
1109
|
+
}))
|
|
1110
|
+
: [],
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function usageReport(
|
|
1115
|
+
repoRoot: string,
|
|
1116
|
+
heldBytes: number,
|
|
1117
|
+
largest: ArtifactHeldSummary[],
|
|
1118
|
+
freeBytes: number | null,
|
|
1119
|
+
measuredAt: string | null,
|
|
1120
|
+
): ArtifactUsageReport {
|
|
1121
|
+
const maxHeld = artifactMaxHeldBytes(repoRoot);
|
|
1122
|
+
const minFree = artifactMinFreeBytes(repoRoot);
|
|
1123
|
+
const heldOver = heldBytes > maxHeld;
|
|
1124
|
+
const lowDisk = minFree > 0 && freeBytes !== null && freeBytes < minFree;
|
|
1125
|
+
const bin = resolveBinName(repoRoot);
|
|
1126
|
+
const warnings: string[] = [];
|
|
1127
|
+
if (heldOver) {
|
|
1128
|
+
const list = largest
|
|
1129
|
+
.map(
|
|
1130
|
+
(item) =>
|
|
1131
|
+
`${item.name} (${gib(item.bytes)}${item.holds.length ? `, hold ${item.holds.map((hold) => hold.id).join(", ")}` : ""})`,
|
|
1132
|
+
)
|
|
1133
|
+
.join("; ");
|
|
1134
|
+
warnings.push(
|
|
1135
|
+
`Held artifacts use ${gib(heldBytes)}${measuredAt ? ` as of ${measuredAt}` : ""}, above the ${gib(maxHeld)} held budget (artifacts.max_held_bytes). Holds are never deleted automatically; release or prune the largest${list ? `: ${list}` : ""}. Remove a finished hold with \`${bin} artifacts unhold <name> --id <id>\`.`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
if (lowDisk) {
|
|
1139
|
+
warnings.push(
|
|
1140
|
+
`Only ${gib(freeBytes!)} is free on the artifact store's disk, below the ${gib(minFree)} floor (artifacts.min_free_bytes). Free space with \`${bin} artifacts clean --yes\`, discard reviewed workspaces, or unhold finished work; \`${bin} artifacts create --big\` refuses until then.`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
held_bytes: heldBytes,
|
|
1145
|
+
max_held_bytes: maxHeld,
|
|
1146
|
+
held_over_budget: heldOver,
|
|
1147
|
+
largest_holds: largest,
|
|
1148
|
+
free_bytes: freeBytes,
|
|
1149
|
+
min_free_bytes: minFree,
|
|
1150
|
+
low_disk: lowDisk,
|
|
1151
|
+
warnings,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function writeUsageCache(repoRoot: string, cache: ArtifactUsageCache): void {
|
|
1156
|
+
try {
|
|
1157
|
+
const path = join(resolve(repoRoot), USAGE_CACHE);
|
|
1158
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
1159
|
+
writeFileSync(temp, `${JSON.stringify(cache, null, 2)}\n`, { mode: stateFileMode() });
|
|
1160
|
+
renameSync(temp, path);
|
|
1161
|
+
} catch {
|
|
1162
|
+
// The cache only feeds warnings; a failed write must not fail the inventory.
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function gib(bytes: number): string {
|
|
1167
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
679
1170
|
/** Sibling of the artifacts root so the stamp never appears in the inventory scan. */
|
|
680
1171
|
const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
|
|
681
1172
|
|
|
@@ -691,9 +1182,11 @@ export interface ArtifactAutoCleanResult {
|
|
|
691
1182
|
*
|
|
692
1183
|
* Retention was previously enforced only when someone remembered to run
|
|
693
1184
|
* `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
|
|
694
|
-
* busy hosts. This runs the exact same guarded deletion
|
|
695
|
-
*
|
|
696
|
-
*
|
|
1185
|
+
* busy hosts. This runs the exact same guarded deletion as `artifacts clean
|
|
1186
|
+
* --yes` (expired entries, plus oversize or over-budget entries idle past
|
|
1187
|
+
* `artifacts.idle_grace_hours`, each re-classified immediately before removal
|
|
1188
|
+
* and recorded in the deletion log; unmanaged and legacy directories are never
|
|
1189
|
+
* touched) at most once per
|
|
697
1190
|
* interval (default 1h after completion, 1m between partial/failed slices).
|
|
698
1191
|
* The owner-aware lock serializes callers; interrupted attempts can retry.
|
|
699
1192
|
* Disable with `artifacts.auto_clean: false` or
|
|
@@ -773,6 +1266,7 @@ function autoCleanArtifactsUnlocked(
|
|
|
773
1266
|
.filter((row) => row.classification === "unknown")
|
|
774
1267
|
.map((row) => ({ name: row.name, reason: row.reason }));
|
|
775
1268
|
const status = failures.length ? "failed" : remaining ? "partial" : "completed";
|
|
1269
|
+
artifactUsageReport(repoRoot, rows, { now });
|
|
776
1270
|
writeStamp({
|
|
777
1271
|
...attempt,
|
|
778
1272
|
status,
|
|
@@ -846,13 +1340,23 @@ function classifyArtifactPath(
|
|
|
846
1340
|
return rowFor(path, name, repoRoot, classification, parsed.reason);
|
|
847
1341
|
}
|
|
848
1342
|
const manifest = parsed.manifest;
|
|
849
|
-
const
|
|
1343
|
+
const usage = safeTreeUsage(path);
|
|
850
1344
|
const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
|
|
851
1345
|
const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
852
1346
|
const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
|
|
853
1347
|
const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
|
|
854
|
-
const
|
|
855
|
-
|
|
1348
|
+
const holds = artifactHoldState(manifest, now);
|
|
1349
|
+
// A lapsed hold protected the files until its deadline, so ordinary expiry
|
|
1350
|
+
// and the size-rule idle clock both count from the latest lapse.
|
|
1351
|
+
const lapsedMs = Math.max(0, ...holds.lapsed.map((hold) => Date.parse(hold.expires_at!)));
|
|
1352
|
+
const effectiveExpiresAt = new Date(
|
|
1353
|
+
Math.max(effectiveLastModifiedMs + retentionWindowMs, lapsedMs),
|
|
1354
|
+
).toISOString();
|
|
1355
|
+
const base = rowFor(path, name, repoRoot, "managed-current", "retention has not expired", usage);
|
|
1356
|
+
const releasedMs = Math.max(
|
|
1357
|
+
manifest.released_at ? Date.parse(manifest.released_at) : 0,
|
|
1358
|
+
lapsedMs,
|
|
1359
|
+
);
|
|
856
1360
|
Object.assign(base, {
|
|
857
1361
|
artifact_id: manifest.artifact_id,
|
|
858
1362
|
slug: manifest.slug,
|
|
@@ -861,14 +1365,16 @@ function classifyArtifactPath(
|
|
|
861
1365
|
expires_at: effectiveExpiresAt,
|
|
862
1366
|
owner_instance_id: manifest.created_by?.instance_id ?? null,
|
|
863
1367
|
oversize_acknowledged: manifest.oversize_acknowledged === true,
|
|
1368
|
+
idle_since: new Date(Math.max(effectiveLastModifiedMs, releasedMs)).toISOString(),
|
|
864
1369
|
});
|
|
865
1370
|
|
|
866
|
-
if (
|
|
1371
|
+
if (holds.active.length > 0) {
|
|
867
1372
|
return {
|
|
868
1373
|
...base,
|
|
869
1374
|
classification: "managed-held",
|
|
870
|
-
reason: `held: ${
|
|
1375
|
+
reason: `held: ${holds.active.map((hold) => hold.id).join(", ")}`,
|
|
871
1376
|
action: "keep",
|
|
1377
|
+
warning: holdWarning(repoRoot, name, holds.active, now),
|
|
872
1378
|
};
|
|
873
1379
|
}
|
|
874
1380
|
if (base.bytes === null || lastModifiedMs === null) {
|
|
@@ -888,7 +1394,17 @@ function classifyArtifactPath(
|
|
|
888
1394
|
};
|
|
889
1395
|
}
|
|
890
1396
|
if (!manifest.released_at && manifest.created_by?.instance_id) {
|
|
891
|
-
const live = ownerLiveness(
|
|
1397
|
+
const { state: live, heartbeatMs } = ownerLiveness(
|
|
1398
|
+
repoRoot,
|
|
1399
|
+
manifest.created_by.instance_id,
|
|
1400
|
+
now,
|
|
1401
|
+
freshnessSeconds,
|
|
1402
|
+
);
|
|
1403
|
+
// A stale heartbeat only starts the idle clock. The owner may be waiting
|
|
1404
|
+
// for a human reply, so size rules still wait out the grace from here.
|
|
1405
|
+
if (heartbeatMs !== null && heartbeatMs > Date.parse(base.idle_since!)) {
|
|
1406
|
+
base.idle_since = new Date(Math.min(heartbeatMs, now.getTime())).toISOString();
|
|
1407
|
+
}
|
|
892
1408
|
if (live === "live") {
|
|
893
1409
|
return {
|
|
894
1410
|
...base,
|
|
@@ -907,44 +1423,101 @@ function classifyArtifactPath(
|
|
|
907
1423
|
}
|
|
908
1424
|
}
|
|
909
1425
|
if (Date.parse(effectiveExpiresAt) > now.getTime()) return base;
|
|
1426
|
+
const lapsedNote = holds.lapsed.length
|
|
1427
|
+
? `; hold ${holds.lapsed.map((hold) => `${hold.id} lapsed at ${hold.expires_at}`).join(", ")}`
|
|
1428
|
+
: "";
|
|
910
1429
|
return {
|
|
911
1430
|
...base,
|
|
912
1431
|
classification: "managed-expired",
|
|
913
|
-
reason: `retention expired at ${effectiveExpiresAt}`,
|
|
1432
|
+
reason: `retention expired at ${effectiveExpiresAt}${lapsedNote}`,
|
|
914
1433
|
action: "would-delete",
|
|
915
1434
|
};
|
|
916
1435
|
}
|
|
917
1436
|
|
|
1437
|
+
/** Advice for held rows: an imminent lapse, or a hold that never expires. */
|
|
1438
|
+
function holdWarning(
|
|
1439
|
+
repoRoot: string,
|
|
1440
|
+
name: string,
|
|
1441
|
+
active: ArtifactHold[],
|
|
1442
|
+
now: Date,
|
|
1443
|
+
): string | null {
|
|
1444
|
+
const bin = resolveBinName(repoRoot);
|
|
1445
|
+
const notes: string[] = [];
|
|
1446
|
+
for (const hold of active) {
|
|
1447
|
+
if (hold.persistent) continue;
|
|
1448
|
+
const renew = `${bin} artifacts hold ${name} --id ${hold.id} --reason ${JSON.stringify(hold.reason)}`;
|
|
1449
|
+
if (!hold.expires_at) {
|
|
1450
|
+
notes.push(
|
|
1451
|
+
`hold ${hold.id} has no expiry because it predates hold expiry; re-holding with \`${renew}\` starts one, or unhold it when the work is done`,
|
|
1452
|
+
);
|
|
1453
|
+
} else if (Date.parse(hold.expires_at) - now.getTime() <= HOLD_LAPSE_WARNING_MS) {
|
|
1454
|
+
notes.push(`hold ${hold.id} lapses at ${hold.expires_at}; renew with \`${renew}\``);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return notes.length ? notes.join("; ") : null;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** When a size rule may first delete this unit, or null when it never may. */
|
|
1461
|
+
function sizeEvictableAt(repoRoot: string, row: ArtifactInventoryEntry): number | null {
|
|
1462
|
+
if (!row.idle_since) return null;
|
|
1463
|
+
const idleSince = Date.parse(row.idle_since);
|
|
1464
|
+
if (!Number.isFinite(idleSince)) return null;
|
|
1465
|
+
return idleSince + artifactIdleGraceHours(repoRoot) * 60 * 60 * 1000;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
918
1468
|
function applyArtifactUnitBudget(
|
|
919
1469
|
repoRoot: string,
|
|
920
1470
|
row: ArtifactInventoryEntry,
|
|
1471
|
+
now: Date,
|
|
921
1472
|
): ArtifactInventoryEntry {
|
|
922
1473
|
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
1474
|
+
if (
|
|
1475
|
+
!["managed-current", "managed-active"].includes(row.classification) ||
|
|
1476
|
+
row.bytes === null ||
|
|
1477
|
+
row.bytes <= maxUnitBytes ||
|
|
1478
|
+
row.oversize_acknowledged
|
|
1479
|
+
) {
|
|
1480
|
+
return row;
|
|
1481
|
+
}
|
|
1482
|
+
const evictableAt = sizeEvictableAt(repoRoot, row);
|
|
923
1483
|
if (
|
|
924
1484
|
row.classification === "managed-current" &&
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
!row.oversize_acknowledged
|
|
1485
|
+
evictableAt !== null &&
|
|
1486
|
+
now.getTime() >= evictableAt
|
|
928
1487
|
) {
|
|
929
1488
|
return {
|
|
930
1489
|
...row,
|
|
931
1490
|
classification: "managed-oversize",
|
|
932
1491
|
action: "would-delete",
|
|
933
|
-
reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
|
|
1492
|
+
reason: `bundle uses ${row.bytes} bytes on disk, above the ${maxUnitBytes}-byte ceiling without --big, and has been idle for ${artifactIdleGraceHours(repoRoot)}h`,
|
|
934
1493
|
};
|
|
935
1494
|
}
|
|
936
|
-
|
|
1495
|
+
const when =
|
|
1496
|
+
row.classification === "managed-active" || evictableAt === null
|
|
1497
|
+
? `${artifactIdleGraceHours(repoRoot)}h after its owner goes idle`
|
|
1498
|
+
: `after ${new Date(evictableAt).toISOString()} unless it changes first`;
|
|
1499
|
+
return {
|
|
1500
|
+
...row,
|
|
1501
|
+
warning: `uses ${row.bytes} bytes on disk, above the ${maxUnitBytes}-byte per-workspace ceiling; cleanup will delete it ${when}. If it is meant to be this large, run ${resolveBinName(repoRoot)} artifacts allow-big ${row.name}; otherwise move rebuildable content out of the artifact store`,
|
|
1502
|
+
};
|
|
937
1503
|
}
|
|
938
1504
|
|
|
939
1505
|
function applyArtifactBudgets(
|
|
940
1506
|
repoRoot: string,
|
|
941
1507
|
inputRows: ArtifactInventoryEntry[],
|
|
1508
|
+
now: Date,
|
|
942
1509
|
): ArtifactInventoryEntry[] {
|
|
943
1510
|
const maxBytes = artifactMaxBytes(repoRoot);
|
|
944
|
-
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
|
|
1511
|
+
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }, now));
|
|
945
1512
|
|
|
1513
|
+
// Held units can never be evicted, so counting them would push every other
|
|
1514
|
+
// unit out without bringing the store under budget.
|
|
946
1515
|
const managedBytes = rows.reduce(
|
|
947
|
-
(sum, row) =>
|
|
1516
|
+
(sum, row) =>
|
|
1517
|
+
sum +
|
|
1518
|
+
(row.artifact_id && row.classification !== "managed-held" && row.bytes !== null
|
|
1519
|
+
? row.bytes
|
|
1520
|
+
: 0),
|
|
948
1521
|
0,
|
|
949
1522
|
);
|
|
950
1523
|
let retainedBytes =
|
|
@@ -956,10 +1529,12 @@ function applyArtifactBudgets(
|
|
|
956
1529
|
if (retainedBytes <= maxBytes) return rows;
|
|
957
1530
|
|
|
958
1531
|
const candidates = rows
|
|
959
|
-
.filter(
|
|
960
|
-
(row)
|
|
961
|
-
|
|
962
|
-
|
|
1532
|
+
.filter((row) => {
|
|
1533
|
+
if (row.classification !== "managed-current" || row.action !== "keep" || row.bytes === null)
|
|
1534
|
+
return false;
|
|
1535
|
+
const evictableAt = sizeEvictableAt(repoRoot, row);
|
|
1536
|
+
return evictableAt !== null && now.getTime() >= evictableAt;
|
|
1537
|
+
})
|
|
963
1538
|
.sort((left, right) =>
|
|
964
1539
|
`${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(
|
|
965
1540
|
`${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`,
|
|
@@ -968,7 +1543,7 @@ function applyArtifactBudgets(
|
|
|
968
1543
|
for (const row of candidates) {
|
|
969
1544
|
if (retainedBytes <= maxBytes) break;
|
|
970
1545
|
row.classification = "managed-over-budget";
|
|
971
|
-
row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring
|
|
1546
|
+
row.reason = `repository artifact budget is ${maxBytes} bytes of disk use; earliest-expiring bundles idle for ${artifactIdleGraceHours(repoRoot)}h are removed first`;
|
|
972
1547
|
row.action = "would-delete";
|
|
973
1548
|
retainedBytes -= row.bytes ?? 0;
|
|
974
1549
|
}
|
|
@@ -1068,7 +1643,7 @@ function atomicWriteManifest(path: string, manifest: ArtifactManifestV2, now = n
|
|
|
1068
1643
|
try {
|
|
1069
1644
|
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
1070
1645
|
encoding: "utf8",
|
|
1071
|
-
mode:
|
|
1646
|
+
mode: stateFileMode(),
|
|
1072
1647
|
flag: "wx",
|
|
1073
1648
|
});
|
|
1074
1649
|
renameSync(tmp, target);
|
|
@@ -1143,7 +1718,7 @@ export function migrateArtifacts(
|
|
|
1143
1718
|
};
|
|
1144
1719
|
mkdirSync(dirname(preimagePath), { recursive: true });
|
|
1145
1720
|
try {
|
|
1146
|
-
writeFileSync(preimagePath, preimage, { flag: "wx", mode:
|
|
1721
|
+
writeFileSync(preimagePath, preimage, { flag: "wx", mode: stateFileMode() });
|
|
1147
1722
|
} catch (error) {
|
|
1148
1723
|
if (
|
|
1149
1724
|
(error as NodeJS.ErrnoException).code !== "EEXIST" ||
|
|
@@ -1287,7 +1862,7 @@ export function repairArtifactActivity(
|
|
|
1287
1862
|
null,
|
|
1288
1863
|
2,
|
|
1289
1864
|
)}\n`,
|
|
1290
|
-
{ flag: "wx", mode:
|
|
1865
|
+
{ flag: "wx", mode: stateFileMode() },
|
|
1291
1866
|
);
|
|
1292
1867
|
const current = lstatSync(path);
|
|
1293
1868
|
if (
|
|
@@ -1318,26 +1893,73 @@ function validHold(value: unknown): value is ArtifactHold {
|
|
|
1318
1893
|
typeof hold.reason === "string" &&
|
|
1319
1894
|
!!hold.reason.trim() &&
|
|
1320
1895
|
validActor(hold.set_by) &&
|
|
1321
|
-
validIso(hold.set_at)
|
|
1896
|
+
validIso(hold.set_at) &&
|
|
1897
|
+
(hold.expires_at === undefined || validIso(hold.expires_at)) &&
|
|
1898
|
+
(hold.persistent === undefined || hold.persistent === true) &&
|
|
1899
|
+
!(hold.persistent && hold.expires_at !== undefined)
|
|
1322
1900
|
);
|
|
1323
1901
|
}
|
|
1324
1902
|
|
|
1903
|
+
const HOLD_LAPSE_WARNING_MS = 48 * 60 * 60 * 1000;
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Split holds into those still in force and those that lapsed. A persistent
|
|
1907
|
+
* hold never lapses. A hold recorded before holds expired has no `expires_at`
|
|
1908
|
+
* and stays in force until its owner removes or renews it.
|
|
1909
|
+
*/
|
|
1910
|
+
export function artifactHoldState(
|
|
1911
|
+
manifest: Pick<ArtifactManifestV2, "holds">,
|
|
1912
|
+
now: Date = new Date(),
|
|
1913
|
+
): { active: ArtifactHold[]; lapsed: ArtifactHold[] } {
|
|
1914
|
+
const active: ArtifactHold[] = [];
|
|
1915
|
+
const lapsed: ArtifactHold[] = [];
|
|
1916
|
+
for (const hold of manifest.holds) {
|
|
1917
|
+
if (hold.persistent || !hold.expires_at || Date.parse(hold.expires_at) > now.getTime())
|
|
1918
|
+
active.push(hold);
|
|
1919
|
+
else lapsed.push(hold);
|
|
1920
|
+
}
|
|
1921
|
+
return { active, lapsed };
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1325
1924
|
function makeHold(
|
|
1326
1925
|
input: ArtifactHoldInput,
|
|
1327
1926
|
actor: ArtifactActor | undefined,
|
|
1328
1927
|
now: Date,
|
|
1928
|
+
defaultDays: number,
|
|
1329
1929
|
): ArtifactHold {
|
|
1330
1930
|
assertValidDate(now, "now");
|
|
1331
1931
|
if (!validActor(actor)) throw new Error("a valid hold actor is required");
|
|
1332
1932
|
if (!validHoldId(input.id)) throw new Error("invalid hold id");
|
|
1333
1933
|
if (typeof input.reason !== "string" || !input.reason.trim())
|
|
1334
1934
|
throw new Error("hold reason must not be empty");
|
|
1335
|
-
|
|
1935
|
+
if (input.days !== undefined && input.minutes !== undefined)
|
|
1936
|
+
throw new Error("choose either hold days or hold minutes");
|
|
1937
|
+
if (input.persistent && (input.days !== undefined || input.minutes !== undefined))
|
|
1938
|
+
throw new Error("a persistent hold takes no duration");
|
|
1939
|
+
const hold: ArtifactHold = {
|
|
1336
1940
|
id: input.id,
|
|
1337
1941
|
reason: input.reason.trim(),
|
|
1338
1942
|
set_by: { ...actor },
|
|
1339
1943
|
set_at: now.toISOString(),
|
|
1340
1944
|
};
|
|
1945
|
+
if (input.persistent) return { ...hold, persistent: true };
|
|
1946
|
+
const minutes =
|
|
1947
|
+
input.minutes !== undefined
|
|
1948
|
+
? holdMinutes(input.minutes)
|
|
1949
|
+
: holdDaysValue(input.days ?? defaultDays) * 24 * 60;
|
|
1950
|
+
return { ...hold, expires_at: addMinutes(now, minutes).toISOString() };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function holdDaysValue(value: number): number {
|
|
1954
|
+
if (!Number.isInteger(value) || value <= 0 || value > 365)
|
|
1955
|
+
throw new Error("hold days must be between 1 and 365");
|
|
1956
|
+
return value;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function holdMinutes(value: number): number {
|
|
1960
|
+
if (!Number.isInteger(value) || value <= 0 || value > 365 * 24 * 60)
|
|
1961
|
+
throw new Error("hold minutes must be between 1 and 525600");
|
|
1962
|
+
return value;
|
|
1341
1963
|
}
|
|
1342
1964
|
|
|
1343
1965
|
function ownerLiveness(
|
|
@@ -1345,15 +1967,18 @@ function ownerLiveness(
|
|
|
1345
1967
|
instanceId: string,
|
|
1346
1968
|
now: Date,
|
|
1347
1969
|
freshnessSeconds: number,
|
|
1348
|
-
): "live" | "stale" | "unknown" {
|
|
1970
|
+
): { state: "live" | "stale" | "unknown"; heartbeatMs: number | null } {
|
|
1349
1971
|
try {
|
|
1350
1972
|
const row = readLiveCoordinationRow(repoRoot, instanceId);
|
|
1351
|
-
if (!row) return "stale";
|
|
1973
|
+
if (!row) return { state: "stale", heartbeatMs: null };
|
|
1352
1974
|
const ts = Date.parse(row.last_heartbeat);
|
|
1353
|
-
if (!Number.isFinite(ts)) return "unknown";
|
|
1354
|
-
return
|
|
1975
|
+
if (!Number.isFinite(ts)) return { state: "unknown", heartbeatMs: null };
|
|
1976
|
+
return {
|
|
1977
|
+
state: now.getTime() - ts <= freshnessSeconds * 1000 ? "live" : "stale",
|
|
1978
|
+
heartbeatMs: ts,
|
|
1979
|
+
};
|
|
1355
1980
|
} catch {
|
|
1356
|
-
return "unknown";
|
|
1981
|
+
return { state: "unknown", heartbeatMs: null };
|
|
1357
1982
|
}
|
|
1358
1983
|
}
|
|
1359
1984
|
|
|
@@ -1368,17 +1993,39 @@ function containsTrackedPath(repoRoot: string, path: string): boolean {
|
|
|
1368
1993
|
return result.status !== 0 || result.stdout.length > 0;
|
|
1369
1994
|
}
|
|
1370
1995
|
|
|
1371
|
-
|
|
1996
|
+
interface TreeUsage {
|
|
1997
|
+
/** Allocated disk bytes, counting each hard-linked inode once. */
|
|
1998
|
+
disk: number;
|
|
1999
|
+
/** Sum of file lengths, as `ls -l` reports them. */
|
|
2000
|
+
apparent: number;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
/** Disk bytes for one entry. Windows reports no block count, so length stands in. */
|
|
2004
|
+
function allocatedBytes(st: Stats): number {
|
|
2005
|
+
return process.platform !== "win32" && Number.isFinite(st.blocks) ? st.blocks * 512 : st.size;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Measure a tree without following symlinks. Size rules read `disk`: a sparse
|
|
2010
|
+
* image or a hard-linked environment would otherwise count many times its real
|
|
2011
|
+
* footprint and push healthy workspaces over a budget they do not exceed.
|
|
2012
|
+
*/
|
|
2013
|
+
function safeTreeUsage(path: string, seen = new Set<string>()): TreeUsage | null {
|
|
1372
2014
|
try {
|
|
1373
2015
|
const st = lstatSync(path);
|
|
1374
|
-
if (st.
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2016
|
+
if (!st.isDirectory()) {
|
|
2017
|
+
if (!st.isFile() && !st.isSymbolicLink()) return { disk: 0, apparent: 0 };
|
|
2018
|
+
const key = `${st.dev}:${st.ino}`;
|
|
2019
|
+
if (st.nlink > 1 && seen.has(key)) return { disk: 0, apparent: st.size };
|
|
2020
|
+
if (st.nlink > 1) seen.add(key);
|
|
2021
|
+
return { disk: allocatedBytes(st), apparent: st.size };
|
|
2022
|
+
}
|
|
2023
|
+
const total: TreeUsage = { disk: allocatedBytes(st), apparent: st.size };
|
|
1378
2024
|
for (const child of readdirSync(path)) {
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
total +=
|
|
2025
|
+
const usage = safeTreeUsage(join(path, child), seen);
|
|
2026
|
+
if (usage === null) return null;
|
|
2027
|
+
total.disk += usage.disk;
|
|
2028
|
+
total.apparent += usage.apparent;
|
|
1382
2029
|
}
|
|
1383
2030
|
return total;
|
|
1384
2031
|
} catch {
|
|
@@ -1386,6 +2033,10 @@ function safeTreeSize(path: string): number | null {
|
|
|
1386
2033
|
}
|
|
1387
2034
|
}
|
|
1388
2035
|
|
|
2036
|
+
function safeTreeSize(path: string): number | null {
|
|
2037
|
+
return safeTreeUsage(path)?.disk ?? null;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1389
2040
|
/**
|
|
1390
2041
|
* Return the newest filesystem change in a managed tree without following
|
|
1391
2042
|
* symlinks. A small future tolerance protects a write racing the inventory
|
|
@@ -1405,7 +2056,7 @@ function rowFor(
|
|
|
1405
2056
|
repoRoot: string,
|
|
1406
2057
|
classification: ArtifactClassification,
|
|
1407
2058
|
reason: string,
|
|
1408
|
-
|
|
2059
|
+
usage: TreeUsage | null = safeTreeUsage(path),
|
|
1409
2060
|
): ArtifactInventoryEntry {
|
|
1410
2061
|
return {
|
|
1411
2062
|
name,
|
|
@@ -1414,7 +2065,8 @@ function rowFor(
|
|
|
1414
2065
|
classification,
|
|
1415
2066
|
reason,
|
|
1416
2067
|
action: classification === "managed-expired" ? "would-delete" : "keep",
|
|
1417
|
-
bytes,
|
|
2068
|
+
bytes: usage?.disk ?? null,
|
|
2069
|
+
apparent_bytes: usage?.apparent ?? null,
|
|
1418
2070
|
artifact_id: null,
|
|
1419
2071
|
slug: null,
|
|
1420
2072
|
created_at: null,
|
|
@@ -1422,6 +2074,8 @@ function rowFor(
|
|
|
1422
2074
|
expires_at: null,
|
|
1423
2075
|
owner_instance_id: null,
|
|
1424
2076
|
oversize_acknowledged: false,
|
|
2077
|
+
idle_since: null,
|
|
2078
|
+
warning: null,
|
|
1425
2079
|
};
|
|
1426
2080
|
}
|
|
1427
2081
|
|