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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name-addressed durable mailbox.
|
|
3
|
+
*
|
|
4
|
+
* Peer messaging used to require a live recipient: the sender resolved a name
|
|
5
|
+
* to a running instance and wrote into that instance's journal. Two failures
|
|
6
|
+
* followed from that. A message to a name whose session had ended was refused
|
|
7
|
+
* outright, so the sender spent a long sequence of discovery calls looking for
|
|
8
|
+
* somewhere else to put it. A message to a live peer was accepted but never
|
|
9
|
+
* shown to them, because nothing surfaces another session's journal.
|
|
10
|
+
*
|
|
11
|
+
* This module addresses the durable persona name instead of the instance, so a
|
|
12
|
+
* message can be queued before its recipient exists. Delivery happens on the
|
|
13
|
+
* recipient's side: their next SessionStart or prompt drains the queue, shows
|
|
14
|
+
* the messages, and records them in their journal.
|
|
15
|
+
*
|
|
16
|
+
* Storage: `.harnery/mailbox/<slug>.jsonl`, one JSON record per line, appended
|
|
17
|
+
* by senders and removed by an atomic claim on the recipient's side. Delivered
|
|
18
|
+
* records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit.
|
|
19
|
+
*
|
|
20
|
+
* Related but distinct: `core/inbox` is an instance-keyed transport with a
|
|
21
|
+
* storage-framework service behind it. This mailbox is name-keyed, file-only,
|
|
22
|
+
* and follows the same shape as council manifests and journals.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import {
|
|
27
|
+
appendFileSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
renameSync,
|
|
33
|
+
statSync,
|
|
34
|
+
unlinkSync,
|
|
35
|
+
writeFileSync,
|
|
36
|
+
} from "node:fs";
|
|
37
|
+
import { join, resolve } from "node:path";
|
|
38
|
+
|
|
39
|
+
import { lookupByName } from "../../lib/identities/index.ts";
|
|
40
|
+
import { resolveBinName } from "../config.ts";
|
|
41
|
+
import { readLiveCoordinationRows } from "./state/live-coordination-view.ts";
|
|
42
|
+
import { COORD_NAMES } from "./state/names.ts";
|
|
43
|
+
|
|
44
|
+
export const MAILBOX_MESSAGE_SCHEMA = "harnery.mailbox-message/v1" as const;
|
|
45
|
+
|
|
46
|
+
/** Per-message body ceiling. Larger payloads belong in a managed artifact, with
|
|
47
|
+
* the path sent as the message. */
|
|
48
|
+
export const MAX_BODY_BYTES = 4000;
|
|
49
|
+
/** Pending messages held for one name before senders are refused. */
|
|
50
|
+
export const MAX_PENDING = 25;
|
|
51
|
+
/** Delivered-archive ceiling; the file is truncated from the head past this. */
|
|
52
|
+
const MAX_DELIVERED_BYTES = 256 * 1024;
|
|
53
|
+
|
|
54
|
+
export interface MailboxMessageV1 {
|
|
55
|
+
schema: typeof MAILBOX_MESSAGE_SCHEMA;
|
|
56
|
+
message_id: string;
|
|
57
|
+
/** Bare display name the sender addressed (registry casing when known). */
|
|
58
|
+
to_name: string;
|
|
59
|
+
from_name: string;
|
|
60
|
+
from_instance_id: string;
|
|
61
|
+
created_at: string;
|
|
62
|
+
body: string;
|
|
63
|
+
/** True when the sender already appended this to a live recipient's journal,
|
|
64
|
+
* so the drain surfaces it without writing a duplicate entry. */
|
|
65
|
+
journaled: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MailboxCapacityReason = "body_limit" | "pending_limit";
|
|
69
|
+
|
|
70
|
+
export class MailboxCapacityError extends Error {
|
|
71
|
+
constructor(
|
|
72
|
+
readonly reason_code: MailboxCapacityReason,
|
|
73
|
+
readonly current: number,
|
|
74
|
+
readonly limit: number,
|
|
75
|
+
) {
|
|
76
|
+
super(
|
|
77
|
+
`mailbox capacity ${reason_code}: ${current}/${limit}; write the payload to a managed artifact and send its path instead`,
|
|
78
|
+
);
|
|
79
|
+
this.name = "MailboxCapacityError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Strip an `agent-` prefix and normalize to the registry's bare form. */
|
|
84
|
+
export function bareAgentName(raw: string): string {
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
return trimmed.toLowerCase().startsWith("agent-") ? trimmed.slice("agent-".length) : trimmed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Filesystem-safe slug for a name. Rejects anything that could escape the
|
|
90
|
+
* mailbox directory rather than sanitizing it into a different agent's file. */
|
|
91
|
+
function slugFor(name: string): string {
|
|
92
|
+
const bare = bareAgentName(name).toLowerCase();
|
|
93
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(bare)) {
|
|
94
|
+
throw new Error(`invalid agent name for mailbox addressing: ${JSON.stringify(name)}`);
|
|
95
|
+
}
|
|
96
|
+
return bare;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mailboxDir(coordRoot: string): string {
|
|
100
|
+
return resolve(coordRoot, ".harnery", "mailbox");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function mailboxPath(coordRoot: string, name: string): string {
|
|
104
|
+
return join(mailboxDir(coordRoot), `${slugFor(name)}.jsonl`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deliveredPath(coordRoot: string, name: string): string {
|
|
108
|
+
return join(mailboxDir(coordRoot), "delivered", `${slugFor(name)}.jsonl`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseLines(content: string): MailboxMessageV1[] {
|
|
112
|
+
const out: MailboxMessageV1[] = [];
|
|
113
|
+
for (const line of content.split("\n")) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed) continue;
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(trimmed) as MailboxMessageV1;
|
|
118
|
+
if (parsed?.schema === MAILBOX_MESSAGE_SCHEMA) out.push(parsed);
|
|
119
|
+
} catch {
|
|
120
|
+
/* skip a torn line rather than losing the whole mailbox */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Every name a message may be addressed to: live sessions, the durable
|
|
127
|
+
* identity registry, and the built-in name pool. A name outside all three is a
|
|
128
|
+
* typo, and queueing it would silently swallow the message. */
|
|
129
|
+
export function isAddressableName(coordRoot: string, name: string): boolean {
|
|
130
|
+
const bare = bareAgentName(name).toLowerCase();
|
|
131
|
+
if (!bare) return false;
|
|
132
|
+
if (COORD_NAMES.some((n) => n.toLowerCase() === bare)) return true;
|
|
133
|
+
if (lookupByName(bare, coordRoot)) return true;
|
|
134
|
+
try {
|
|
135
|
+
return readLiveCoordinationRows(coordRoot).some((r) => (r.name ?? "").toLowerCase() === bare);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Names close enough to a miss to be worth suggesting (prefix or one edit). */
|
|
142
|
+
export function suggestNames(coordRoot: string, name: string, limit = 5): string[] {
|
|
143
|
+
const bare = bareAgentName(name).toLowerCase();
|
|
144
|
+
const known = new Set<string>(COORD_NAMES);
|
|
145
|
+
try {
|
|
146
|
+
for (const row of readLiveCoordinationRows(coordRoot)) if (row.name) known.add(row.name);
|
|
147
|
+
} catch {
|
|
148
|
+
/* live rows are advisory here */
|
|
149
|
+
}
|
|
150
|
+
const scored: Array<{ name: string; distance: number }> = [];
|
|
151
|
+
for (const candidate of known) {
|
|
152
|
+
const lower = candidate.toLowerCase();
|
|
153
|
+
if (lower.startsWith(bare.slice(0, 2)) || editDistance(lower, bare) <= 2) {
|
|
154
|
+
scored.push({ name: candidate, distance: editDistance(lower, bare) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
scored.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
|
|
158
|
+
return scored.slice(0, limit).map((s) => s.name);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function editDistance(a: string, b: string): number {
|
|
162
|
+
const rows = a.length + 1;
|
|
163
|
+
const cols = b.length + 1;
|
|
164
|
+
let prev = Array.from({ length: cols }, (_, i) => i);
|
|
165
|
+
for (let i = 1; i < rows; i++) {
|
|
166
|
+
const curr = [i, ...Array.from({ length: cols - 1 }, () => 0)];
|
|
167
|
+
for (let j = 1; j < cols; j++) {
|
|
168
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
169
|
+
curr[j] = Math.min(curr[j - 1]! + 1, prev[j]! + 1, prev[j - 1]! + cost);
|
|
170
|
+
}
|
|
171
|
+
prev = curr;
|
|
172
|
+
}
|
|
173
|
+
return prev[cols - 1]!;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface QueueInput {
|
|
177
|
+
coordRoot: string;
|
|
178
|
+
toName: string;
|
|
179
|
+
fromName: string;
|
|
180
|
+
fromInstanceId: string;
|
|
181
|
+
body: string;
|
|
182
|
+
journaled?: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface QueueResult {
|
|
186
|
+
message: MailboxMessageV1;
|
|
187
|
+
path: string;
|
|
188
|
+
pending: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Append one message to a name's mailbox. Throws `MailboxCapacityError` when
|
|
193
|
+
* the body or the pending queue is over its ceiling; the caller turns that into
|
|
194
|
+
* an actionable CLI error.
|
|
195
|
+
*/
|
|
196
|
+
export function queueMailboxMessage(input: QueueInput): QueueResult {
|
|
197
|
+
const body = input.body.trim();
|
|
198
|
+
const bodyBytes = Buffer.byteLength(body, "utf8");
|
|
199
|
+
if (bodyBytes > MAX_BODY_BYTES) {
|
|
200
|
+
throw new MailboxCapacityError("body_limit", bodyBytes, MAX_BODY_BYTES);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const path = mailboxPath(input.coordRoot, input.toName);
|
|
204
|
+
mkdirSync(mailboxDir(input.coordRoot), { recursive: true });
|
|
205
|
+
const existing = existsSync(path) ? parseLines(readFileSync(path, "utf8")) : [];
|
|
206
|
+
if (existing.length >= MAX_PENDING) {
|
|
207
|
+
throw new MailboxCapacityError("pending_limit", existing.length, MAX_PENDING);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Preserve registry casing when the persona is known, so the recipient sees
|
|
211
|
+
// their own name spelled the way it is everywhere else.
|
|
212
|
+
const identity = lookupByName(input.toName, input.coordRoot);
|
|
213
|
+
const message: MailboxMessageV1 = {
|
|
214
|
+
schema: MAILBOX_MESSAGE_SCHEMA,
|
|
215
|
+
message_id: randomUUID(),
|
|
216
|
+
to_name: identity?.name ?? bareAgentName(input.toName),
|
|
217
|
+
from_name: input.fromName,
|
|
218
|
+
from_instance_id: input.fromInstanceId,
|
|
219
|
+
created_at: new Date().toISOString(),
|
|
220
|
+
body,
|
|
221
|
+
journaled: input.journaled ?? false,
|
|
222
|
+
};
|
|
223
|
+
appendFileSync(path, `${JSON.stringify(message)}\n`, "utf8");
|
|
224
|
+
return { message, path, pending: existing.length + 1 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Pending messages for a name, without claiming them. */
|
|
228
|
+
export function peekMailbox(coordRoot: string, name: string): MailboxMessageV1[] {
|
|
229
|
+
let path: string;
|
|
230
|
+
try {
|
|
231
|
+
path = mailboxPath(coordRoot, name);
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
if (!existsSync(path)) return [];
|
|
236
|
+
try {
|
|
237
|
+
return parseLines(readFileSync(path, "utf8"));
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Claim and return every pending message for a name.
|
|
245
|
+
*
|
|
246
|
+
* The claim is an atomic rename, so two sessions sharing a name cannot deliver
|
|
247
|
+
* the same message twice: whichever rename wins owns the batch. Claimed records
|
|
248
|
+
* are appended to the delivered archive before the claim file is removed, so a
|
|
249
|
+
* crash between the two leaves an auditable copy rather than a silent loss.
|
|
250
|
+
*/
|
|
251
|
+
export function drainMailbox(coordRoot: string, name: string): MailboxMessageV1[] {
|
|
252
|
+
let path: string;
|
|
253
|
+
try {
|
|
254
|
+
path = mailboxPath(coordRoot, name);
|
|
255
|
+
} catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
// Heal first: a claim file left by a crashed reader may hold the only copy of
|
|
259
|
+
// a message for this very name, and sweeping after the existence check would
|
|
260
|
+
// skip it whenever the live queue is empty.
|
|
261
|
+
sweepStaleClaims(coordRoot);
|
|
262
|
+
if (!existsSync(path)) return [];
|
|
263
|
+
|
|
264
|
+
const claim = `${path}.claim-${process.pid}-${Date.now()}`;
|
|
265
|
+
try {
|
|
266
|
+
renameSync(path, claim);
|
|
267
|
+
} catch {
|
|
268
|
+
// Another reader claimed it first, or the file vanished. Either way there
|
|
269
|
+
// is nothing for this caller to deliver.
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let messages: MailboxMessageV1[] = [];
|
|
274
|
+
try {
|
|
275
|
+
messages = parseLines(readFileSync(claim, "utf8"));
|
|
276
|
+
} catch {
|
|
277
|
+
messages = [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (messages.length > 0) {
|
|
281
|
+
try {
|
|
282
|
+
const archive = deliveredPath(coordRoot, name);
|
|
283
|
+
mkdirSync(join(mailboxDir(coordRoot), "delivered"), { recursive: true });
|
|
284
|
+
const deliveredAt = new Date().toISOString();
|
|
285
|
+
appendFileSync(
|
|
286
|
+
archive,
|
|
287
|
+
`${messages.map((m) => JSON.stringify({ ...m, delivered_at: deliveredAt })).join("\n")}\n`,
|
|
288
|
+
"utf8",
|
|
289
|
+
);
|
|
290
|
+
trimArchive(archive);
|
|
291
|
+
} catch {
|
|
292
|
+
/* audit copy is best-effort; never block delivery on it */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
unlinkSync(claim);
|
|
298
|
+
} catch {
|
|
299
|
+
/* a leftover claim is re-queued by the next drain's sweep */
|
|
300
|
+
}
|
|
301
|
+
return messages;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Re-queue any claim file left behind by a crashed reader (older than 5 min),
|
|
305
|
+
* so a killed session cannot strand a message. Runs at the head of every drain. */
|
|
306
|
+
function sweepStaleClaims(coordRoot: string): void {
|
|
307
|
+
const dir = mailboxDir(coordRoot);
|
|
308
|
+
if (!existsSync(dir)) return;
|
|
309
|
+
const cutoff = Date.now() - 5 * 60_000;
|
|
310
|
+
try {
|
|
311
|
+
for (const file of readdirSync(dir)) {
|
|
312
|
+
const marker = file.indexOf(".jsonl.claim-");
|
|
313
|
+
if (marker === -1) continue;
|
|
314
|
+
const full = join(dir, file);
|
|
315
|
+
let mtime: number;
|
|
316
|
+
try {
|
|
317
|
+
mtime = statSync(full).mtimeMs;
|
|
318
|
+
} catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (mtime > cutoff) continue;
|
|
322
|
+
const owner = join(dir, `${file.slice(0, marker)}.jsonl`);
|
|
323
|
+
try {
|
|
324
|
+
const content = readFileSync(full, "utf8");
|
|
325
|
+
if (content.trim())
|
|
326
|
+
appendFileSync(owner, content.endsWith("\n") ? content : `${content}\n`);
|
|
327
|
+
unlinkSync(full);
|
|
328
|
+
} catch {
|
|
329
|
+
/* skip; next sweep retries */
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
/* directory read is best-effort */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function trimArchive(path: string): void {
|
|
338
|
+
try {
|
|
339
|
+
if (statSync(path).size <= MAX_DELIVERED_BYTES) return;
|
|
340
|
+
const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
341
|
+
const keep = lines.slice(Math.floor(lines.length / 2));
|
|
342
|
+
writeFileSync(path, `${keep.join("\n")}\n`, "utf8");
|
|
343
|
+
} catch {
|
|
344
|
+
/* trimming is housekeeping, never load-bearing */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Render a drained batch as the context block a recipient reads. */
|
|
349
|
+
export function formatMailboxDelivery(
|
|
350
|
+
coordRoot: string,
|
|
351
|
+
messages: readonly MailboxMessageV1[],
|
|
352
|
+
): string {
|
|
353
|
+
if (messages.length === 0) return "";
|
|
354
|
+
const bin = resolveBinName(coordRoot);
|
|
355
|
+
const header =
|
|
356
|
+
messages.length === 1
|
|
357
|
+
? `Message from a peer agent (delivered now, recorded in your journal):`
|
|
358
|
+
: `${messages.length} messages from peer agents (delivered now, recorded in your journal):`;
|
|
359
|
+
const body = messages
|
|
360
|
+
.map((m) => {
|
|
361
|
+
const when = m.created_at.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
362
|
+
return ` from agent-${m.from_name} (${when}):\n${indent(m.body)}`;
|
|
363
|
+
})
|
|
364
|
+
.join("\n\n");
|
|
365
|
+
return `${header}\n\n${body}\n\nReply with \`${bin} agents ping ${messages[0]!.from_name} "<your reply>"\`; it reaches them whether or not their session is still running.`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function indent(text: string): string {
|
|
369
|
+
return text
|
|
370
|
+
.split("\n")
|
|
371
|
+
.map((line) => ` ${line}`)
|
|
372
|
+
.join("\n");
|
|
373
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session page-QA signal: the pointer that lets `agents status` report a
|
|
3
|
+
* verdict with the runner's own clock instead of leaving an operator to read
|
|
4
|
+
* session age as if it were QA time.
|
|
5
|
+
*
|
|
6
|
+
* The problem this closes: a status box showing `session 58m` invites the
|
|
7
|
+
* reading "page QA took 58 minutes". Session age measures the agent, not the
|
|
8
|
+
* run. So the runner's wall time is recorded here and rendered beside the
|
|
9
|
+
* verdict, and admission-queue wait is carried as a separate number because
|
|
10
|
+
* `wall_time_ms.total` is runner stages only and never includes the queue
|
|
11
|
+
* (see QaRunResult.wall_time_ms in the toolkit contracts).
|
|
12
|
+
*
|
|
13
|
+
* Layering: the qa-run matrix runner is toolkit tier (`src/lib/browser/**`)
|
|
14
|
+
* and must not import `src/core`, so the runner cannot write this pointer
|
|
15
|
+
* itself. The command layer owns the write and calls `recordQaSignal()` after
|
|
16
|
+
* a run finishes. Core importing toolkit *types* is the permitted direction.
|
|
17
|
+
*
|
|
18
|
+
* Storage: `<coordRoot>/.harnery/qa/<instance-id>.json`, one file per session
|
|
19
|
+
* generation, atomic temp+rename so a concurrent status read never sees a
|
|
20
|
+
* torn write. Last run wins; each run directory remains the authoritative
|
|
21
|
+
* record of the run itself.
|
|
22
|
+
*
|
|
23
|
+
* Every function here is best-effort by contract. A missing, unreadable,
|
|
24
|
+
* malformed, or partial pointer yields null rather than throwing: the status
|
|
25
|
+
* box must render even when QA state is broken.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { resolve } from "node:path";
|
|
30
|
+
import type {
|
|
31
|
+
QaRunEvidenceSource,
|
|
32
|
+
QaRunResult,
|
|
33
|
+
QaRunVerdict,
|
|
34
|
+
} from "../../lib/browser/qa-run-contracts.ts";
|
|
35
|
+
import { resolveCoordRoot, resolveOwner } from "./coord-client.ts";
|
|
36
|
+
|
|
37
|
+
export const QA_SIGNAL_SCHEMA_VERSION = 1 as const;
|
|
38
|
+
|
|
39
|
+
/** Pointers older than this render as `stale (<age>)` and nothing else: an
|
|
40
|
+
* age-of-day-old verdict says nothing about the page as it stands now, and
|
|
41
|
+
* showing its timings beside a current session invites the same conflation
|
|
42
|
+
* this signal exists to prevent. */
|
|
43
|
+
export const QA_SIGNAL_STALE_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
/** Timings split the way the result contract splits them: `total` is runner
|
|
46
|
+
* stages only, `queue` is admission wait before any browser work and is never
|
|
47
|
+
* part of `total`. */
|
|
48
|
+
export interface QaSignalWallTime {
|
|
49
|
+
total: number;
|
|
50
|
+
queue?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QaSignalPointer {
|
|
54
|
+
schema_version: typeof QA_SIGNAL_SCHEMA_VERSION;
|
|
55
|
+
run_id: string;
|
|
56
|
+
verdict: QaRunVerdict;
|
|
57
|
+
evidence_source: QaRunEvidenceSource;
|
|
58
|
+
/** ISO-8601 UTC instant the run completed. */
|
|
59
|
+
completed_at: string;
|
|
60
|
+
/** Absolute run directory holding the authoritative result document. */
|
|
61
|
+
out_dir: string;
|
|
62
|
+
wall_time_ms: QaSignalWallTime;
|
|
63
|
+
target: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const INSTANCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
67
|
+
const VERDICTS: readonly QaRunVerdict[] = ["passed", "failed", "incomplete"];
|
|
68
|
+
const EVIDENCE_SOURCES: readonly QaRunEvidenceSource[] = ["runner", "manual"];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Absolute path of one session's QA pointer. Throws on an instance id that
|
|
72
|
+
* could escape the directory; callers in this module treat that as "no
|
|
73
|
+
* pointer" rather than propagating it.
|
|
74
|
+
*/
|
|
75
|
+
export function qaSignalPath(coordRoot: string, instanceId: string): string {
|
|
76
|
+
if (!INSTANCE_ID_PATTERN.test(instanceId)) {
|
|
77
|
+
throw new Error("instance_id must be 1-128 ASCII letters, digits, hyphens, or underscores");
|
|
78
|
+
}
|
|
79
|
+
const directory = resolve(coordRoot, ".harnery", "qa");
|
|
80
|
+
const candidate = resolve(directory, `${instanceId}.json`);
|
|
81
|
+
if (!candidate.startsWith(`${directory}/`)) {
|
|
82
|
+
throw new Error("resolved qa signal path escapes the qa directory");
|
|
83
|
+
}
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface QaSignalTarget {
|
|
88
|
+
/** Defaults to the resolved coordination root. */
|
|
89
|
+
coordRoot?: string | null;
|
|
90
|
+
/** Defaults to the current session's instance id. */
|
|
91
|
+
instanceId?: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveTarget(target: QaSignalTarget): { coordRoot: string; instanceId: string } | null {
|
|
95
|
+
const coordRoot = target.coordRoot ?? resolveCoordRoot();
|
|
96
|
+
const instanceId = target.instanceId ?? resolveOwner();
|
|
97
|
+
if (!coordRoot || !instanceId) return null;
|
|
98
|
+
return { coordRoot, instanceId };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write the pointer for one completed run. Returns what was written, or null
|
|
103
|
+
* when the session/root could not be resolved or the write failed — a QA run
|
|
104
|
+
* must never fail because its status breadcrumb could not be recorded.
|
|
105
|
+
*/
|
|
106
|
+
export function recordQaSignal(
|
|
107
|
+
result: QaRunResult,
|
|
108
|
+
target: QaSignalTarget = {},
|
|
109
|
+
): QaSignalPointer | null {
|
|
110
|
+
try {
|
|
111
|
+
const resolved = resolveTarget(target);
|
|
112
|
+
if (!resolved) return null;
|
|
113
|
+
const pointer: QaSignalPointer = {
|
|
114
|
+
schema_version: QA_SIGNAL_SCHEMA_VERSION,
|
|
115
|
+
run_id: result.run.run_id,
|
|
116
|
+
verdict: result.verdict,
|
|
117
|
+
evidence_source: result.evidence_source,
|
|
118
|
+
completed_at: result.run.completed_at,
|
|
119
|
+
out_dir: result.run.out_dir,
|
|
120
|
+
wall_time_ms: {
|
|
121
|
+
total: result.wall_time_ms.total,
|
|
122
|
+
...(typeof result.wall_time_ms.queue === "number"
|
|
123
|
+
? { queue: result.wall_time_ms.queue }
|
|
124
|
+
: {}),
|
|
125
|
+
},
|
|
126
|
+
target: result.target,
|
|
127
|
+
};
|
|
128
|
+
const path = qaSignalPath(resolved.coordRoot, resolved.instanceId);
|
|
129
|
+
mkdirSync(resolve(path, ".."), { recursive: true });
|
|
130
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
131
|
+
writeFileSync(tmp, `${JSON.stringify(pointer, null, 2)}\n`, "utf8");
|
|
132
|
+
renameSync(tmp, path);
|
|
133
|
+
return pointer;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Read one session's pointer. Null when absent, unreadable, or malformed. */
|
|
140
|
+
export function readQaSignal(target: QaSignalTarget = {}): QaSignalPointer | null {
|
|
141
|
+
try {
|
|
142
|
+
const resolved = resolveTarget(target);
|
|
143
|
+
if (!resolved) return null;
|
|
144
|
+
const path = qaSignalPath(resolved.coordRoot, resolved.instanceId);
|
|
145
|
+
if (!existsSync(path)) return null;
|
|
146
|
+
return parseQaSignal(JSON.parse(readFileSync(path, "utf8")));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate an untrusted pointer document. Fail-closed: a partial document —
|
|
154
|
+
* the shape a torn write or a schema change produces — is not a pointer, and
|
|
155
|
+
* renders no row at all rather than a half-true one.
|
|
156
|
+
*/
|
|
157
|
+
export function parseQaSignal(document: unknown): QaSignalPointer | null {
|
|
158
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) return null;
|
|
159
|
+
const value = document as Record<string, unknown>;
|
|
160
|
+
if (value.schema_version !== QA_SIGNAL_SCHEMA_VERSION) return null;
|
|
161
|
+
if (typeof value.run_id !== "string" || value.run_id.length === 0) return null;
|
|
162
|
+
if (!VERDICTS.includes(value.verdict as QaRunVerdict)) return null;
|
|
163
|
+
if (!EVIDENCE_SOURCES.includes(value.evidence_source as QaRunEvidenceSource)) return null;
|
|
164
|
+
if (typeof value.completed_at !== "string" || Number.isNaN(Date.parse(value.completed_at))) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (typeof value.out_dir !== "string" || value.out_dir.length === 0) return null;
|
|
168
|
+
if (typeof value.target !== "string" || value.target.length === 0) return null;
|
|
169
|
+
const wall = value.wall_time_ms;
|
|
170
|
+
if (!wall || typeof wall !== "object" || Array.isArray(wall)) return null;
|
|
171
|
+
const { total, queue } = wall as Record<string, unknown>;
|
|
172
|
+
if (typeof total !== "number" || !Number.isFinite(total) || total < 0) return null;
|
|
173
|
+
if (queue !== undefined && (typeof queue !== "number" || !Number.isFinite(queue) || queue < 0)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
schema_version: QA_SIGNAL_SCHEMA_VERSION,
|
|
178
|
+
run_id: value.run_id,
|
|
179
|
+
verdict: value.verdict as QaRunVerdict,
|
|
180
|
+
evidence_source: value.evidence_source as QaRunEvidenceSource,
|
|
181
|
+
completed_at: value.completed_at,
|
|
182
|
+
out_dir: value.out_dir,
|
|
183
|
+
wall_time_ms: { total, ...(typeof queue === "number" ? { queue } : {}) },
|
|
184
|
+
target: value.target,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Coarse single-unit age, matching the status box's existing age vocabulary
|
|
190
|
+
* (`4m`, `3h`, `2d`). Deliberately coarser than the duration formatter: an
|
|
191
|
+
* operator reads age to judge relevance, not to compare timings.
|
|
192
|
+
*/
|
|
193
|
+
export function formatQaSignalAge(ms: number): string {
|
|
194
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
195
|
+
if (seconds < 60) return `${seconds}s`;
|
|
196
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
197
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
198
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Duration in the units an operator compares runs by. Seconds stay seconds up
|
|
203
|
+
* to two minutes so a 90-second run reads `90s` rather than being rounded into
|
|
204
|
+
* a minute bucket that hides the difference between fast runs.
|
|
205
|
+
*/
|
|
206
|
+
export function formatQaSignalDuration(ms: number): string {
|
|
207
|
+
const value = Math.max(0, ms);
|
|
208
|
+
if (value < 120_000) return `${Math.round(value / 1000)}s`;
|
|
209
|
+
if (value < 3_600_000) return `${Math.round(value / 60_000)}m`;
|
|
210
|
+
const hours = Math.floor(value / 3_600_000);
|
|
211
|
+
const minutes = Math.round((value % 3_600_000) / 60_000);
|
|
212
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Render the status-box `qa` value, or null when there is nothing honest to
|
|
217
|
+
* say. Three shapes:
|
|
218
|
+
* - fresh runner evidence: `passed 4m ago · 90s runner (2m queued)`
|
|
219
|
+
* - fresh manual evidence: `manual 12m ago · not a pass`
|
|
220
|
+
* - anything over the staleness horizon: `stale (2d)`
|
|
221
|
+
*
|
|
222
|
+
* Manual evidence never reports a verdict or a runner clock: nothing
|
|
223
|
+
* re-executable ran, so the contract caps it below a pass and there is no
|
|
224
|
+
* runner time to attribute.
|
|
225
|
+
*/
|
|
226
|
+
export function formatQaSignalRow(
|
|
227
|
+
pointer: QaSignalPointer | null,
|
|
228
|
+
now: number = Date.now(),
|
|
229
|
+
): string | null {
|
|
230
|
+
if (!pointer) return null;
|
|
231
|
+
const completedAt = Date.parse(pointer.completed_at);
|
|
232
|
+
if (Number.isNaN(completedAt)) return null;
|
|
233
|
+
const ageMs = Math.max(0, now - completedAt);
|
|
234
|
+
const age = formatQaSignalAge(ageMs);
|
|
235
|
+
if (ageMs > QA_SIGNAL_STALE_AFTER_MS) return `stale (${age})`;
|
|
236
|
+
if (pointer.evidence_source === "manual") return `manual ${age} ago · not a pass`;
|
|
237
|
+
const runner = `${formatQaSignalDuration(pointer.wall_time_ms.total)} runner`;
|
|
238
|
+
const queued =
|
|
239
|
+
typeof pointer.wall_time_ms.queue === "number" && pointer.wall_time_ms.queue > 0
|
|
240
|
+
? ` (${formatQaSignalDuration(pointer.wall_time_ms.queue)} queued)`
|
|
241
|
+
: "";
|
|
242
|
+
return `${pointer.verdict} ${age} ago · ${runner}${queued}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The whole status-box contribution in one best-effort call: read this
|
|
247
|
+
* session's pointer and render its row. Null means render no `qa` row.
|
|
248
|
+
*/
|
|
249
|
+
export function qaSignalStatusRow(
|
|
250
|
+
target: QaSignalTarget = {},
|
|
251
|
+
now: number = Date.now(),
|
|
252
|
+
): { value: string; pointer: QaSignalPointer } | null {
|
|
253
|
+
try {
|
|
254
|
+
const pointer = readQaSignal(target);
|
|
255
|
+
const value = formatQaSignalRow(pointer, now);
|
|
256
|
+
if (!pointer || !value) return null;
|
|
257
|
+
return { value, pointer };
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|