harnery 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +10 -1
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +148 -47
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +73 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +360 -21
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.d.ts.map +1 -0
- package/dist/commands/diagnostics.js +229 -0
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/files.d.ts +12 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -1
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +4 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +71 -2
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +8 -0
- package/dist/commands/qa-run.d.ts.map +1 -0
- package/dist/commands/qa-run.js +396 -0
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +29 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +101 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -0
- package/dist/core/agents/cli.js +7 -3
- package/dist/core/agents/health.d.ts +17 -0
- package/dist/core/agents/health.d.ts.map +1 -0
- package/dist/core/agents/health.js +109 -0
- package/dist/core/agents/mailbox.d.ts +92 -0
- package/dist/core/agents/mailbox.d.ts.map +1 -0
- package/dist/core/agents/mailbox.js +325 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
- package/dist/core/agents/reconcile-coordination-v3.js +58 -0
- package/dist/core/agents/render/prompt-context.d.ts +5 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +34 -13
- package/dist/core/agents/render/session-context.d.ts +8 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +34 -3
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +50 -1
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +6 -10
- package/dist/core/agents/session-finalizer-v3.js +4 -1
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +82 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +410 -15
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +59 -0
- package/dist/core/diagnostics/advice.d.ts +9 -0
- package/dist/core/diagnostics/advice.d.ts.map +1 -0
- package/dist/core/diagnostics/advice.js +112 -0
- package/dist/core/diagnostics/bundle.d.ts +51 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -0
- package/dist/core/diagnostics/bundle.js +968 -0
- package/dist/core/diagnostics/comparison.d.ts +5 -0
- package/dist/core/diagnostics/comparison.d.ts.map +1 -0
- package/dist/core/diagnostics/comparison.js +444 -0
- package/dist/core/diagnostics/contract.d.ts +245 -0
- package/dist/core/diagnostics/contract.d.ts.map +1 -0
- package/dist/core/diagnostics/contract.js +18 -0
- package/dist/core/diagnostics/identity.d.ts +3 -0
- package/dist/core/diagnostics/identity.d.ts.map +1 -0
- package/dist/core/diagnostics/identity.js +10 -0
- package/dist/core/diagnostics/index.d.ts +8 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -0
- package/dist/core/diagnostics/index.js +7 -0
- package/dist/core/diagnostics/replay.d.ts +8 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -0
- package/dist/core/diagnostics/replay.js +213 -0
- package/dist/core/diagnostics/sanitize.d.ts +9 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
- package/dist/core/diagnostics/sanitize.js +84 -0
- package/dist/core/events/legacy-storage/compression.d.ts +13 -0
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
- package/dist/core/events/legacy-storage/compression.js +107 -0
- package/dist/core/events/legacy-storage/index.d.ts +1 -0
- package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/index.js +1 -0
- package/dist/core/events/v3/archive-retention.d.ts +33 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
- package/dist/core/events/v3/archive-retention.js +195 -0
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/capabilities.d.ts +1 -1
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +1 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/index.d.ts +1 -0
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +13 -2
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +10 -2
- package/dist/core/events/v3/producers/recorder.d.ts +39 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +350 -17
- package/dist/core/hooks/adapter/output.d.ts +8 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +11 -1
- package/dist/core/hooks/cli.js +303 -219
- package/dist/core/hooks/health.d.ts +59 -0
- package/dist/core/hooks/health.d.ts.map +1 -0
- package/dist/core/hooks/health.js +84 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +96 -0
- package/dist/core/resources/contract.d.ts.map +1 -0
- package/dist/core/resources/contract.js +2 -0
- package/dist/core/resources/index.d.ts +6 -0
- package/dist/core/resources/index.d.ts.map +1 -0
- package/dist/core/resources/index.js +5 -0
- package/dist/core/resources/sampler.d.ts +34 -0
- package/dist/core/resources/sampler.d.ts.map +1 -0
- package/dist/core/resources/sampler.js +458 -0
- package/dist/core/resources/service-status.d.ts +3 -0
- package/dist/core/resources/service-status.d.ts.map +1 -0
- package/dist/core/resources/service-status.js +48 -0
- package/dist/core/resources/service.d.ts +26 -0
- package/dist/core/resources/service.d.ts.map +1 -0
- package/dist/core/resources/service.js +292 -0
- package/dist/core/resources/storage.d.ts +13 -0
- package/dist/core/resources/storage.d.ts.map +1 -0
- package/dist/core/resources/storage.js +30 -0
- package/dist/core/storage/atomic-json.d.ts +3 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -0
- package/dist/core/storage/atomic-json.js +15 -0
- package/dist/core/storage/builtins.d.ts.map +1 -1
- package/dist/core/storage/builtins.js +45 -6
- package/dist/core/storage/logger.d.ts +2 -0
- package/dist/core/storage/logger.d.ts.map +1 -1
- package/dist/core/storage/logger.js +2 -0
- package/dist/core/storage/query.d.ts +14 -0
- package/dist/core/storage/query.d.ts.map +1 -1
- package/dist/core/storage/query.js +51 -0
- package/dist/core/supervisor/activity.d.ts +4 -0
- package/dist/core/supervisor/activity.d.ts.map +1 -0
- package/dist/core/supervisor/activity.js +141 -0
- package/dist/core/supervisor/contract.d.ts +268 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -0
- package/dist/core/supervisor/contract.js +35 -0
- package/dist/core/supervisor/explanations.d.ts +4 -0
- package/dist/core/supervisor/explanations.d.ts.map +1 -0
- package/dist/core/supervisor/explanations.js +71 -0
- package/dist/core/supervisor/findings.d.ts +20 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -0
- package/dist/core/supervisor/findings.js +399 -0
- package/dist/core/supervisor/history.d.ts +9 -0
- package/dist/core/supervisor/history.d.ts.map +1 -0
- package/dist/core/supervisor/history.js +46 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-alerts.js +70 -0
- package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
- package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-storage.js +24 -0
- package/dist/core/supervisor/hook-health.d.ts +72 -0
- package/dist/core/supervisor/hook-health.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health.js +203 -0
- package/dist/core/supervisor/hooks.d.ts +10 -0
- package/dist/core/supervisor/hooks.d.ts.map +1 -0
- package/dist/core/supervisor/hooks.js +28 -0
- package/dist/core/supervisor/index.d.ts +15 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +14 -0
- package/dist/core/supervisor/log-feed.d.ts +9 -0
- package/dist/core/supervisor/log-feed.d.ts.map +1 -0
- package/dist/core/supervisor/log-feed.js +103 -0
- package/dist/core/supervisor/service.d.ts +29 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +450 -0
- package/dist/core/supervisor/services.d.ts +11 -0
- package/dist/core/supervisor/services.d.ts.map +1 -0
- package/dist/core/supervisor/services.js +155 -0
- package/dist/core/supervisor/status.d.ts +3 -0
- package/dist/core/supervisor/status.d.ts.map +1 -0
- package/dist/core/supervisor/status.js +48 -0
- package/dist/core/supervisor/storage.d.ts +30 -0
- package/dist/core/supervisor/storage.d.ts.map +1 -0
- package/dist/core/supervisor/storage.js +73 -0
- package/dist/core/supervisor/timeline.d.ts +3 -0
- package/dist/core/supervisor/timeline.d.ts.map +1 -0
- package/dist/core/supervisor/timeline.js +87 -0
- package/dist/core/workflow/admission.d.ts +23 -0
- package/dist/core/workflow/admission.d.ts.map +1 -0
- package/dist/core/workflow/admission.js +136 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +77 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +39 -1
- package/dist/core/workflow/run-state.d.ts +3 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +8 -1
- package/dist/core/workflow/types.d.ts +33 -1
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +2 -1
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +46 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +168 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +4 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +377 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
- package/dist/lib/browser/qa-run-contracts.js +361 -0
- package/dist/lib/browser/qa-run.d.ts +141 -0
- package/dist/lib/browser/qa-run.d.ts.map +1 -0
- package/dist/lib/browser/qa-run.js +1283 -0
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/coord-root-id.d.ts +9 -0
- package/dist/lib/coord-root-id.d.ts.map +1 -0
- package/dist/lib/coord-root-id.js +17 -0
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/apply.d.ts +3 -1
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +74 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +11 -2
- package/package.json +19 -2
- package/src/commander.ts +75 -1
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +189 -50
- package/src/commands/artifacts.ts +132 -18
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +496 -20
- package/src/commands/diagnostics.ts +271 -0
- package/src/commands/fetch.ts +1 -0
- package/src/commands/files.ts +102 -2
- package/src/commands/governor.ts +8 -0
- package/src/commands/ledger-v3.ts +82 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +503 -0
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/resources.ts +38 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/commands/supervisor.ts +141 -0
- package/src/commands/web.ts +19 -0
- package/src/commands/workflow.ts +8 -0
- package/src/core/agents/cli.ts +7 -3
- package/src/core/agents/health.ts +142 -0
- package/src/core/agents/mailbox.ts +373 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/reconcile-coordination-v3.ts +77 -0
- package/src/core/agents/render/prompt-context.ts +42 -12
- package/src/core/agents/render/session-context.ts +37 -3
- package/src/core/agents/rules/commit-conflict.ts +58 -1
- package/src/core/agents/rules/stop-hook.ts +6 -12
- package/src/core/agents/session-finalizer-v3.ts +4 -1
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +565 -30
- package/src/core/config.ts +134 -2
- package/src/core/diagnostics/advice.ts +147 -0
- package/src/core/diagnostics/bundle.ts +1250 -0
- package/src/core/diagnostics/comparison.ts +565 -0
- package/src/core/diagnostics/contract.ts +311 -0
- package/src/core/diagnostics/identity.ts +11 -0
- package/src/core/diagnostics/index.ts +7 -0
- package/src/core/diagnostics/replay.ts +314 -0
- package/src/core/diagnostics/sanitize.ts +97 -0
- package/src/core/events/legacy-storage/compression.ts +130 -0
- package/src/core/events/legacy-storage/index.ts +1 -0
- package/src/core/events/v3/archive-retention.ts +251 -0
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/capabilities.ts +2 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/index.ts +9 -0
- package/src/core/events/v3/live-routing.ts +1 -0
- package/src/core/events/v3/producers/hook-base.ts +14 -2
- package/src/core/events/v3/producers/intake.ts +10 -2
- package/src/core/events/v3/producers/recorder.ts +420 -17
- package/src/core/hooks/adapter/output.ts +12 -1
- package/src/core/hooks/cli.ts +332 -247
- package/src/core/hooks/health.ts +146 -0
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +105 -0
- package/src/core/resources/index.ts +5 -0
- package/src/core/resources/sampler.ts +590 -0
- package/src/core/resources/service-status.ts +62 -0
- package/src/core/resources/service.ts +337 -0
- package/src/core/resources/storage.ts +44 -0
- package/src/core/storage/atomic-json.ts +15 -0
- package/src/core/storage/builtins.ts +62 -6
- package/src/core/storage/logger.ts +2 -0
- package/src/core/storage/query.ts +58 -0
- package/src/core/supervisor/activity.ts +193 -0
- package/src/core/supervisor/contract.ts +312 -0
- package/src/core/supervisor/explanations.ts +104 -0
- package/src/core/supervisor/findings.ts +788 -0
- package/src/core/supervisor/history.ts +61 -0
- package/src/core/supervisor/hook-health-alerts.ts +108 -0
- package/src/core/supervisor/hook-health-storage.ts +25 -0
- package/src/core/supervisor/hook-health.ts +301 -0
- package/src/core/supervisor/hooks.ts +35 -0
- package/src/core/supervisor/index.ts +14 -0
- package/src/core/supervisor/log-feed.ts +131 -0
- package/src/core/supervisor/service.ts +511 -0
- package/src/core/supervisor/services.ts +203 -0
- package/src/core/supervisor/status.ts +59 -0
- package/src/core/supervisor/storage.ts +126 -0
- package/src/core/supervisor/timeline.ts +133 -0
- package/src/core/workflow/admission.ts +215 -0
- package/src/core/workflow/engine.ts +93 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +54 -1
- package/src/core/workflow/run-state.ts +13 -1
- package/src/core/workflow/types.ts +39 -1
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +212 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +65 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +743 -0
- package/src/lib/browser/qa-run.ts +1462 -0
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/coord-root-id.ts +23 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/apply.ts +81 -2
- package/src/lib/instructions/templates.ts +11 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import type { Stats } from "node:fs";
|
|
14
14
|
import {
|
|
15
15
|
existsSync,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
readdirSync,
|
|
19
19
|
readFileSync,
|
|
20
20
|
renameSync,
|
|
21
|
+
rmdirSync,
|
|
21
22
|
rmSync,
|
|
22
23
|
writeFileSync,
|
|
23
24
|
} from "node:fs";
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
artifactAutoCleanEnabled,
|
|
28
29
|
artifactAutoCleanIntervalHours,
|
|
29
30
|
artifactDefaultRetentionDays,
|
|
31
|
+
artifactMaxBytes,
|
|
32
|
+
artifactMaxUnitBytes,
|
|
30
33
|
} from "../config.ts";
|
|
31
34
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
|
|
32
35
|
|
|
@@ -38,8 +41,9 @@ export interface ArtifactActor {
|
|
|
38
41
|
name?: string;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
export interface
|
|
44
|
+
export interface ArtifactManifestV2 {
|
|
42
45
|
schema_version: typeof ARTIFACT_SCHEMA_VERSION;
|
|
46
|
+
holds: ArtifactHold[];
|
|
43
47
|
artifact_id: string;
|
|
44
48
|
slug: string;
|
|
45
49
|
purpose: string;
|
|
@@ -52,12 +56,38 @@ export interface ArtifactManifestV1 {
|
|
|
52
56
|
};
|
|
53
57
|
released_at?: string;
|
|
54
58
|
released_by?: ArtifactActor;
|
|
59
|
+
oversize_acknowledged?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ArtifactHold {
|
|
63
|
+
id: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
set_by: ArtifactActor;
|
|
66
|
+
set_at: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ArtifactHoldInput {
|
|
70
|
+
id: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function artifactCapabilities() {
|
|
75
|
+
return {
|
|
76
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
77
|
+
holds: true,
|
|
78
|
+
atomic_create_holds: true,
|
|
79
|
+
owner_scoped_unhold: true,
|
|
80
|
+
explicit_v1_migration: true,
|
|
81
|
+
} as const;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
export type ArtifactClassification =
|
|
85
|
+
| "managed-held"
|
|
58
86
|
| "managed-active"
|
|
59
87
|
| "managed-current"
|
|
60
88
|
| "managed-expired"
|
|
89
|
+
| "managed-oversize"
|
|
90
|
+
| "managed-over-budget"
|
|
61
91
|
| "managed-tracked"
|
|
62
92
|
| "invalid-manifest"
|
|
63
93
|
| "unmanaged"
|
|
@@ -78,15 +108,22 @@ export interface ArtifactInventoryEntry {
|
|
|
78
108
|
last_modified_at: string | null;
|
|
79
109
|
expires_at: string | null;
|
|
80
110
|
owner_instance_id: string | null;
|
|
111
|
+
oversize_acknowledged: boolean;
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
export interface ArtifactCreateInput {
|
|
84
115
|
slug: string;
|
|
85
116
|
purpose: string;
|
|
86
117
|
retentionDays: number;
|
|
118
|
+
/** Sub-day retention. When set, it replaces `retentionDays` for this unit
|
|
119
|
+
* (a page review pack expires in minutes, not days). 1 to 5,256,000. */
|
|
120
|
+
retentionMinutes?: number;
|
|
87
121
|
actor?: ArtifactActor;
|
|
122
|
+
/** Holds are persisted with the first manifest; a valid actor is required. */
|
|
123
|
+
holds?: ArtifactHoldInput[];
|
|
88
124
|
now?: Date;
|
|
89
125
|
id?: string;
|
|
126
|
+
big?: boolean;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
export interface ArtifactMutationInput {
|
|
@@ -94,9 +131,18 @@ export interface ArtifactMutationInput {
|
|
|
94
131
|
now?: Date;
|
|
95
132
|
}
|
|
96
133
|
|
|
134
|
+
export interface ArtifactAdoptionResult {
|
|
135
|
+
candidates: Array<{ path: string; name: string; bytes: number; kind: "file" | "directory" }>;
|
|
136
|
+
candidate_bytes: number;
|
|
137
|
+
requires_big: boolean;
|
|
138
|
+
adopted_artifact_id: string | null;
|
|
139
|
+
adopted_path: string | null;
|
|
140
|
+
adopted_directories: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
interface ParsedManifest {
|
|
98
144
|
ok: true;
|
|
99
|
-
manifest:
|
|
145
|
+
manifest: ArtifactManifestV2;
|
|
100
146
|
}
|
|
101
147
|
|
|
102
148
|
interface ManifestError {
|
|
@@ -116,18 +162,32 @@ export function configuredArtifactRetentionDays(repoRoot: string): number {
|
|
|
116
162
|
export function createArtifact(
|
|
117
163
|
repoRoot: string,
|
|
118
164
|
input: ArtifactCreateInput,
|
|
119
|
-
): { path: string; manifest:
|
|
165
|
+
): { path: string; manifest: ArtifactManifestV2 } {
|
|
166
|
+
return withArtifactLock(repoRoot, () => createArtifactUnlocked(repoRoot, input));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createArtifactUnlocked(
|
|
170
|
+
repoRoot: string,
|
|
171
|
+
input: ArtifactCreateInput,
|
|
172
|
+
): { path: string; manifest: ArtifactManifestV2 } {
|
|
120
173
|
const now = input.now ?? new Date();
|
|
121
174
|
assertValidDate(now, "now");
|
|
122
175
|
const slug = normalizeSlug(input.slug);
|
|
123
176
|
if (!slug) throw new Error("slug must contain at least one ASCII letter or digit");
|
|
124
177
|
const purpose = input.purpose.trim();
|
|
125
178
|
if (!purpose) throw new Error("purpose must not be empty");
|
|
126
|
-
const
|
|
179
|
+
const expiresAt =
|
|
180
|
+
input.retentionMinutes !== undefined
|
|
181
|
+
? addMinutes(now, positiveMinutes(input.retentionMinutes))
|
|
182
|
+
: addDays(now, positiveDays(input.retentionDays));
|
|
127
183
|
const artifactId = input.id ?? randomUUID();
|
|
128
184
|
if (!isSafeId(artifactId)) {
|
|
129
185
|
throw new Error("artifact id must use ASCII letters, digits, hyphens, or underscores");
|
|
130
186
|
}
|
|
187
|
+
const holds = (input.holds ?? []).map((hold) => makeHold(hold, input.actor, now));
|
|
188
|
+
if (new Set(holds.map((hold) => hold.id)).size !== holds.length) {
|
|
189
|
+
throw new Error("duplicate hold id");
|
|
190
|
+
}
|
|
131
191
|
|
|
132
192
|
const root = artifactsRoot(repoRoot);
|
|
133
193
|
mkdirSync(root, { recursive: true });
|
|
@@ -135,16 +195,18 @@ export function createArtifact(
|
|
|
135
195
|
const path = join(root, `${date}_${slug}_${artifactId.slice(0, 8)}`);
|
|
136
196
|
mkdirSync(path);
|
|
137
197
|
|
|
138
|
-
const manifest:
|
|
198
|
+
const manifest: ArtifactManifestV2 = {
|
|
139
199
|
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
200
|
+
holds,
|
|
140
201
|
artifact_id: artifactId,
|
|
141
202
|
slug,
|
|
142
203
|
purpose,
|
|
143
204
|
created_at: now.toISOString(),
|
|
144
205
|
created_by: input.actor,
|
|
145
206
|
retention: {
|
|
146
|
-
expires_at:
|
|
207
|
+
expires_at: expiresAt.toISOString(),
|
|
147
208
|
},
|
|
209
|
+
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
148
210
|
};
|
|
149
211
|
atomicWriteManifest(path, manifest);
|
|
150
212
|
return { path, manifest };
|
|
@@ -177,21 +239,17 @@ export function inventoryArtifacts(
|
|
|
177
239
|
for (const name of names) {
|
|
178
240
|
rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
|
|
179
241
|
}
|
|
180
|
-
return rows;
|
|
242
|
+
return applyArtifactBudgets(repoRoot, rows);
|
|
181
243
|
}
|
|
182
244
|
|
|
183
245
|
export function showArtifact(
|
|
184
246
|
repoRoot: string,
|
|
185
247
|
ref: string,
|
|
186
248
|
opts: { now?: Date; freshnessSeconds?: number } = {},
|
|
187
|
-
): { entry: ArtifactInventoryEntry; manifest:
|
|
249
|
+
): { entry: ArtifactInventoryEntry; manifest: ArtifactManifestV2 } {
|
|
188
250
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
189
|
-
const entry =
|
|
190
|
-
|
|
191
|
-
path,
|
|
192
|
-
opts.now ?? new Date(),
|
|
193
|
-
opts.freshnessSeconds ?? 600,
|
|
194
|
-
);
|
|
251
|
+
const entry = inventoryArtifacts(repoRoot, opts).find((row) => row.path === path);
|
|
252
|
+
if (!entry) throw new Error(`artifact "${ref}" was not found`);
|
|
195
253
|
const parsed = readManifest(path);
|
|
196
254
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
197
255
|
return { entry, manifest: parsed.manifest };
|
|
@@ -203,7 +261,19 @@ export function renewArtifact(
|
|
|
203
261
|
days: number,
|
|
204
262
|
reason: string,
|
|
205
263
|
input: ArtifactMutationInput = {},
|
|
206
|
-
):
|
|
264
|
+
): ArtifactManifestV2 {
|
|
265
|
+
return withArtifactLock(repoRoot, () =>
|
|
266
|
+
renewArtifactUnlocked(repoRoot, ref, days, reason, input),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function renewArtifactUnlocked(
|
|
271
|
+
repoRoot: string,
|
|
272
|
+
ref: string,
|
|
273
|
+
days: number,
|
|
274
|
+
reason: string,
|
|
275
|
+
input: ArtifactMutationInput,
|
|
276
|
+
): ArtifactManifestV2 {
|
|
207
277
|
const now = input.now ?? new Date();
|
|
208
278
|
assertValidDate(now, "now");
|
|
209
279
|
const retentionDays = positiveDays(days);
|
|
@@ -212,7 +282,7 @@ export function renewArtifact(
|
|
|
212
282
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
213
283
|
const parsed = readManifest(path);
|
|
214
284
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
215
|
-
const manifest:
|
|
285
|
+
const manifest: ArtifactManifestV2 = {
|
|
216
286
|
...parsed.manifest,
|
|
217
287
|
retention: {
|
|
218
288
|
expires_at: addDays(now, retentionDays).toISOString(),
|
|
@@ -228,13 +298,21 @@ export function releaseArtifact(
|
|
|
228
298
|
repoRoot: string,
|
|
229
299
|
ref: string,
|
|
230
300
|
input: ArtifactMutationInput = {},
|
|
231
|
-
):
|
|
301
|
+
): ArtifactManifestV2 {
|
|
302
|
+
return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function releaseArtifactUnlocked(
|
|
306
|
+
repoRoot: string,
|
|
307
|
+
ref: string,
|
|
308
|
+
input: ArtifactMutationInput,
|
|
309
|
+
): ArtifactManifestV2 {
|
|
232
310
|
const now = input.now ?? new Date();
|
|
233
311
|
assertValidDate(now, "now");
|
|
234
312
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
235
313
|
const parsed = readManifest(path);
|
|
236
314
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
237
|
-
const manifest:
|
|
315
|
+
const manifest: ArtifactManifestV2 = {
|
|
238
316
|
...parsed.manifest,
|
|
239
317
|
released_at: now.toISOString(),
|
|
240
318
|
released_by: input.actor,
|
|
@@ -243,9 +321,201 @@ export function releaseArtifact(
|
|
|
243
321
|
return manifest;
|
|
244
322
|
}
|
|
245
323
|
|
|
324
|
+
export function holdArtifact(
|
|
325
|
+
repoRoot: string,
|
|
326
|
+
ref: string,
|
|
327
|
+
input: ArtifactHoldInput & { actor: ArtifactActor; now?: Date },
|
|
328
|
+
): ArtifactManifestV2 {
|
|
329
|
+
const hold = makeHold(input, input.actor, input.now ?? new Date());
|
|
330
|
+
return withArtifactLock(repoRoot, () => {
|
|
331
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
332
|
+
const parsed = readManifest(path);
|
|
333
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
334
|
+
const previous = parsed.manifest.holds.find((item) => item.id === hold.id);
|
|
335
|
+
if (previous) {
|
|
336
|
+
if (
|
|
337
|
+
previous.set_by.instance_id !== input.actor.instance_id ||
|
|
338
|
+
previous.reason !== hold.reason
|
|
339
|
+
) {
|
|
340
|
+
throw new Error("hold id already exists with a different owner or reason");
|
|
341
|
+
}
|
|
342
|
+
return parsed.manifest;
|
|
343
|
+
}
|
|
344
|
+
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
345
|
+
atomicWriteManifest(path, manifest);
|
|
346
|
+
return manifest;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function unholdArtifact(
|
|
351
|
+
repoRoot: string,
|
|
352
|
+
ref: string,
|
|
353
|
+
id: string,
|
|
354
|
+
input: { actor: ArtifactActor; now?: Date },
|
|
355
|
+
): ArtifactManifestV2 {
|
|
356
|
+
if (!validHoldId(id)) throw new Error("invalid hold id");
|
|
357
|
+
if (!validActor(input.actor)) throw new Error("a valid hold actor is required");
|
|
358
|
+
return withArtifactLock(repoRoot, () => {
|
|
359
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
360
|
+
const parsed = readManifest(path);
|
|
361
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
362
|
+
const hold = parsed.manifest.holds.find((item) => item.id === id);
|
|
363
|
+
if (!hold) return parsed.manifest;
|
|
364
|
+
if (hold.set_by.instance_id !== input.actor.instance_id) {
|
|
365
|
+
throw new Error("only the hold owner may remove this hold");
|
|
366
|
+
}
|
|
367
|
+
const manifest = {
|
|
368
|
+
...parsed.manifest,
|
|
369
|
+
holds: parsed.manifest.holds.filter((item) => item.id !== id),
|
|
370
|
+
};
|
|
371
|
+
atomicWriteManifest(path, manifest);
|
|
372
|
+
return manifest;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Adopt untracked loose files and legacy directories without changing directory paths. */
|
|
377
|
+
export function adoptUnmanagedArtifactFiles(
|
|
378
|
+
repoRoot: string,
|
|
379
|
+
input: {
|
|
380
|
+
yes?: boolean;
|
|
381
|
+
big?: boolean;
|
|
382
|
+
purpose: string;
|
|
383
|
+
retentionDays: number;
|
|
384
|
+
actor?: ArtifactActor;
|
|
385
|
+
now?: Date;
|
|
386
|
+
},
|
|
387
|
+
): ArtifactAdoptionResult {
|
|
388
|
+
if (input.yes)
|
|
389
|
+
return withArtifactLock(repoRoot, () => adoptUnmanagedArtifactFilesUnlocked(repoRoot, input));
|
|
390
|
+
return adoptUnmanagedArtifactFilesUnlocked(repoRoot, input);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function adoptUnmanagedArtifactFilesUnlocked(
|
|
394
|
+
repoRoot: string,
|
|
395
|
+
input: {
|
|
396
|
+
yes?: boolean;
|
|
397
|
+
big?: boolean;
|
|
398
|
+
purpose: string;
|
|
399
|
+
retentionDays: number;
|
|
400
|
+
actor?: ArtifactActor;
|
|
401
|
+
now?: Date;
|
|
402
|
+
},
|
|
403
|
+
): ArtifactAdoptionResult {
|
|
404
|
+
const now = input.now ?? new Date();
|
|
405
|
+
const candidates: ArtifactAdoptionResult["candidates"] = [];
|
|
406
|
+
for (const row of inventoryArtifacts(repoRoot, { now })) {
|
|
407
|
+
if (row.classification !== "unmanaged") continue;
|
|
408
|
+
try {
|
|
409
|
+
const stat = lstatSync(row.path);
|
|
410
|
+
if (stat.isSymbolicLink() || containsTrackedPath(repoRoot, row.path)) continue;
|
|
411
|
+
if (stat.isFile()) {
|
|
412
|
+
candidates.push({ path: row.path, name: row.name, bytes: stat.size, kind: "file" });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (stat.isDirectory() && !existsSync(join(row.path, ARTIFACT_MANIFEST))) {
|
|
416
|
+
const bytes = safeTreeSize(row.path);
|
|
417
|
+
if (bytes !== null && normalizeSlug(row.name)) {
|
|
418
|
+
candidates.push({ path: row.path, name: row.name, bytes, kind: "directory" });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
// A racing or unreadable entry stays unmanaged.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const candidateBytes = candidates.reduce((sum, row) => sum + row.bytes, 0);
|
|
426
|
+
const fileBytes = candidates.reduce((sum, row) => sum + (row.kind === "file" ? row.bytes : 0), 0);
|
|
427
|
+
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
428
|
+
const requiresBig =
|
|
429
|
+
fileBytes > maxUnitBytes ||
|
|
430
|
+
candidates.some((row) => row.kind === "directory" && row.bytes > maxUnitBytes);
|
|
431
|
+
const preview: ArtifactAdoptionResult = {
|
|
432
|
+
candidates,
|
|
433
|
+
candidate_bytes: candidateBytes,
|
|
434
|
+
requires_big: requiresBig,
|
|
435
|
+
adopted_artifact_id: null,
|
|
436
|
+
adopted_path: null,
|
|
437
|
+
adopted_directories: 0,
|
|
438
|
+
};
|
|
439
|
+
if (!input.yes || candidates.length === 0) return preview;
|
|
440
|
+
if (requiresBig && !input.big) {
|
|
441
|
+
throw new Error("unmanaged adoption exceeds the per-bundle ceiling; repeat with --big");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Revalidate every exact source before creating a destination. A changed,
|
|
445
|
+
// tracked, linked, or non-regular entry aborts the whole adoption.
|
|
446
|
+
for (const candidate of candidates) {
|
|
447
|
+
const stat = lstatSync(candidate.path);
|
|
448
|
+
const bytes = stat.isDirectory() ? safeTreeSize(candidate.path) : stat.size;
|
|
449
|
+
if (
|
|
450
|
+
stat.isSymbolicLink() ||
|
|
451
|
+
bytes !== candidate.bytes ||
|
|
452
|
+
containsTrackedPath(repoRoot, candidate.path)
|
|
453
|
+
) {
|
|
454
|
+
throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
|
|
455
|
+
}
|
|
456
|
+
if (candidate.kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
|
|
457
|
+
throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const files = candidates.filter((candidate) => candidate.kind === "file");
|
|
461
|
+
const created = files.length
|
|
462
|
+
? createArtifactUnlocked(repoRoot, {
|
|
463
|
+
slug: "adopted-unmanaged",
|
|
464
|
+
purpose: input.purpose,
|
|
465
|
+
retentionDays: input.retentionDays,
|
|
466
|
+
actor: input.actor,
|
|
467
|
+
now,
|
|
468
|
+
big: input.big,
|
|
469
|
+
})
|
|
470
|
+
: null;
|
|
471
|
+
for (const candidate of files) renameSync(candidate.path, join(created!.path, candidate.name));
|
|
472
|
+
const retentionDays = positiveDays(input.retentionDays);
|
|
473
|
+
const directories = candidates.filter((candidate) => candidate.kind === "directory");
|
|
474
|
+
for (const candidate of directories) {
|
|
475
|
+
atomicWriteManifest(candidate.path, {
|
|
476
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
477
|
+
holds: [],
|
|
478
|
+
artifact_id: randomUUID(),
|
|
479
|
+
slug: normalizeSlug(candidate.name),
|
|
480
|
+
purpose: `${input.purpose}: ${candidate.name}`,
|
|
481
|
+
created_at: now.toISOString(),
|
|
482
|
+
created_by: input.actor,
|
|
483
|
+
retention: { expires_at: addDays(now, retentionDays).toISOString() },
|
|
484
|
+
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
...preview,
|
|
489
|
+
adopted_artifact_id: created?.manifest.artifact_id ?? null,
|
|
490
|
+
adopted_path: created?.path ?? null,
|
|
491
|
+
adopted_directories: directories.length,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
246
495
|
export function cleanArtifacts(
|
|
247
496
|
repoRoot: string,
|
|
248
497
|
opts: { yes?: boolean; now?: Date; freshnessSeconds?: number } = {},
|
|
498
|
+
): ArtifactInventoryEntry[] {
|
|
499
|
+
if (!opts.yes) return inventoryArtifacts(repoRoot, opts);
|
|
500
|
+
try {
|
|
501
|
+
return withArtifactLock(repoRoot, () => cleanArtifactsUnlocked(repoRoot, opts));
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return inventoryArtifacts(repoRoot, opts).map((row) =>
|
|
504
|
+
row.action === "would-delete"
|
|
505
|
+
? {
|
|
506
|
+
...row,
|
|
507
|
+
classification: "unknown",
|
|
508
|
+
action: "keep",
|
|
509
|
+
reason: errorMessage("cleanup refused", error),
|
|
510
|
+
}
|
|
511
|
+
: row,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function cleanArtifactsUnlocked(
|
|
517
|
+
repoRoot: string,
|
|
518
|
+
opts: { yes?: boolean; now?: Date; freshnessSeconds?: number },
|
|
249
519
|
): ArtifactInventoryEntry[] {
|
|
250
520
|
const now = opts.now ?? new Date();
|
|
251
521
|
const freshnessSeconds = opts.freshnessSeconds ?? 600;
|
|
@@ -253,11 +523,24 @@ export function cleanArtifacts(
|
|
|
253
523
|
if (!opts.yes) return rows;
|
|
254
524
|
|
|
255
525
|
return rows.map((entry) => {
|
|
256
|
-
if (entry.
|
|
526
|
+
if (entry.action !== "would-delete") return entry;
|
|
257
527
|
// Reclassify immediately before removal. A renewal, release-state change,
|
|
258
528
|
// heartbeat, symlink swap, or tracked file added since inventory must win.
|
|
259
|
-
const current =
|
|
260
|
-
|
|
529
|
+
const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
|
|
530
|
+
(row) => row.path === entry.path,
|
|
531
|
+
);
|
|
532
|
+
if (!current) {
|
|
533
|
+
return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
|
|
534
|
+
}
|
|
535
|
+
if (
|
|
536
|
+
current.action !== "would-delete" ||
|
|
537
|
+
current.artifact_id !== entry.artifact_id ||
|
|
538
|
+
current.bytes !== entry.bytes ||
|
|
539
|
+
current.last_modified_at !== entry.last_modified_at ||
|
|
540
|
+
current.expires_at !== entry.expires_at
|
|
541
|
+
) {
|
|
542
|
+
return current;
|
|
543
|
+
}
|
|
261
544
|
try {
|
|
262
545
|
const top = lstatSync(current.path);
|
|
263
546
|
if (!top.isDirectory() || top.isSymbolicLink()) {
|
|
@@ -404,8 +687,17 @@ function classifyArtifactPath(
|
|
|
404
687
|
last_modified_at: new Date(effectiveLastModifiedMs).toISOString(),
|
|
405
688
|
expires_at: effectiveExpiresAt,
|
|
406
689
|
owner_instance_id: manifest.created_by?.instance_id ?? null,
|
|
690
|
+
oversize_acknowledged: manifest.oversize_acknowledged === true,
|
|
407
691
|
});
|
|
408
692
|
|
|
693
|
+
if (manifest.holds.length > 0) {
|
|
694
|
+
return {
|
|
695
|
+
...base,
|
|
696
|
+
classification: "managed-held",
|
|
697
|
+
reason: `held: ${manifest.holds.map((hold) => hold.id).join(", ")}`,
|
|
698
|
+
action: "keep",
|
|
699
|
+
};
|
|
700
|
+
}
|
|
409
701
|
if (base.bytes === null || lastModifiedMs === null) {
|
|
410
702
|
return {
|
|
411
703
|
...base,
|
|
@@ -450,6 +742,59 @@ function classifyArtifactPath(
|
|
|
450
742
|
};
|
|
451
743
|
}
|
|
452
744
|
|
|
745
|
+
function applyArtifactBudgets(
|
|
746
|
+
repoRoot: string,
|
|
747
|
+
inputRows: ArtifactInventoryEntry[],
|
|
748
|
+
): ArtifactInventoryEntry[] {
|
|
749
|
+
const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
|
|
750
|
+
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
|
+
}
|
|
765
|
+
|
|
766
|
+
const managedBytes = rows.reduce(
|
|
767
|
+
(sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
|
|
768
|
+
0,
|
|
769
|
+
);
|
|
770
|
+
let retainedBytes =
|
|
771
|
+
managedBytes -
|
|
772
|
+
rows.reduce(
|
|
773
|
+
(sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0),
|
|
774
|
+
0,
|
|
775
|
+
);
|
|
776
|
+
if (retainedBytes <= maxBytes) return rows;
|
|
777
|
+
|
|
778
|
+
const candidates = rows
|
|
779
|
+
.filter(
|
|
780
|
+
(row) =>
|
|
781
|
+
row.classification === "managed-current" && row.action === "keep" && row.bytes !== null,
|
|
782
|
+
)
|
|
783
|
+
.sort((left, right) =>
|
|
784
|
+
`${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(
|
|
785
|
+
`${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`,
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
for (const row of candidates) {
|
|
789
|
+
if (retainedBytes <= maxBytes) break;
|
|
790
|
+
row.classification = "managed-over-budget";
|
|
791
|
+
row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring inactive bundles are removed first`;
|
|
792
|
+
row.action = "would-delete";
|
|
793
|
+
retainedBytes -= row.bytes ?? 0;
|
|
794
|
+
}
|
|
795
|
+
return rows;
|
|
796
|
+
}
|
|
797
|
+
|
|
453
798
|
function readManifest(path: string): ParsedManifest | ManifestError {
|
|
454
799
|
const manifestPath = join(path, ARTIFACT_MANIFEST);
|
|
455
800
|
if (!existsSync(manifestPath)) {
|
|
@@ -457,17 +802,40 @@ function readManifest(path: string): ParsedManifest | ManifestError {
|
|
|
457
802
|
}
|
|
458
803
|
let value: unknown;
|
|
459
804
|
try {
|
|
805
|
+
const unit = lstatSync(path);
|
|
806
|
+
const stat = lstatSync(manifestPath);
|
|
807
|
+
if (
|
|
808
|
+
!unit.isDirectory() ||
|
|
809
|
+
unit.isSymbolicLink() ||
|
|
810
|
+
!stat.isFile() ||
|
|
811
|
+
stat.isSymbolicLink() ||
|
|
812
|
+
stat.size > 1024 * 1024
|
|
813
|
+
) {
|
|
814
|
+
return { ok: false, reason: "manifest must be a bounded regular file in a direct directory" };
|
|
815
|
+
}
|
|
460
816
|
value = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
461
817
|
} catch (error) {
|
|
462
818
|
return { ok: false, reason: errorMessage("manifest is unreadable", error) };
|
|
463
819
|
}
|
|
820
|
+
return parseArtifactManifest(value);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Validate only the current schema. Legacy conversion belongs to migrateArtifacts. */
|
|
824
|
+
export function parseArtifactManifest(value: unknown): ParsedManifest | ManifestError {
|
|
464
825
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
465
826
|
return { ok: false, reason: "manifest must be a JSON object" };
|
|
466
827
|
}
|
|
467
|
-
const m = value as Partial<
|
|
828
|
+
const m = value as Partial<ArtifactManifestV2>;
|
|
468
829
|
if (m.schema_version !== ARTIFACT_SCHEMA_VERSION) {
|
|
469
830
|
return { ok: false, reason: `unsupported schema_version ${String(m.schema_version)}` };
|
|
470
831
|
}
|
|
832
|
+
if (
|
|
833
|
+
!Array.isArray(m.holds) ||
|
|
834
|
+
m.holds.some((hold) => !validHold(hold)) ||
|
|
835
|
+
new Set(m.holds.map((hold) => hold.id)).size !== m.holds.length
|
|
836
|
+
) {
|
|
837
|
+
return { ok: false, reason: "invalid holds" };
|
|
838
|
+
}
|
|
471
839
|
if (!isSafeId(m.artifact_id)) return { ok: false, reason: "invalid artifact_id" };
|
|
472
840
|
if (typeof m.slug !== "string" || !m.slug || normalizeSlug(m.slug) !== m.slug) {
|
|
473
841
|
return { ok: false, reason: "invalid slug" };
|
|
@@ -495,17 +863,172 @@ function readManifest(path: string): ParsedManifest | ManifestError {
|
|
|
495
863
|
if (m.released_by !== undefined && !validActor(m.released_by)) {
|
|
496
864
|
return { ok: false, reason: "invalid released_by" };
|
|
497
865
|
}
|
|
498
|
-
|
|
866
|
+
if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
|
|
867
|
+
return { ok: false, reason: "invalid oversize_acknowledged" };
|
|
868
|
+
}
|
|
869
|
+
return { ok: true, manifest: m as ArtifactManifestV2 };
|
|
499
870
|
}
|
|
500
871
|
|
|
501
|
-
function atomicWriteManifest(path: string, manifest:
|
|
872
|
+
function atomicWriteManifest(path: string, manifest: ArtifactManifestV2): void {
|
|
502
873
|
const target = join(path, ARTIFACT_MANIFEST);
|
|
503
874
|
const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
875
|
+
try {
|
|
876
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
877
|
+
encoding: "utf8",
|
|
878
|
+
mode: 0o600,
|
|
879
|
+
flag: "wx",
|
|
880
|
+
});
|
|
881
|
+
renameSync(tmp, target);
|
|
882
|
+
} finally {
|
|
883
|
+
rmSync(tmp, { force: true });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export interface ArtifactMigrationEntry {
|
|
888
|
+
path: string;
|
|
889
|
+
action: "keep" | "would-migrate" | "migrated";
|
|
890
|
+
reason: string;
|
|
891
|
+
preimage_path?: string;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
|
|
895
|
+
export function migrateArtifacts(
|
|
896
|
+
repoRoot: string,
|
|
897
|
+
opts: { yes?: boolean } = {},
|
|
898
|
+
): ArtifactMigrationEntry[] {
|
|
899
|
+
const migrate = (): ArtifactMigrationEntry[] => {
|
|
900
|
+
const root = artifactsRoot(repoRoot);
|
|
901
|
+
if (!existsSync(root)) return [];
|
|
902
|
+
return readdirSync(root)
|
|
903
|
+
.sort()
|
|
904
|
+
.map((name): ArtifactMigrationEntry => {
|
|
905
|
+
const path = join(root, name);
|
|
906
|
+
try {
|
|
907
|
+
const stat = lstatSync(path);
|
|
908
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
909
|
+
throw new Error("not a direct directory");
|
|
910
|
+
if (containsTrackedPath(repoRoot, path))
|
|
911
|
+
throw new Error("artifact contains tracked files");
|
|
912
|
+
const target = join(path, ARTIFACT_MANIFEST);
|
|
913
|
+
const manifestStat = lstatSync(target);
|
|
914
|
+
if (
|
|
915
|
+
!manifestStat.isFile() ||
|
|
916
|
+
manifestStat.isSymbolicLink() ||
|
|
917
|
+
manifestStat.size > 1024 * 1024
|
|
918
|
+
) {
|
|
919
|
+
throw new Error("manifest is not a bounded regular file");
|
|
920
|
+
}
|
|
921
|
+
const preimage = readFileSync(target, "utf8");
|
|
922
|
+
const legacy = JSON.parse(preimage);
|
|
923
|
+
if (legacy?.schema_version !== 1) {
|
|
924
|
+
return {
|
|
925
|
+
path,
|
|
926
|
+
action: "keep",
|
|
927
|
+
reason: `schema_version ${String(legacy?.schema_version)} is not a migration source`,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
if (Object.hasOwn(legacy, "holds"))
|
|
931
|
+
throw new Error("v1 manifest unexpectedly contains holds");
|
|
932
|
+
const parsed = parseArtifactManifest({
|
|
933
|
+
...legacy,
|
|
934
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
935
|
+
holds: [],
|
|
936
|
+
});
|
|
937
|
+
if (!parsed.ok) throw new Error(parsed.reason);
|
|
938
|
+
const digest = createHash("sha256").update(preimage).digest("hex");
|
|
939
|
+
const preimagePath = join(
|
|
940
|
+
resolve(repoRoot),
|
|
941
|
+
".harnery/artifact-migrations",
|
|
942
|
+
`${digest}.v1.json`,
|
|
943
|
+
);
|
|
944
|
+
if (!opts.yes)
|
|
945
|
+
return {
|
|
946
|
+
path,
|
|
947
|
+
action: "would-migrate",
|
|
948
|
+
reason: "valid v1 manifest",
|
|
949
|
+
preimage_path: preimagePath,
|
|
950
|
+
};
|
|
951
|
+
mkdirSync(dirname(preimagePath), { recursive: true });
|
|
952
|
+
try {
|
|
953
|
+
writeFileSync(preimagePath, preimage, { flag: "wx", mode: 0o600 });
|
|
954
|
+
} catch (error) {
|
|
955
|
+
if (
|
|
956
|
+
(error as NodeJS.ErrnoException).code !== "EEXIST" ||
|
|
957
|
+
lstatSync(preimagePath).isSymbolicLink() ||
|
|
958
|
+
readFileSync(preimagePath, "utf8") !== preimage
|
|
959
|
+
)
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
if (readFileSync(target, "utf8") !== preimage)
|
|
963
|
+
throw new Error("manifest changed before migration");
|
|
964
|
+
atomicWriteManifest(path, parsed.manifest);
|
|
965
|
+
return {
|
|
966
|
+
path,
|
|
967
|
+
action: "migrated",
|
|
968
|
+
reason: "v1 preimage preserved; identity and retention unchanged",
|
|
969
|
+
preimage_path: preimagePath,
|
|
970
|
+
};
|
|
971
|
+
} catch (error) {
|
|
972
|
+
return { path, action: "keep", reason: errorMessage("migration refused", error) };
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
};
|
|
976
|
+
return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** A persistent directory lock fails closed on contention or a crashed owner. */
|
|
980
|
+
function withArtifactLock<T>(repoRoot: string, action: () => T): T {
|
|
981
|
+
const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
|
|
982
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
983
|
+
try {
|
|
984
|
+
mkdirSync(lock);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
errorMessage(
|
|
988
|
+
"artifact store mutation lock unavailable; retry after its owner finishes",
|
|
989
|
+
error,
|
|
990
|
+
),
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
return action();
|
|
995
|
+
} finally {
|
|
996
|
+
rmdirSync(lock);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function validHoldId(value: unknown): value is string {
|
|
1001
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function validHold(value: unknown): value is ArtifactHold {
|
|
1005
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1006
|
+
const hold = value as Partial<ArtifactHold>;
|
|
1007
|
+
return (
|
|
1008
|
+
validHoldId(hold.id) &&
|
|
1009
|
+
typeof hold.reason === "string" &&
|
|
1010
|
+
!!hold.reason.trim() &&
|
|
1011
|
+
validActor(hold.set_by) &&
|
|
1012
|
+
validIso(hold.set_at)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function makeHold(
|
|
1017
|
+
input: ArtifactHoldInput,
|
|
1018
|
+
actor: ArtifactActor | undefined,
|
|
1019
|
+
now: Date,
|
|
1020
|
+
): ArtifactHold {
|
|
1021
|
+
assertValidDate(now, "now");
|
|
1022
|
+
if (!validActor(actor)) throw new Error("a valid hold actor is required");
|
|
1023
|
+
if (!validHoldId(input.id)) throw new Error("invalid hold id");
|
|
1024
|
+
if (typeof input.reason !== "string" || !input.reason.trim())
|
|
1025
|
+
throw new Error("hold reason must not be empty");
|
|
1026
|
+
return {
|
|
1027
|
+
id: input.id,
|
|
1028
|
+
reason: input.reason.trim(),
|
|
1029
|
+
set_by: { ...actor },
|
|
1030
|
+
set_at: now.toISOString(),
|
|
1031
|
+
};
|
|
509
1032
|
}
|
|
510
1033
|
|
|
511
1034
|
function ownerLiveness(
|
|
@@ -604,6 +1127,7 @@ function rowFor(
|
|
|
604
1127
|
last_modified_at: null,
|
|
605
1128
|
expires_at: null,
|
|
606
1129
|
owner_instance_id: null,
|
|
1130
|
+
oversize_acknowledged: false,
|
|
607
1131
|
};
|
|
608
1132
|
}
|
|
609
1133
|
|
|
@@ -629,6 +1153,17 @@ function addDays(date: Date, days: number): Date {
|
|
|
629
1153
|
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
630
1154
|
}
|
|
631
1155
|
|
|
1156
|
+
function positiveMinutes(value: number): number {
|
|
1157
|
+
if (!Number.isInteger(value) || value <= 0 || value > 3650 * 24 * 60) {
|
|
1158
|
+
throw new Error("retention minutes must be between 1 and 5256000");
|
|
1159
|
+
}
|
|
1160
|
+
return value;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function addMinutes(date: Date, minutes: number): Date {
|
|
1164
|
+
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
632
1167
|
function isSafeId(value: unknown): value is string {
|
|
633
1168
|
return (
|
|
634
1169
|
typeof value === "string" &&
|