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
|
@@ -18,11 +18,11 @@ import {
|
|
|
18
18
|
readdirSync,
|
|
19
19
|
readFileSync,
|
|
20
20
|
renameSync,
|
|
21
|
-
rmdirSync,
|
|
22
21
|
rmSync,
|
|
23
22
|
writeFileSync,
|
|
24
23
|
} from "node:fs";
|
|
25
24
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
import { isDeepStrictEqual } from "node:util";
|
|
26
26
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
27
27
|
import {
|
|
28
28
|
artifactAutoCleanEnabled,
|
|
@@ -30,10 +30,36 @@ import {
|
|
|
30
30
|
artifactDefaultRetentionDays,
|
|
31
31
|
artifactMaxBytes,
|
|
32
32
|
artifactMaxUnitBytes,
|
|
33
|
+
coordFreshnessSeconds,
|
|
34
|
+
resolveBinName,
|
|
33
35
|
} from "../config.ts";
|
|
36
|
+
import {
|
|
37
|
+
type ArtifactActivity,
|
|
38
|
+
artifactRootEntries,
|
|
39
|
+
readArtifactActivity,
|
|
40
|
+
validArtifactActivity,
|
|
41
|
+
} from "./activity.ts";
|
|
34
42
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
|
|
43
|
+
import { withArtifactLock } from "./mutation-lock.ts";
|
|
35
44
|
|
|
36
45
|
export { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
|
|
46
|
+
export type {
|
|
47
|
+
ArtifactDeliveryCard,
|
|
48
|
+
ArtifactDeliveryItem,
|
|
49
|
+
ArtifactDeliveryManifest,
|
|
50
|
+
ArtifactDeliveryPath,
|
|
51
|
+
ArtifactDeliveryUrl,
|
|
52
|
+
} from "./delivery-card.ts";
|
|
53
|
+
export {
|
|
54
|
+
ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT,
|
|
55
|
+
ARTIFACT_DELIVERY_MANIFEST,
|
|
56
|
+
ARTIFACT_DELIVERY_SCHEMA_VERSION,
|
|
57
|
+
parseArtifactDeliverySpec,
|
|
58
|
+
readArtifactDeliveryManifest,
|
|
59
|
+
renderArtifactDeliveryCard,
|
|
60
|
+
resolveArtifactDeliveryManifest,
|
|
61
|
+
writeArtifactDeliveryManifest,
|
|
62
|
+
} from "./delivery-card.ts";
|
|
37
63
|
|
|
38
64
|
export interface ArtifactActor {
|
|
39
65
|
instance_id: string;
|
|
@@ -57,6 +83,7 @@ export interface ArtifactManifestV2 {
|
|
|
57
83
|
released_at?: string;
|
|
58
84
|
released_by?: ArtifactActor;
|
|
59
85
|
oversize_acknowledged?: boolean;
|
|
86
|
+
activity?: ArtifactActivity;
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
export interface ArtifactHold {
|
|
@@ -78,6 +105,8 @@ export function artifactCapabilities() {
|
|
|
78
105
|
atomic_create_holds: true,
|
|
79
106
|
owner_scoped_unhold: true,
|
|
80
107
|
explicit_v1_migration: true,
|
|
108
|
+
minute_retention: true,
|
|
109
|
+
discard_after_review: true,
|
|
81
110
|
} as const;
|
|
82
111
|
}
|
|
83
112
|
|
|
@@ -208,7 +237,7 @@ function createArtifactUnlocked(
|
|
|
208
237
|
},
|
|
209
238
|
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
210
239
|
};
|
|
211
|
-
atomicWriteManifest(path, manifest);
|
|
240
|
+
atomicWriteManifest(path, manifest, now);
|
|
212
241
|
return { path, manifest };
|
|
213
242
|
}
|
|
214
243
|
|
|
@@ -258,7 +287,7 @@ export function showArtifact(
|
|
|
258
287
|
export function renewArtifact(
|
|
259
288
|
repoRoot: string,
|
|
260
289
|
ref: string,
|
|
261
|
-
days: number,
|
|
290
|
+
days: number | { minutes: number },
|
|
262
291
|
reason: string,
|
|
263
292
|
input: ArtifactMutationInput = {},
|
|
264
293
|
): ArtifactManifestV2 {
|
|
@@ -270,13 +299,16 @@ export function renewArtifact(
|
|
|
270
299
|
function renewArtifactUnlocked(
|
|
271
300
|
repoRoot: string,
|
|
272
301
|
ref: string,
|
|
273
|
-
days: number,
|
|
302
|
+
days: number | { minutes: number },
|
|
274
303
|
reason: string,
|
|
275
304
|
input: ArtifactMutationInput,
|
|
276
305
|
): ArtifactManifestV2 {
|
|
277
306
|
const now = input.now ?? new Date();
|
|
278
307
|
assertValidDate(now, "now");
|
|
279
|
-
const
|
|
308
|
+
const expiresAt =
|
|
309
|
+
typeof days === "number"
|
|
310
|
+
? addDays(now, positiveDays(days))
|
|
311
|
+
: addMinutes(now, positiveMinutes(days.minutes));
|
|
280
312
|
const why = reason.trim();
|
|
281
313
|
if (!why) throw new Error("renewal reason must not be empty");
|
|
282
314
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
@@ -285,12 +317,12 @@ function renewArtifactUnlocked(
|
|
|
285
317
|
const manifest: ArtifactManifestV2 = {
|
|
286
318
|
...parsed.manifest,
|
|
287
319
|
retention: {
|
|
288
|
-
expires_at:
|
|
320
|
+
expires_at: expiresAt.toISOString(),
|
|
289
321
|
renewed_at: now.toISOString(),
|
|
290
322
|
reason: why,
|
|
291
323
|
},
|
|
292
324
|
};
|
|
293
|
-
atomicWriteManifest(path, manifest);
|
|
325
|
+
atomicWriteManifest(path, manifest, now);
|
|
294
326
|
return manifest;
|
|
295
327
|
}
|
|
296
328
|
|
|
@@ -302,6 +334,64 @@ export function releaseArtifact(
|
|
|
302
334
|
return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
|
|
303
335
|
}
|
|
304
336
|
|
|
337
|
+
/** Retire reviewed evidence without deleting it or extending an earlier deadline. */
|
|
338
|
+
export function discardArtifact(
|
|
339
|
+
repoRoot: string,
|
|
340
|
+
ref: string,
|
|
341
|
+
reason: string,
|
|
342
|
+
input: ArtifactMutationInput & { minutes?: number } = {},
|
|
343
|
+
): ArtifactManifestV2 {
|
|
344
|
+
return withArtifactLock(repoRoot, () => {
|
|
345
|
+
const now = input.now ?? new Date();
|
|
346
|
+
assertValidDate(now, "now");
|
|
347
|
+
const minutes = positiveMinutes(input.minutes ?? 60);
|
|
348
|
+
const why = reason.trim();
|
|
349
|
+
if (!why)
|
|
350
|
+
throw new Error("discard requires a reason confirming the files are no longer needed");
|
|
351
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
352
|
+
const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
|
|
353
|
+
if (
|
|
354
|
+
!["managed-current", "managed-expired", "managed-active"].includes(entry.classification) ||
|
|
355
|
+
(entry.classification === "managed-active" &&
|
|
356
|
+
entry.owner_instance_id !== input.actor?.instance_id)
|
|
357
|
+
)
|
|
358
|
+
throw new Error(`cannot discard artifact: ${entry.reason}`);
|
|
359
|
+
const parsed = readManifest(path);
|
|
360
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
361
|
+
const manifest = parsed.manifest;
|
|
362
|
+
const remaining = Date.parse(entry.expires_at!) - now.getTime();
|
|
363
|
+
const priorWindow =
|
|
364
|
+
Date.parse(manifest.retention.expires_at) -
|
|
365
|
+
Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
366
|
+
const windowMs = Math.min(minutes * 60_000, remaining, priorWindow);
|
|
367
|
+
const updated: ArtifactManifestV2 = {
|
|
368
|
+
...manifest,
|
|
369
|
+
retention: {
|
|
370
|
+
...manifest.retention,
|
|
371
|
+
...(windowMs > 0
|
|
372
|
+
? {
|
|
373
|
+
renewed_at: now.toISOString(),
|
|
374
|
+
expires_at: new Date(now.getTime() + windowMs).toISOString(),
|
|
375
|
+
}
|
|
376
|
+
: {}),
|
|
377
|
+
reason: why,
|
|
378
|
+
},
|
|
379
|
+
released_at: now.toISOString(),
|
|
380
|
+
released_by: input.actor,
|
|
381
|
+
};
|
|
382
|
+
atomicWriteManifest(path, updated, now);
|
|
383
|
+
return updated;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Advice only: a successful check does not establish that its evidence is disposable. */
|
|
388
|
+
export function artifactReviewGuidance(repoRoot: string, ref: string): string {
|
|
389
|
+
const target = isAbsolute(ref) ? basename(ref) : ref;
|
|
390
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(target))
|
|
391
|
+
throw new Error("review guidance requires an artifact id or directory name");
|
|
392
|
+
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.`;
|
|
393
|
+
}
|
|
394
|
+
|
|
305
395
|
function releaseArtifactUnlocked(
|
|
306
396
|
repoRoot: string,
|
|
307
397
|
ref: string,
|
|
@@ -317,7 +407,7 @@ function releaseArtifactUnlocked(
|
|
|
317
407
|
released_at: now.toISOString(),
|
|
318
408
|
released_by: input.actor,
|
|
319
409
|
};
|
|
320
|
-
atomicWriteManifest(path, manifest);
|
|
410
|
+
atomicWriteManifest(path, manifest, now);
|
|
321
411
|
return manifest;
|
|
322
412
|
}
|
|
323
413
|
|
|
@@ -342,7 +432,7 @@ export function holdArtifact(
|
|
|
342
432
|
return parsed.manifest;
|
|
343
433
|
}
|
|
344
434
|
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
345
|
-
atomicWriteManifest(path, manifest);
|
|
435
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
346
436
|
return manifest;
|
|
347
437
|
});
|
|
348
438
|
}
|
|
@@ -368,7 +458,7 @@ export function unholdArtifact(
|
|
|
368
458
|
...parsed.manifest,
|
|
369
459
|
holds: parsed.manifest.holds.filter((item) => item.id !== id),
|
|
370
460
|
};
|
|
371
|
-
atomicWriteManifest(path, manifest);
|
|
461
|
+
atomicWriteManifest(path, manifest, input.now);
|
|
372
462
|
return manifest;
|
|
373
463
|
});
|
|
374
464
|
}
|
|
@@ -515,20 +605,42 @@ export function cleanArtifacts(
|
|
|
515
605
|
|
|
516
606
|
function cleanArtifactsUnlocked(
|
|
517
607
|
repoRoot: string,
|
|
518
|
-
opts: {
|
|
608
|
+
opts: {
|
|
609
|
+
yes?: boolean;
|
|
610
|
+
now?: Date;
|
|
611
|
+
freshnessSeconds?: number;
|
|
612
|
+
maxDeletes?: number;
|
|
613
|
+
timeBudgetMs?: number;
|
|
614
|
+
},
|
|
519
615
|
): ArtifactInventoryEntry[] {
|
|
616
|
+
const started = performance.now();
|
|
520
617
|
const now = opts.now ?? new Date();
|
|
521
618
|
const freshnessSeconds = opts.freshnessSeconds ?? 600;
|
|
522
619
|
const rows = inventoryArtifacts(repoRoot, { now, freshnessSeconds });
|
|
523
620
|
if (!opts.yes) return rows;
|
|
524
621
|
|
|
622
|
+
let attempted = 0;
|
|
525
623
|
return rows.map((entry) => {
|
|
526
624
|
if (entry.action !== "would-delete") return entry;
|
|
625
|
+
if (
|
|
626
|
+
attempted >= (opts.maxDeletes ?? Infinity) ||
|
|
627
|
+
(attempted > 0 && performance.now() - started >= (opts.timeBudgetMs ?? Infinity))
|
|
628
|
+
)
|
|
629
|
+
return entry;
|
|
630
|
+
attempted++;
|
|
527
631
|
// Reclassify immediately before removal. A renewal, release-state change,
|
|
528
632
|
// heartbeat, symlink swap, or tracked file added since inventory must win.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
633
|
+
// Only repository-budget eviction needs a new whole-store plan. Expiry and
|
|
634
|
+
// per-unit size are local decisions; recheck all their guards on the target.
|
|
635
|
+
const current =
|
|
636
|
+
entry.classification === "managed-over-budget"
|
|
637
|
+
? inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
|
|
638
|
+
(row) => row.path === entry.path,
|
|
639
|
+
)
|
|
640
|
+
: applyArtifactUnitBudget(
|
|
641
|
+
repoRoot,
|
|
642
|
+
classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds),
|
|
643
|
+
);
|
|
532
644
|
if (!current) {
|
|
533
645
|
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
534
646
|
}
|
|
@@ -569,26 +681,40 @@ const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
|
|
|
569
681
|
|
|
570
682
|
export interface ArtifactAutoCleanResult {
|
|
571
683
|
ran: boolean;
|
|
572
|
-
reason: "swept" | "disabled" | "fresh" | "no-root";
|
|
684
|
+
reason: "swept" | "partial" | "failed" | "disabled" | "fresh" | "no-root";
|
|
573
685
|
deleted: number;
|
|
574
686
|
bytes: number;
|
|
575
687
|
}
|
|
576
688
|
|
|
577
689
|
/**
|
|
578
|
-
* Throttled expired-artifact sweep, fired
|
|
690
|
+
* Throttled expired-artifact sweep, fired at SessionStart and before new work.
|
|
579
691
|
*
|
|
580
692
|
* Retention was previously enforced only when someone remembered to run
|
|
581
693
|
* `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
|
|
582
|
-
* busy hosts. This runs the exact same guarded deletion (
|
|
583
|
-
*
|
|
694
|
+
* busy hosts. This runs the exact same guarded deletion (expired or over-budget
|
|
695
|
+
* managed entries, each re-classified immediately before removal;
|
|
584
696
|
* unmanaged and legacy directories are never touched) at most once per
|
|
585
|
-
* interval (default
|
|
586
|
-
*
|
|
697
|
+
* interval (default 1h after completion, 1m between partial/failed slices).
|
|
698
|
+
* The owner-aware lock serializes callers; interrupted attempts can retry.
|
|
699
|
+
* Disable with `artifacts.auto_clean: false` or
|
|
587
700
|
* `HARNERY_ARTIFACT_AUTO_CLEAN=0`.
|
|
588
701
|
*/
|
|
589
702
|
export function autoCleanArtifacts(
|
|
590
703
|
repoRoot: string,
|
|
591
|
-
opts: { now?: Date } = {},
|
|
704
|
+
opts: { now?: Date; maxDeletes?: number; timeBudgetMs?: number } = {},
|
|
705
|
+
): ArtifactAutoCleanResult {
|
|
706
|
+
if (!existsSync(artifactsRoot(repoRoot))) {
|
|
707
|
+
return { ran: false, reason: "no-root", deleted: 0, bytes: 0 };
|
|
708
|
+
}
|
|
709
|
+
if (!artifactAutoCleanEnabled(repoRoot)) {
|
|
710
|
+
return { ran: false, reason: "disabled", deleted: 0, bytes: 0 };
|
|
711
|
+
}
|
|
712
|
+
return withArtifactLock(repoRoot, () => autoCleanArtifactsUnlocked(repoRoot, opts));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function autoCleanArtifactsUnlocked(
|
|
716
|
+
repoRoot: string,
|
|
717
|
+
opts: { now?: Date; maxDeletes?: number; timeBudgetMs?: number },
|
|
592
718
|
): ArtifactAutoCleanResult {
|
|
593
719
|
const now = opts.now ?? new Date();
|
|
594
720
|
assertValidDate(now, "now");
|
|
@@ -601,26 +727,73 @@ export function autoCleanArtifacts(
|
|
|
601
727
|
const stampPath = join(resolve(repoRoot), AUTO_CLEAN_STAMP);
|
|
602
728
|
const intervalMs = artifactAutoCleanIntervalHours() * 60 * 60 * 1000;
|
|
603
729
|
try {
|
|
604
|
-
const stamp = JSON.parse(readFileSync(stampPath, "utf8")) as {
|
|
605
|
-
|
|
606
|
-
|
|
730
|
+
const stamp = JSON.parse(readFileSync(stampPath, "utf8")) as {
|
|
731
|
+
status?: string;
|
|
732
|
+
last_completed_at?: string;
|
|
733
|
+
retry_after?: string;
|
|
734
|
+
};
|
|
735
|
+
const last = Date.parse(stamp.last_completed_at ?? "");
|
|
736
|
+
if (
|
|
737
|
+
(stamp.status === "completed" &&
|
|
738
|
+
Number.isFinite(last) &&
|
|
739
|
+
now.getTime() - last < intervalMs) ||
|
|
740
|
+
(["partial", "failed"].includes(stamp.status ?? "") &&
|
|
741
|
+
Date.parse(stamp.retry_after ?? "") > now.getTime())
|
|
742
|
+
) {
|
|
607
743
|
return { ran: false, reason: "fresh", deleted: 0, bytes: 0 };
|
|
608
744
|
}
|
|
609
745
|
} catch {
|
|
610
746
|
// Missing or unreadable stamp: sweep now and write a fresh one.
|
|
611
747
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
748
|
+
const maxDeletes = opts.maxDeletes ?? 10;
|
|
749
|
+
const timeBudgetMs = opts.timeBudgetMs ?? 5000;
|
|
750
|
+
if (
|
|
751
|
+
!Number.isSafeInteger(maxDeletes) ||
|
|
752
|
+
maxDeletes < 1 ||
|
|
753
|
+
!Number.isFinite(timeBudgetMs) ||
|
|
754
|
+
timeBudgetMs < 0
|
|
755
|
+
)
|
|
756
|
+
throw new Error(
|
|
757
|
+
"cleanup limits must allow at least one deletion and a nonnegative time budget",
|
|
758
|
+
);
|
|
759
|
+
const attempt = { last_attempt_at: now.toISOString() };
|
|
760
|
+
const writeStamp = (state: object) => {
|
|
761
|
+
const temp = `${stampPath}.${randomUUID()}.tmp`;
|
|
762
|
+
writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`);
|
|
763
|
+
renameSync(temp, stampPath);
|
|
764
|
+
};
|
|
765
|
+
writeStamp({ ...attempt, status: "running" });
|
|
766
|
+
try {
|
|
767
|
+
const rows = cleanArtifactsUnlocked(repoRoot, { yes: true, now, maxDeletes, timeBudgetMs });
|
|
768
|
+
const deletedRows = rows.filter((row) => row.action === "deleted");
|
|
769
|
+
const deleted = deletedRows.length;
|
|
770
|
+
const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
|
|
771
|
+
const remaining = rows.filter((row) => row.action === "would-delete").length;
|
|
772
|
+
const failures = rows
|
|
773
|
+
.filter((row) => row.classification === "unknown")
|
|
774
|
+
.map((row) => ({ name: row.name, reason: row.reason }));
|
|
775
|
+
const status = failures.length ? "failed" : remaining ? "partial" : "completed";
|
|
776
|
+
writeStamp({
|
|
777
|
+
...attempt,
|
|
778
|
+
status,
|
|
779
|
+
deleted,
|
|
780
|
+
bytes,
|
|
781
|
+
remaining,
|
|
782
|
+
failures,
|
|
783
|
+
...(status === "completed"
|
|
784
|
+
? { last_completed_at: now.toISOString() }
|
|
785
|
+
: { retry_after: new Date(now.getTime() + 60_000).toISOString() }),
|
|
786
|
+
});
|
|
787
|
+
return { ran: true, reason: status === "completed" ? "swept" : status, deleted, bytes };
|
|
788
|
+
} catch (error) {
|
|
789
|
+
writeStamp({
|
|
790
|
+
...attempt,
|
|
791
|
+
status: "failed",
|
|
792
|
+
error: errorMessage("cleanup failed", error),
|
|
793
|
+
retry_after: new Date(now.getTime() + 60_000).toISOString(),
|
|
794
|
+
});
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
624
797
|
}
|
|
625
798
|
|
|
626
799
|
export function resolveArtifactRef(repoRoot: string, ref: string): string {
|
|
@@ -674,7 +847,7 @@ function classifyArtifactPath(
|
|
|
674
847
|
}
|
|
675
848
|
const manifest = parsed.manifest;
|
|
676
849
|
const bytes = safeTreeSize(path);
|
|
677
|
-
const lastModifiedMs = safeTreeLastModified(path, now);
|
|
850
|
+
const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
|
|
678
851
|
const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
|
|
679
852
|
const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
|
|
680
853
|
const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
|
|
@@ -742,26 +915,33 @@ function classifyArtifactPath(
|
|
|
742
915
|
};
|
|
743
916
|
}
|
|
744
917
|
|
|
918
|
+
function applyArtifactUnitBudget(
|
|
919
|
+
repoRoot: string,
|
|
920
|
+
row: ArtifactInventoryEntry,
|
|
921
|
+
): ArtifactInventoryEntry {
|
|
922
|
+
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
923
|
+
if (
|
|
924
|
+
row.classification === "managed-current" &&
|
|
925
|
+
row.bytes !== null &&
|
|
926
|
+
row.bytes > maxUnitBytes &&
|
|
927
|
+
!row.oversize_acknowledged
|
|
928
|
+
) {
|
|
929
|
+
return {
|
|
930
|
+
...row,
|
|
931
|
+
classification: "managed-oversize",
|
|
932
|
+
action: "would-delete",
|
|
933
|
+
reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return row;
|
|
937
|
+
}
|
|
938
|
+
|
|
745
939
|
function applyArtifactBudgets(
|
|
746
940
|
repoRoot: string,
|
|
747
941
|
inputRows: ArtifactInventoryEntry[],
|
|
748
942
|
): ArtifactInventoryEntry[] {
|
|
749
|
-
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
750
943
|
const maxBytes = artifactMaxBytes(repoRoot);
|
|
751
|
-
const rows = inputRows.map((row) => ({ ...row }));
|
|
752
|
-
|
|
753
|
-
for (const row of rows) {
|
|
754
|
-
if (
|
|
755
|
-
row.classification === "managed-current" &&
|
|
756
|
-
row.bytes !== null &&
|
|
757
|
-
row.bytes > maxUnitBytes &&
|
|
758
|
-
!row.oversize_acknowledged
|
|
759
|
-
) {
|
|
760
|
-
row.classification = "managed-oversize";
|
|
761
|
-
row.reason = `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`;
|
|
762
|
-
row.action = "would-delete";
|
|
763
|
-
}
|
|
764
|
-
}
|
|
944
|
+
const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
|
|
765
945
|
|
|
766
946
|
const managedBytes = rows.reduce(
|
|
767
947
|
(sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
|
|
@@ -866,11 +1046,24 @@ export function parseArtifactManifest(value: unknown): ParsedManifest | Manifest
|
|
|
866
1046
|
if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
|
|
867
1047
|
return { ok: false, reason: "invalid oversize_acknowledged" };
|
|
868
1048
|
}
|
|
1049
|
+
if (m.activity !== undefined && !validArtifactActivity(m.activity)) {
|
|
1050
|
+
return { ok: false, reason: "invalid activity checkpoint" };
|
|
1051
|
+
}
|
|
869
1052
|
return { ok: true, manifest: m as ArtifactManifestV2 };
|
|
870
1053
|
}
|
|
871
1054
|
|
|
872
|
-
function atomicWriteManifest(path: string, manifest: ArtifactManifestV2): void {
|
|
1055
|
+
function atomicWriteManifest(path: string, manifest: ArtifactManifestV2, now = new Date()): void {
|
|
873
1056
|
const target = join(path, ARTIFACT_MANIFEST);
|
|
1057
|
+
manifest.activity = readArtifactActivity(
|
|
1058
|
+
path,
|
|
1059
|
+
now,
|
|
1060
|
+
existsSync(target)
|
|
1061
|
+
? manifest.activity
|
|
1062
|
+
: {
|
|
1063
|
+
last_changed_at: manifest.created_at,
|
|
1064
|
+
root_entries_sha256: artifactRootEntries(path),
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
874
1067
|
const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
|
|
875
1068
|
try {
|
|
876
1069
|
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
@@ -894,7 +1087,7 @@ export interface ArtifactMigrationEntry {
|
|
|
894
1087
|
/** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
|
|
895
1088
|
export function migrateArtifacts(
|
|
896
1089
|
repoRoot: string,
|
|
897
|
-
opts: { yes?: boolean } = {},
|
|
1090
|
+
opts: { yes?: boolean; now?: Date } = {},
|
|
898
1091
|
): ArtifactMigrationEntry[] {
|
|
899
1092
|
const migrate = (): ArtifactMigrationEntry[] => {
|
|
900
1093
|
const root = artifactsRoot(repoRoot);
|
|
@@ -961,7 +1154,7 @@ export function migrateArtifacts(
|
|
|
961
1154
|
}
|
|
962
1155
|
if (readFileSync(target, "utf8") !== preimage)
|
|
963
1156
|
throw new Error("manifest changed before migration");
|
|
964
|
-
atomicWriteManifest(path, parsed.manifest);
|
|
1157
|
+
atomicWriteManifest(path, parsed.manifest, opts.now);
|
|
965
1158
|
return {
|
|
966
1159
|
path,
|
|
967
1160
|
action: "migrated",
|
|
@@ -976,25 +1169,141 @@ export function migrateArtifacts(
|
|
|
976
1169
|
return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
|
|
977
1170
|
}
|
|
978
1171
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
1172
|
+
export interface ArtifactActivityRepairEntry {
|
|
1173
|
+
path: string;
|
|
1174
|
+
action: "keep" | "would-repair" | "repaired";
|
|
1175
|
+
reason: string;
|
|
1176
|
+
previous_expires_at?: string;
|
|
1177
|
+
repaired_expires_at?: string;
|
|
1178
|
+
receipt_path?: string;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** Repair only a provable, untouched v1-to-v2 migration. Never delete payloads.
|
|
1182
|
+
* Old migrations saved the original manifest but not the original root stat.
|
|
1183
|
+
* The exact preimage and coincident root/manifest/receipt timestamps bound this
|
|
1184
|
+
* correction. Any later root change or metadata mutation requires manual review.
|
|
1185
|
+
*/
|
|
1186
|
+
export function repairArtifactActivity(
|
|
1187
|
+
repoRoot: string,
|
|
1188
|
+
opts: { yes?: boolean; now?: Date } = {},
|
|
1189
|
+
): ArtifactActivityRepairEntry[] {
|
|
1190
|
+
const repair = (): ArtifactActivityRepairEntry[] => {
|
|
1191
|
+
const now = opts.now ?? new Date();
|
|
1192
|
+
assertValidDate(now, "now");
|
|
1193
|
+
const preimageRoot = join(resolve(repoRoot), ".harnery/artifact-migrations");
|
|
1194
|
+
const preimages = new Map<string, { value: unknown; path: string; mtime: number }[]>();
|
|
1195
|
+
if (existsSync(preimageRoot) && !lstatSync(preimageRoot).isSymbolicLink()) {
|
|
1196
|
+
for (const name of readdirSync(preimageRoot)) {
|
|
1197
|
+
if (!/^[a-f0-9]{64}\.v1\.json$/.test(name)) continue;
|
|
1198
|
+
const file = join(preimageRoot, name);
|
|
1199
|
+
try {
|
|
1200
|
+
const stat = lstatSync(file);
|
|
1201
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) continue;
|
|
1202
|
+
const bytes = readFileSync(file, "utf8");
|
|
1203
|
+
if (`${createHash("sha256").update(bytes).digest("hex")}.v1.json` !== name) continue;
|
|
1204
|
+
const old = JSON.parse(bytes);
|
|
1205
|
+
if (
|
|
1206
|
+
old?.schema_version !== 1 ||
|
|
1207
|
+
Object.hasOwn(old, "holds") ||
|
|
1208
|
+
Object.hasOwn(old, "activity")
|
|
1209
|
+
)
|
|
1210
|
+
continue;
|
|
1211
|
+
const upgraded = { ...old, schema_version: ARTIFACT_SCHEMA_VERSION, holds: [] };
|
|
1212
|
+
const parsed = parseArtifactManifest(upgraded);
|
|
1213
|
+
if (!parsed.ok) continue;
|
|
1214
|
+
const items = preimages.get(parsed.manifest.artifact_id) ?? [];
|
|
1215
|
+
items.push({ value: upgraded, path: file, mtime: stat.mtimeMs });
|
|
1216
|
+
preimages.set(parsed.manifest.artifact_id, items);
|
|
1217
|
+
} catch {
|
|
1218
|
+
// Unverifiable preimages confer no repair authority.
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const root = artifactsRoot(repoRoot);
|
|
1223
|
+
if (!existsSync(root)) return [];
|
|
1224
|
+
return readdirSync(root)
|
|
1225
|
+
.sort()
|
|
1226
|
+
.map((name): ArtifactActivityRepairEntry => {
|
|
1227
|
+
const path = join(root, name);
|
|
1228
|
+
try {
|
|
1229
|
+
const parsed = readManifest(path);
|
|
1230
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
1231
|
+
const manifest = parsed.manifest;
|
|
1232
|
+
if (manifest.activity)
|
|
1233
|
+
return { path, action: "keep", reason: "activity already recorded" };
|
|
1234
|
+
const source = preimages
|
|
1235
|
+
.get(manifest.artifact_id)
|
|
1236
|
+
?.find((item) => isDeepStrictEqual(item.value, manifest));
|
|
1237
|
+
if (!source) throw new Error("no exact migration preimage");
|
|
1238
|
+
if (containsTrackedPath(repoRoot, path))
|
|
1239
|
+
throw new Error("artifact contains tracked files");
|
|
1240
|
+
const target = join(path, ARTIFACT_MANIFEST);
|
|
1241
|
+
const before = readFileSync(target, "utf8");
|
|
1242
|
+
const unitStat = lstatSync(path);
|
|
1243
|
+
const manifestStat = lstatSync(target);
|
|
1244
|
+
if (
|
|
1245
|
+
unitStat.mtimeMs !== manifestStat.mtimeMs ||
|
|
1246
|
+
unitStat.ctimeMs !== manifestStat.ctimeMs ||
|
|
1247
|
+
manifestStat.mtimeMs !== manifestStat.ctimeMs ||
|
|
1248
|
+
manifestStat.mtimeMs < source.mtime ||
|
|
1249
|
+
manifestStat.mtimeMs - source.mtime > 5000
|
|
1250
|
+
)
|
|
1251
|
+
throw new Error("root or manifest changed outside the recorded migration");
|
|
1252
|
+
const anchor = manifest.retention.renewed_at ?? manifest.created_at;
|
|
1253
|
+
const oldActivity = readArtifactActivity(path, now);
|
|
1254
|
+
const activity = readArtifactActivity(path, now, {
|
|
1255
|
+
last_changed_at: anchor,
|
|
1256
|
+
root_entries_sha256: artifactRootEntries(path),
|
|
1257
|
+
});
|
|
1258
|
+
const windowMs = Date.parse(manifest.retention.expires_at) - Date.parse(anchor);
|
|
1259
|
+
const expiry = (changedAt: string) =>
|
|
1260
|
+
new Date(Math.max(Date.parse(anchor), Date.parse(changedAt)) + windowMs).toISOString();
|
|
1261
|
+
const row: ArtifactActivityRepairEntry = {
|
|
1262
|
+
path,
|
|
1263
|
+
action: opts.yes ? "repaired" : "would-repair",
|
|
1264
|
+
reason:
|
|
1265
|
+
"exact migration preimage; unchanged root; payload activity and retention preserved",
|
|
1266
|
+
previous_expires_at: expiry(oldActivity.last_changed_at),
|
|
1267
|
+
repaired_expires_at: expiry(activity.last_changed_at),
|
|
1268
|
+
};
|
|
1269
|
+
if (!opts.yes) return row;
|
|
1270
|
+
const receipt = join(
|
|
1271
|
+
preimageRoot,
|
|
1272
|
+
`${manifest.artifact_id}.${randomUUID()}.activity-repair.json`,
|
|
1273
|
+
);
|
|
1274
|
+
writeFileSync(
|
|
1275
|
+
receipt,
|
|
1276
|
+
`${JSON.stringify(
|
|
1277
|
+
{
|
|
1278
|
+
schema_version: 1,
|
|
1279
|
+
repaired_at: now.toISOString(),
|
|
1280
|
+
path: relative(repoRoot, path),
|
|
1281
|
+
preimage_path: relative(repoRoot, source.path),
|
|
1282
|
+
original_manifest: before,
|
|
1283
|
+
activity,
|
|
1284
|
+
previous_expires_at: row.previous_expires_at,
|
|
1285
|
+
repaired_expires_at: row.repaired_expires_at,
|
|
1286
|
+
},
|
|
1287
|
+
null,
|
|
1288
|
+
2,
|
|
1289
|
+
)}\n`,
|
|
1290
|
+
{ flag: "wx", mode: 0o600 },
|
|
1291
|
+
);
|
|
1292
|
+
const current = lstatSync(path);
|
|
1293
|
+
if (
|
|
1294
|
+
readFileSync(target, "utf8") !== before ||
|
|
1295
|
+
current.mtimeMs !== unitStat.mtimeMs ||
|
|
1296
|
+
current.ctimeMs !== unitStat.ctimeMs
|
|
1297
|
+
)
|
|
1298
|
+
throw new Error("artifact changed before repair");
|
|
1299
|
+
atomicWriteManifest(path, { ...manifest, activity }, now);
|
|
1300
|
+
return { ...row, receipt_path: receipt };
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
return { path, action: "keep", reason: errorMessage("activity repair refused", error) };
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
};
|
|
1306
|
+
return opts.yes ? withArtifactLock(repoRoot, repair) : repair();
|
|
998
1307
|
}
|
|
999
1308
|
|
|
1000
1309
|
function validHoldId(value: unknown): value is string {
|
|
@@ -1082,24 +1391,9 @@ function safeTreeSize(path: string): number | null {
|
|
|
1082
1391
|
* symlinks. A small future tolerance protects a write racing the inventory
|
|
1083
1392
|
* scan; timestamps farther ahead are ignored as clock-skewed metadata.
|
|
1084
1393
|
*/
|
|
1085
|
-
function safeTreeLastModified(path: string, now: Date): number | null {
|
|
1086
|
-
const nowMs = now.getTime();
|
|
1087
|
-
const futureToleranceMs = 5 * 60 * 1000;
|
|
1394
|
+
function safeTreeLastModified(path: string, now: Date, activity?: ArtifactActivity): number | null {
|
|
1088
1395
|
try {
|
|
1089
|
-
|
|
1090
|
-
let latest = 0;
|
|
1091
|
-
for (const timestamp of [st.mtimeMs, st.ctimeMs]) {
|
|
1092
|
-
if (Number.isFinite(timestamp) && timestamp <= nowMs + futureToleranceMs) {
|
|
1093
|
-
latest = Math.max(latest, Math.min(timestamp, nowMs));
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
if (st.isSymbolicLink() || !st.isDirectory()) return latest;
|
|
1097
|
-
for (const child of readdirSync(path)) {
|
|
1098
|
-
const childLatest = safeTreeLastModified(join(path, child), now);
|
|
1099
|
-
if (childLatest === null) return null;
|
|
1100
|
-
latest = Math.max(latest, childLatest);
|
|
1101
|
-
}
|
|
1102
|
-
return latest;
|
|
1396
|
+
return Date.parse(readArtifactActivity(path, now, activity).last_changed_at);
|
|
1103
1397
|
} catch {
|
|
1104
1398
|
return null;
|
|
1105
1399
|
}
|