harnery 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +32 -8
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +97 -6
- package/dist/commands/backup.d.ts +16 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +239 -33
- package/dist/commands/browse.d.ts +8 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +466 -231
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +6 -3
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +20 -4
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +26 -3
- package/dist/commands/qa-status.d.ts.map +1 -1
- package/dist/commands/qa-status.js +7 -2
- package/dist/commands/resources.d.ts.map +1 -1
- package/dist/commands/resources.js +23 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +98 -23
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -2
- package/dist/core/adapters/profiles.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts +3 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -0
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +5 -3
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.d.ts +7 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +63 -8
- package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +2 -1
- package/dist/core/agents/state/names.d.ts +35 -2
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +80 -5
- package/dist/core/agents/state/stale-sweep.d.ts +2 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -6
- package/dist/core/agents/turn-elapsed.d.ts +51 -0
- package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
- package/dist/core/agents/turn-elapsed.js +91 -0
- package/dist/core/artifacts/activity.d.ts +14 -0
- package/dist/core/artifacts/activity.d.ts.map +1 -0
- package/dist/core/artifacts/activity.js +52 -0
- package/dist/core/artifacts/delivery-card.d.ts +39 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
- package/dist/core/artifacts/delivery-card.js +282 -0
- package/dist/core/artifacts/index.d.ts +42 -7
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +288 -76
- package/dist/core/artifacts/mutation-lock.d.ts +2 -0
- package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
- package/dist/core/artifacts/mutation-lock.js +91 -0
- package/dist/core/backup/host-snapshot.d.ts +18 -0
- package/dist/core/backup/host-snapshot.d.ts.map +1 -0
- package/dist/core/backup/host-snapshot.js +64 -0
- package/dist/core/config.d.ts +20 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +44 -10
- package/dist/core/diagnostics/advice.d.ts +38 -3
- package/dist/core/diagnostics/advice.d.ts.map +1 -1
- package/dist/core/diagnostics/advice.js +103 -99
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +111 -47
- package/dist/core/diagnostics/comparison.d.ts.map +1 -1
- package/dist/core/diagnostics/comparison.js +3 -2
- package/dist/core/diagnostics/contract.d.ts +15 -31
- package/dist/core/diagnostics/contract.d.ts.map +1 -1
- package/dist/core/diagnostics/contract.js +2 -5
- package/dist/core/diagnostics/index.d.ts +2 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -1
- package/dist/core/diagnostics/index.js +2 -0
- package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
- package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure-contract.js +70 -0
- package/dist/core/diagnostics/pressure.d.ts +25 -0
- package/dist/core/diagnostics/pressure.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure.js +1138 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -1
- package/dist/core/diagnostics/replay.js +90 -10
- package/dist/core/events/v3/adapter-id.d.ts +6 -0
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
- package/dist/core/events/v3/adapter-id.js +7 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
- package/dist/core/events/v3/archive-retention.js +3 -2
- package/dist/core/events/v3/bootstrap.d.ts +41 -4
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +120 -17
- package/dist/core/events/v3/capabilities.d.ts +16 -9
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +37 -2
- package/dist/core/events/v3/capability-drift.d.ts +2 -2
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.d.ts +48 -5
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +91 -36
- package/dist/core/events/v3/control.d.ts.map +1 -1
- package/dist/core/events/v3/control.js +22 -2
- package/dist/core/events/v3/index.d.ts +2 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +2 -1
- package/dist/core/events/v3/live-feed.d.ts +7 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +21 -3
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +10 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +2 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
- package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +15 -5
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +17 -4
- package/dist/core/events/v3/producers/intake.d.ts +4 -4
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +3 -2
- package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
- package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
- package/dist/core/events/v3/producers/recorder.d.ts +16 -5
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +31 -9
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +10 -13
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +3 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.d.ts +16 -14
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +36 -38
- package/dist/core/hooks/backup-schedule.d.ts +34 -0
- package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
- package/dist/core/hooks/backup-schedule.js +114 -0
- package/dist/core/hooks/cli.js +61 -18
- package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
- package/dist/core/hooks/codex-wsl-bridge.js +18 -4
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/contract.js +102 -0
- package/dist/core/hooks/prompt-context/index.d.ts +5 -0
- package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/index.js +2 -0
- package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
- package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/runner.js +249 -0
- package/dist/core/qa-artifacts.d.ts +3 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -1
- package/dist/core/qa-artifacts.js +25 -3
- package/dist/core/resources/contract.d.ts +69 -3
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/contract.js +1 -1
- package/dist/core/resources/index.d.ts +1 -0
- package/dist/core/resources/index.d.ts.map +1 -1
- package/dist/core/resources/index.js +1 -0
- package/dist/core/resources/native.d.ts +36 -0
- package/dist/core/resources/native.d.ts.map +1 -0
- package/dist/core/resources/native.js +418 -0
- package/dist/core/resources/sampler.d.ts +2 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +31 -1
- package/dist/core/resources/status.d.ts +61 -0
- package/dist/core/resources/status.d.ts.map +1 -0
- package/dist/core/resources/status.js +536 -0
- package/dist/core/resources/system.d.ts +27 -0
- package/dist/core/resources/system.d.ts.map +1 -0
- package/dist/core/resources/system.js +439 -0
- package/dist/core/semantic/contract.d.ts +13 -13
- package/dist/core/semantic/service.d.ts +2 -0
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +27 -4
- package/dist/core/storage/builtins.js +41 -0
- package/dist/core/supervisor/contract.d.ts +41 -3
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/contract.js +3 -2
- package/dist/core/supervisor/findings.d.ts +3 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +88 -0
- package/dist/core/supervisor/history.d.ts.map +1 -1
- package/dist/core/supervisor/history.js +14 -0
- package/dist/core/supervisor/ledger-state.d.ts +55 -0
- package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
- package/dist/core/supervisor/ledger-state.js +0 -0
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +120 -7
- package/dist/core/supervisor/storage.d.ts +3 -1
- package/dist/core/supervisor/storage.d.ts.map +1 -1
- package/dist/core/supervisor/storage.js +4 -0
- package/dist/core/workflow/admission.d.ts +3 -0
- package/dist/core/workflow/admission.d.ts.map +1 -1
- package/dist/core/workflow/admission.js +28 -16
- package/dist/core/workflow/engine.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +6 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -3
- package/dist/lib/browser/capture-fidelity.d.ts +7 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
- package/dist/lib/browser/capture-fidelity.js +17 -0
- package/dist/lib/browser/client.d.ts +9 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +85 -37
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/page-review-budget.d.ts +13 -0
- package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
- package/dist/lib/browser/page-review-budget.js +251 -0
- package/dist/lib/browser/page-review-contracts.d.ts +127 -0
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
- package/dist/lib/browser/page-review-contracts.js +5 -0
- package/dist/lib/browser/page-review-evidence.d.ts +48 -0
- package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
- package/dist/lib/browser/page-review-evidence.js +340 -0
- package/dist/lib/browser/page-review-judge.d.ts +1 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +46 -38
- package/dist/lib/browser/page-review-pack.d.ts +44 -22
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +116 -36
- package/dist/lib/browser/page-review-source.d.ts +12 -0
- package/dist/lib/browser/page-review-source.d.ts.map +1 -0
- package/dist/lib/browser/page-review-source.js +22 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +48 -14
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +215 -68
- package/dist/lib/browser/qa-snapshot.d.ts +6 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
- package/dist/lib/browser/qa-snapshot.js +259 -48
- package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
- package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
- package/dist/lib/browser/review-capture-transaction.js +79 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.js +4 -2
- package/dist/lib/browser/thumbnail-association.d.ts +34 -0
- package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
- package/dist/lib/browser/thumbnail-association.js +158 -0
- package/dist/lib/browser/tiling.d.ts +30 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +38 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +4 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +7 -1
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +1 -0
- package/dist/lib/local-file-url.d.ts +12 -0
- package/dist/lib/local-file-url.d.ts.map +1 -1
- package/dist/lib/local-file-url.js +16 -2
- package/dist/lib/tunnel/state.d.ts +19 -4
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +53 -13
- package/package.json +7 -4
- package/schemas/config.schema.json +69 -2
- package/src/commands/agents.ts +42 -9
- package/src/commands/artifacts.ts +130 -6
- package/src/commands/backup.ts +287 -34
- package/src/commands/browse.ts +564 -267
- package/src/commands/diagnostics.ts +6 -3
- package/src/commands/ledger-v3.ts +20 -3
- package/src/commands/qa-run.ts +37 -3
- package/src/commands/qa-status.ts +11 -2
- package/src/commands/resources.ts +23 -3
- package/src/commands/review-pack.ts +165 -62
- package/src/commands/tunnel.ts +2 -1
- package/src/core/agents/canonical-emit.ts +4 -2
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/live-authority-v3.ts +7 -3
- package/src/core/agents/live-lifecycle-v3.ts +2 -2
- package/src/core/agents/render/prompt-context.ts +68 -14
- package/src/core/agents/session-finalizer-v3.ts +2 -2
- package/src/core/agents/state/live-coordination-view.ts +4 -4
- package/src/core/agents/state/names.ts +92 -7
- package/src/core/agents/state/stale-sweep.ts +22 -8
- package/src/core/agents/turn-elapsed.ts +135 -0
- package/src/core/artifacts/activity.ts +71 -0
- package/src/core/artifacts/delivery-card.ts +375 -0
- package/src/core/artifacts/index.ts +385 -91
- package/src/core/artifacts/mutation-lock.ts +106 -0
- package/src/core/backup/host-snapshot.ts +76 -0
- package/src/core/config.ts +66 -13
- package/src/core/diagnostics/advice.ts +148 -120
- package/src/core/diagnostics/bundle.ts +138 -52
- package/src/core/diagnostics/comparison.ts +6 -2
- package/src/core/diagnostics/contract.ts +39 -42
- package/src/core/diagnostics/index.ts +2 -0
- package/src/core/diagnostics/pressure-contract.ts +294 -0
- package/src/core/diagnostics/pressure.ts +1361 -0
- package/src/core/diagnostics/replay.ts +116 -11
- package/src/core/events/v3/adapter-id.ts +12 -0
- package/src/core/events/v3/archive-retention.ts +3 -6
- package/src/core/events/v3/bootstrap.ts +163 -16
- package/src/core/events/v3/capabilities.ts +75 -25
- package/src/core/events/v3/capability-drift.ts +2 -2
- package/src/core/events/v3/control-witness.ts +172 -41
- package/src/core/events/v3/control.ts +34 -2
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/live-feed.ts +23 -5
- package/src/core/events/v3/live-routing.ts +14 -2
- package/src/core/events/v3/producers/command-recorder.ts +4 -4
- package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
- package/src/core/events/v3/producers/hook-base.ts +21 -16
- package/src/core/events/v3/producers/hook.ts +21 -4
- package/src/core/events/v3/producers/intake.ts +6 -6
- package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +50 -21
- package/src/core/events/v3/reader.ts +14 -13
- package/src/core/events/v3/runtime-build.ts +3 -2
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
- package/src/core/events/v3/turn-telemetry.ts +4 -4
- package/src/core/hooks/adapter/output.ts +50 -36
- package/src/core/hooks/backup-schedule.ts +147 -0
- package/src/core/hooks/cli.ts +65 -14
- package/src/core/hooks/codex-wsl-bridge.ts +21 -5
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/prompt-context/contract.ts +177 -0
- package/src/core/hooks/prompt-context/index.ts +22 -0
- package/src/core/hooks/prompt-context/runner.ts +303 -0
- package/src/core/qa-artifacts.ts +29 -3
- package/src/core/resources/contract.ts +76 -3
- package/src/core/resources/index.ts +1 -0
- package/src/core/resources/native.ts +481 -0
- package/src/core/resources/sampler.ts +46 -1
- package/src/core/resources/status.ts +717 -0
- package/src/core/resources/system.ts +530 -0
- package/src/core/semantic/service.ts +37 -3
- package/src/core/storage/builtins.ts +41 -0
- package/src/core/supervisor/contract.ts +45 -3
- package/src/core/supervisor/findings.ts +159 -0
- package/src/core/supervisor/history.ts +15 -0
- package/src/core/supervisor/ledger-state.ts +0 -0
- package/src/core/supervisor/service.ts +133 -5
- package/src/core/supervisor/storage.ts +7 -0
- package/src/core/workflow/admission.ts +33 -17
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/live-session-v3.ts +6 -8
- package/src/core/workflow/proof.ts +4 -3
- package/src/lib/browser/capture-fidelity.ts +23 -0
- package/src/lib/browser/client.ts +96 -44
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/page-review-budget.ts +309 -0
- package/src/lib/browser/page-review-contracts.ts +129 -0
- package/src/lib/browser/page-review-evidence.ts +465 -0
- package/src/lib/browser/page-review-judge.ts +56 -40
- package/src/lib/browser/page-review-pack.ts +181 -43
- package/src/lib/browser/page-review-source.ts +30 -0
- package/src/lib/browser/qa-run-contracts.ts +62 -18
- package/src/lib/browser/qa-run.ts +259 -71
- package/src/lib/browser/qa-snapshot.ts +289 -48
- package/src/lib/browser/review-capture-transaction.ts +106 -0
- package/src/lib/browser/standalone-html.ts +4 -2
- package/src/lib/browser/thumbnail-association.ts +190 -0
- package/src/lib/browser/tiling.ts +63 -0
- package/src/lib/docs-metadata-managed.ts +4 -1
- package/src/lib/identities/assume.ts +12 -4
- package/src/lib/instructions/templates.ts +1 -0
- package/src/lib/local-file-url.ts +17 -2
- package/src/lib/tunnel/state.ts +70 -13
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync,
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
13
13
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
15
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
15
|
-
import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, } from "../config.js";
|
|
16
|
+
import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, coordFreshnessSeconds, resolveBinName, } from "../config.js";
|
|
17
|
+
import { artifactRootEntries, readArtifactActivity, validArtifactActivity, } from "./activity.js";
|
|
16
18
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
|
|
19
|
+
import { withArtifactLock } from "./mutation-lock.js";
|
|
17
20
|
export { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
|
|
21
|
+
export { ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT, ARTIFACT_DELIVERY_MANIFEST, ARTIFACT_DELIVERY_SCHEMA_VERSION, parseArtifactDeliverySpec, readArtifactDeliveryManifest, renderArtifactDeliveryCard, resolveArtifactDeliveryManifest, writeArtifactDeliveryManifest, } from "./delivery-card.js";
|
|
18
22
|
export function artifactCapabilities() {
|
|
19
23
|
return {
|
|
20
24
|
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
@@ -22,6 +26,8 @@ export function artifactCapabilities() {
|
|
|
22
26
|
atomic_create_holds: true,
|
|
23
27
|
owner_scoped_unhold: true,
|
|
24
28
|
explicit_v1_migration: true,
|
|
29
|
+
minute_retention: true,
|
|
30
|
+
discard_after_review: true,
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
export function artifactsRoot(repoRoot) {
|
|
@@ -72,7 +78,7 @@ function createArtifactUnlocked(repoRoot, input) {
|
|
|
72
78
|
},
|
|
73
79
|
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
74
80
|
};
|
|
75
|
-
atomicWriteManifest(path, manifest);
|
|
81
|
+
atomicWriteManifest(path, manifest, now);
|
|
76
82
|
return { path, manifest };
|
|
77
83
|
}
|
|
78
84
|
export function inventoryArtifacts(repoRoot, opts = {}) {
|
|
@@ -113,7 +119,9 @@ export function renewArtifact(repoRoot, ref, days, reason, input = {}) {
|
|
|
113
119
|
function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
|
|
114
120
|
const now = input.now ?? new Date();
|
|
115
121
|
assertValidDate(now, "now");
|
|
116
|
-
const
|
|
122
|
+
const expiresAt = typeof days === "number"
|
|
123
|
+
? addDays(now, positiveDays(days))
|
|
124
|
+
: addMinutes(now, positiveMinutes(days.minutes));
|
|
117
125
|
const why = reason.trim();
|
|
118
126
|
if (!why)
|
|
119
127
|
throw new Error("renewal reason must not be empty");
|
|
@@ -124,17 +132,66 @@ function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
|
|
|
124
132
|
const manifest = {
|
|
125
133
|
...parsed.manifest,
|
|
126
134
|
retention: {
|
|
127
|
-
expires_at:
|
|
135
|
+
expires_at: expiresAt.toISOString(),
|
|
128
136
|
renewed_at: now.toISOString(),
|
|
129
137
|
reason: why,
|
|
130
138
|
},
|
|
131
139
|
};
|
|
132
|
-
atomicWriteManifest(path, manifest);
|
|
140
|
+
atomicWriteManifest(path, manifest, now);
|
|
133
141
|
return manifest;
|
|
134
142
|
}
|
|
135
143
|
export function releaseArtifact(repoRoot, ref, input = {}) {
|
|
136
144
|
return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
|
|
137
145
|
}
|
|
146
|
+
/** Retire reviewed evidence without deleting it or extending an earlier deadline. */
|
|
147
|
+
export function discardArtifact(repoRoot, ref, reason, input = {}) {
|
|
148
|
+
return withArtifactLock(repoRoot, () => {
|
|
149
|
+
const now = input.now ?? new Date();
|
|
150
|
+
assertValidDate(now, "now");
|
|
151
|
+
const minutes = positiveMinutes(input.minutes ?? 60);
|
|
152
|
+
const why = reason.trim();
|
|
153
|
+
if (!why)
|
|
154
|
+
throw new Error("discard requires a reason confirming the files are no longer needed");
|
|
155
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
156
|
+
const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
|
|
157
|
+
if (!["managed-current", "managed-expired", "managed-active"].includes(entry.classification) ||
|
|
158
|
+
(entry.classification === "managed-active" &&
|
|
159
|
+
entry.owner_instance_id !== input.actor?.instance_id))
|
|
160
|
+
throw new Error(`cannot discard artifact: ${entry.reason}`);
|
|
161
|
+
const parsed = readManifest(path);
|
|
162
|
+
if (!parsed.ok)
|
|
163
|
+
throw new Error(parsed.reason);
|
|
164
|
+
const manifest = parsed.manifest;
|
|
165
|
+
const remaining = Date.parse(entry.expires_at) - now.getTime();
|
|
166
|
+
const priorWindow = Date.parse(manifest.retention.expires_at) -
|
|
167
|
+
Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
168
|
+
const windowMs = Math.min(minutes * 60_000, remaining, priorWindow);
|
|
169
|
+
const updated = {
|
|
170
|
+
...manifest,
|
|
171
|
+
retention: {
|
|
172
|
+
...manifest.retention,
|
|
173
|
+
...(windowMs > 0
|
|
174
|
+
? {
|
|
175
|
+
renewed_at: now.toISOString(),
|
|
176
|
+
expires_at: new Date(now.getTime() + windowMs).toISOString(),
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
reason: why,
|
|
180
|
+
},
|
|
181
|
+
released_at: now.toISOString(),
|
|
182
|
+
released_by: input.actor,
|
|
183
|
+
};
|
|
184
|
+
atomicWriteManifest(path, updated, now);
|
|
185
|
+
return updated;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** Advice only: a successful check does not establish that its evidence is disposable. */
|
|
189
|
+
export function artifactReviewGuidance(repoRoot, ref) {
|
|
190
|
+
const target = isAbsolute(ref) ? basename(ref) : ref;
|
|
191
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(target))
|
|
192
|
+
throw new Error("review guidance requires an artifact id or directory name");
|
|
193
|
+
return `After reviewing these files, if they are disposable or superseded and no review, handoff, failure investigation, or final evidence depends on them, run ${resolveBinName(repoRoot)} artifacts discard ${target} --reason "Reviewed; no longer needed" (60-minute grace). Otherwise retain them; use a hold for pending review.`;
|
|
194
|
+
}
|
|
138
195
|
function releaseArtifactUnlocked(repoRoot, ref, input) {
|
|
139
196
|
const now = input.now ?? new Date();
|
|
140
197
|
assertValidDate(now, "now");
|
|
@@ -147,7 +204,7 @@ function releaseArtifactUnlocked(repoRoot, ref, input) {
|
|
|
147
204
|
released_at: now.toISOString(),
|
|
148
205
|
released_by: input.actor,
|
|
149
206
|
};
|
|
150
|
-
atomicWriteManifest(path, manifest);
|
|
207
|
+
atomicWriteManifest(path, manifest, now);
|
|
151
208
|
return manifest;
|
|
152
209
|
}
|
|
153
210
|
export function holdArtifact(repoRoot, ref, input) {
|
|
@@ -166,7 +223,7 @@ export function holdArtifact(repoRoot, ref, input) {
|
|
|
166
223
|
return parsed.manifest;
|
|
167
224
|
}
|
|
168
225
|
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
169
|
-
atomicWriteManifest(path, manifest);
|
|
226
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
170
227
|
return manifest;
|
|
171
228
|
});
|
|
172
229
|
}
|
|
@@ -190,7 +247,7 @@ export function unholdArtifact(repoRoot, ref, id, input) {
|
|
|
190
247
|
...parsed.manifest,
|
|
191
248
|
holds: parsed.manifest.holds.filter((item) => item.id !== id),
|
|
192
249
|
};
|
|
193
|
-
atomicWriteManifest(path, manifest);
|
|
250
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
194
251
|
return manifest;
|
|
195
252
|
});
|
|
196
253
|
}
|
|
@@ -310,17 +367,27 @@ export function cleanArtifacts(repoRoot, opts = {}) {
|
|
|
310
367
|
}
|
|
311
368
|
}
|
|
312
369
|
function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
370
|
+
const started = performance.now();
|
|
313
371
|
const now = opts.now ?? new Date();
|
|
314
372
|
const freshnessSeconds = opts.freshnessSeconds ?? 600;
|
|
315
373
|
const rows = inventoryArtifacts(repoRoot, { now, freshnessSeconds });
|
|
316
374
|
if (!opts.yes)
|
|
317
375
|
return rows;
|
|
376
|
+
let attempted = 0;
|
|
318
377
|
return rows.map((entry) => {
|
|
319
378
|
if (entry.action !== "would-delete")
|
|
320
379
|
return entry;
|
|
380
|
+
if (attempted >= (opts.maxDeletes ?? Infinity) ||
|
|
381
|
+
(attempted > 0 && performance.now() - started >= (opts.timeBudgetMs ?? Infinity)))
|
|
382
|
+
return entry;
|
|
383
|
+
attempted++;
|
|
321
384
|
// Reclassify immediately before removal. A renewal, release-state change,
|
|
322
385
|
// heartbeat, symlink swap, or tracked file added since inventory must win.
|
|
323
|
-
|
|
386
|
+
// Only repository-budget eviction needs a new whole-store plan. Expiry and
|
|
387
|
+
// per-unit size are local decisions; recheck all their guards on the target.
|
|
388
|
+
const current = entry.classification === "managed-over-budget"
|
|
389
|
+
? inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find((row) => row.path === entry.path)
|
|
390
|
+
: applyArtifactUnitBudget(repoRoot, classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds));
|
|
324
391
|
if (!current) {
|
|
325
392
|
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
326
393
|
}
|
|
@@ -357,18 +424,28 @@ function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
|
357
424
|
/** Sibling of the artifacts root so the stamp never appears in the inventory scan. */
|
|
358
425
|
const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
|
|
359
426
|
/**
|
|
360
|
-
* Throttled expired-artifact sweep, fired
|
|
427
|
+
* Throttled expired-artifact sweep, fired at SessionStart and before new work.
|
|
361
428
|
*
|
|
362
429
|
* Retention was previously enforced only when someone remembered to run
|
|
363
430
|
* `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
|
|
364
|
-
* busy hosts. This runs the exact same guarded deletion (
|
|
365
|
-
*
|
|
431
|
+
* busy hosts. This runs the exact same guarded deletion (expired or over-budget
|
|
432
|
+
* managed entries, each re-classified immediately before removal;
|
|
366
433
|
* unmanaged and legacy directories are never touched) at most once per
|
|
367
|
-
* interval (default
|
|
368
|
-
*
|
|
434
|
+
* interval (default 1h after completion, 1m between partial/failed slices).
|
|
435
|
+
* The owner-aware lock serializes callers; interrupted attempts can retry.
|
|
436
|
+
* Disable with `artifacts.auto_clean: false` or
|
|
369
437
|
* `HARNERY_ARTIFACT_AUTO_CLEAN=0`.
|
|
370
438
|
*/
|
|
371
439
|
export function autoCleanArtifacts(repoRoot, opts = {}) {
|
|
440
|
+
if (!existsSync(artifactsRoot(repoRoot))) {
|
|
441
|
+
return { ran: false, reason: "no-root", deleted: 0, bytes: 0 };
|
|
442
|
+
}
|
|
443
|
+
if (!artifactAutoCleanEnabled(repoRoot)) {
|
|
444
|
+
return { ran: false, reason: "disabled", deleted: 0, bytes: 0 };
|
|
445
|
+
}
|
|
446
|
+
return withArtifactLock(repoRoot, () => autoCleanArtifactsUnlocked(repoRoot, opts));
|
|
447
|
+
}
|
|
448
|
+
function autoCleanArtifactsUnlocked(repoRoot, opts) {
|
|
372
449
|
const now = opts.now ?? new Date();
|
|
373
450
|
assertValidDate(now, "now");
|
|
374
451
|
if (!existsSync(artifactsRoot(repoRoot))) {
|
|
@@ -381,23 +458,64 @@ export function autoCleanArtifacts(repoRoot, opts = {}) {
|
|
|
381
458
|
const intervalMs = artifactAutoCleanIntervalHours() * 60 * 60 * 1000;
|
|
382
459
|
try {
|
|
383
460
|
const stamp = JSON.parse(readFileSync(stampPath, "utf8"));
|
|
384
|
-
const last = Date.parse(stamp.
|
|
385
|
-
if (
|
|
461
|
+
const last = Date.parse(stamp.last_completed_at ?? "");
|
|
462
|
+
if ((stamp.status === "completed" &&
|
|
463
|
+
Number.isFinite(last) &&
|
|
464
|
+
now.getTime() - last < intervalMs) ||
|
|
465
|
+
(["partial", "failed"].includes(stamp.status ?? "") &&
|
|
466
|
+
Date.parse(stamp.retry_after ?? "") > now.getTime())) {
|
|
386
467
|
return { ran: false, reason: "fresh", deleted: 0, bytes: 0 };
|
|
387
468
|
}
|
|
388
469
|
}
|
|
389
470
|
catch {
|
|
390
471
|
// Missing or unreadable stamp: sweep now and write a fresh one.
|
|
391
472
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
473
|
+
const maxDeletes = opts.maxDeletes ?? 10;
|
|
474
|
+
const timeBudgetMs = opts.timeBudgetMs ?? 5000;
|
|
475
|
+
if (!Number.isSafeInteger(maxDeletes) ||
|
|
476
|
+
maxDeletes < 1 ||
|
|
477
|
+
!Number.isFinite(timeBudgetMs) ||
|
|
478
|
+
timeBudgetMs < 0)
|
|
479
|
+
throw new Error("cleanup limits must allow at least one deletion and a nonnegative time budget");
|
|
480
|
+
const attempt = { last_attempt_at: now.toISOString() };
|
|
481
|
+
const writeStamp = (state) => {
|
|
482
|
+
const temp = `${stampPath}.${randomUUID()}.tmp`;
|
|
483
|
+
writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`);
|
|
484
|
+
renameSync(temp, stampPath);
|
|
485
|
+
};
|
|
486
|
+
writeStamp({ ...attempt, status: "running" });
|
|
487
|
+
try {
|
|
488
|
+
const rows = cleanArtifactsUnlocked(repoRoot, { yes: true, now, maxDeletes, timeBudgetMs });
|
|
489
|
+
const deletedRows = rows.filter((row) => row.action === "deleted");
|
|
490
|
+
const deleted = deletedRows.length;
|
|
491
|
+
const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
|
|
492
|
+
const remaining = rows.filter((row) => row.action === "would-delete").length;
|
|
493
|
+
const failures = rows
|
|
494
|
+
.filter((row) => row.classification === "unknown")
|
|
495
|
+
.map((row) => ({ name: row.name, reason: row.reason }));
|
|
496
|
+
const status = failures.length ? "failed" : remaining ? "partial" : "completed";
|
|
497
|
+
writeStamp({
|
|
498
|
+
...attempt,
|
|
499
|
+
status,
|
|
500
|
+
deleted,
|
|
501
|
+
bytes,
|
|
502
|
+
remaining,
|
|
503
|
+
failures,
|
|
504
|
+
...(status === "completed"
|
|
505
|
+
? { last_completed_at: now.toISOString() }
|
|
506
|
+
: { retry_after: new Date(now.getTime() + 60_000).toISOString() }),
|
|
507
|
+
});
|
|
508
|
+
return { ran: true, reason: status === "completed" ? "swept" : status, deleted, bytes };
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
writeStamp({
|
|
512
|
+
...attempt,
|
|
513
|
+
status: "failed",
|
|
514
|
+
error: errorMessage("cleanup failed", error),
|
|
515
|
+
retry_after: new Date(now.getTime() + 60_000).toISOString(),
|
|
516
|
+
});
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
401
519
|
}
|
|
402
520
|
export function resolveArtifactRef(repoRoot, ref) {
|
|
403
521
|
const root = artifactsRoot(repoRoot);
|
|
@@ -446,7 +564,7 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
446
564
|
}
|
|
447
565
|
const manifest = parsed.manifest;
|
|
448
566
|
const bytes = safeTreeSize(path);
|
|
449
|
-
const lastModifiedMs = safeTreeLastModified(path, now);
|
|
567
|
+
const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
|
|
450
568
|
const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
451
569
|
const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
|
|
452
570
|
const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
|
|
@@ -513,20 +631,24 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
513
631
|
action: "would-delete",
|
|
514
632
|
};
|
|
515
633
|
}
|
|
516
|
-
function
|
|
634
|
+
function applyArtifactUnitBudget(repoRoot, row) {
|
|
517
635
|
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
row
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
636
|
+
if (row.classification === "managed-current" &&
|
|
637
|
+
row.bytes !== null &&
|
|
638
|
+
row.bytes > maxUnitBytes &&
|
|
639
|
+
!row.oversize_acknowledged) {
|
|
640
|
+
return {
|
|
641
|
+
...row,
|
|
642
|
+
classification: "managed-oversize",
|
|
643
|
+
action: "would-delete",
|
|
644
|
+
reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
|
|
645
|
+
};
|
|
529
646
|
}
|
|
647
|
+
return row;
|
|
648
|
+
}
|
|
649
|
+
function applyArtifactBudgets(repoRoot, inputRows) {
|
|
650
|
+
const maxBytes = artifactMaxBytes(repoRoot);
|
|
651
|
+
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
|
|
530
652
|
const managedBytes = rows.reduce((sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0), 0);
|
|
531
653
|
let retainedBytes = managedBytes -
|
|
532
654
|
rows.reduce((sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0), 0);
|
|
@@ -614,10 +736,19 @@ export function parseArtifactManifest(value) {
|
|
|
614
736
|
if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
|
|
615
737
|
return { ok: false, reason: "invalid oversize_acknowledged" };
|
|
616
738
|
}
|
|
739
|
+
if (m.activity !== undefined && !validArtifactActivity(m.activity)) {
|
|
740
|
+
return { ok: false, reason: "invalid activity checkpoint" };
|
|
741
|
+
}
|
|
617
742
|
return { ok: true, manifest: m };
|
|
618
743
|
}
|
|
619
|
-
function atomicWriteManifest(path, manifest) {
|
|
744
|
+
function atomicWriteManifest(path, manifest, now = new Date()) {
|
|
620
745
|
const target = join(path, ARTIFACT_MANIFEST);
|
|
746
|
+
manifest.activity = readArtifactActivity(path, now, existsSync(target)
|
|
747
|
+
? manifest.activity
|
|
748
|
+
: {
|
|
749
|
+
last_changed_at: manifest.created_at,
|
|
750
|
+
root_entries_sha256: artifactRootEntries(path),
|
|
751
|
+
});
|
|
621
752
|
const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
|
|
622
753
|
try {
|
|
623
754
|
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
@@ -693,7 +824,7 @@ export function migrateArtifacts(repoRoot, opts = {}) {
|
|
|
693
824
|
}
|
|
694
825
|
if (readFileSync(target, "utf8") !== preimage)
|
|
695
826
|
throw new Error("manifest changed before migration");
|
|
696
|
-
atomicWriteManifest(path, parsed.manifest);
|
|
827
|
+
atomicWriteManifest(path, parsed.manifest, opts.now);
|
|
697
828
|
return {
|
|
698
829
|
path,
|
|
699
830
|
action: "migrated",
|
|
@@ -708,22 +839,120 @@ export function migrateArtifacts(repoRoot, opts = {}) {
|
|
|
708
839
|
};
|
|
709
840
|
return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
|
|
710
841
|
}
|
|
711
|
-
/**
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
842
|
+
/** Repair only a provable, untouched v1-to-v2 migration. Never delete payloads.
|
|
843
|
+
* Old migrations saved the original manifest but not the original root stat.
|
|
844
|
+
* The exact preimage and coincident root/manifest/receipt timestamps bound this
|
|
845
|
+
* correction. Any later root change or metadata mutation requires manual review.
|
|
846
|
+
*/
|
|
847
|
+
export function repairArtifactActivity(repoRoot, opts = {}) {
|
|
848
|
+
const repair = () => {
|
|
849
|
+
const now = opts.now ?? new Date();
|
|
850
|
+
assertValidDate(now, "now");
|
|
851
|
+
const preimageRoot = join(resolve(repoRoot), ".harnery/artifact-migrations");
|
|
852
|
+
const preimages = new Map();
|
|
853
|
+
if (existsSync(preimageRoot) && !lstatSync(preimageRoot).isSymbolicLink()) {
|
|
854
|
+
for (const name of readdirSync(preimageRoot)) {
|
|
855
|
+
if (!/^[a-f0-9]{64}\.v1\.json$/.test(name))
|
|
856
|
+
continue;
|
|
857
|
+
const file = join(preimageRoot, name);
|
|
858
|
+
try {
|
|
859
|
+
const stat = lstatSync(file);
|
|
860
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
861
|
+
continue;
|
|
862
|
+
const bytes = readFileSync(file, "utf8");
|
|
863
|
+
if (`${createHash("sha256").update(bytes).digest("hex")}.v1.json` !== name)
|
|
864
|
+
continue;
|
|
865
|
+
const old = JSON.parse(bytes);
|
|
866
|
+
if (old?.schema_version !== 1 ||
|
|
867
|
+
Object.hasOwn(old, "holds") ||
|
|
868
|
+
Object.hasOwn(old, "activity"))
|
|
869
|
+
continue;
|
|
870
|
+
const upgraded = { ...old, schema_version: ARTIFACT_SCHEMA_VERSION, holds: [] };
|
|
871
|
+
const parsed = parseArtifactManifest(upgraded);
|
|
872
|
+
if (!parsed.ok)
|
|
873
|
+
continue;
|
|
874
|
+
const items = preimages.get(parsed.manifest.artifact_id) ?? [];
|
|
875
|
+
items.push({ value: upgraded, path: file, mtime: stat.mtimeMs });
|
|
876
|
+
preimages.set(parsed.manifest.artifact_id, items);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// Unverifiable preimages confer no repair authority.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const root = artifactsRoot(repoRoot);
|
|
884
|
+
if (!existsSync(root))
|
|
885
|
+
return [];
|
|
886
|
+
return readdirSync(root)
|
|
887
|
+
.sort()
|
|
888
|
+
.map((name) => {
|
|
889
|
+
const path = join(root, name);
|
|
890
|
+
try {
|
|
891
|
+
const parsed = readManifest(path);
|
|
892
|
+
if (!parsed.ok)
|
|
893
|
+
throw new Error(parsed.reason);
|
|
894
|
+
const manifest = parsed.manifest;
|
|
895
|
+
if (manifest.activity)
|
|
896
|
+
return { path, action: "keep", reason: "activity already recorded" };
|
|
897
|
+
const source = preimages
|
|
898
|
+
.get(manifest.artifact_id)
|
|
899
|
+
?.find((item) => isDeepStrictEqual(item.value, manifest));
|
|
900
|
+
if (!source)
|
|
901
|
+
throw new Error("no exact migration preimage");
|
|
902
|
+
if (containsTrackedPath(repoRoot, path))
|
|
903
|
+
throw new Error("artifact contains tracked files");
|
|
904
|
+
const target = join(path, ARTIFACT_MANIFEST);
|
|
905
|
+
const before = readFileSync(target, "utf8");
|
|
906
|
+
const unitStat = lstatSync(path);
|
|
907
|
+
const manifestStat = lstatSync(target);
|
|
908
|
+
if (unitStat.mtimeMs !== manifestStat.mtimeMs ||
|
|
909
|
+
unitStat.ctimeMs !== manifestStat.ctimeMs ||
|
|
910
|
+
manifestStat.mtimeMs !== manifestStat.ctimeMs ||
|
|
911
|
+
manifestStat.mtimeMs < source.mtime ||
|
|
912
|
+
manifestStat.mtimeMs - source.mtime > 5000)
|
|
913
|
+
throw new Error("root or manifest changed outside the recorded migration");
|
|
914
|
+
const anchor = manifest.retention.renewed_at ?? manifest.created_at;
|
|
915
|
+
const oldActivity = readArtifactActivity(path, now);
|
|
916
|
+
const activity = readArtifactActivity(path, now, {
|
|
917
|
+
last_changed_at: anchor,
|
|
918
|
+
root_entries_sha256: artifactRootEntries(path),
|
|
919
|
+
});
|
|
920
|
+
const windowMs = Date.parse(manifest.retention.expires_at) - Date.parse(anchor);
|
|
921
|
+
const expiry = (changedAt) => new Date(Math.max(Date.parse(anchor), Date.parse(changedAt)) + windowMs).toISOString();
|
|
922
|
+
const row = {
|
|
923
|
+
path,
|
|
924
|
+
action: opts.yes ? "repaired" : "would-repair",
|
|
925
|
+
reason: "exact migration preimage; unchanged root; payload activity and retention preserved",
|
|
926
|
+
previous_expires_at: expiry(oldActivity.last_changed_at),
|
|
927
|
+
repaired_expires_at: expiry(activity.last_changed_at),
|
|
928
|
+
};
|
|
929
|
+
if (!opts.yes)
|
|
930
|
+
return row;
|
|
931
|
+
const receipt = join(preimageRoot, `${manifest.artifact_id}.${randomUUID()}.activity-repair.json`);
|
|
932
|
+
writeFileSync(receipt, `${JSON.stringify({
|
|
933
|
+
schema_version: 1,
|
|
934
|
+
repaired_at: now.toISOString(),
|
|
935
|
+
path: relative(repoRoot, path),
|
|
936
|
+
preimage_path: relative(repoRoot, source.path),
|
|
937
|
+
original_manifest: before,
|
|
938
|
+
activity,
|
|
939
|
+
previous_expires_at: row.previous_expires_at,
|
|
940
|
+
repaired_expires_at: row.repaired_expires_at,
|
|
941
|
+
}, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
942
|
+
const current = lstatSync(path);
|
|
943
|
+
if (readFileSync(target, "utf8") !== before ||
|
|
944
|
+
current.mtimeMs !== unitStat.mtimeMs ||
|
|
945
|
+
current.ctimeMs !== unitStat.ctimeMs)
|
|
946
|
+
throw new Error("artifact changed before repair");
|
|
947
|
+
atomicWriteManifest(path, { ...manifest, activity }, now);
|
|
948
|
+
return { ...row, receipt_path: receipt };
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
return { path, action: "keep", reason: errorMessage("activity repair refused", error) };
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
};
|
|
955
|
+
return opts.yes ? withArtifactLock(repoRoot, repair) : repair();
|
|
727
956
|
}
|
|
728
957
|
function validHoldId(value) {
|
|
729
958
|
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
|
|
@@ -805,26 +1034,9 @@ function safeTreeSize(path) {
|
|
|
805
1034
|
* symlinks. A small future tolerance protects a write racing the inventory
|
|
806
1035
|
* scan; timestamps farther ahead are ignored as clock-skewed metadata.
|
|
807
1036
|
*/
|
|
808
|
-
function safeTreeLastModified(path, now) {
|
|
809
|
-
const nowMs = now.getTime();
|
|
810
|
-
const futureToleranceMs = 5 * 60 * 1000;
|
|
1037
|
+
function safeTreeLastModified(path, now, activity) {
|
|
811
1038
|
try {
|
|
812
|
-
|
|
813
|
-
let latest = 0;
|
|
814
|
-
for (const timestamp of [st.mtimeMs, st.ctimeMs]) {
|
|
815
|
-
if (Number.isFinite(timestamp) && timestamp <= nowMs + futureToleranceMs) {
|
|
816
|
-
latest = Math.max(latest, Math.min(timestamp, nowMs));
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (st.isSymbolicLink() || !st.isDirectory())
|
|
820
|
-
return latest;
|
|
821
|
-
for (const child of readdirSync(path)) {
|
|
822
|
-
const childLatest = safeTreeLastModified(join(path, child), now);
|
|
823
|
-
if (childLatest === null)
|
|
824
|
-
return null;
|
|
825
|
-
latest = Math.max(latest, childLatest);
|
|
826
|
-
}
|
|
827
|
-
return latest;
|
|
1039
|
+
return Date.parse(readArtifactActivity(path, now, activity).last_changed_at);
|
|
828
1040
|
}
|
|
829
1041
|
catch {
|
|
830
1042
|
return null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-lock.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/mutation-lock.ts"],"names":[],"mappings":"AAqEA,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAoCxE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { processStartToken } from "../agents/state/proc-start.js";
|
|
6
|
+
/** Age is never authority to evict an owner. Unknown owners fail closed. */
|
|
7
|
+
function reclaimDeadOwner(lock) {
|
|
8
|
+
try {
|
|
9
|
+
const stat = lstatSync(lock);
|
|
10
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
11
|
+
return false;
|
|
12
|
+
const names = readdirSync(lock);
|
|
13
|
+
if (names.length !== 1 || !/^owner-[0-9a-f-]{36}\.json$/.test(names[0]))
|
|
14
|
+
return false;
|
|
15
|
+
const ownerPath = join(lock, names[0]);
|
|
16
|
+
const ownerStat = lstatSync(ownerPath);
|
|
17
|
+
if (!ownerStat.isFile() || ownerStat.isSymbolicLink() || ownerStat.size > 4096)
|
|
18
|
+
return false;
|
|
19
|
+
const owner = JSON.parse(readFileSync(ownerPath, "utf8"));
|
|
20
|
+
if (owner.version !== 1 ||
|
|
21
|
+
owner.host !== hostname() ||
|
|
22
|
+
!Number.isSafeInteger(owner.pid) ||
|
|
23
|
+
owner.pid <= 0 ||
|
|
24
|
+
!(owner.process_start === null || typeof owner.process_start === "string"))
|
|
25
|
+
return false;
|
|
26
|
+
let dead = false;
|
|
27
|
+
try {
|
|
28
|
+
process.kill(owner.pid, 0);
|
|
29
|
+
const current = processStartToken(owner.pid);
|
|
30
|
+
// Wall-clock ps lstart output can drift even for a live process. Only
|
|
31
|
+
// boot-scoped procfs ticks establish PID reuse; other probes need ESRCH.
|
|
32
|
+
const bootScoped = /^l[0-9a-f]{8}\.\d+$/;
|
|
33
|
+
dead =
|
|
34
|
+
typeof owner.process_start === "string" &&
|
|
35
|
+
bootScoped.test(owner.process_start) &&
|
|
36
|
+
typeof current === "string" &&
|
|
37
|
+
bootScoped.test(current) &&
|
|
38
|
+
current !== owner.process_start;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
dead = error.code === "ESRCH";
|
|
42
|
+
}
|
|
43
|
+
if (!dead)
|
|
44
|
+
return false;
|
|
45
|
+
// Only the contender that removes this unique owner's file may remove the
|
|
46
|
+
// directory. A losing reaper must not unlink a replacement owner's lock.
|
|
47
|
+
unlinkSync(ownerPath);
|
|
48
|
+
rmdirSync(lock);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function withArtifactLock(repoRoot, action) {
|
|
56
|
+
// Probe before mkdir so a slow platform process probe cannot hold an
|
|
57
|
+
// ownerless lock open while other writers are trying to acquire it.
|
|
58
|
+
const owner = {
|
|
59
|
+
version: 1,
|
|
60
|
+
host: hostname(),
|
|
61
|
+
pid: process.pid,
|
|
62
|
+
process_start: processStartToken(process.pid),
|
|
63
|
+
acquired_at: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
|
|
66
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(lock);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
if (!reclaimDeadOwner(lock)) {
|
|
72
|
+
throw new Error("artifact store mutation lock unavailable; a live or unverifiable owner holds it. " +
|
|
73
|
+
"Dead local owners recover automatically; an empty or invalid lock requires explicit inspection.");
|
|
74
|
+
}
|
|
75
|
+
// Another caller may win after recovery. Never retry by removing its lock.
|
|
76
|
+
mkdirSync(lock);
|
|
77
|
+
}
|
|
78
|
+
const ownerPath = join(lock, `owner-${randomUUID()}.json`);
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(ownerPath, JSON.stringify(owner), { flag: "wx" });
|
|
81
|
+
return action();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
// A crash before publication or between these two calls leaves an empty
|
|
85
|
+
// directory. It cannot be distinguished safely from an initializing owner.
|
|
86
|
+
// Publication may have failed before creating a file. Remove only our
|
|
87
|
+
// unique file, then the empty directory; never recursively delete contents.
|
|
88
|
+
rmSync(ownerPath, { force: true });
|
|
89
|
+
rmdirSync(lock);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness helpers shared by `harn backup snapshot` and the session-start
|
|
3
|
+
* scheduler. The host cache is a local, best-effort record of this machine's
|
|
4
|
+
* newest known snapshot: the scheduler reads it to skip a remote restic round
|
|
5
|
+
* trip when the host is plainly current, and restic stays the authority
|
|
6
|
+
* whenever the cache is missing, stale, or written by another host.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseBackupDuration(value: string): number | null;
|
|
9
|
+
/** Newest snapshot time (epoch ms) in `restic snapshots --json` output, or null. */
|
|
10
|
+
export declare function newestSnapshotTime(output: string): number | null;
|
|
11
|
+
export interface HostSnapshotCache {
|
|
12
|
+
host: string;
|
|
13
|
+
snapshotAt: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function hostSnapshotCachePath(harneryDir: string): string;
|
|
16
|
+
export declare function readHostSnapshotCache(harneryDir: string): HostSnapshotCache | null;
|
|
17
|
+
export declare function recordHostSnapshot(harneryDir: string, snapshotAt: number): void;
|
|
18
|
+
//# sourceMappingURL=host-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-snapshot.d.ts","sourceRoot":"","sources":["../../../src/core/backup/host-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAShE;AAED,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWhE;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAalF;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAc/E"}
|