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,15 +5,20 @@
|
|
|
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
|
import { spawnSync } from "node:child_process";
|
|
11
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
14
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statfsSync, writeFileSync, } from "node:fs";
|
|
13
15
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
16
|
import { isDeepStrictEqual } from "node:util";
|
|
15
17
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
16
|
-
import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, coordFreshnessSeconds, resolveBinName, } from "../config.js";
|
|
18
|
+
import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactHoldDays, artifactIdleGraceHours, artifactMaxBytes, artifactMaxHeldBytes, artifactMaxUnitBytes, artifactMinFreeBytes, coordFreshnessSeconds, resolveBinName, } from "../config.js";
|
|
19
|
+
import { readCoordinationViewV3, requireAuthoritySafeCoordinationViewV3, } from "../events/v3/coordination-view.js";
|
|
20
|
+
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.js";
|
|
21
|
+
import { stateFileMode } from "../storage/modes.js";
|
|
17
22
|
import { artifactRootEntries, readArtifactActivity, validArtifactActivity, } from "./activity.js";
|
|
18
23
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
|
|
19
24
|
import { withArtifactLock } from "./mutation-lock.js";
|
|
@@ -28,6 +33,12 @@ export function artifactCapabilities() {
|
|
|
28
33
|
explicit_v1_migration: true,
|
|
29
34
|
minute_retention: true,
|
|
30
35
|
discard_after_review: true,
|
|
36
|
+
owner_scoped_remove: true,
|
|
37
|
+
allow_big_after_create: true,
|
|
38
|
+
hold_expiry: true,
|
|
39
|
+
persistent_holds: true,
|
|
40
|
+
held_budget: true,
|
|
41
|
+
disk_free_report: true,
|
|
31
42
|
};
|
|
32
43
|
}
|
|
33
44
|
export function artifactsRoot(repoRoot) {
|
|
@@ -56,7 +67,8 @@ function createArtifactUnlocked(repoRoot, input) {
|
|
|
56
67
|
if (!isSafeId(artifactId)) {
|
|
57
68
|
throw new Error("artifact id must use ASCII letters, digits, hyphens, or underscores");
|
|
58
69
|
}
|
|
59
|
-
const
|
|
70
|
+
const holdDays = artifactHoldDays(repoRoot);
|
|
71
|
+
const holds = (input.holds ?? []).map((hold) => makeHold(hold, input.actor, now, holdDays));
|
|
60
72
|
if (new Set(holds.map((hold) => hold.id)).size !== holds.length) {
|
|
61
73
|
throw new Error("duplicate hold id");
|
|
62
74
|
}
|
|
@@ -101,7 +113,7 @@ export function inventoryArtifacts(repoRoot, opts = {}) {
|
|
|
101
113
|
for (const name of names) {
|
|
102
114
|
rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
|
|
103
115
|
}
|
|
104
|
-
return applyArtifactBudgets(repoRoot, rows);
|
|
116
|
+
return applyArtifactBudgets(repoRoot, rows, now);
|
|
105
117
|
}
|
|
106
118
|
export function showArtifact(repoRoot, ref, opts = {}) {
|
|
107
119
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
@@ -140,6 +152,24 @@ function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
|
|
|
140
152
|
atomicWriteManifest(path, manifest, now);
|
|
141
153
|
return manifest;
|
|
142
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Record the `--big` acknowledgement on an existing workspace, for work that
|
|
157
|
+
* turns out larger than expected. Unlike a hold, it leaves expiry in force and
|
|
158
|
+
* exempts the unit only from the per-bundle ceiling. Idempotent.
|
|
159
|
+
*/
|
|
160
|
+
export function allowBigArtifact(repoRoot, ref, input = {}) {
|
|
161
|
+
return withArtifactLock(repoRoot, () => {
|
|
162
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
163
|
+
const parsed = readManifest(path);
|
|
164
|
+
if (!parsed.ok)
|
|
165
|
+
throw new Error(parsed.reason);
|
|
166
|
+
if (parsed.manifest.oversize_acknowledged)
|
|
167
|
+
return parsed.manifest;
|
|
168
|
+
const manifest = { ...parsed.manifest, oversize_acknowledged: true };
|
|
169
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
170
|
+
return manifest;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
143
173
|
export function releaseArtifact(repoRoot, ref, input = {}) {
|
|
144
174
|
return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
|
|
145
175
|
}
|
|
@@ -185,6 +215,95 @@ export function discardArtifact(repoRoot, ref, reason, input = {}) {
|
|
|
185
215
|
return updated;
|
|
186
216
|
});
|
|
187
217
|
}
|
|
218
|
+
/** Preview or immediately remove one reviewed workspace belonging to this actor. */
|
|
219
|
+
export function removeArtifact(repoRoot, ref, reason, input = {}) {
|
|
220
|
+
const why = reason.trim();
|
|
221
|
+
if (!why)
|
|
222
|
+
throw new Error("remove requires a reason confirming the files are no longer needed");
|
|
223
|
+
if (!validActor(input.actor))
|
|
224
|
+
throw new Error("remove requires a current artifact owner identity");
|
|
225
|
+
const actor = input.actor;
|
|
226
|
+
const now = input.now ?? new Date();
|
|
227
|
+
assertValidDate(now, "now");
|
|
228
|
+
const store = artifactsRoot(repoRoot);
|
|
229
|
+
const checkStore = () => {
|
|
230
|
+
for (const path of [resolve(repoRoot), dirname(store), store]) {
|
|
231
|
+
const stat = lstatSync(path);
|
|
232
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
233
|
+
throw new Error("artifact store must use real directories, not symlinks");
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
checkStore();
|
|
237
|
+
return withArtifactLock(repoRoot, () => {
|
|
238
|
+
checkStore();
|
|
239
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
240
|
+
const inspect = () => {
|
|
241
|
+
const target = realpathSync(path);
|
|
242
|
+
const cwd = realpathSync(process.cwd());
|
|
243
|
+
if (cwd === target || cwd.startsWith(`${target}${sep}`))
|
|
244
|
+
throw new Error("cannot remove the working directory or its ancestor");
|
|
245
|
+
if (lstatSync(path).dev !== lstatSync(store).dev)
|
|
246
|
+
throw new Error("artifact removal cannot cross a mount boundary");
|
|
247
|
+
const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
|
|
248
|
+
if (!["managed-current", "managed-expired", "managed-active"].includes(entry.classification))
|
|
249
|
+
throw new Error(`cannot remove artifact: ${entry.reason}`);
|
|
250
|
+
if (entry.owner_instance_id !== actor.instance_id)
|
|
251
|
+
throw new Error("only the creating artifact owner may remove this workspace");
|
|
252
|
+
const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(repoRoot));
|
|
253
|
+
for (const peer of Object.values(view.instances)) {
|
|
254
|
+
if (!peer.authority_eligible || peer.instance_id === liveInstanceIdV3(actor.instance_id))
|
|
255
|
+
continue;
|
|
256
|
+
for (const claim of peer.files_touched) {
|
|
257
|
+
const claimed = resolve(repoRoot, claim);
|
|
258
|
+
if (path === claimed ||
|
|
259
|
+
path.startsWith(`${claimed}${sep}`) ||
|
|
260
|
+
claimed.startsWith(`${path}${sep}`))
|
|
261
|
+
throw new Error(`artifact overlaps another agent's claim: ${claim}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return entry;
|
|
265
|
+
};
|
|
266
|
+
const entry = inspect();
|
|
267
|
+
const before = artifactRemovalSnapshot(path);
|
|
268
|
+
if (!input.yes)
|
|
269
|
+
return { entry, reason: why, deleted: false };
|
|
270
|
+
// Metadata mutations share this lock. Recheck payload, claims, and protections
|
|
271
|
+
// immediately before removal because ordinary file writers do not take it.
|
|
272
|
+
checkStore();
|
|
273
|
+
const current = inspect();
|
|
274
|
+
if (!isDeepStrictEqual(entry, current) ||
|
|
275
|
+
!isDeepStrictEqual(before, artifactRemovalSnapshot(path)))
|
|
276
|
+
throw new Error("artifact changed during removal inspection; preview it again");
|
|
277
|
+
rmSync(path, { recursive: true, force: false });
|
|
278
|
+
const removed = { ...current, action: "deleted", reason: why };
|
|
279
|
+
recordArtifactDeletion(repoRoot, removed, now, actor);
|
|
280
|
+
return { entry: removed, reason: why, deleted: true };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/** Refuse links, mounts, special files, and embedded stores; fingerprint every entry. */
|
|
284
|
+
function artifactRemovalSnapshot(path) {
|
|
285
|
+
const device = lstatSync(path).dev;
|
|
286
|
+
const entries = new Map();
|
|
287
|
+
const walk = (target) => {
|
|
288
|
+
const stat = lstatSync(target);
|
|
289
|
+
if (stat.isSymbolicLink() || stat.dev !== device || (!stat.isFile() && !stat.isDirectory()))
|
|
290
|
+
throw new Error(`unsafe artifact removal path: ${target}`);
|
|
291
|
+
if (target !== path && [".git", ".harnery"].includes(basename(target)))
|
|
292
|
+
throw new Error(`embedded repository or coordination state cannot be removed: ${target}`);
|
|
293
|
+
if (target !== join(path, ARTIFACT_MANIFEST) && basename(target) === ARTIFACT_MANIFEST)
|
|
294
|
+
throw new Error(`nested artifact must be managed separately: ${target}`);
|
|
295
|
+
entries.set(target, [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":"));
|
|
296
|
+
if (stat.isDirectory()) {
|
|
297
|
+
const names = readdirSync(target).sort();
|
|
298
|
+
if (names.includes("HEAD") && names.includes("objects") && names.includes("config"))
|
|
299
|
+
throw new Error(`embedded Git metadata cannot be removed: ${target}`);
|
|
300
|
+
for (const name of names)
|
|
301
|
+
walk(join(target, name));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
walk(path);
|
|
305
|
+
return entries;
|
|
306
|
+
}
|
|
188
307
|
/** Advice only: a successful check does not establish that its evidence is disposable. */
|
|
189
308
|
export function artifactReviewGuidance(repoRoot, ref) {
|
|
190
309
|
const target = isAbsolute(ref) ? basename(ref) : ref;
|
|
@@ -208,7 +327,7 @@ function releaseArtifactUnlocked(repoRoot, ref, input) {
|
|
|
208
327
|
return manifest;
|
|
209
328
|
}
|
|
210
329
|
export function holdArtifact(repoRoot, ref, input) {
|
|
211
|
-
const hold = makeHold(input, input.actor, input.now ?? new Date());
|
|
330
|
+
const hold = makeHold(input, input.actor, input.now ?? new Date(), artifactHoldDays(repoRoot));
|
|
212
331
|
return withArtifactLock(repoRoot, () => {
|
|
213
332
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
214
333
|
const parsed = readManifest(path);
|
|
@@ -220,7 +339,18 @@ export function holdArtifact(repoRoot, ref, input) {
|
|
|
220
339
|
previous.reason !== hold.reason) {
|
|
221
340
|
throw new Error("hold id already exists with a different owner or reason");
|
|
222
341
|
}
|
|
223
|
-
|
|
342
|
+
// Repeating a hold renews it: the original owner and set_at stay, and the
|
|
343
|
+
// window restarts from now. A persistent hold stays persistent.
|
|
344
|
+
const { expires_at: _expires, persistent: _persistent, ...kept } = previous;
|
|
345
|
+
const renewed = previous.persistent || hold.persistent
|
|
346
|
+
? { ...kept, persistent: true }
|
|
347
|
+
: { ...kept, expires_at: hold.expires_at };
|
|
348
|
+
const manifest = {
|
|
349
|
+
...parsed.manifest,
|
|
350
|
+
holds: parsed.manifest.holds.map((item) => (item.id === hold.id ? renewed : item)),
|
|
351
|
+
};
|
|
352
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
353
|
+
return manifest;
|
|
224
354
|
}
|
|
225
355
|
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
226
356
|
atomicWriteManifest(path, manifest, input.now);
|
|
@@ -387,7 +517,7 @@ function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
|
387
517
|
// per-unit size are local decisions; recheck all their guards on the target.
|
|
388
518
|
const current = entry.classification === "managed-over-budget"
|
|
389
519
|
? inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find((row) => row.path === entry.path)
|
|
390
|
-
: applyArtifactUnitBudget(repoRoot, classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds));
|
|
520
|
+
: applyArtifactUnitBudget(repoRoot, classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds), now);
|
|
391
521
|
if (!current) {
|
|
392
522
|
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
393
523
|
}
|
|
@@ -409,6 +539,7 @@ function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
|
409
539
|
};
|
|
410
540
|
}
|
|
411
541
|
rmSync(current.path, { recursive: true, force: false });
|
|
542
|
+
recordArtifactDeletion(repoRoot, current, now);
|
|
412
543
|
return { ...current, action: "deleted" };
|
|
413
544
|
}
|
|
414
545
|
catch (error) {
|
|
@@ -421,6 +552,220 @@ function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
|
421
552
|
}
|
|
422
553
|
});
|
|
423
554
|
}
|
|
555
|
+
/** Sibling of the artifacts root, like the stamp, so it never enters the inventory. */
|
|
556
|
+
const DELETION_LOG = ".harnery/artifact-deletions.jsonl";
|
|
557
|
+
const DELETION_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
558
|
+
/**
|
|
559
|
+
* Append one deletion to the log, keeping 30 days. Callers hold the artifact
|
|
560
|
+
* lock. Best-effort: the directory is already gone, so a failed write must not
|
|
561
|
+
* turn a completed deletion into a reported failure.
|
|
562
|
+
*/
|
|
563
|
+
function recordArtifactDeletion(repoRoot, row, now, removedBy) {
|
|
564
|
+
const record = {
|
|
565
|
+
deleted_at: now.toISOString(),
|
|
566
|
+
name: row.name,
|
|
567
|
+
relative_path: row.relative_path,
|
|
568
|
+
artifact_id: row.artifact_id,
|
|
569
|
+
slug: row.slug,
|
|
570
|
+
owner_instance_id: row.owner_instance_id,
|
|
571
|
+
classification: row.classification,
|
|
572
|
+
reason: row.reason,
|
|
573
|
+
bytes: row.bytes,
|
|
574
|
+
apparent_bytes: row.apparent_bytes,
|
|
575
|
+
expires_at: row.expires_at,
|
|
576
|
+
idle_since: row.idle_since,
|
|
577
|
+
...(removedBy ? { removed_by: removedBy } : {}),
|
|
578
|
+
};
|
|
579
|
+
try {
|
|
580
|
+
const path = join(resolve(repoRoot), DELETION_LOG);
|
|
581
|
+
const kept = readArtifactDeletions(repoRoot, {
|
|
582
|
+
since: new Date(now.getTime() - DELETION_LOG_RETENTION_MS),
|
|
583
|
+
});
|
|
584
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
585
|
+
writeFileSync(temp, [...kept, record].map((item) => `${JSON.stringify(item)}\n`).join(""), {
|
|
586
|
+
mode: stateFileMode(),
|
|
587
|
+
});
|
|
588
|
+
renameSync(temp, path);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// See the doc comment: the deletion stands even if its record cannot.
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/** Deletions the cleanup recorded, oldest first. Unreadable lines are skipped. */
|
|
595
|
+
export function readArtifactDeletions(repoRoot, opts = {}) {
|
|
596
|
+
let text;
|
|
597
|
+
try {
|
|
598
|
+
text = readFileSync(join(resolve(repoRoot), DELETION_LOG), "utf8");
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
const since = opts.since?.getTime() ?? -Infinity;
|
|
604
|
+
const records = [];
|
|
605
|
+
for (const line of text.split("\n")) {
|
|
606
|
+
if (!line.trim())
|
|
607
|
+
continue;
|
|
608
|
+
try {
|
|
609
|
+
const record = JSON.parse(line);
|
|
610
|
+
if (typeof record?.name === "string" && Date.parse(record.deleted_at) >= since)
|
|
611
|
+
records.push(record);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// A torn or foreign line does not hide the others.
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return records;
|
|
618
|
+
}
|
|
619
|
+
/** Sibling of the artifacts root, like the deletion log, so it never enters the inventory. */
|
|
620
|
+
const USAGE_CACHE = ".harnery/artifact-usage.json";
|
|
621
|
+
const LARGEST_HOLDS = 5;
|
|
622
|
+
/** Bytes available to this user on the artifact store's filesystem, or null. */
|
|
623
|
+
export function artifactFreeBytes(repoRoot) {
|
|
624
|
+
try {
|
|
625
|
+
const root = artifactsRoot(repoRoot);
|
|
626
|
+
const stat = statfsSync(existsSync(root) ? root : resolve(repoRoot));
|
|
627
|
+
const free = Number(stat.bavail) * Number(stat.bsize);
|
|
628
|
+
return Number.isFinite(free) && free >= 0 ? free : null;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Held bytes, free disk, and warnings for one full inventory. Held work is
|
|
636
|
+
* never deleted: the held budget and the disk floor only report. Records the
|
|
637
|
+
* measurement in `.harnery/artifact-usage.json` so `create` and `hold` can
|
|
638
|
+
* warn without walking the store.
|
|
639
|
+
*/
|
|
640
|
+
export function artifactUsageReport(repoRoot, rows, opts = {}) {
|
|
641
|
+
const now = opts.now ?? new Date();
|
|
642
|
+
const kept = rows.filter((row) => row.action !== "deleted");
|
|
643
|
+
const held = kept.filter((row) => row.classification === "managed-held");
|
|
644
|
+
const heldBytes = held.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
|
|
645
|
+
const largest = [...held]
|
|
646
|
+
.sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0))
|
|
647
|
+
.slice(0, LARGEST_HOLDS)
|
|
648
|
+
.map((row) => heldSummary(row));
|
|
649
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
650
|
+
if (existsSync(artifactsRoot(repoRoot))) {
|
|
651
|
+
writeUsageCache(repoRoot, {
|
|
652
|
+
measured_at: now.toISOString(),
|
|
653
|
+
bytes: kept.reduce((sum, row) => sum + (row.bytes ?? 0), 0),
|
|
654
|
+
held_bytes: heldBytes,
|
|
655
|
+
free_bytes: freeBytes,
|
|
656
|
+
largest_holds: largest,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return usageReport(repoRoot, heldBytes, largest, freeBytes, null);
|
|
660
|
+
}
|
|
661
|
+
/** The cached measurement, or null when none was recorded or it is unreadable. */
|
|
662
|
+
export function readArtifactUsageCache(repoRoot) {
|
|
663
|
+
try {
|
|
664
|
+
const value = JSON.parse(readFileSync(join(resolve(repoRoot), USAGE_CACHE), "utf8"));
|
|
665
|
+
if (!value ||
|
|
666
|
+
!validIso(value.measured_at) ||
|
|
667
|
+
typeof value.held_bytes !== "number" ||
|
|
668
|
+
typeof value.bytes !== "number")
|
|
669
|
+
return null;
|
|
670
|
+
return {
|
|
671
|
+
measured_at: value.measured_at,
|
|
672
|
+
bytes: value.bytes,
|
|
673
|
+
held_bytes: value.held_bytes,
|
|
674
|
+
free_bytes: typeof value.free_bytes === "number" ? value.free_bytes : null,
|
|
675
|
+
largest_holds: Array.isArray(value.largest_holds) ? value.largest_holds : [],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Usage for commands that must stay cheap (`create`, `hold`): held bytes from
|
|
684
|
+
* the last full inventory, free disk measured now. Never walks the store.
|
|
685
|
+
*/
|
|
686
|
+
export function artifactQuickUsage(repoRoot) {
|
|
687
|
+
const cache = readArtifactUsageCache(repoRoot);
|
|
688
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
689
|
+
const report = usageReport(repoRoot, cache?.held_bytes ?? 0, cache?.largest_holds ?? [], freeBytes, cache?.measured_at ?? null);
|
|
690
|
+
return {
|
|
691
|
+
usage: {
|
|
692
|
+
held_bytes: cache?.held_bytes ?? null,
|
|
693
|
+
held_measured_at: cache?.measured_at ?? null,
|
|
694
|
+
free_bytes: freeBytes,
|
|
695
|
+
},
|
|
696
|
+
low_disk: report.low_disk,
|
|
697
|
+
min_free_bytes: report.min_free_bytes,
|
|
698
|
+
warnings: report.warnings,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Refuse new large work when the artifact store's disk is below its floor.
|
|
703
|
+
* `create --big` calls this; `--allow-low-disk` is the deliberate override.
|
|
704
|
+
*/
|
|
705
|
+
export function assertArtifactDiskFloor(repoRoot) {
|
|
706
|
+
const minFree = artifactMinFreeBytes(repoRoot);
|
|
707
|
+
const freeBytes = artifactFreeBytes(repoRoot);
|
|
708
|
+
if (minFree === 0 || freeBytes === null || freeBytes >= minFree)
|
|
709
|
+
return;
|
|
710
|
+
const bin = resolveBinName(repoRoot);
|
|
711
|
+
throw new Error(`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.`);
|
|
712
|
+
}
|
|
713
|
+
function heldSummary(row) {
|
|
714
|
+
const parsed = readManifest(row.path);
|
|
715
|
+
return {
|
|
716
|
+
name: row.name,
|
|
717
|
+
bytes: row.bytes ?? 0,
|
|
718
|
+
holds: parsed.ok
|
|
719
|
+
? parsed.manifest.holds.map((hold) => ({
|
|
720
|
+
id: hold.id,
|
|
721
|
+
set_by: hold.set_by.name ?? hold.set_by.instance_id,
|
|
722
|
+
expires_at: hold.expires_at ?? null,
|
|
723
|
+
persistent: hold.persistent === true,
|
|
724
|
+
}))
|
|
725
|
+
: [],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function usageReport(repoRoot, heldBytes, largest, freeBytes, measuredAt) {
|
|
729
|
+
const maxHeld = artifactMaxHeldBytes(repoRoot);
|
|
730
|
+
const minFree = artifactMinFreeBytes(repoRoot);
|
|
731
|
+
const heldOver = heldBytes > maxHeld;
|
|
732
|
+
const lowDisk = minFree > 0 && freeBytes !== null && freeBytes < minFree;
|
|
733
|
+
const bin = resolveBinName(repoRoot);
|
|
734
|
+
const warnings = [];
|
|
735
|
+
if (heldOver) {
|
|
736
|
+
const list = largest
|
|
737
|
+
.map((item) => `${item.name} (${gib(item.bytes)}${item.holds.length ? `, hold ${item.holds.map((hold) => hold.id).join(", ")}` : ""})`)
|
|
738
|
+
.join("; ");
|
|
739
|
+
warnings.push(`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>\`.`);
|
|
740
|
+
}
|
|
741
|
+
if (lowDisk) {
|
|
742
|
+
warnings.push(`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.`);
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
held_bytes: heldBytes,
|
|
746
|
+
max_held_bytes: maxHeld,
|
|
747
|
+
held_over_budget: heldOver,
|
|
748
|
+
largest_holds: largest,
|
|
749
|
+
free_bytes: freeBytes,
|
|
750
|
+
min_free_bytes: minFree,
|
|
751
|
+
low_disk: lowDisk,
|
|
752
|
+
warnings,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function writeUsageCache(repoRoot, cache) {
|
|
756
|
+
try {
|
|
757
|
+
const path = join(resolve(repoRoot), USAGE_CACHE);
|
|
758
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
759
|
+
writeFileSync(temp, `${JSON.stringify(cache, null, 2)}\n`, { mode: stateFileMode() });
|
|
760
|
+
renameSync(temp, path);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// The cache only feeds warnings; a failed write must not fail the inventory.
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function gib(bytes) {
|
|
767
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
768
|
+
}
|
|
424
769
|
/** Sibling of the artifacts root so the stamp never appears in the inventory scan. */
|
|
425
770
|
const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
|
|
426
771
|
/**
|
|
@@ -428,9 +773,11 @@ const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
|
|
|
428
773
|
*
|
|
429
774
|
* Retention was previously enforced only when someone remembered to run
|
|
430
775
|
* `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
|
|
431
|
-
* busy hosts. This runs the exact same guarded deletion
|
|
432
|
-
*
|
|
433
|
-
*
|
|
776
|
+
* busy hosts. This runs the exact same guarded deletion as `artifacts clean
|
|
777
|
+
* --yes` (expired entries, plus oversize or over-budget entries idle past
|
|
778
|
+
* `artifacts.idle_grace_hours`, each re-classified immediately before removal
|
|
779
|
+
* and recorded in the deletion log; unmanaged and legacy directories are never
|
|
780
|
+
* touched) at most once per
|
|
434
781
|
* interval (default 1h after completion, 1m between partial/failed slices).
|
|
435
782
|
* The owner-aware lock serializes callers; interrupted attempts can retry.
|
|
436
783
|
* Disable with `artifacts.auto_clean: false` or
|
|
@@ -494,6 +841,7 @@ function autoCleanArtifactsUnlocked(repoRoot, opts) {
|
|
|
494
841
|
.filter((row) => row.classification === "unknown")
|
|
495
842
|
.map((row) => ({ name: row.name, reason: row.reason }));
|
|
496
843
|
const status = failures.length ? "failed" : remaining ? "partial" : "completed";
|
|
844
|
+
artifactUsageReport(repoRoot, rows, { now });
|
|
497
845
|
writeStamp({
|
|
498
846
|
...attempt,
|
|
499
847
|
status,
|
|
@@ -563,13 +911,18 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
563
911
|
return rowFor(path, name, repoRoot, classification, parsed.reason);
|
|
564
912
|
}
|
|
565
913
|
const manifest = parsed.manifest;
|
|
566
|
-
const
|
|
914
|
+
const usage = safeTreeUsage(path);
|
|
567
915
|
const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
|
|
568
916
|
const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
569
917
|
const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
|
|
570
918
|
const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
|
|
571
|
-
const
|
|
572
|
-
|
|
919
|
+
const holds = artifactHoldState(manifest, now);
|
|
920
|
+
// A lapsed hold protected the files until its deadline, so ordinary expiry
|
|
921
|
+
// and the size-rule idle clock both count from the latest lapse.
|
|
922
|
+
const lapsedMs = Math.max(0, ...holds.lapsed.map((hold) => Date.parse(hold.expires_at)));
|
|
923
|
+
const effectiveExpiresAt = new Date(Math.max(effectiveLastModifiedMs + retentionWindowMs, lapsedMs)).toISOString();
|
|
924
|
+
const base = rowFor(path, name, repoRoot, "managed-current", "retention has not expired", usage);
|
|
925
|
+
const releasedMs = Math.max(manifest.released_at ? Date.parse(manifest.released_at) : 0, lapsedMs);
|
|
573
926
|
Object.assign(base, {
|
|
574
927
|
artifact_id: manifest.artifact_id,
|
|
575
928
|
slug: manifest.slug,
|
|
@@ -578,13 +931,15 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
578
931
|
expires_at: effectiveExpiresAt,
|
|
579
932
|
owner_instance_id: manifest.created_by?.instance_id ?? null,
|
|
580
933
|
oversize_acknowledged: manifest.oversize_acknowledged === true,
|
|
934
|
+
idle_since: new Date(Math.max(effectiveLastModifiedMs, releasedMs)).toISOString(),
|
|
581
935
|
});
|
|
582
|
-
if (
|
|
936
|
+
if (holds.active.length > 0) {
|
|
583
937
|
return {
|
|
584
938
|
...base,
|
|
585
939
|
classification: "managed-held",
|
|
586
|
-
reason: `held: ${
|
|
940
|
+
reason: `held: ${holds.active.map((hold) => hold.id).join(", ")}`,
|
|
587
941
|
action: "keep",
|
|
942
|
+
warning: holdWarning(repoRoot, name, holds.active, now),
|
|
588
943
|
};
|
|
589
944
|
}
|
|
590
945
|
if (base.bytes === null || lastModifiedMs === null) {
|
|
@@ -604,7 +959,12 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
604
959
|
};
|
|
605
960
|
}
|
|
606
961
|
if (!manifest.released_at && manifest.created_by?.instance_id) {
|
|
607
|
-
const live = ownerLiveness(repoRoot, manifest.created_by.instance_id, now, freshnessSeconds);
|
|
962
|
+
const { state: live, heartbeatMs } = ownerLiveness(repoRoot, manifest.created_by.instance_id, now, freshnessSeconds);
|
|
963
|
+
// A stale heartbeat only starts the idle clock. The owner may be waiting
|
|
964
|
+
// for a human reply, so size rules still wait out the grace from here.
|
|
965
|
+
if (heartbeatMs !== null && heartbeatMs > Date.parse(base.idle_since)) {
|
|
966
|
+
base.idle_since = new Date(Math.min(heartbeatMs, now.getTime())).toISOString();
|
|
967
|
+
}
|
|
608
968
|
if (live === "live") {
|
|
609
969
|
return {
|
|
610
970
|
...base,
|
|
@@ -624,44 +984,95 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
624
984
|
}
|
|
625
985
|
if (Date.parse(effectiveExpiresAt) > now.getTime())
|
|
626
986
|
return base;
|
|
987
|
+
const lapsedNote = holds.lapsed.length
|
|
988
|
+
? `; hold ${holds.lapsed.map((hold) => `${hold.id} lapsed at ${hold.expires_at}`).join(", ")}`
|
|
989
|
+
: "";
|
|
627
990
|
return {
|
|
628
991
|
...base,
|
|
629
992
|
classification: "managed-expired",
|
|
630
|
-
reason: `retention expired at ${effectiveExpiresAt}`,
|
|
993
|
+
reason: `retention expired at ${effectiveExpiresAt}${lapsedNote}`,
|
|
631
994
|
action: "would-delete",
|
|
632
995
|
};
|
|
633
996
|
}
|
|
634
|
-
|
|
997
|
+
/** Advice for held rows: an imminent lapse, or a hold that never expires. */
|
|
998
|
+
function holdWarning(repoRoot, name, active, now) {
|
|
999
|
+
const bin = resolveBinName(repoRoot);
|
|
1000
|
+
const notes = [];
|
|
1001
|
+
for (const hold of active) {
|
|
1002
|
+
if (hold.persistent)
|
|
1003
|
+
continue;
|
|
1004
|
+
const renew = `${bin} artifacts hold ${name} --id ${hold.id} --reason ${JSON.stringify(hold.reason)}`;
|
|
1005
|
+
if (!hold.expires_at) {
|
|
1006
|
+
notes.push(`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`);
|
|
1007
|
+
}
|
|
1008
|
+
else if (Date.parse(hold.expires_at) - now.getTime() <= HOLD_LAPSE_WARNING_MS) {
|
|
1009
|
+
notes.push(`hold ${hold.id} lapses at ${hold.expires_at}; renew with \`${renew}\``);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return notes.length ? notes.join("; ") : null;
|
|
1013
|
+
}
|
|
1014
|
+
/** When a size rule may first delete this unit, or null when it never may. */
|
|
1015
|
+
function sizeEvictableAt(repoRoot, row) {
|
|
1016
|
+
if (!row.idle_since)
|
|
1017
|
+
return null;
|
|
1018
|
+
const idleSince = Date.parse(row.idle_since);
|
|
1019
|
+
if (!Number.isFinite(idleSince))
|
|
1020
|
+
return null;
|
|
1021
|
+
return idleSince + artifactIdleGraceHours(repoRoot) * 60 * 60 * 1000;
|
|
1022
|
+
}
|
|
1023
|
+
function applyArtifactUnitBudget(repoRoot, row, now) {
|
|
635
1024
|
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
1025
|
+
if (!["managed-current", "managed-active"].includes(row.classification) ||
|
|
1026
|
+
row.bytes === null ||
|
|
1027
|
+
row.bytes <= maxUnitBytes ||
|
|
1028
|
+
row.oversize_acknowledged) {
|
|
1029
|
+
return row;
|
|
1030
|
+
}
|
|
1031
|
+
const evictableAt = sizeEvictableAt(repoRoot, row);
|
|
636
1032
|
if (row.classification === "managed-current" &&
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
!row.oversize_acknowledged) {
|
|
1033
|
+
evictableAt !== null &&
|
|
1034
|
+
now.getTime() >= evictableAt) {
|
|
640
1035
|
return {
|
|
641
1036
|
...row,
|
|
642
1037
|
classification: "managed-oversize",
|
|
643
1038
|
action: "would-delete",
|
|
644
|
-
reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
|
|
1039
|
+
reason: `bundle uses ${row.bytes} bytes on disk, above the ${maxUnitBytes}-byte ceiling without --big, and has been idle for ${artifactIdleGraceHours(repoRoot)}h`,
|
|
645
1040
|
};
|
|
646
1041
|
}
|
|
647
|
-
|
|
1042
|
+
const when = row.classification === "managed-active" || evictableAt === null
|
|
1043
|
+
? `${artifactIdleGraceHours(repoRoot)}h after its owner goes idle`
|
|
1044
|
+
: `after ${new Date(evictableAt).toISOString()} unless it changes first`;
|
|
1045
|
+
return {
|
|
1046
|
+
...row,
|
|
1047
|
+
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`,
|
|
1048
|
+
};
|
|
648
1049
|
}
|
|
649
|
-
function applyArtifactBudgets(repoRoot, inputRows) {
|
|
1050
|
+
function applyArtifactBudgets(repoRoot, inputRows, now) {
|
|
650
1051
|
const maxBytes = artifactMaxBytes(repoRoot);
|
|
651
|
-
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
|
|
652
|
-
|
|
1052
|
+
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }, now));
|
|
1053
|
+
// Held units can never be evicted, so counting them would push every other
|
|
1054
|
+
// unit out without bringing the store under budget.
|
|
1055
|
+
const managedBytes = rows.reduce((sum, row) => sum +
|
|
1056
|
+
(row.artifact_id && row.classification !== "managed-held" && row.bytes !== null
|
|
1057
|
+
? row.bytes
|
|
1058
|
+
: 0), 0);
|
|
653
1059
|
let retainedBytes = managedBytes -
|
|
654
1060
|
rows.reduce((sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0), 0);
|
|
655
1061
|
if (retainedBytes <= maxBytes)
|
|
656
1062
|
return rows;
|
|
657
1063
|
const candidates = rows
|
|
658
|
-
.filter((row) =>
|
|
1064
|
+
.filter((row) => {
|
|
1065
|
+
if (row.classification !== "managed-current" || row.action !== "keep" || row.bytes === null)
|
|
1066
|
+
return false;
|
|
1067
|
+
const evictableAt = sizeEvictableAt(repoRoot, row);
|
|
1068
|
+
return evictableAt !== null && now.getTime() >= evictableAt;
|
|
1069
|
+
})
|
|
659
1070
|
.sort((left, right) => `${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(`${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`));
|
|
660
1071
|
for (const row of candidates) {
|
|
661
1072
|
if (retainedBytes <= maxBytes)
|
|
662
1073
|
break;
|
|
663
1074
|
row.classification = "managed-over-budget";
|
|
664
|
-
row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring
|
|
1075
|
+
row.reason = `repository artifact budget is ${maxBytes} bytes of disk use; earliest-expiring bundles idle for ${artifactIdleGraceHours(repoRoot)}h are removed first`;
|
|
665
1076
|
row.action = "would-delete";
|
|
666
1077
|
retainedBytes -= row.bytes ?? 0;
|
|
667
1078
|
}
|
|
@@ -753,7 +1164,7 @@ function atomicWriteManifest(path, manifest, now = new Date()) {
|
|
|
753
1164
|
try {
|
|
754
1165
|
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
755
1166
|
encoding: "utf8",
|
|
756
|
-
mode:
|
|
1167
|
+
mode: stateFileMode(),
|
|
757
1168
|
flag: "wx",
|
|
758
1169
|
});
|
|
759
1170
|
renameSync(tmp, target);
|
|
@@ -814,7 +1225,7 @@ export function migrateArtifacts(repoRoot, opts = {}) {
|
|
|
814
1225
|
};
|
|
815
1226
|
mkdirSync(dirname(preimagePath), { recursive: true });
|
|
816
1227
|
try {
|
|
817
|
-
writeFileSync(preimagePath, preimage, { flag: "wx", mode:
|
|
1228
|
+
writeFileSync(preimagePath, preimage, { flag: "wx", mode: stateFileMode() });
|
|
818
1229
|
}
|
|
819
1230
|
catch (error) {
|
|
820
1231
|
if (error.code !== "EEXIST" ||
|
|
@@ -938,7 +1349,7 @@ export function repairArtifactActivity(repoRoot, opts = {}) {
|
|
|
938
1349
|
activity,
|
|
939
1350
|
previous_expires_at: row.previous_expires_at,
|
|
940
1351
|
repaired_expires_at: row.repaired_expires_at,
|
|
941
|
-
}, null, 2)}\n`, { flag: "wx", mode:
|
|
1352
|
+
}, null, 2)}\n`, { flag: "wx", mode: stateFileMode() });
|
|
942
1353
|
const current = lstatSync(path);
|
|
943
1354
|
if (readFileSync(target, "utf8") !== before ||
|
|
944
1355
|
current.mtimeMs !== unitStat.mtimeMs ||
|
|
@@ -965,9 +1376,29 @@ function validHold(value) {
|
|
|
965
1376
|
typeof hold.reason === "string" &&
|
|
966
1377
|
!!hold.reason.trim() &&
|
|
967
1378
|
validActor(hold.set_by) &&
|
|
968
|
-
validIso(hold.set_at)
|
|
1379
|
+
validIso(hold.set_at) &&
|
|
1380
|
+
(hold.expires_at === undefined || validIso(hold.expires_at)) &&
|
|
1381
|
+
(hold.persistent === undefined || hold.persistent === true) &&
|
|
1382
|
+
!(hold.persistent && hold.expires_at !== undefined));
|
|
1383
|
+
}
|
|
1384
|
+
const HOLD_LAPSE_WARNING_MS = 48 * 60 * 60 * 1000;
|
|
1385
|
+
/**
|
|
1386
|
+
* Split holds into those still in force and those that lapsed. A persistent
|
|
1387
|
+
* hold never lapses. A hold recorded before holds expired has no `expires_at`
|
|
1388
|
+
* and stays in force until its owner removes or renews it.
|
|
1389
|
+
*/
|
|
1390
|
+
export function artifactHoldState(manifest, now = new Date()) {
|
|
1391
|
+
const active = [];
|
|
1392
|
+
const lapsed = [];
|
|
1393
|
+
for (const hold of manifest.holds) {
|
|
1394
|
+
if (hold.persistent || !hold.expires_at || Date.parse(hold.expires_at) > now.getTime())
|
|
1395
|
+
active.push(hold);
|
|
1396
|
+
else
|
|
1397
|
+
lapsed.push(hold);
|
|
1398
|
+
}
|
|
1399
|
+
return { active, lapsed };
|
|
969
1400
|
}
|
|
970
|
-
function makeHold(input, actor, now) {
|
|
1401
|
+
function makeHold(input, actor, now, defaultDays) {
|
|
971
1402
|
assertValidDate(now, "now");
|
|
972
1403
|
if (!validActor(actor))
|
|
973
1404
|
throw new Error("a valid hold actor is required");
|
|
@@ -975,25 +1406,48 @@ function makeHold(input, actor, now) {
|
|
|
975
1406
|
throw new Error("invalid hold id");
|
|
976
1407
|
if (typeof input.reason !== "string" || !input.reason.trim())
|
|
977
1408
|
throw new Error("hold reason must not be empty");
|
|
978
|
-
|
|
1409
|
+
if (input.days !== undefined && input.minutes !== undefined)
|
|
1410
|
+
throw new Error("choose either hold days or hold minutes");
|
|
1411
|
+
if (input.persistent && (input.days !== undefined || input.minutes !== undefined))
|
|
1412
|
+
throw new Error("a persistent hold takes no duration");
|
|
1413
|
+
const hold = {
|
|
979
1414
|
id: input.id,
|
|
980
1415
|
reason: input.reason.trim(),
|
|
981
1416
|
set_by: { ...actor },
|
|
982
1417
|
set_at: now.toISOString(),
|
|
983
1418
|
};
|
|
1419
|
+
if (input.persistent)
|
|
1420
|
+
return { ...hold, persistent: true };
|
|
1421
|
+
const minutes = input.minutes !== undefined
|
|
1422
|
+
? holdMinutes(input.minutes)
|
|
1423
|
+
: holdDaysValue(input.days ?? defaultDays) * 24 * 60;
|
|
1424
|
+
return { ...hold, expires_at: addMinutes(now, minutes).toISOString() };
|
|
1425
|
+
}
|
|
1426
|
+
function holdDaysValue(value) {
|
|
1427
|
+
if (!Number.isInteger(value) || value <= 0 || value > 365)
|
|
1428
|
+
throw new Error("hold days must be between 1 and 365");
|
|
1429
|
+
return value;
|
|
1430
|
+
}
|
|
1431
|
+
function holdMinutes(value) {
|
|
1432
|
+
if (!Number.isInteger(value) || value <= 0 || value > 365 * 24 * 60)
|
|
1433
|
+
throw new Error("hold minutes must be between 1 and 525600");
|
|
1434
|
+
return value;
|
|
984
1435
|
}
|
|
985
1436
|
function ownerLiveness(repoRoot, instanceId, now, freshnessSeconds) {
|
|
986
1437
|
try {
|
|
987
1438
|
const row = readLiveCoordinationRow(repoRoot, instanceId);
|
|
988
1439
|
if (!row)
|
|
989
|
-
return "stale";
|
|
1440
|
+
return { state: "stale", heartbeatMs: null };
|
|
990
1441
|
const ts = Date.parse(row.last_heartbeat);
|
|
991
1442
|
if (!Number.isFinite(ts))
|
|
992
|
-
return "unknown";
|
|
993
|
-
return
|
|
1443
|
+
return { state: "unknown", heartbeatMs: null };
|
|
1444
|
+
return {
|
|
1445
|
+
state: now.getTime() - ts <= freshnessSeconds * 1000 ? "live" : "stale",
|
|
1446
|
+
heartbeatMs: ts,
|
|
1447
|
+
};
|
|
994
1448
|
}
|
|
995
1449
|
catch {
|
|
996
|
-
return "unknown";
|
|
1450
|
+
return { state: "unknown", heartbeatMs: null };
|
|
997
1451
|
}
|
|
998
1452
|
}
|
|
999
1453
|
function containsTrackedPath(repoRoot, path) {
|
|
@@ -1007,21 +1461,35 @@ function containsTrackedPath(repoRoot, path) {
|
|
|
1007
1461
|
// If Git itself cannot answer, fail closed.
|
|
1008
1462
|
return result.status !== 0 || result.stdout.length > 0;
|
|
1009
1463
|
}
|
|
1010
|
-
|
|
1464
|
+
/** Disk bytes for one entry. Windows reports no block count, so length stands in. */
|
|
1465
|
+
function allocatedBytes(st) {
|
|
1466
|
+
return process.platform !== "win32" && Number.isFinite(st.blocks) ? st.blocks * 512 : st.size;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Measure a tree without following symlinks. Size rules read `disk`: a sparse
|
|
1470
|
+
* image or a hard-linked environment would otherwise count many times its real
|
|
1471
|
+
* footprint and push healthy workspaces over a budget they do not exceed.
|
|
1472
|
+
*/
|
|
1473
|
+
function safeTreeUsage(path, seen = new Set()) {
|
|
1011
1474
|
try {
|
|
1012
1475
|
const st = lstatSync(path);
|
|
1013
|
-
if (st.
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1476
|
+
if (!st.isDirectory()) {
|
|
1477
|
+
if (!st.isFile() && !st.isSymbolicLink())
|
|
1478
|
+
return { disk: 0, apparent: 0 };
|
|
1479
|
+
const key = `${st.dev}:${st.ino}`;
|
|
1480
|
+
if (st.nlink > 1 && seen.has(key))
|
|
1481
|
+
return { disk: 0, apparent: st.size };
|
|
1482
|
+
if (st.nlink > 1)
|
|
1483
|
+
seen.add(key);
|
|
1484
|
+
return { disk: allocatedBytes(st), apparent: st.size };
|
|
1485
|
+
}
|
|
1486
|
+
const total = { disk: allocatedBytes(st), apparent: st.size };
|
|
1020
1487
|
for (const child of readdirSync(path)) {
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1488
|
+
const usage = safeTreeUsage(join(path, child), seen);
|
|
1489
|
+
if (usage === null)
|
|
1023
1490
|
return null;
|
|
1024
|
-
total +=
|
|
1491
|
+
total.disk += usage.disk;
|
|
1492
|
+
total.apparent += usage.apparent;
|
|
1025
1493
|
}
|
|
1026
1494
|
return total;
|
|
1027
1495
|
}
|
|
@@ -1029,6 +1497,9 @@ function safeTreeSize(path) {
|
|
|
1029
1497
|
return null;
|
|
1030
1498
|
}
|
|
1031
1499
|
}
|
|
1500
|
+
function safeTreeSize(path) {
|
|
1501
|
+
return safeTreeUsage(path)?.disk ?? null;
|
|
1502
|
+
}
|
|
1032
1503
|
/**
|
|
1033
1504
|
* Return the newest filesystem change in a managed tree without following
|
|
1034
1505
|
* symlinks. A small future tolerance protects a write racing the inventory
|
|
@@ -1042,7 +1513,7 @@ function safeTreeLastModified(path, now, activity) {
|
|
|
1042
1513
|
return null;
|
|
1043
1514
|
}
|
|
1044
1515
|
}
|
|
1045
|
-
function rowFor(path, name, repoRoot, classification, reason,
|
|
1516
|
+
function rowFor(path, name, repoRoot, classification, reason, usage = safeTreeUsage(path)) {
|
|
1046
1517
|
return {
|
|
1047
1518
|
name,
|
|
1048
1519
|
path,
|
|
@@ -1050,7 +1521,8 @@ function rowFor(path, name, repoRoot, classification, reason, bytes = safeTreeSi
|
|
|
1050
1521
|
classification,
|
|
1051
1522
|
reason,
|
|
1052
1523
|
action: classification === "managed-expired" ? "would-delete" : "keep",
|
|
1053
|
-
bytes,
|
|
1524
|
+
bytes: usage?.disk ?? null,
|
|
1525
|
+
apparent_bytes: usage?.apparent ?? null,
|
|
1054
1526
|
artifact_id: null,
|
|
1055
1527
|
slug: null,
|
|
1056
1528
|
created_at: null,
|
|
@@ -1058,6 +1530,8 @@ function rowFor(path, name, repoRoot, classification, reason, bytes = safeTreeSi
|
|
|
1058
1530
|
expires_at: null,
|
|
1059
1531
|
owner_instance_id: null,
|
|
1060
1532
|
oversize_acknowledged: false,
|
|
1533
|
+
idle_since: null,
|
|
1534
|
+
warning: null,
|
|
1061
1535
|
};
|
|
1062
1536
|
}
|
|
1063
1537
|
function normalizeSlug(value) {
|