harnery 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/commander.d.ts +8 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +14 -13
- package/dist/commands/adapter.d.ts +12 -0
- package/dist/commands/adapter.d.ts.map +1 -0
- package/dist/commands/{harness.js → adapter.js} +35 -35
- package/dist/commands/agents.d.ts +3 -2
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -60
- package/dist/commands/artifacts.d.ts +4 -0
- package/dist/commands/artifacts.d.ts.map +1 -0
- package/dist/commands/artifacts.js +143 -0
- package/dist/commands/backup.d.ts +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +5 -2
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +310 -7
- package/dist/commands/checkpoint.d.ts +12 -0
- package/dist/commands/checkpoint.d.ts.map +1 -0
- package/dist/commands/checkpoint.js +162 -0
- package/dist/commands/context.d.ts +2 -2
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +3 -151
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +31 -7
- package/dist/commands/deinit.d.ts +2 -2
- package/dist/commands/deinit.js +15 -15
- package/dist/commands/doctor.js +23 -23
- package/dist/commands/governor.d.ts +4 -0
- package/dist/commands/governor.d.ts.map +1 -0
- package/dist/commands/{supervisor.js → governor.js} +133 -108
- package/dist/commands/init.d.ts +12 -12
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +44 -30
- package/dist/commands/{session.d.ts → journal.d.ts} +2 -2
- package/dist/commands/journal.d.ts.map +1 -0
- package/dist/commands/{scratch.js → journal.js} +47 -47
- package/dist/commands/policy.js +1 -1
- package/dist/commands/presence.js +2 -2
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.js +3 -3
- package/dist/commands/tunnel.d.ts +16 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +193 -23
- package/dist/commands/web-fetch.js +1 -1
- package/dist/commands/web.d.ts +19 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +51 -0
- package/dist/commands/work.d.ts +2 -1
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +55 -25
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +35 -28
- package/dist/core/{harnesses → adapters}/attest-projection.d.ts +2 -2
- package/dist/core/adapters/attest-projection.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/attest.d.ts +16 -16
- package/dist/core/adapters/attest.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/attest.js +9 -9
- package/dist/core/{harnesses → adapters}/attestation.d.ts +16 -16
- package/dist/core/adapters/attestation.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/attestation.js +20 -20
- package/dist/core/{harnesses → adapters}/bench.d.ts +12 -12
- package/dist/core/adapters/bench.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/bench.js +12 -12
- package/dist/core/adapters/index.d.ts +13 -0
- package/dist/core/adapters/index.d.ts.map +1 -0
- package/dist/core/adapters/index.js +6 -0
- package/dist/core/{harnesses → adapters}/profiles.d.ts +11 -11
- package/dist/core/adapters/profiles.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/profiles.js +11 -11
- package/dist/core/adapters/registry.d.ts +15 -0
- package/dist/core/adapters/registry.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/registry.js +11 -11
- package/dist/core/{harnesses → adapters}/types.d.ts +28 -23
- package/dist/core/adapters/types.d.ts.map +1 -0
- package/dist/core/{harnesses → adapters}/types.js +1 -1
- package/dist/core/agents/canonical-emit.d.ts +4 -4
- package/dist/core/agents/canonical-emit.js +5 -5
- package/dist/core/agents/cli-emit.js +1 -1
- package/dist/core/agents/cli.js +30 -26
- package/dist/core/agents/codex-replay.d.ts +1 -1
- package/dist/core/agents/codex-replay.js +6 -6
- package/dist/core/agents/coord-client.d.ts +21 -2
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +122 -27
- package/dist/core/agents/events/consume.d.ts +1 -1
- package/dist/core/agents/events/emit.d.ts +5 -6
- package/dist/core/agents/events/emit.d.ts.map +1 -1
- package/dist/core/agents/events/emit.js +2 -2
- package/dist/core/agents/render/prompt-context.d.ts +21 -9
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +82 -30
- package/dist/core/agents/render/session-context.d.ts +1 -1
- package/dist/core/agents/render/session-context.js +5 -5
- package/dist/core/agents/rules/claim-conflict.d.ts +2 -6
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +1 -1
- package/dist/core/agents/rules/commit-conflict.js +98 -22
- package/dist/core/agents/rules/stop-hook.d.ts +9 -9
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +23 -5
- package/dist/core/agents/rules/verdict.d.ts +14 -0
- package/dist/core/agents/rules/verdict.d.ts.map +1 -0
- package/dist/core/agents/rules/verdict.js +1 -0
- package/dist/core/agents/session-events.js +3 -3
- package/dist/core/agents/state/council.d.ts +1 -1
- package/dist/core/agents/state/council.js +1 -1
- package/dist/core/agents/state/heartbeat-projector.d.ts +3 -2
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +14 -10
- package/dist/core/agents/state/heartbeat-writer.d.ts +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +12 -12
- package/dist/core/agents/state/{scratch.d.ts → journal.d.ts} +7 -7
- package/dist/core/agents/state/{scratch.d.ts.map → journal.d.ts.map} +1 -1
- package/dist/core/agents/state/{scratch.js → journal.js} +12 -12
- package/dist/core/agents/state/pidmap.d.ts +28 -4
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +130 -15
- package/dist/core/agents/state/proc-start.d.ts +95 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -0
- package/dist/core/agents/state/proc-start.js +199 -0
- package/dist/core/agents/state/shell-mutation.d.ts +1 -1
- package/dist/core/agents/state/shell-mutation.js +1 -1
- package/dist/core/agents/state/stale-sweep.js +7 -7
- package/dist/core/artifacts/index.d.ts +86 -0
- package/dist/core/artifacts/index.d.ts.map +1 -0
- package/dist/core/artifacts/index.js +421 -0
- package/dist/core/config.d.ts +9 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +26 -2
- package/dist/core/context/index.d.ts +12 -12
- package/dist/core/context/index.js +10 -10
- package/dist/core/governor/adapter-spread.d.ts +52 -0
- package/dist/core/governor/adapter-spread.d.ts.map +1 -0
- package/dist/core/governor/adapter-spread.js +52 -0
- package/dist/core/governor/index.d.ts +7 -0
- package/dist/core/governor/index.d.ts.map +1 -0
- package/dist/core/governor/index.js +6 -0
- package/dist/core/governor/plan-api.d.ts +38 -0
- package/dist/core/governor/plan-api.d.ts.map +1 -0
- package/dist/core/governor/plan-api.js +83 -0
- package/dist/core/governor/plan-read.d.ts +5 -0
- package/dist/core/governor/plan-read.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/plan-read.js +71 -51
- package/dist/core/governor/plan-types.d.ts +181 -0
- package/dist/core/governor/plan-types.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/plan-types.js +4 -4
- package/dist/core/governor/planning.d.ts +48 -0
- package/dist/core/governor/planning.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/planning.js +143 -94
- package/dist/core/governor/read.d.ts +6 -0
- package/dist/core/governor/read.d.ts.map +1 -0
- package/dist/core/governor/read.js +4 -0
- package/dist/core/governor/runner.d.ts +37 -0
- package/dist/core/governor/runner.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/runner.js +15 -15
- package/dist/core/governor/service-read.d.ts +6 -0
- package/dist/core/governor/service-read.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/service-read.js +12 -12
- package/dist/core/governor/service.d.ts +114 -0
- package/dist/core/governor/service.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/service.js +57 -57
- package/dist/core/governor/state.d.ts +116 -0
- package/dist/core/governor/state.d.ts.map +1 -0
- package/dist/core/{supervisor → governor}/state.js +290 -116
- package/dist/core/hooks/adapter/detect.d.ts +9 -0
- package/dist/core/hooks/{harness → adapter}/detect.d.ts.map +1 -1
- package/dist/core/hooks/{harness → adapter}/detect.js +8 -8
- package/dist/core/hooks/{harness → adapter}/events.d.ts +15 -14
- package/dist/core/hooks/adapter/events.d.ts.map +1 -0
- package/dist/core/hooks/{harness → adapter}/events.js +5 -5
- package/dist/core/hooks/{harness → adapter}/output.d.ts +20 -14
- package/dist/core/hooks/adapter/output.d.ts.map +1 -0
- package/dist/core/hooks/{harness → adapter}/output.js +30 -22
- package/dist/core/hooks/{harness → adapter}/parse.d.ts +5 -5
- package/dist/core/hooks/{harness → adapter}/parse.d.ts.map +1 -1
- package/dist/core/hooks/{harness → adapter}/parse.js +4 -4
- package/dist/core/hooks/{harness → adapter}/wiring.d.ts +15 -15
- package/dist/core/hooks/{harness → adapter}/wiring.d.ts.map +1 -1
- package/dist/core/hooks/{harness → adapter}/wiring.js +13 -13
- package/dist/core/hooks/cli.d.ts +3 -3
- package/dist/core/hooks/cli.js +175 -151
- package/dist/core/hooks/effects/image-capture.d.ts +5 -5
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +3 -3
- package/dist/core/hooks/effects/index.d.ts +12 -12
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +17 -17
- package/dist/core/hooks/events/emit.d.ts +2 -2
- package/dist/core/hooks/events/emit.d.ts.map +1 -1
- package/dist/core/hooks/events/emit.js +1 -1
- package/dist/core/hooks/events/rotate.js +1 -1
- package/dist/core/hooks/events/schema.d.ts +13 -5
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/guard-path.d.ts +2 -2
- package/dist/core/hooks/guard-path.js +2 -2
- package/dist/core/hooks/index.d.ts +1 -1
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/resolve/anchor.d.ts +14 -5
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -1
- package/dist/core/hooks/resolve/anchor.js +64 -7
- package/dist/core/hooks/resolve/coord-root.d.ts +3 -3
- package/dist/core/hooks/resolve/coord-root.js +4 -4
- package/dist/core/hooks/resolve/intent.js +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +1 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +11 -5
- package/dist/core/{scratch → journal}/index.d.ts +25 -25
- package/dist/core/{scratch → journal}/index.d.ts.map +1 -1
- package/dist/core/{scratch → journal}/index.js +38 -38
- package/dist/core/policy/evaluate.d.ts +1 -1
- package/dist/core/policy/evaluate.d.ts.map +1 -1
- package/dist/core/policy/evaluate.js +7 -7
- package/dist/core/policy/types.d.ts +3 -3
- package/dist/core/policy/types.d.ts.map +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +11 -2
- package/dist/core/work/state.d.ts +28 -3
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +148 -16
- package/dist/core/workflow/adapters.d.ts +17 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -0
- package/dist/core/workflow/adapters.js +21 -0
- package/dist/core/workflow/approvals.js +6 -6
- package/dist/core/workflow/billing.d.ts +9 -9
- package/dist/core/workflow/billing.js +9 -9
- package/dist/core/workflow/child-env.d.ts +10 -2
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +12 -4
- package/dist/core/workflow/engine.d.ts +15 -5
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +151 -83
- package/dist/core/workflow/evidence-preflight.d.ts +24 -0
- package/dist/core/workflow/evidence-preflight.d.ts.map +1 -0
- package/dist/core/workflow/evidence-preflight.js +134 -0
- 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 +28 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +77 -34
- package/dist/core/workflow/run-state.d.ts +1 -1
- package/dist/core/workflow/run-state.js +7 -7
- package/dist/core/workflow/sandbox-projection.d.ts +6 -6
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +11 -11
- package/dist/core/workflow/spawn-claude.d.ts +3 -3
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +28 -8
- package/dist/core/workflow/spawn-codex.d.ts +4 -4
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +26 -8
- package/dist/core/workflow/spawn-cursor.d.ts +4 -4
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +27 -8
- package/dist/core/workflow/spawn-failure.d.ts +15 -1
- package/dist/core/workflow/spawn-failure.d.ts.map +1 -1
- package/dist/core/workflow/spawn-failure.js +35 -1
- package/dist/core/workflow/specialists.js +2 -2
- package/dist/core/workflow/{journal.d.ts → transcript.d.ts} +8 -8
- package/dist/core/workflow/transcript.d.ts.map +1 -0
- package/dist/core/workflow/{journal.js → transcript.js} +14 -14
- package/dist/core/workflow/types.d.ts +119 -46
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +11 -1
- package/dist/core/workflow/validate.d.ts +1 -1
- package/dist/core/workflow/validate.js +1 -1
- package/dist/core/workflow/workspaces/cancellation.js +2 -2
- package/dist/core/workflow/workspaces/cleanup.js +2 -2
- package/dist/core/workflow/workspaces/execution.js +6 -6
- package/dist/core/workflow/workspaces/inspect.js +9 -9
- package/dist/core/workflow/workspaces/integration.js +14 -14
- package/dist/core/workflow/workspaces/lifecycle.d.ts +1 -1
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/lifecycle.js +9 -9
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +39 -5
- package/dist/core/workflow/workspaces/recovery.js +3 -3
- package/dist/core/workflow/workspaces/state.d.ts +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +3 -3
- package/dist/core/workflow/workspaces/types.d.ts +1 -1
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/lib/browser/asserts.d.ts +28 -0
- package/dist/lib/browser/asserts.d.ts.map +1 -0
- package/dist/lib/browser/asserts.js +152 -0
- package/dist/lib/browser/client.d.ts +49 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +155 -14
- package/dist/lib/browser/content-checks.d.ts +108 -0
- package/dist/lib/browser/content-checks.d.ts.map +1 -0
- package/dist/lib/browser/content-checks.js +474 -0
- package/dist/lib/browser/critique.d.ts +91 -0
- package/dist/lib/browser/critique.d.ts.map +1 -0
- package/dist/lib/browser/critique.js +167 -0
- package/dist/lib/browser/geometry.d.ts +32 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +244 -1
- package/dist/lib/browser/index.d.ts +4 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +4 -1
- package/dist/lib/council/index.d.ts +6 -6
- package/dist/lib/council/index.d.ts.map +1 -1
- package/dist/lib/council/index.js +8 -8
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +6 -3
- package/dist/lib/identities/assume.d.ts +2 -2
- package/dist/lib/identities/assume.js +7 -7
- package/dist/lib/instructions/apply.d.ts +3 -3
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +75 -27
- package/dist/lib/instructions/host-addendum.d.ts +54 -0
- package/dist/lib/instructions/host-addendum.d.ts.map +1 -0
- package/dist/lib/instructions/host-addendum.js +107 -0
- package/dist/lib/instructions/templates.d.ts +10 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +41 -12
- package/dist/lib/machine.d.ts +2 -2
- package/dist/lib/machine.js +2 -2
- package/package.json +26 -21
- package/schemas/config.schema.json +26 -1
- package/src/commander.ts +22 -13
- package/src/commands/{harness.ts → adapter.ts} +46 -46
- package/src/commands/agents.ts +71 -64
- package/src/commands/artifacts.ts +168 -0
- package/src/commands/backup.ts +47 -33
- package/src/commands/browse.ts +438 -2
- package/src/commands/checkpoint.ts +196 -0
- package/src/commands/context.ts +3 -176
- package/src/commands/decision.ts +41 -7
- package/src/commands/deinit.ts +16 -16
- package/src/commands/doctor.ts +28 -28
- package/src/commands/{supervisor.ts → governor.ts} +184 -154
- package/src/commands/init.ts +50 -34
- package/src/commands/{scratch.ts → journal.ts} +60 -60
- package/src/commands/policy.ts +1 -1
- package/src/commands/presence.ts +2 -2
- package/src/commands/sync.ts +3 -3
- package/src/commands/tunnel.ts +237 -34
- package/src/commands/web-fetch.ts +1 -1
- package/src/commands/web.ts +56 -1
- package/src/commands/work.ts +72 -38
- package/src/commands/workflow.ts +43 -34
- package/src/core/{harnesses → adapters}/attest-projection.ts +2 -2
- package/src/core/{harnesses → adapters}/attest.ts +27 -27
- package/src/core/{harnesses → adapters}/attestation.ts +41 -41
- package/src/core/{harnesses → adapters}/bench.ts +34 -43
- package/src/core/adapters/index.ts +56 -0
- package/src/core/{harnesses → adapters}/profiles.ts +16 -16
- package/src/core/{harnesses → adapters}/registry.ts +23 -23
- package/src/core/{harnesses → adapters}/types.ts +28 -23
- package/src/core/agents/canonical-emit.ts +6 -6
- package/src/core/agents/cli-emit.ts +1 -1
- package/src/core/agents/cli.ts +30 -28
- package/src/core/agents/codex-replay.ts +6 -6
- package/src/core/agents/coord-client.ts +119 -28
- package/src/core/agents/events/consume.ts +1 -1
- package/src/core/agents/events/emit.ts +7 -6
- package/src/core/agents/render/prompt-context.ts +107 -31
- package/src/core/agents/render/session-context.ts +6 -6
- package/src/core/agents/rules/claim-conflict.ts +4 -7
- package/src/core/agents/rules/commit-conflict.ts +103 -22
- package/src/core/agents/rules/stop-hook.ts +33 -17
- package/src/core/agents/rules/verdict.ts +13 -0
- package/src/core/agents/session-events.ts +4 -4
- package/src/core/agents/state/council.ts +1 -1
- package/src/core/agents/state/heartbeat-projector.ts +16 -12
- package/src/core/agents/state/heartbeat-writer.ts +12 -12
- package/src/core/agents/state/{scratch.ts → journal.ts} +12 -12
- package/src/core/agents/state/pidmap.ts +130 -15
- package/src/core/agents/state/proc-start.ts +208 -0
- package/src/core/agents/state/shell-mutation.ts +1 -1
- package/src/core/agents/state/stale-sweep.ts +7 -7
- package/src/core/artifacts/index.ts +557 -0
- package/src/core/config.ts +30 -2
- package/src/core/context/index.ts +15 -15
- package/src/core/governor/adapter-spread.ts +82 -0
- package/src/core/governor/index.ts +91 -0
- package/src/core/governor/plan-api.ts +115 -0
- package/src/core/{supervisor → governor}/plan-read.ts +120 -96
- package/src/core/{supervisor → governor}/plan-types.ts +64 -50
- package/src/core/{supervisor → governor}/planning.ts +240 -188
- package/src/core/governor/read.ts +59 -0
- package/src/core/{supervisor → governor}/runner.ts +39 -39
- package/src/core/{supervisor → governor}/service-read.ts +22 -22
- package/src/core/{supervisor → governor}/service.ts +117 -119
- package/src/core/{supervisor → governor}/state.ts +389 -174
- package/src/core/hooks/adapter/detect.ts +30 -0
- package/src/core/hooks/{harness → adapter}/events.ts +16 -14
- package/src/core/hooks/{harness → adapter}/output.ts +31 -23
- package/src/core/hooks/{harness → adapter}/parse.ts +5 -5
- package/src/core/hooks/{harness → adapter}/wiring.ts +21 -21
- package/src/core/hooks/cli.ts +185 -162
- package/src/core/hooks/effects/image-capture.ts +6 -6
- package/src/core/hooks/effects/index.ts +17 -17
- package/src/core/hooks/events/emit.ts +3 -3
- package/src/core/hooks/events/rotate.ts +1 -1
- package/src/core/hooks/events/schema.ts +15 -7
- package/src/core/hooks/guard-path.ts +2 -2
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/resolve/anchor.ts +63 -8
- package/src/core/hooks/resolve/coord-root.ts +4 -4
- package/src/core/hooks/resolve/intent.ts +1 -1
- package/src/core/hooks/resolve/owner.ts +11 -5
- package/src/core/{scratch → journal}/index.ts +53 -53
- package/src/core/policy/evaluate.ts +8 -8
- package/src/core/policy/types.ts +3 -3
- package/src/core/work/runner.ts +11 -2
- package/src/core/work/state.ts +185 -21
- package/src/core/workflow/adapters.ts +33 -0
- package/src/core/workflow/approvals.ts +7 -7
- package/src/core/workflow/billing.ts +12 -12
- package/src/core/workflow/child-env.ts +13 -4
- package/src/core/workflow/engine.ts +166 -90
- package/src/core/workflow/evidence-preflight.ts +165 -0
- package/src/core/workflow/index.ts +6 -3
- package/src/core/workflow/proof.ts +94 -48
- package/src/core/workflow/run-state.ts +7 -7
- package/src/core/workflow/sandbox-projection.ts +16 -16
- package/src/core/workflow/spawn-claude.ts +34 -12
- package/src/core/workflow/spawn-codex.ts +30 -12
- package/src/core/workflow/spawn-cursor.ts +31 -12
- package/src/core/workflow/spawn-failure.ts +41 -1
- package/src/core/workflow/specialists.ts +2 -2
- package/src/core/workflow/{journal.ts → transcript.ts} +16 -16
- package/src/core/workflow/types.ts +130 -46
- package/src/core/workflow/validate.ts +1 -1
- package/src/core/workflow/workspaces/cancellation.ts +2 -2
- package/src/core/workflow/workspaces/cleanup.ts +2 -2
- package/src/core/workflow/workspaces/execution.ts +6 -6
- package/src/core/workflow/workspaces/inspect.ts +14 -11
- package/src/core/workflow/workspaces/integration.ts +18 -14
- package/src/core/workflow/workspaces/lifecycle.ts +10 -10
- package/src/core/workflow/workspaces/local-git.ts +47 -5
- package/src/core/workflow/workspaces/recovery.ts +3 -3
- package/src/core/workflow/workspaces/state.ts +4 -3
- package/src/core/workflow/workspaces/types.ts +1 -1
- package/src/lib/browser/asserts.ts +177 -0
- package/src/lib/browser/client.ts +189 -14
- package/src/lib/browser/content-checks.ts +571 -0
- package/src/lib/browser/critique.ts +231 -0
- package/src/lib/browser/geometry.ts +297 -1
- package/src/lib/browser/index.ts +34 -0
- package/src/lib/claude-desktop.ts +4 -1
- package/src/lib/council/index.ts +9 -9
- package/src/lib/exec.ts +13 -3
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +87 -25
- package/src/lib/instructions/host-addendum.ts +126 -0
- package/src/lib/instructions/templates.ts +44 -15
- package/src/lib/machine.ts +2 -2
- package/dist/commands/harness.d.ts +0 -12
- package/dist/commands/harness.d.ts.map +0 -1
- package/dist/commands/scratch.d.ts +0 -4
- package/dist/commands/scratch.d.ts.map +0 -1
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -162
- package/dist/commands/supervisor.d.ts +0 -4
- package/dist/commands/supervisor.d.ts.map +0 -1
- package/dist/core/harnesses/attest-projection.d.ts.map +0 -1
- package/dist/core/harnesses/attest.d.ts.map +0 -1
- package/dist/core/harnesses/attestation.d.ts.map +0 -1
- package/dist/core/harnesses/bench.d.ts.map +0 -1
- package/dist/core/harnesses/index.d.ts +0 -13
- package/dist/core/harnesses/index.d.ts.map +0 -1
- package/dist/core/harnesses/index.js +0 -6
- package/dist/core/harnesses/profiles.d.ts.map +0 -1
- package/dist/core/harnesses/registry.d.ts +0 -15
- package/dist/core/harnesses/registry.d.ts.map +0 -1
- package/dist/core/harnesses/types.d.ts.map +0 -1
- package/dist/core/hooks/harness/detect.d.ts +0 -9
- package/dist/core/hooks/harness/events.d.ts.map +0 -1
- package/dist/core/hooks/harness/output.d.ts.map +0 -1
- package/dist/core/supervisor/index.d.ts +0 -7
- package/dist/core/supervisor/index.d.ts.map +0 -1
- package/dist/core/supervisor/index.js +0 -6
- package/dist/core/supervisor/plan-api.d.ts +0 -23
- package/dist/core/supervisor/plan-api.d.ts.map +0 -1
- package/dist/core/supervisor/plan-api.js +0 -50
- package/dist/core/supervisor/plan-read.d.ts +0 -5
- package/dist/core/supervisor/plan-read.d.ts.map +0 -1
- package/dist/core/supervisor/plan-types.d.ts +0 -170
- package/dist/core/supervisor/plan-types.d.ts.map +0 -1
- package/dist/core/supervisor/planning.d.ts +0 -33
- package/dist/core/supervisor/planning.d.ts.map +0 -1
- package/dist/core/supervisor/read.d.ts +0 -6
- package/dist/core/supervisor/read.d.ts.map +0 -1
- package/dist/core/supervisor/read.js +0 -4
- package/dist/core/supervisor/runner.d.ts +0 -37
- package/dist/core/supervisor/runner.d.ts.map +0 -1
- package/dist/core/supervisor/service-read.d.ts +0 -6
- package/dist/core/supervisor/service-read.d.ts.map +0 -1
- package/dist/core/supervisor/service.d.ts +0 -114
- package/dist/core/supervisor/service.d.ts.map +0 -1
- package/dist/core/supervisor/state.d.ts +0 -95
- package/dist/core/supervisor/state.d.ts.map +0 -1
- package/dist/core/workflow/harnesses.d.ts +0 -17
- package/dist/core/workflow/harnesses.d.ts.map +0 -1
- package/dist/core/workflow/harnesses.js +0 -21
- package/dist/core/workflow/journal.d.ts.map +0 -1
- package/src/commands/session.ts +0 -187
- package/src/core/harnesses/index.ts +0 -56
- package/src/core/hooks/harness/detect.ts +0 -30
- package/src/core/supervisor/index.ts +0 -89
- package/src/core/supervisor/plan-api.ts +0 -73
- package/src/core/supervisor/read.ts +0 -59
- package/src/core/workflow/harnesses.ts +0 -33
- /package/dist/core/{harnesses → adapters}/attest-projection.js +0 -0
|
@@ -135,7 +135,7 @@ export interface WorkspaceLifecycleProjectionInput {
|
|
|
135
135
|
binding?: WorkspaceBinding;
|
|
136
136
|
provider_events?: readonly WorkspaceProviderEvent[];
|
|
137
137
|
manifest?: WorkflowRunManifest;
|
|
138
|
-
|
|
138
|
+
workflow_transcript?: readonly {
|
|
139
139
|
event: string;
|
|
140
140
|
ok?: boolean;
|
|
141
141
|
status?: "blocked" | "lost" | WorkspaceCancellationResult["status"];
|
|
@@ -207,22 +207,22 @@ export function deriveWorkspaceLifecycle(
|
|
|
207
207
|
transition(next);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
const
|
|
211
|
-
for (const event of
|
|
210
|
+
const transcript = input.workflow_transcript ?? [];
|
|
211
|
+
for (const event of transcript) {
|
|
212
212
|
if (input.binding && (event.event === "run.start" || event.event === "run.resume")) {
|
|
213
213
|
transition("running");
|
|
214
214
|
}
|
|
215
215
|
if (event.event === "run.parked") transition("parked");
|
|
216
216
|
}
|
|
217
|
-
const failedReattachment = [...
|
|
217
|
+
const failedReattachment = [...transcript]
|
|
218
218
|
.reverse()
|
|
219
219
|
.find((event) => event.event === "workspace.reattach.failed");
|
|
220
220
|
if (failedReattachment) transition(failedReattachment.status === "blocked" ? "blocked" : "lost");
|
|
221
221
|
let workflowOutcome = proofOutcome(input.proof, input.binding);
|
|
222
|
-
const
|
|
222
|
+
const terminalTranscript = [...transcript].reverse().find((event) => event.event === "run.end");
|
|
223
223
|
if (input.proof && !failedReattachment) transition(workflowOutcome!);
|
|
224
|
-
if (
|
|
225
|
-
workflowOutcome =
|
|
224
|
+
if (terminalTranscript && !input.proof && !failedReattachment) {
|
|
225
|
+
workflowOutcome = terminalTranscript.ok === false ? "failed_retained" : null;
|
|
226
226
|
transition("blocked");
|
|
227
227
|
}
|
|
228
228
|
if (projectedIntegrationState !== "none") transition("integration_requested");
|
|
@@ -269,7 +269,7 @@ export function deriveWorkspaceLifecycle(
|
|
|
269
269
|
((input.cancellation_receipt?.status === "cancelled" &&
|
|
270
270
|
input.cancellation_receipt.work_event_seq === latestGovernance.seq &&
|
|
271
271
|
input.cancellation_receipt.work_event_sha256 === workEventSha256) ||
|
|
272
|
-
|
|
272
|
+
transcript.some(
|
|
273
273
|
(event) =>
|
|
274
274
|
event.event === "workspace.cancel" &&
|
|
275
275
|
event.status === "cancelled" &&
|
|
@@ -459,8 +459,8 @@ function validateIntegrationAuthorization(
|
|
|
459
459
|
!digest(authorization.policy_sha256) ||
|
|
460
460
|
authorization.decision?.verdict !== "allow" ||
|
|
461
461
|
authorization.decision_sha256 !== stableDigest(authorization.decision) ||
|
|
462
|
-
authorization.
|
|
463
|
-
authorization.
|
|
462
|
+
authorization.transcript_anchor?.event !== "integration.plan" ||
|
|
463
|
+
authorization.transcript_anchor.plan_sha256 !== planSha256 ||
|
|
464
464
|
!validTimestamp(authorization.authorized_at) ||
|
|
465
465
|
(authorization.approval_id !== undefined &&
|
|
466
466
|
(typeof authorization.approval_actor !== "string" || !digest(authorization.approval_sha256)))
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
11
|
import { WorkspaceAttestationError } from "./attestation-error.ts";
|
|
5
12
|
import {
|
|
6
13
|
git,
|
|
@@ -68,6 +75,8 @@ import { isWorkspaceAttestation } from "./validate.ts";
|
|
|
68
75
|
|
|
69
76
|
const PROVIDER_ID = "local-git-worktree";
|
|
70
77
|
const PROVIDER_VERSION = "1";
|
|
78
|
+
const WORKSPACE_PARENT_SEGMENT = ".harnery-workspaces";
|
|
79
|
+
const LEGACY_WORKSPACE_PARENT_SEGMENT = "harnery-workspaces";
|
|
71
80
|
const LOCK_STALE_MS = 5 * 60 * 1_000;
|
|
72
81
|
const OBJECT_FORMAT = /^[a-f0-9]{40,64}$/;
|
|
73
82
|
const REF_FORMAT = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]{0,180}$/;
|
|
@@ -221,7 +230,11 @@ async function allocate(
|
|
|
221
230
|
);
|
|
222
231
|
const authorityOutside = describeAuthorityOutsideRoot(repo, root.realpath);
|
|
223
232
|
if (authorityOutside) throw new Error(authorityOutside);
|
|
224
|
-
|
|
233
|
+
// Claims are immutable, so a retry must reproduce the path the first attempt
|
|
234
|
+
// froze. Only a genuinely new allocation picks the current parent.
|
|
235
|
+
const priorClaim = readWorkspaceClaim(coordRoot, PROVIDER_ID, bindingId);
|
|
236
|
+
const parentSegment = priorClaim ? claimedParentSegment(priorClaim) : WORKSPACE_PARENT_SEGMENT;
|
|
237
|
+
const workspaceRoot = candidateUnderRoot(root, [parentSegment, bindingId]);
|
|
225
238
|
const activeRoot = resolve(workspaceRoot, repo.requestedRelativePath);
|
|
226
239
|
if (!containsPath(workspaceRoot, activeRoot)) {
|
|
227
240
|
throw new Error("requested working directory escapes the isolated workspace");
|
|
@@ -287,6 +300,33 @@ async function allocate(
|
|
|
287
300
|
}
|
|
288
301
|
}
|
|
289
302
|
|
|
303
|
+
/**
|
|
304
|
+
* A claim freezes its workspace path at allocation time, so reconciliation must
|
|
305
|
+
* follow the parent recorded there rather than the parent this version would
|
|
306
|
+
* choose. Only the names this provider has ever allocated under are accepted, so
|
|
307
|
+
* a tampered claim cannot redirect creation into an unrelated directory.
|
|
308
|
+
*/
|
|
309
|
+
function claimedParentSegment(claim: WorkspaceClaim): string {
|
|
310
|
+
const segment = basename(dirname(claim.workspace_root));
|
|
311
|
+
if (segment !== WORKSPACE_PARENT_SEGMENT && segment !== LEGACY_WORKSPACE_PARENT_SEGMENT) {
|
|
312
|
+
throw new Error("claimed workspace path is not under a provider-owned workspace parent");
|
|
313
|
+
}
|
|
314
|
+
return segment;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The parent holds real checkouts that would otherwise surface as untracked
|
|
319
|
+
* content in the surrounding repository. A directory whose every entry is
|
|
320
|
+
* ignored, including the ignore file itself, does not appear in `git status` at
|
|
321
|
+
* all, which keeps the guarantee with the provider instead of asking each
|
|
322
|
+
* consumer to maintain a rule.
|
|
323
|
+
*/
|
|
324
|
+
function hideWorkspaceParentFromGit(parent: string): void {
|
|
325
|
+
const ignoreFile = join(parent, ".gitignore");
|
|
326
|
+
if (existsSync(ignoreFile)) return;
|
|
327
|
+
writeFileSync(ignoreFile, "*\n", { mode: 0o600 });
|
|
328
|
+
}
|
|
329
|
+
|
|
290
330
|
function reconcileAllocation(
|
|
291
331
|
coordRoot: string,
|
|
292
332
|
claim: WorkspaceClaim,
|
|
@@ -328,7 +368,9 @@ function reconcileAllocation(
|
|
|
328
368
|
throw new Error("provider-owned workspace branch no longer matches its frozen base");
|
|
329
369
|
}
|
|
330
370
|
|
|
331
|
-
const
|
|
371
|
+
const parentSegment = claimedParentSegment(claim);
|
|
372
|
+
const workspaceParent = createContainedDirectories(root, [parentSegment]);
|
|
373
|
+
hideWorkspaceParentFromGit(workspaceParent);
|
|
332
374
|
const workspaceParentIdentity = filesystemIdentity(workspaceParent);
|
|
333
375
|
const registered = worktreeInventory(claim.repository.source_root.realpath);
|
|
334
376
|
const exact = registered.find((item) => resolve(item.path) === claim.workspace_root);
|
|
@@ -385,7 +427,7 @@ function reconcileAllocation(
|
|
|
385
427
|
parent_identity: workspaceParentIdentity,
|
|
386
428
|
});
|
|
387
429
|
}
|
|
388
|
-
const openedWorkspace = openContainedDirectory(root, [
|
|
430
|
+
const openedWorkspace = openContainedDirectory(root, [parentSegment, claim.binding_id]);
|
|
389
431
|
if (openedWorkspace.path !== claim.workspace_root) {
|
|
390
432
|
openedWorkspace.close();
|
|
391
433
|
throw new Error("descriptor-backed workspace directory differs from the claim");
|
|
@@ -16,11 +16,11 @@ export function readWorkflowLiveness(coordRoot: string, runId: string): Workflow
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
if (!existsSync(
|
|
19
|
+
const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
|
|
20
|
+
if (!existsSync(transcriptPath)) return "unknown";
|
|
21
21
|
let events: Array<{ event?: string; run_id?: string; ok?: unknown; status?: unknown }>;
|
|
22
22
|
try {
|
|
23
|
-
events = readFileSync(
|
|
23
|
+
events = readFileSync(transcriptPath, "utf8")
|
|
24
24
|
.split("\n")
|
|
25
25
|
.filter(Boolean)
|
|
26
26
|
.map(
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeImmutableJson,
|
|
19
19
|
} from "../durable-record.ts";
|
|
20
20
|
|
|
21
|
-
export {
|
|
21
|
+
export { appendWorkflowTranscriptEvent } from "../transcript.ts";
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
24
|
GitRepositoryBinding,
|
|
@@ -232,7 +232,8 @@ export function readWorkspaceEvents(
|
|
|
232
232
|
): WorkspaceProviderEvent[] {
|
|
233
233
|
const path = workspaceEventsPath(coordRoot, providerId, bindingId);
|
|
234
234
|
if (!existsSync(path)) return [];
|
|
235
|
-
if (statSync(path).size > JSON_LIMIT * 4)
|
|
235
|
+
if (statSync(path).size > JSON_LIMIT * 4)
|
|
236
|
+
throw new Error("workspace event transcript is too large");
|
|
236
237
|
const claim = readWorkspaceClaim(coordRoot, providerId, bindingId);
|
|
237
238
|
if (!claim) throw new Error("workspace event authority claim is missing");
|
|
238
239
|
const records = readFileSync(path, "utf8")
|
|
@@ -391,7 +392,7 @@ function readChainedRecords<
|
|
|
391
392
|
>(path: string, label: string): T[] {
|
|
392
393
|
if (!existsSync(path)) return [];
|
|
393
394
|
if (statSync(path).size > JSON_LIMIT * 4) {
|
|
394
|
-
throw new Error(`${label}
|
|
395
|
+
throw new Error(`${label} transcript is too large`);
|
|
395
396
|
}
|
|
396
397
|
const records = readFileSync(path, "utf8")
|
|
397
398
|
.split("\n")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Value assertions for `harn browse --assert`.
|
|
2
|
+
//
|
|
3
|
+
// The layout + content checks answer "does the page look right"; asserts answer
|
|
4
|
+
// "does the page SAY the right thing" — the h1 text, a price, how many cards
|
|
5
|
+
// rendered, whether an error banner is absent. It lets an agent confirm the
|
|
6
|
+
// values it expects without a human reading the page back.
|
|
7
|
+
//
|
|
8
|
+
// One repeatable flag with a small grammar:
|
|
9
|
+
// --assert 'text h1 => Welcome' first match's trimmed text equals
|
|
10
|
+
// --assert 'contains .total => $49' first match's text contains
|
|
11
|
+
// --assert 'matches .sku => ^BN-\d+$' first match's text matches a regex
|
|
12
|
+
// --assert 'count .card => >=3' match count vs a number or comparator
|
|
13
|
+
// --assert 'exists .cta' at least one match
|
|
14
|
+
// --assert 'absent .error' zero matches
|
|
15
|
+
//
|
|
16
|
+
// `op selector => expected`; `exists`/`absent` take no `=> expected`.
|
|
17
|
+
|
|
18
|
+
export type AssertOp = "text" | "contains" | "matches" | "count" | "exists" | "absent";
|
|
19
|
+
|
|
20
|
+
export interface AssertSpec {
|
|
21
|
+
raw: string;
|
|
22
|
+
op: AssertOp;
|
|
23
|
+
selector: string;
|
|
24
|
+
expected: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AssertResult {
|
|
28
|
+
raw: string;
|
|
29
|
+
op: AssertOp;
|
|
30
|
+
selector: string;
|
|
31
|
+
expected: string;
|
|
32
|
+
/** Observed value: the text, or the match count as a string. */
|
|
33
|
+
actual: string;
|
|
34
|
+
found: boolean;
|
|
35
|
+
outcome: "pass" | "fail";
|
|
36
|
+
/** Populated when the spec itself was malformed (e.g. bad regex, bad count). */
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const OPS: readonly AssertOp[] = ["text", "contains", "matches", "count", "exists", "absent"];
|
|
41
|
+
|
|
42
|
+
/** Parse one `--assert` expression. Throws with a clear message on malformed input. */
|
|
43
|
+
export function parseAssertSpec(raw: string): AssertSpec {
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
46
|
+
if (spaceIdx < 0) {
|
|
47
|
+
throw new Error(`--assert needs '<op> <selector> [=> expected]' (got: '${raw}')`);
|
|
48
|
+
}
|
|
49
|
+
const op = trimmed.slice(0, spaceIdx).toLowerCase() as AssertOp;
|
|
50
|
+
if (!OPS.includes(op)) {
|
|
51
|
+
throw new Error(`--assert op must be one of ${OPS.join(", ")} (got: '${op}')`);
|
|
52
|
+
}
|
|
53
|
+
const rest = trimmed.slice(spaceIdx + 1).trim();
|
|
54
|
+
const sep = rest.indexOf("=>");
|
|
55
|
+
if (op === "exists" || op === "absent") {
|
|
56
|
+
return { raw, op, selector: (sep < 0 ? rest : rest.slice(0, sep)).trim(), expected: "" };
|
|
57
|
+
}
|
|
58
|
+
if (sep < 0) {
|
|
59
|
+
throw new Error(`--assert '${op}' needs 'selector => expected' (got: '${raw}')`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
raw,
|
|
63
|
+
op,
|
|
64
|
+
selector: rest.slice(0, sep).trim(),
|
|
65
|
+
expected: rest.slice(sep + 2).trim(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the page evaluator. Serialized into the page by Playwright, so helpers
|
|
71
|
+
* are nested. Comparison logic that does not touch the DOM (count comparators,
|
|
72
|
+
* regex) also runs in-page to keep it one round trip.
|
|
73
|
+
*/
|
|
74
|
+
export function buildAssertCheck(): (specs: AssertSpec[]) => AssertResult[] {
|
|
75
|
+
return (specs) => {
|
|
76
|
+
const textOf = (el: Element): string => (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
77
|
+
|
|
78
|
+
const compareCount = (count: number, expected: string): { ok: boolean; error?: string } => {
|
|
79
|
+
const m = /^(>=|<=|>|<|=)?\s*(\d+)$/.exec(expected.trim());
|
|
80
|
+
if (!m) return { ok: false, error: `bad count expected '${expected}'` };
|
|
81
|
+
const op = m[1] || "=";
|
|
82
|
+
const n = Number.parseInt(m[2]!, 10);
|
|
83
|
+
switch (op) {
|
|
84
|
+
case ">=":
|
|
85
|
+
return { ok: count >= n };
|
|
86
|
+
case "<=":
|
|
87
|
+
return { ok: count <= n };
|
|
88
|
+
case ">":
|
|
89
|
+
return { ok: count > n };
|
|
90
|
+
case "<":
|
|
91
|
+
return { ok: count < n };
|
|
92
|
+
default:
|
|
93
|
+
return { ok: count === n };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return specs.map((spec): AssertResult => {
|
|
98
|
+
const base = {
|
|
99
|
+
raw: spec.raw,
|
|
100
|
+
op: spec.op,
|
|
101
|
+
selector: spec.selector,
|
|
102
|
+
expected: spec.expected,
|
|
103
|
+
};
|
|
104
|
+
let nodes: Element[];
|
|
105
|
+
try {
|
|
106
|
+
nodes = Array.from(document.querySelectorAll(spec.selector));
|
|
107
|
+
} catch {
|
|
108
|
+
return { ...base, actual: "", found: false, outcome: "fail", error: "invalid selector" };
|
|
109
|
+
}
|
|
110
|
+
const first = nodes[0] ?? null;
|
|
111
|
+
const firstText = first ? textOf(first) : "";
|
|
112
|
+
|
|
113
|
+
switch (spec.op) {
|
|
114
|
+
case "exists":
|
|
115
|
+
return {
|
|
116
|
+
...base,
|
|
117
|
+
actual: String(nodes.length),
|
|
118
|
+
found: nodes.length > 0,
|
|
119
|
+
outcome: nodes.length > 0 ? "pass" : "fail",
|
|
120
|
+
};
|
|
121
|
+
case "absent":
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
actual: String(nodes.length),
|
|
125
|
+
found: nodes.length > 0,
|
|
126
|
+
outcome: nodes.length === 0 ? "pass" : "fail",
|
|
127
|
+
};
|
|
128
|
+
case "count": {
|
|
129
|
+
const { ok, error } = compareCount(nodes.length, spec.expected);
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
actual: String(nodes.length),
|
|
133
|
+
found: true,
|
|
134
|
+
outcome: ok ? "pass" : "fail",
|
|
135
|
+
...(error ? { error } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case "text":
|
|
139
|
+
return {
|
|
140
|
+
...base,
|
|
141
|
+
actual: firstText,
|
|
142
|
+
found: !!first,
|
|
143
|
+
outcome: first && firstText === spec.expected ? "pass" : "fail",
|
|
144
|
+
};
|
|
145
|
+
case "contains":
|
|
146
|
+
return {
|
|
147
|
+
...base,
|
|
148
|
+
actual: firstText,
|
|
149
|
+
found: !!first,
|
|
150
|
+
outcome: first && firstText.includes(spec.expected) ? "pass" : "fail",
|
|
151
|
+
};
|
|
152
|
+
case "matches": {
|
|
153
|
+
let re: RegExp;
|
|
154
|
+
try {
|
|
155
|
+
re = new RegExp(spec.expected);
|
|
156
|
+
} catch {
|
|
157
|
+
return {
|
|
158
|
+
...base,
|
|
159
|
+
actual: firstText,
|
|
160
|
+
found: !!first,
|
|
161
|
+
outcome: "fail",
|
|
162
|
+
error: `invalid regex '${spec.expected}'`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
actual: firstText,
|
|
168
|
+
found: !!first,
|
|
169
|
+
outcome: first && re.test(firstText) ? "pass" : "fail",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
return { ...base, actual: "", found: false, outcome: "fail", error: "unknown op" };
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -11,6 +11,15 @@ import {
|
|
|
11
11
|
type Request,
|
|
12
12
|
} from "playwright";
|
|
13
13
|
import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
|
|
14
|
+
import { type AssertResult, type AssertSpec, buildAssertCheck } from "./asserts.js";
|
|
15
|
+
import {
|
|
16
|
+
buildClearContentAnnotationsScript,
|
|
17
|
+
buildContentAnnotateScript,
|
|
18
|
+
buildContentChecks,
|
|
19
|
+
type ContentAnnotationBox,
|
|
20
|
+
type ContentChecksRequest,
|
|
21
|
+
type ContentChecksResult,
|
|
22
|
+
} from "./content-checks.js";
|
|
14
23
|
import {
|
|
15
24
|
buildClearLayoutLintAnnotationsScript,
|
|
16
25
|
buildLayoutLintAnnotateScript,
|
|
@@ -141,6 +150,38 @@ export interface Diagnostics {
|
|
|
141
150
|
|
|
142
151
|
const DEFAULT_PROFILE = resolve(homedir(), ".cache", "harnery", "browser-profile");
|
|
143
152
|
|
|
153
|
+
// Bound how many Chromium processes SPAWN at the same moment. When many Browser
|
|
154
|
+
// instances open at once — a full test suite, a fan-out of browse calls — the
|
|
155
|
+
// simultaneous `child_process.spawn`s exhaust the OS's stdio-pipe/socket
|
|
156
|
+
// resources and Chromium launch dies with an unhandled ENOENT that a per-call
|
|
157
|
+
// retry can't catch. Serializing only the brief launch phase (never the
|
|
158
|
+
// browser's lifetime, so N browsers still run concurrently) removes the burst
|
|
159
|
+
// that causes it. Overridable via HARNERY_MAX_BROWSER_LAUNCHES.
|
|
160
|
+
const MAX_CONCURRENT_LAUNCHES = Math.max(
|
|
161
|
+
1,
|
|
162
|
+
Number.parseInt(process.env.HARNERY_MAX_BROWSER_LAUNCHES ?? "3", 10) || 3,
|
|
163
|
+
);
|
|
164
|
+
let activeLaunches = 0;
|
|
165
|
+
const launchWaiters: Array<() => void> = [];
|
|
166
|
+
|
|
167
|
+
async function acquireLaunchSlot(): Promise<void> {
|
|
168
|
+
if (activeLaunches < MAX_CONCURRENT_LAUNCHES) {
|
|
169
|
+
activeLaunches++;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Queue; the releaser transfers its slot to us (activeLaunches unchanged).
|
|
173
|
+
await new Promise<void>((resolve) => launchWaiters.push(resolve));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function releaseLaunchSlot(): void {
|
|
177
|
+
const next = launchWaiters.shift();
|
|
178
|
+
if (next) {
|
|
179
|
+
next();
|
|
180
|
+
} else {
|
|
181
|
+
activeLaunches = Math.max(0, activeLaunches - 1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
144
185
|
export class Browser {
|
|
145
186
|
private context: BrowserContext | null = null;
|
|
146
187
|
private page: Page | null = null;
|
|
@@ -161,20 +202,27 @@ export class Browser {
|
|
|
161
202
|
return this.page;
|
|
162
203
|
}
|
|
163
204
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Launch the context and wire up the first page. Factored out of `open()` so
|
|
207
|
+
* the whole sequence — not just the launch — can be retried as a unit.
|
|
208
|
+
*/
|
|
209
|
+
private async openOnce(): Promise<void> {
|
|
210
|
+
// Hold a launch slot only across the spawn itself, not the browser's life.
|
|
211
|
+
await acquireLaunchSlot();
|
|
212
|
+
try {
|
|
213
|
+
this.context = await chromium.launchPersistentContext(this.profileDir, {
|
|
214
|
+
headless: !this.opts.headed,
|
|
215
|
+
viewport: this.opts.viewport ?? { width: 1280, height: 800 },
|
|
216
|
+
...(this.opts.launchArgs && this.opts.launchArgs.length > 0
|
|
217
|
+
? { args: this.opts.launchArgs }
|
|
218
|
+
: {}),
|
|
219
|
+
...(this.opts.recordHarPath
|
|
220
|
+
? { recordHar: { path: this.opts.recordHarPath, mode: "full" as const } }
|
|
221
|
+
: {}),
|
|
222
|
+
});
|
|
223
|
+
} finally {
|
|
224
|
+
releaseLaunchSlot();
|
|
225
|
+
}
|
|
178
226
|
this.context.setDefaultNavigationTimeout(this.opts.navigationTimeout ?? 30_000);
|
|
179
227
|
|
|
180
228
|
if (this.opts.jar) {
|
|
@@ -202,6 +250,54 @@ export class Browser {
|
|
|
202
250
|
this.attachDiagnosticListeners(this.page);
|
|
203
251
|
}
|
|
204
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Open the browser, retrying a few times on a transient startup failure.
|
|
255
|
+
* Chromium occasionally fails to hand off its CDP port ("Failed to connect",
|
|
256
|
+
* "Target closed") when many instances launch at once — a busy CI box or a
|
|
257
|
+
* full test suite — and the failure can surface on the launch OR on a
|
|
258
|
+
* follow-up call (addCookies, newPage) when the process dies right after
|
|
259
|
+
* spawning. So the entire open sequence retries as a unit, tearing down any
|
|
260
|
+
* half-built context between attempts. A genuinely broken config fails every
|
|
261
|
+
* attempt and throws with the underlying message.
|
|
262
|
+
*/
|
|
263
|
+
async open(): Promise<void> {
|
|
264
|
+
if (this.context) return;
|
|
265
|
+
mkdirSync(this.profileDir, { recursive: true });
|
|
266
|
+
|
|
267
|
+
const maxAttempts = 3;
|
|
268
|
+
let lastErr: unknown;
|
|
269
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
270
|
+
try {
|
|
271
|
+
await this.openOnce();
|
|
272
|
+
return;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
lastErr = err;
|
|
275
|
+
// Tear down a partial context + reset diagnostics collected on the
|
|
276
|
+
// failed attempt, so the retry starts clean. (openOnce() reassigns
|
|
277
|
+
// this.context, which TS can't see across the call, so re-widen.)
|
|
278
|
+
const partial = this.context as BrowserContext | null;
|
|
279
|
+
if (partial) {
|
|
280
|
+
try {
|
|
281
|
+
await partial.close();
|
|
282
|
+
} catch {
|
|
283
|
+
// ignore — the process is likely already gone
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
this.context = null;
|
|
287
|
+
this.page = null;
|
|
288
|
+
this.consoleEvents = [];
|
|
289
|
+
this.pageErrors = [];
|
|
290
|
+
this.failedRequests = [];
|
|
291
|
+
if (attempt < maxAttempts) {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 250));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Failed to open browser after ${maxAttempts} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
205
301
|
/**
|
|
206
302
|
* Hook console + pageerror + requestfailed events. Called on `open()`
|
|
207
303
|
* before any navigation so we don't miss early-fired events.
|
|
@@ -295,6 +391,65 @@ export class Browser {
|
|
|
295
391
|
return buf.length;
|
|
296
392
|
}
|
|
297
393
|
|
|
394
|
+
/** Full document + viewport dimensions, for tiling a page into bands. */
|
|
395
|
+
async pageMetrics(): Promise<{
|
|
396
|
+
scrollWidth: number;
|
|
397
|
+
scrollHeight: number;
|
|
398
|
+
viewportWidth: number;
|
|
399
|
+
viewportHeight: number;
|
|
400
|
+
}> {
|
|
401
|
+
return await this.currentPage.evaluate(() => ({
|
|
402
|
+
scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
403
|
+
scrollHeight: Math.max(
|
|
404
|
+
document.documentElement.scrollHeight,
|
|
405
|
+
document.body?.scrollHeight ?? 0,
|
|
406
|
+
),
|
|
407
|
+
viewportWidth: window.innerWidth,
|
|
408
|
+
viewportHeight: window.innerHeight,
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Full-page PNG as a Buffer, for cropping critique tiles in pixel space. */
|
|
413
|
+
async fullPageScreenshotBuffer(): Promise<Buffer> {
|
|
414
|
+
return await this.currentPage.screenshot({ type: "png", fullPage: true });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Base64 PNG of a document-space clip rect. Used to capture one critique tile. */
|
|
418
|
+
async screenshotClipBase64(rect: {
|
|
419
|
+
x: number;
|
|
420
|
+
y: number;
|
|
421
|
+
width: number;
|
|
422
|
+
height: number;
|
|
423
|
+
}): Promise<string> {
|
|
424
|
+
const buf = await this.currentPage.screenshot({ type: "png", clip: rect });
|
|
425
|
+
return buf.toString("base64");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Document-space rects + labels for each element matching `selector` (semantic tiling). */
|
|
429
|
+
async elementTiles(
|
|
430
|
+
selector: string,
|
|
431
|
+
): Promise<Array<{ label: string; x: number; y: number; width: number; height: number }>> {
|
|
432
|
+
return await this.currentPage.evaluate((sel) => {
|
|
433
|
+
const out: Array<{ label: string; x: number; y: number; width: number; height: number }> = [];
|
|
434
|
+
const sx = window.scrollX;
|
|
435
|
+
const sy = window.scrollY;
|
|
436
|
+
for (const el of Array.from(document.querySelectorAll(sel))) {
|
|
437
|
+
const r = el.getBoundingClientRect();
|
|
438
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
439
|
+
const tag = el.tagName.toLowerCase();
|
|
440
|
+
const cls = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : "";
|
|
441
|
+
out.push({
|
|
442
|
+
label: el.id ? `${tag}#${el.id}` : cls ? `${tag}.${cls}` : tag,
|
|
443
|
+
x: Math.max(0, r.x + sx),
|
|
444
|
+
y: Math.max(0, r.y + sy),
|
|
445
|
+
width: r.width,
|
|
446
|
+
height: r.height,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}, selector);
|
|
451
|
+
}
|
|
452
|
+
|
|
298
453
|
/**
|
|
299
454
|
* Capture a full-page screenshot from an explicit capture viewport and
|
|
300
455
|
* evaluate the caller's final evidence expression immediately before the
|
|
@@ -600,6 +755,26 @@ export class Browser {
|
|
|
600
755
|
await this.currentPage.evaluate(buildClearLayoutLintAnnotationsScript());
|
|
601
756
|
}
|
|
602
757
|
|
|
758
|
+
/** Run the requested content checks (placeholder/image/truncation/contrast) in one evaluation. */
|
|
759
|
+
async checkContent(request: ContentChecksRequest): Promise<ContentChecksResult> {
|
|
760
|
+
return await this.currentPage.evaluate(buildContentChecks(), request);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Evaluate value assertions (text/contains/matches/count/exists/absent) against the page. */
|
|
764
|
+
async checkAsserts(specs: AssertSpec[]): Promise<AssertResult[]> {
|
|
765
|
+
return await this.currentPage.evaluate(buildAssertCheck(), specs);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Draw one document-space annotation layer for content-check hits. */
|
|
769
|
+
async annotateContent(boxes: ContentAnnotationBox[]): Promise<void> {
|
|
770
|
+
await this.currentPage.evaluate(buildContentAnnotateScript(), { boxes });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Remove content-check annotations. */
|
|
774
|
+
async clearContentAnnotations(): Promise<void> {
|
|
775
|
+
await this.currentPage.evaluate(buildClearContentAnnotationsScript());
|
|
776
|
+
}
|
|
777
|
+
|
|
603
778
|
/**
|
|
604
779
|
* Run the target-size rule against the page or explicit scopes. The
|
|
605
780
|
* dependency's browser bundle is evaluated directly in the page context so
|