harnery 0.40.0 → 0.41.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 +1 -1
- package/dist/commander.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +25 -19
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +4 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +141 -10
- package/dist/commands/checkpoint.js +8 -12
- package/dist/commands/deinit.d.ts +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +8 -4
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +2 -0
- package/dist/commands/doctor.js +10 -2
- package/dist/commands/fetch.d.ts.map +1 -1
- package/dist/commands/fetch.js +10 -0
- package/dist/commands/init.d.ts +19 -3
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +232 -104
- package/dist/commands/instructions.js +5 -4
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +2 -1
- package/dist/commands/webview.d.ts +4 -0
- package/dist/commands/webview.d.ts.map +1 -0
- package/dist/commands/webview.js +94 -0
- package/dist/core/adapter.d.ts +50 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +59 -1
- package/dist/core/adapters/attestation.d.ts.map +1 -1
- package/dist/core/adapters/attestation.js +3 -2
- package/dist/core/adapters/profiles.d.ts +20 -3
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +32 -1
- package/dist/core/adapters/registry.d.ts.map +1 -1
- package/dist/core/adapters/registry.js +33 -0
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -7
- package/dist/core/agents/cli.js +18 -16
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +7 -2
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +3 -9
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +2 -8
- package/dist/core/agents/rules/stop-hook.d.ts +2 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +1 -1
- package/dist/core/agents/session-events.d.ts +6 -2
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +105 -26
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalization-state-v3.js +2 -1
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalizer-v3.js +5 -4
- package/dist/core/agents/session-name-display.d.ts +28 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +43 -0
- 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 +10 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -2
- 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 +3 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +2 -10
- package/dist/core/agents/turn-changes.d.ts +17 -0
- package/dist/core/agents/turn-changes.d.ts.map +1 -0
- package/dist/core/agents/turn-changes.js +76 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +4 -3
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -0
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +2 -1
- package/dist/core/conversations/archive.d.ts.map +1 -1
- package/dist/core/conversations/archive.js +4 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +3 -2
- package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/canary.js +5 -4
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/compression.js +2 -1
- package/dist/core/events/v3/adapter-id.d.ts +10 -2
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
- package/dist/core/events/v3/adapter-id.js +16 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +2 -1
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +5 -4
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +17 -0
- package/dist/core/events/v3/contract.d.ts +5 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -1
- package/dist/core/events/v3/contract.js +7 -1
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +3 -2
- package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
- package/dist/core/events/v3/fingerprint-keys.js +7 -6
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +5 -4
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +8 -7
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
- package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
- package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +10 -4
- package/dist/core/events/v3/producers/hook.d.ts +2 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +16 -1
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +16 -6
- package/dist/core/events/v3/producers/recorder.d.ts +2 -4
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +109 -101
- package/dist/core/events/v3/recovery.d.ts.map +1 -1
- package/dist/core/events/v3/recovery.js +4 -3
- package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/inventory.js +2 -1
- package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/maintenance.js +4 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
- package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
- package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
- package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
- package/dist/core/events/v3/tool-line-changes.js +135 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -1
- package/dist/core/events/v3/writer.js +8 -7
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +6 -5
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +12 -11
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +7 -6
- package/dist/core/guard/evaluator.js +1 -0
- package/dist/core/guard/storage.d.ts.map +1 -1
- package/dist/core/guard/storage.js +3 -2
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/index.js +20 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +1 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +2 -3
- package/dist/core/hooks/adapter/events.d.ts +29 -1
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +31 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
- package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
- package/dist/core/hooks/adapter/output.d.ts +7 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +8 -1
- package/dist/core/hooks/adapter/wiring.d.ts +15 -1
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +74 -11
- package/dist/core/hooks/cli.js +635 -529
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +2 -1
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
- package/dist/core/hooks/prompt-context/contract.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +1 -1
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -4
- package/dist/core/hooks/session-name-presence.d.ts +3 -1
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +7 -2
- package/dist/core/hooks/shell-waiter.d.ts +2 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
- package/dist/core/hooks/shell-waiter.js +173 -0
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +5 -4
- package/dist/core/semantic/review.d.ts.map +1 -1
- package/dist/core/semantic/review.js +5 -4
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +15 -14
- package/dist/core/semantic/storage.d.ts.map +1 -1
- package/dist/core/semantic/storage.js +5 -4
- package/dist/core/storage/atomic-json.d.ts.map +1 -1
- package/dist/core/storage/atomic-json.js +7 -3
- package/dist/core/storage/durable-history.d.ts.map +1 -1
- package/dist/core/storage/durable-history.js +6 -5
- package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
- package/dist/core/storage/maintenance-providers.js +2 -1
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +7 -6
- package/dist/core/storage/metrics.d.ts.map +1 -1
- package/dist/core/storage/metrics.js +5 -1
- package/dist/core/storage/modes.d.ts +17 -0
- package/dist/core/storage/modes.d.ts.map +1 -0
- package/dist/core/storage/modes.js +47 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +3 -2
- package/dist/core/storage/receipts.d.ts.map +1 -1
- package/dist/core/storage/receipts.js +4 -3
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +7 -6
- package/dist/core/supervisor/hook-health.d.ts +2 -1
- package/dist/core/supervisor/hook-health.d.ts.map +1 -1
- package/dist/core/supervisor/hook-health.js +4 -3
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +7 -6
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +8 -7
- package/dist/core/workflow/adapters.d.ts +7 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -1
- package/dist/core/workflow/adapters.js +9 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +6 -5
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +33 -4
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +4 -3
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +2 -3
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -2
- package/dist/core/workflow/spawn-opencode.d.ts +36 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
- package/dist/core/workflow/spawn-opencode.js +132 -0
- package/dist/core/workflow/transcript.d.ts.map +1 -1
- package/dist/core/workflow/transcript.js +3 -2
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +5 -4
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +3 -2
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/paths.js +2 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +10 -9
- package/dist/lazy-commands.js +8 -1
- package/dist/lib/agent-browser/client.d.ts +9 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +13 -0
- package/dist/lib/browser/client.d.ts +35 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +117 -8
- package/dist/lib/browser/identity.d.ts +25 -0
- package/dist/lib/browser/identity.d.ts.map +1 -0
- package/dist/lib/browser/identity.js +66 -0
- package/dist/lib/browser/index.d.ts +2 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/launch-args.d.ts +26 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -1
- package/dist/lib/browser/launch-args.js +41 -1
- package/dist/lib/browser/user-agent.d.ts +53 -0
- package/dist/lib/browser/user-agent.d.ts.map +1 -0
- package/dist/lib/browser/user-agent.js +79 -0
- package/dist/lib/docs-metadata-managed.d.ts +1 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +17 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -9
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +2 -0
- package/dist/lib/pace/index.d.ts +2 -0
- package/dist/lib/pace/index.d.ts.map +1 -0
- package/dist/lib/pace/index.js +1 -0
- package/dist/lib/pace/pace.d.ts +95 -0
- package/dist/lib/pace/pace.d.ts.map +1 -0
- package/dist/lib/pace/pace.js +313 -0
- package/dist/lib/tunnel/forward-headers.d.ts +11 -0
- package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
- package/dist/lib/tunnel/forward-headers.js +17 -0
- package/dist/lib/tunnel/gate.js +15 -10
- package/dist/lib/webview/index.d.ts +2 -0
- package/dist/lib/webview/index.d.ts.map +1 -0
- package/dist/lib/webview/index.js +1 -0
- package/dist/lib/webview/webview.d.ts +116 -0
- package/dist/lib/webview/webview.d.ts.map +1 -0
- package/dist/lib/webview/webview.js +289 -0
- package/opencode-plugin/package.json +19 -0
- package/opencode-plugin/src/index.ts +791 -0
- package/package.json +11 -4
- package/schemas/config.schema.json +13 -0
- package/schemas/event-v3.schema.json +164 -0
- package/src/commander.ts +7 -0
- package/src/commands/agents.ts +33 -18
- package/src/commands/browse-ai.ts +8 -0
- package/src/commands/browse.ts +189 -10
- package/src/commands/checkpoint.ts +7 -9
- package/src/commands/deinit.ts +9 -4
- package/src/commands/docs.ts +2 -0
- package/src/commands/doctor.ts +11 -2
- package/src/commands/fetch.ts +14 -0
- package/src/commands/init.ts +281 -119
- package/src/commands/instructions.ts +8 -4
- package/src/commands/ledger-v3.ts +2 -1
- package/src/commands/webview.ts +159 -0
- package/src/core/adapter.ts +102 -1
- package/src/core/adapters/attestation.ts +3 -2
- package/src/core/adapters/profiles.ts +42 -1
- package/src/core/adapters/registry.ts +37 -0
- package/src/core/agents/canonical-emit.ts +2 -5
- package/src/core/agents/cli.ts +18 -17
- package/src/core/agents/coord-client.ts +9 -3
- package/src/core/agents/rules/claim-conflict.ts +3 -8
- package/src/core/agents/rules/commit-conflict.ts +2 -7
- package/src/core/agents/rules/stop-hook.ts +3 -1
- package/src/core/agents/session-events.ts +120 -26
- package/src/core/agents/session-finalization-state-v3.ts +2 -1
- package/src/core/agents/session-finalizer-v3.ts +5 -4
- package/src/core/agents/session-name-display.ts +48 -0
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +24 -2
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +3 -0
- package/src/core/agents/state/stale-sweep.ts +2 -9
- package/src/core/agents/turn-changes.ts +97 -0
- package/src/core/artifacts/delivery-card.ts +2 -1
- package/src/core/artifacts/index.ts +4 -3
- package/src/core/config.ts +23 -0
- package/src/core/context/index.ts +2 -1
- package/src/core/conversations/archive.ts +4 -3
- package/src/core/diagnostics/bundle.ts +3 -2
- package/src/core/events/legacy-storage/canary.ts +5 -4
- package/src/core/events/legacy-storage/compression.ts +2 -1
- package/src/core/events/v3/adapter-id.ts +29 -1
- package/src/core/events/v3/authority-outbox.ts +2 -1
- package/src/core/events/v3/bootstrap.ts +5 -4
- package/src/core/events/v3/capabilities.ts +17 -0
- package/src/core/events/v3/contract.ts +9 -1
- package/src/core/events/v3/control-witness.ts +3 -2
- package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
- package/src/core/events/v3/fingerprint-keys.ts +7 -6
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/live-feed.ts +5 -4
- package/src/core/events/v3/producers/command-recorder.ts +8 -6
- package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
- package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
- package/src/core/events/v3/producers/hook-base.ts +11 -4
- package/src/core/events/v3/producers/hook.ts +19 -1
- package/src/core/events/v3/producers/intake.ts +18 -6
- package/src/core/events/v3/producers/recorder.ts +123 -114
- package/src/core/events/v3/recovery.ts +4 -3
- package/src/core/events/v3/support-storage/inventory.ts +2 -1
- package/src/core/events/v3/support-storage/maintenance.ts +8 -3
- package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
- package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
- package/src/core/events/v3/tool-line-changes.ts +139 -0
- package/src/core/events/v3/writer.ts +8 -7
- package/src/core/governor/planning.ts +6 -5
- package/src/core/governor/service.ts +12 -11
- package/src/core/governor/state.ts +7 -6
- package/src/core/guard/evaluator.ts +1 -0
- package/src/core/guard/storage.ts +3 -2
- package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
- package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
- package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
- package/src/core/hooks/adapter/behaviors/index.ts +31 -0
- package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
- package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
- package/src/core/hooks/adapter/behaviors/types.ts +110 -0
- package/src/core/hooks/adapter/detect.ts +2 -3
- package/src/core/hooks/adapter/events.ts +53 -1
- package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
- package/src/core/hooks/adapter/output.ts +8 -1
- package/src/core/hooks/adapter/wiring.ts +85 -11
- package/src/core/hooks/cli.ts +733 -537
- package/src/core/hooks/effects/image-capture.ts +2 -1
- package/src/core/hooks/prompt-context/contract.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +42 -5
- package/src/core/hooks/session-name-presence.ts +10 -3
- package/src/core/hooks/shell-waiter.ts +166 -0
- package/src/core/resources/service.ts +5 -4
- package/src/core/semantic/review.ts +5 -4
- package/src/core/semantic/service.ts +15 -14
- package/src/core/semantic/storage.ts +5 -4
- package/src/core/storage/atomic-json.ts +7 -3
- package/src/core/storage/durable-history.ts +6 -5
- package/src/core/storage/maintenance-providers.ts +2 -1
- package/src/core/storage/maintenance.ts +7 -6
- package/src/core/storage/metrics.ts +5 -1
- package/src/core/storage/modes.ts +53 -0
- package/src/core/storage/process-log.ts +3 -2
- package/src/core/storage/receipts.ts +4 -3
- package/src/core/storage/segments.ts +7 -6
- package/src/core/supervisor/hook-health.ts +4 -4
- package/src/core/supervisor/service.ts +7 -6
- package/src/core/work/state.ts +8 -7
- package/src/core/workflow/adapters.ts +15 -0
- package/src/core/workflow/approvals.ts +6 -5
- package/src/core/workflow/billing.ts +34 -4
- package/src/core/workflow/durable-record.ts +4 -3
- package/src/core/workflow/live-session-v3.ts +2 -3
- package/src/core/workflow/run-state.ts +3 -2
- package/src/core/workflow/spawn-opencode.ts +150 -0
- package/src/core/workflow/transcript.ts +3 -2
- package/src/core/workflow/workspaces/leases.ts +5 -4
- package/src/core/workflow/workspaces/local-git.ts +3 -2
- package/src/core/workflow/workspaces/paths.ts +2 -1
- package/src/core/workflow/workspaces/state.ts +10 -9
- package/src/lazy-commands.ts +11 -1
- package/src/lib/agent-browser/client.ts +18 -0
- package/src/lib/browser/client.ts +147 -9
- package/src/lib/browser/identity.ts +64 -0
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/launch-args.ts +53 -1
- package/src/lib/browser/user-agent.ts +130 -0
- package/src/lib/docs-metadata-managed.ts +21 -0
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +2 -0
- package/src/lib/pace/index.ts +14 -0
- package/src/lib/pace/pace.ts +370 -0
- package/src/lib/tunnel/forward-headers.ts +16 -0
- package/src/lib/tunnel/gate.ts +18 -10
- package/src/lib/webview/index.ts +13 -0
- package/src/lib/webview/webview.ts +457 -0
package/src/core/hooks/cli.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import { coordEnv } from "../../lib/env.ts";
|
|
24
24
|
import { buildInstructionBundle } from "../../lib/instructions/bundle.ts";
|
|
25
|
-
import type
|
|
25
|
+
import { type Adapter, adapterFromPlatform as sharedAdapterFromPlatform } from "../adapter.ts";
|
|
26
26
|
import { resolveCoordRoot } from "../agents/coord-client.ts";
|
|
27
27
|
import {
|
|
28
28
|
type ClaimFinalizationDecision,
|
|
@@ -43,12 +43,15 @@ import {
|
|
|
43
43
|
sessionNameDisplayInstruction,
|
|
44
44
|
sessionNameDisplayPending,
|
|
45
45
|
sessionNameDisplayRecoveryInstruction,
|
|
46
|
+
sessionNameDisplayReminder,
|
|
47
|
+
sessionNameDisplayReminderDue,
|
|
46
48
|
toolResponseMintedSessionName,
|
|
47
49
|
} from "../agents/session-name-display.ts";
|
|
48
50
|
import type { Heartbeat } from "../agents/state/heartbeat-reader.ts";
|
|
49
51
|
import {
|
|
50
52
|
readHeartbeat,
|
|
51
53
|
setAssignedNameCache,
|
|
54
|
+
stampSessionNameReminded,
|
|
52
55
|
stampSessionNameRequested,
|
|
53
56
|
stampSessionNameSeen,
|
|
54
57
|
} from "../agents/state/heartbeat-writer.ts";
|
|
@@ -80,10 +83,12 @@ import {
|
|
|
80
83
|
recordLiveHookSignalV3,
|
|
81
84
|
resolveLiveEventLedgerRouteV3,
|
|
82
85
|
} from "../events/v3/live-routing.ts";
|
|
86
|
+
import type { TurnRitualEvidenceV3 } from "../events/v3/producers/hook-base.ts";
|
|
83
87
|
import { captureSpanClockV3 } from "../events/v3/span-state.ts";
|
|
84
88
|
import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.ts";
|
|
85
89
|
import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
|
|
86
90
|
import { stableScopeId } from "../workflow/scope-id.ts";
|
|
91
|
+
import { type AdapterBehavior, adapterBehavior } from "./adapter/behaviors/index.ts";
|
|
87
92
|
import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.ts";
|
|
88
93
|
import {
|
|
89
94
|
extractBashCommand,
|
|
@@ -93,14 +98,7 @@ import {
|
|
|
93
98
|
type ParsedPayload,
|
|
94
99
|
parsePayload,
|
|
95
100
|
} from "./adapter/parse.ts";
|
|
96
|
-
import { discoverCodexSessionTranscript } from "./adapter/runtime-telemetry.ts";
|
|
97
101
|
import { scheduleBackupSnapshot } from "./backup-schedule.ts";
|
|
98
|
-
import {
|
|
99
|
-
codexWslFileLinkTelemetry,
|
|
100
|
-
inspectCodexWslBridge,
|
|
101
|
-
isWslUncPath,
|
|
102
|
-
renderCodexWslFileLinkContext,
|
|
103
|
-
} from "./codex-wsl-bridge.ts";
|
|
104
102
|
import {
|
|
105
103
|
captureImages,
|
|
106
104
|
detectPresence,
|
|
@@ -130,12 +128,11 @@ import type { SessionNameDisplayInspection } from "./resolve/transcript.ts";
|
|
|
130
128
|
import {
|
|
131
129
|
inspectSessionNameDisplayImmediately,
|
|
132
130
|
scanAssistantStatusBoxPresent,
|
|
133
|
-
scanSessionNameDisplayedImmediately,
|
|
134
131
|
scanStatusBoxPresent,
|
|
135
132
|
scanTranscriptRuntime,
|
|
136
133
|
} from "./resolve/transcript.ts";
|
|
137
134
|
import { sessionNamePresence } from "./session-name-presence.ts";
|
|
138
|
-
import {
|
|
135
|
+
import { shellWaiterReason } from "./shell-waiter.ts";
|
|
139
136
|
|
|
140
137
|
interface Argv {
|
|
141
138
|
eventName: string | null;
|
|
@@ -341,12 +338,9 @@ function buildEventData(
|
|
|
341
338
|
const p = ctx.payload;
|
|
342
339
|
switch (eventType) {
|
|
343
340
|
case "session.started": {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
: ctx.adapter === "cursor"
|
|
348
|
-
? "cursor"
|
|
349
|
-
: "codex";
|
|
341
|
+
// The platform label is the adapter id itself; enumerating and defaulting
|
|
342
|
+
// to codex silently mislabels any adapter not in the list.
|
|
343
|
+
const adapterPlatform = ctx.adapter;
|
|
350
344
|
// Recorded fork lineage is NOT detected here. On claude-code a fork
|
|
351
345
|
// never fires its own session.started — SessionStart fires under the
|
|
352
346
|
// PARENT's session id (source=resume) before the fork id is minted
|
|
@@ -433,9 +427,11 @@ function buildEventData(
|
|
|
433
427
|
case "turn.completed": {
|
|
434
428
|
const lastAssistantMessage = (p?.raw.last_assistant_message as string | undefined) ?? "";
|
|
435
429
|
const fileLinkTelemetry =
|
|
436
|
-
ctx.adapter
|
|
437
|
-
|
|
438
|
-
|
|
430
|
+
adapterBehavior(ctx.adapter).fileLinkTelemetry?.(
|
|
431
|
+
ctx.coordRoot,
|
|
432
|
+
p?.cwd,
|
|
433
|
+
lastAssistantMessage,
|
|
434
|
+
) ?? null;
|
|
439
435
|
return {
|
|
440
436
|
// Backfill the model for adapters that omit it at session.started
|
|
441
437
|
// (Claude Code). The transcript is populated with assistant turns by
|
|
@@ -471,13 +467,12 @@ function buildEventData(
|
|
|
471
467
|
// exists (the stop-hook.session_name verdict reads it per turn); the
|
|
472
468
|
// transcript scan itself stops once the name has been sighted.
|
|
473
469
|
...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) =>
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
// session id so the naming ritual can
|
|
470
|
+
inspectSessionNameDisplayImmediately(
|
|
471
|
+
// Some adapters omit transcript_path on stop; the behavior may
|
|
472
|
+
// discover the transcript by session id so the naming ritual can
|
|
473
|
+
// verify and stamp there too.
|
|
477
474
|
p?.transcript_path ??
|
|
478
|
-
(ctx.adapter
|
|
479
|
-
? discoverCodexSessionTranscript(p?.session_id ?? ctx.instanceId)
|
|
480
|
-
: undefined),
|
|
475
|
+
adapterBehavior(ctx.adapter).discoverTranscript?.(p?.session_id ?? ctx.instanceId),
|
|
481
476
|
name,
|
|
482
477
|
assistantTextStartsWithSessionNameBlock,
|
|
483
478
|
),
|
|
@@ -622,12 +617,52 @@ function numberField(value: unknown): number | undefined {
|
|
|
622
617
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
623
618
|
}
|
|
624
619
|
|
|
620
|
+
/** Everything one hook invocation resolved before its event handlers run. */
|
|
621
|
+
interface HookRun {
|
|
622
|
+
coordRoot: string;
|
|
623
|
+
adapter: Adapter;
|
|
624
|
+
behavior: AdapterBehavior;
|
|
625
|
+
eventName: string;
|
|
626
|
+
norm: NonNullable<ReturnType<typeof normalizeEventName>>;
|
|
627
|
+
payload: ParsedPayload | null;
|
|
628
|
+
raw: string;
|
|
629
|
+
owner: NonNullable<ReturnType<typeof resolveOwner>>;
|
|
630
|
+
sessionId: string;
|
|
631
|
+
data: Record<string, unknown>;
|
|
632
|
+
debugBase: Record<string, unknown>;
|
|
633
|
+
ledgerRoute: Exclude<ReturnType<typeof resolveLiveEventLedgerRouteV3>, { state: "blocked" }>;
|
|
634
|
+
hookStartedAt: number;
|
|
635
|
+
hookClock: ReturnType<typeof captureSpanClockV3>;
|
|
636
|
+
priorCoordination: Heartbeat | null;
|
|
637
|
+
topLevelSession: boolean;
|
|
638
|
+
stopRemediation: boolean;
|
|
639
|
+
bridge: ReturnType<AdapterBehavior["bridgeFor"]>;
|
|
640
|
+
turnRitual: TurnRitualEvidenceV3 | undefined;
|
|
641
|
+
/** Set by recordSignal once the canonical V3 event has landed. */
|
|
642
|
+
v3Result?: ReturnType<typeof recordLiveHookSignalV3>;
|
|
643
|
+
v3EventId?: string;
|
|
644
|
+
recordedGenerationId?: `gen_${string}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** The completed-reply ritual path runs before event normalization and owner resolution. */
|
|
648
|
+
interface CompletedResponseRun {
|
|
649
|
+
coordRoot: string;
|
|
650
|
+
adapter: Adapter;
|
|
651
|
+
eventName: string;
|
|
652
|
+
raw: string;
|
|
653
|
+
debugBase: Record<string, unknown>;
|
|
654
|
+
ledgerRoute: ReturnType<typeof resolveLiveEventLedgerRouteV3>;
|
|
655
|
+
hookStartedAt: number;
|
|
656
|
+
hookClock: ReturnType<typeof captureSpanClockV3>;
|
|
657
|
+
}
|
|
658
|
+
|
|
625
659
|
async function main(): Promise<number> {
|
|
626
660
|
const hookStartedAt = performance.now();
|
|
627
661
|
const hookStartedRss = process.memoryUsage().rss;
|
|
628
662
|
const hookClock = captureSpanClockV3();
|
|
629
663
|
const { eventName, extra } = parseArgv(process.argv.slice(2));
|
|
630
664
|
const adapter = detectAdapter(process.argv.slice(2));
|
|
665
|
+
const behavior = adapter ? adapterBehavior(adapter) : null;
|
|
631
666
|
const raw = await readStdin();
|
|
632
667
|
hookHealthState = beginHookHealth({
|
|
633
668
|
started_at_ms: hookStartedAt,
|
|
@@ -667,8 +702,8 @@ async function main(): Promise<number> {
|
|
|
667
702
|
// Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
|
|
668
703
|
// HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
|
|
669
704
|
// bypass: sound playback happens before the kill-switch bailout.
|
|
670
|
-
//
|
|
671
|
-
if (
|
|
705
|
+
// Behavior-gated; stop-failure → error, sub-agent-start → subagent-start.
|
|
706
|
+
if (behavior?.soundEffects && eventName) {
|
|
672
707
|
const s = soundForEvent(eventName);
|
|
673
708
|
if (s) {
|
|
674
709
|
const repoRoot = resolveCoordRoot(process.cwd());
|
|
@@ -724,7 +759,7 @@ async function main(): Promise<number> {
|
|
|
724
759
|
ppid: process.ppid,
|
|
725
760
|
};
|
|
726
761
|
|
|
727
|
-
if (!eventName || !adapter) {
|
|
762
|
+
if (!eventName || !adapter || !behavior) {
|
|
728
763
|
appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-adapter" });
|
|
729
764
|
return 0;
|
|
730
765
|
}
|
|
@@ -736,88 +771,17 @@ async function main(): Promise<number> {
|
|
|
736
771
|
// assistant text. Preserve only privacy-safe ritual booleans in the V3
|
|
737
772
|
// producer's open-turn state; Stop consumes them into the authoritative
|
|
738
773
|
// turn.completed event. The response body never enters V3 or heartbeat.
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
|
|
742
|
-
if (!owner) {
|
|
743
|
-
appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
|
|
744
|
-
return 0;
|
|
745
|
-
}
|
|
746
|
-
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
747
|
-
const name = sessionNameDisplayPending(row);
|
|
748
|
-
const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
|
|
749
|
-
const sighting = matchSessionNameDisplay(row, text);
|
|
750
|
-
if (sighting) {
|
|
751
|
-
stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
|
|
752
|
-
appendDebug(coordRoot, {
|
|
753
|
-
...debugBase,
|
|
754
|
-
effect: "session-name-display-stamped",
|
|
755
|
-
session_name_pending: sighting.pending,
|
|
756
|
-
...(sighting.displayed === sighting.pending
|
|
757
|
-
? {}
|
|
758
|
-
: { session_name_displayed: sighting.displayed, session_name_drift: true }),
|
|
759
|
-
});
|
|
760
|
-
} else if (!name) {
|
|
761
|
-
// Nothing was owed. Distinct from a miss: conflating the two is what made
|
|
762
|
-
// a latched Cursor session undiagnosable from this log.
|
|
763
|
-
appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
|
|
764
|
-
} else {
|
|
765
|
-
appendDebug(coordRoot, {
|
|
766
|
-
...debugBase,
|
|
767
|
-
skipped: "session-name-block-absent",
|
|
768
|
-
session_name_pending: name,
|
|
769
|
-
reply_bytes: text.length,
|
|
770
|
-
reply_leads_with_fence: /^\s*`{3,}/.test(text),
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
if (ledgerRoute.state === "blocked") {
|
|
774
|
-
appendDebug(coordRoot, {
|
|
775
|
-
...debugBase,
|
|
776
|
-
skipped: "v3-control-blocked",
|
|
777
|
-
reason: ledgerRoute.reason,
|
|
778
|
-
owner_source: owner.source,
|
|
779
|
-
});
|
|
780
|
-
return 0;
|
|
781
|
-
}
|
|
782
|
-
const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
|
|
783
|
-
// The hook parser needs the completed reply long enough to scan it, but
|
|
784
|
-
// durable hook intake must never retain that body. Keep every other native
|
|
785
|
-
// field so turn correlation remains available to the producer.
|
|
786
|
-
const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
|
|
787
|
-
const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
|
|
788
|
-
const v3Result = recordLiveHookSignalV3({
|
|
774
|
+
if (behavior && eventName && eventName === behavior.completedResponseEvent) {
|
|
775
|
+
return handleCompletedResponse({
|
|
789
776
|
coordRoot,
|
|
790
|
-
route: ledgerRoute,
|
|
791
|
-
eventName,
|
|
792
|
-
payload: privacySafePayload,
|
|
793
777
|
adapter,
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
status_box_present_strict: statusBoxPresentStrict,
|
|
801
|
-
session_name_required: name !== undefined,
|
|
802
|
-
session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
|
|
803
|
-
},
|
|
804
|
-
});
|
|
805
|
-
observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
|
|
806
|
-
appendDebug(coordRoot, {
|
|
807
|
-
...debugBase,
|
|
808
|
-
effect: "cursor-response-ritual-observed",
|
|
809
|
-
owner_source: owner.source,
|
|
810
|
-
event_v3_state: v3Result.state,
|
|
811
|
-
...(v3Result.state === "observed"
|
|
812
|
-
? {
|
|
813
|
-
generation_id: v3Result.generation_id,
|
|
814
|
-
turn_id: v3Result.turn_id,
|
|
815
|
-
response_observed_at: v3Result.observed_at,
|
|
816
|
-
status_box_present_strict: statusBoxPresentStrict,
|
|
817
|
-
}
|
|
818
|
-
: {}),
|
|
778
|
+
eventName,
|
|
779
|
+
raw,
|
|
780
|
+
debugBase,
|
|
781
|
+
ledgerRoute,
|
|
782
|
+
hookStartedAt,
|
|
783
|
+
hookClock,
|
|
819
784
|
});
|
|
820
|
-
return 0;
|
|
821
785
|
}
|
|
822
786
|
|
|
823
787
|
const norm = normalizeEventName(eventName);
|
|
@@ -827,6 +791,7 @@ async function main(): Promise<number> {
|
|
|
827
791
|
}
|
|
828
792
|
|
|
829
793
|
const payload = parsePayload(raw, adapter);
|
|
794
|
+
const bridge = behavior.bridgeFor(payload?.cwd);
|
|
830
795
|
const owner = resolveOwner({
|
|
831
796
|
payload: payload?.raw ?? null,
|
|
832
797
|
coordRoot,
|
|
@@ -874,8 +839,20 @@ async function main(): Promise<number> {
|
|
|
874
839
|
status_box_present_strict: data.status_box_present_strict === true,
|
|
875
840
|
session_name_required: typeof data.session_name_present_for === "string",
|
|
876
841
|
session_name_present: data.session_name_present === true,
|
|
842
|
+
...(typeof data.session_name_unavailable_reason === "string"
|
|
843
|
+
? { session_name_unavailable_reason: data.session_name_unavailable_reason }
|
|
844
|
+
: {}),
|
|
877
845
|
}
|
|
878
846
|
: undefined;
|
|
847
|
+
if (typeof data.session_name_unavailable_reason === "string") {
|
|
848
|
+
reportSessionNameObservationUnavailable(
|
|
849
|
+
coordRoot,
|
|
850
|
+
owner.instance_id,
|
|
851
|
+
adapter,
|
|
852
|
+
eventName,
|
|
853
|
+
data.session_name_unavailable_reason,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
879
856
|
const stopRemediation =
|
|
880
857
|
norm.event_type === "turn.started" &&
|
|
881
858
|
typeof data.prompt_text === "string" &&
|
|
@@ -892,6 +869,107 @@ async function main(): Promise<number> {
|
|
|
892
869
|
return 0;
|
|
893
870
|
}
|
|
894
871
|
|
|
872
|
+
const run: HookRun = {
|
|
873
|
+
coordRoot,
|
|
874
|
+
adapter,
|
|
875
|
+
behavior,
|
|
876
|
+
eventName,
|
|
877
|
+
norm,
|
|
878
|
+
payload,
|
|
879
|
+
raw,
|
|
880
|
+
owner,
|
|
881
|
+
sessionId,
|
|
882
|
+
data,
|
|
883
|
+
debugBase,
|
|
884
|
+
ledgerRoute,
|
|
885
|
+
hookStartedAt,
|
|
886
|
+
hookClock,
|
|
887
|
+
priorCoordination,
|
|
888
|
+
topLevelSession,
|
|
889
|
+
stopRemediation,
|
|
890
|
+
bridge,
|
|
891
|
+
turnRitual,
|
|
892
|
+
};
|
|
893
|
+
recordSignal(run);
|
|
894
|
+
recordContextTelemetry(run);
|
|
895
|
+
|
|
896
|
+
// Phase 8: SessionStart post-emit: project the event so the heartbeat
|
|
897
|
+
// lands synchronously, run stale-sweep, and emit the adapter-shaped
|
|
898
|
+
// systemMessage JSON (peer table + wiring check + council invites).
|
|
899
|
+
// Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
|
|
900
|
+
// and the equivalent per-adapter bash session_start handlers.
|
|
901
|
+
if (norm.event_type === "session.started") await handleSessionStarted(run);
|
|
902
|
+
|
|
903
|
+
// Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
|
|
904
|
+
// agnostic since v0.5.0.
|
|
905
|
+
if (norm.event_type === "session.ended") handleSessionEnded(run);
|
|
906
|
+
|
|
907
|
+
// Phase 8: SubagentStart: the child generation was opened above from the
|
|
908
|
+
// native delegation identity. Refresh its disposable projection, log the
|
|
909
|
+
// lifecycle event, and emit a context message announcing the subagent.
|
|
910
|
+
if (norm.event_type === "agent.started") await handleAgentStarted(run);
|
|
911
|
+
|
|
912
|
+
// Phase 8: SubagentStop: delete subagent heartbeat + log.
|
|
913
|
+
if (norm.event_type === "agent.completed") await handleAgentCompleted(run);
|
|
914
|
+
|
|
915
|
+
// Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
|
|
916
|
+
// and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
|
|
917
|
+
if (norm.event_type === "turn.started") await handleTurnStarted(run);
|
|
918
|
+
|
|
919
|
+
// turn.completed: telemetry, then the stop verdict. The
|
|
920
|
+
// verdict + codex-replay previously lived in the per-adapter shell adapters;
|
|
921
|
+
// agent-hook owns them now. Runs on the normal "stop" event only;
|
|
922
|
+
// "stop-failure" (API error) gets no gate, matching the previous
|
|
923
|
+
// stop vs stop-failure split.
|
|
924
|
+
if (norm.event_type === "turn.completed" && eventName === "stop") {
|
|
925
|
+
const exit = await handleStop(run);
|
|
926
|
+
if (exit !== undefined) return exit;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
|
|
930
|
+
// Adapter-agnostic: both writes have the same shape regardless of who fired.
|
|
931
|
+
// Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
|
|
932
|
+
// heals here keep the agent-coord layer's view of liveness fresh.
|
|
933
|
+
//
|
|
934
|
+
// The heartbeat + pid-map heals are paired by design; they were wired
|
|
935
|
+
// side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
|
|
936
|
+
// preserved the heartbeat half but dropped the pid-map half; the pid-map
|
|
937
|
+
// call was restored here afterward.
|
|
938
|
+
if (norm.event_type === "tool.requested") {
|
|
939
|
+
const exit = await handleToolRequested(run);
|
|
940
|
+
if (exit !== undefined) return exit;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (norm.event_type === "tool.completed" && eventName === "post-tool-use")
|
|
944
|
+
await handlePostToolUse(run);
|
|
945
|
+
|
|
946
|
+
// Phase 7: PostToolUseFailure: release claim on failed Edit (the file
|
|
947
|
+
// never landed; the claim is stale). Adapter-agnostic.
|
|
948
|
+
if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure")
|
|
949
|
+
handlePostToolUseFailure(run);
|
|
950
|
+
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function recordSignal(run: HookRun): void {
|
|
955
|
+
const {
|
|
956
|
+
coordRoot,
|
|
957
|
+
adapter,
|
|
958
|
+
eventName,
|
|
959
|
+
norm,
|
|
960
|
+
payload,
|
|
961
|
+
owner,
|
|
962
|
+
sessionId,
|
|
963
|
+
data,
|
|
964
|
+
debugBase,
|
|
965
|
+
ledgerRoute,
|
|
966
|
+
hookStartedAt,
|
|
967
|
+
hookClock,
|
|
968
|
+
priorCoordination,
|
|
969
|
+
stopRemediation,
|
|
970
|
+
bridge,
|
|
971
|
+
turnRitual,
|
|
972
|
+
} = run;
|
|
895
973
|
let capturedImages: ReturnType<typeof captureImages> = [];
|
|
896
974
|
if (
|
|
897
975
|
norm.event_type === "tool.requested" ||
|
|
@@ -924,7 +1002,7 @@ async function main(): Promise<number> {
|
|
|
924
1002
|
: {}),
|
|
925
1003
|
}
|
|
926
1004
|
: {}),
|
|
927
|
-
...(
|
|
1005
|
+
...(bridge ? { bridge } : {}),
|
|
928
1006
|
hook_name: eventName,
|
|
929
1007
|
hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
|
|
930
1008
|
monotonic_ns: hookClock.monotonic_ns,
|
|
@@ -980,9 +1058,7 @@ async function main(): Promise<number> {
|
|
|
980
1058
|
parentEvent: v3Result.event,
|
|
981
1059
|
payload,
|
|
982
1060
|
adapter,
|
|
983
|
-
...(
|
|
984
|
-
? { bridge: "codex-wsl" as const }
|
|
985
|
-
: {}),
|
|
1061
|
+
...(bridge ? { bridge } : {}),
|
|
986
1062
|
monotonic_ns: hookClock.monotonic_ns,
|
|
987
1063
|
});
|
|
988
1064
|
const nativeChild = payload.subagent_id ?? payload.agent_id;
|
|
@@ -1030,7 +1106,13 @@ async function main(): Promise<number> {
|
|
|
1030
1106
|
...(v3Result ? { event_v3_state: v3Result.state } : {}),
|
|
1031
1107
|
...(v3Result && "reason" in v3Result ? { event_v3_reason: v3Result.reason } : {}),
|
|
1032
1108
|
});
|
|
1109
|
+
run.v3Result = v3Result;
|
|
1110
|
+
run.v3EventId = v3EventId;
|
|
1111
|
+
run.recordedGenerationId = recordedGenerationId;
|
|
1112
|
+
}
|
|
1033
1113
|
|
|
1114
|
+
function recordContextTelemetry(run: HookRun): void {
|
|
1115
|
+
const { coordRoot, adapter, norm, payload, owner, sessionId } = run;
|
|
1034
1116
|
// Context telemetry is opportunistic and truthful: only persist a sample
|
|
1035
1117
|
// when the adapter payload actually exposes usage/window data. Identical
|
|
1036
1118
|
// measurements are de-duplicated before they reach the canonical stream.
|
|
@@ -1081,437 +1163,545 @@ async function main(): Promise<number> {
|
|
|
1081
1163
|
logError(coordRoot, err, { phase: "context-compaction-completed" });
|
|
1082
1164
|
}
|
|
1083
1165
|
}
|
|
1166
|
+
}
|
|
1084
1167
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1168
|
+
async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
1169
|
+
const { coordRoot, adapter, behavior, payload, owner, sessionId, data } = run;
|
|
1170
|
+
// A configured backup runs out of process and is freshness-gated per host.
|
|
1171
|
+
// Startup never waits for restic or a remote provider; the child owns no
|
|
1172
|
+
// hook stdio, so the harness is not held open until it finishes. A failed
|
|
1173
|
+
// previous run surfaces as one line in this session's context.
|
|
1174
|
+
let backupCue = "";
|
|
1175
|
+
try {
|
|
1176
|
+
backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
logError(coordRoot, err, { phase: "backup-schedule" });
|
|
1179
|
+
}
|
|
1180
|
+
// Effect (journal-maintaining adapters): prune stale journal archives +
|
|
1181
|
+
// sweep orphans. The recovery-cue is merged into the session-start
|
|
1182
|
+
// additionalContext inside emitSessionStartSystemMessage.
|
|
1183
|
+
if (behavior.journalMaintenance) journalJanitor(coordRoot);
|
|
1184
|
+
imageJanitor(coordRoot);
|
|
1185
|
+
// Session start is the only caller of the artifact janitor and the storage
|
|
1186
|
+
// maintenance pass, so both load here instead of on every tool call.
|
|
1187
|
+
const { autoCleanArtifacts } = await import("../artifacts/index.ts");
|
|
1188
|
+
const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
|
|
1189
|
+
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
|
|
1190
|
+
"../storage/maintenance-providers.ts"
|
|
1191
|
+
);
|
|
1192
|
+
// Effect: throttled daily sweep of expired artifact workspaces (guarded,
|
|
1193
|
+
// managed-expired only; see autoCleanArtifacts). Best-effort like every
|
|
1194
|
+
// janitor here: a failure logs and never blocks session start.
|
|
1195
|
+
try {
|
|
1196
|
+
autoCleanArtifacts(coordRoot);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1199
|
+
}
|
|
1200
|
+
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1201
|
+
// recovery epochs remain protected by the archive retention policy.
|
|
1202
|
+
try {
|
|
1203
|
+
autoCleanEventV3Archives(coordRoot);
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1206
|
+
}
|
|
1207
|
+
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1208
|
+
// Existing owner janitors above remain the only active executors until
|
|
1209
|
+
// each provider receives a separate production activation.
|
|
1210
|
+
try {
|
|
1211
|
+
await runAutomaticMaintenancePass(
|
|
1212
|
+
createAutomaticMaintenanceComposition(coordRoot, {
|
|
1213
|
+
journal: () => {
|
|
1214
|
+
if (behavior.journalMaintenance) journalJanitor(coordRoot);
|
|
1215
|
+
},
|
|
1216
|
+
images: () => imageJanitor(coordRoot),
|
|
1217
|
+
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1218
|
+
}),
|
|
1112
1219
|
);
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
} catch (err) {
|
|
1119
|
-
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1120
|
-
}
|
|
1121
|
-
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1122
|
-
// recovery epochs remain protected by the archive retention policy.
|
|
1123
|
-
try {
|
|
1124
|
-
autoCleanEventV3Archives(coordRoot);
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1127
|
-
}
|
|
1128
|
-
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1129
|
-
// Existing owner janitors above remain the only active executors until
|
|
1130
|
-
// each provider receives a separate production activation.
|
|
1131
|
-
try {
|
|
1132
|
-
await runAutomaticMaintenancePass(
|
|
1133
|
-
createAutomaticMaintenanceComposition(coordRoot, {
|
|
1134
|
-
journal: () => {
|
|
1135
|
-
if (adapter === "claude-code") journalJanitor(coordRoot);
|
|
1136
|
-
},
|
|
1137
|
-
images: () => imageJanitor(coordRoot),
|
|
1138
|
-
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1139
|
-
}),
|
|
1140
|
-
);
|
|
1141
|
-
} catch (err) {
|
|
1142
|
-
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1143
|
-
}
|
|
1144
|
-
let recovery: PreparedContextRecovery | null = null;
|
|
1145
|
-
if (payload?.source === "compact") {
|
|
1146
|
-
try {
|
|
1147
|
-
markContextCompactionCompleted(coordRoot, {
|
|
1148
|
-
sessionId,
|
|
1149
|
-
instanceId: owner.instance_id,
|
|
1150
|
-
});
|
|
1151
|
-
recovery = prepareContextRecovery(coordRoot, {
|
|
1152
|
-
instanceId: owner.instance_id,
|
|
1153
|
-
sessionId,
|
|
1154
|
-
cwd: payload?.cwd ?? process.cwd(),
|
|
1155
|
-
});
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
logError(coordRoot, err, { phase: "context-recovery-session-start" });
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1222
|
+
}
|
|
1223
|
+
let recovery: PreparedContextRecovery | null = null;
|
|
1224
|
+
if (payload?.source === "compact") {
|
|
1160
1225
|
try {
|
|
1161
|
-
|
|
1162
|
-
coordRoot,
|
|
1163
|
-
owner.instance_id,
|
|
1226
|
+
markContextCompactionCompleted(coordRoot, {
|
|
1164
1227
|
sessionId,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1228
|
+
instanceId: owner.instance_id,
|
|
1229
|
+
});
|
|
1230
|
+
recovery = prepareContextRecovery(coordRoot, {
|
|
1231
|
+
instanceId: owner.instance_id,
|
|
1232
|
+
sessionId,
|
|
1233
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
1234
|
+
});
|
|
1173
1235
|
} catch (err) {
|
|
1174
|
-
logError(coordRoot, err, { phase: "session-start
|
|
1236
|
+
logError(coordRoot, err, { phase: "context-recovery-session-start" });
|
|
1175
1237
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const injected = await emitSessionStartSystemMessage(
|
|
1241
|
+
coordRoot,
|
|
1242
|
+
owner.instance_id,
|
|
1243
|
+
sessionId,
|
|
1244
|
+
data,
|
|
1245
|
+
adapter,
|
|
1246
|
+
recovery?.briefing ?? "",
|
|
1247
|
+
backupCue,
|
|
1248
|
+
);
|
|
1249
|
+
if (injected && recovery) {
|
|
1250
|
+
completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
|
|
1185
1251
|
}
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
logError(coordRoot, err, { phase: "session-start-systemMessage" });
|
|
1254
|
+
}
|
|
1255
|
+
// Cross-machine presence is derived from the authority-safe V3 projection,
|
|
1256
|
+
// not the disposable heartbeat cache. Announce this session, pull peers,
|
|
1257
|
+
// and keep the optional live relay connected.
|
|
1258
|
+
try {
|
|
1259
|
+
publishPresence(coordRoot);
|
|
1260
|
+
fetchPresence(coordRoot);
|
|
1261
|
+
ensureRelayDaemon(coordRoot);
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
logError(coordRoot, err, { phase: "session-start-presence" });
|
|
1186
1264
|
}
|
|
1265
|
+
}
|
|
1187
1266
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
publishPresence(coordRoot);
|
|
1206
|
-
} catch (err) {
|
|
1207
|
-
logError(coordRoot, err, { phase: "session-end-presence" });
|
|
1208
|
-
}
|
|
1267
|
+
function handleSessionEnded(run: HookRun): void {
|
|
1268
|
+
const { coordRoot, behavior, owner } = run;
|
|
1269
|
+
try {
|
|
1270
|
+
cleanupSessionEnd(coordRoot, owner.instance_id);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
logError(coordRoot, err, { phase: "session-end-cleanup" });
|
|
1273
|
+
}
|
|
1274
|
+
// Effects: archive the ending agent's journal + force a session-telemetry
|
|
1275
|
+
// sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1), each gated by the behavior.
|
|
1276
|
+
if (behavior.journalMaintenance) journalArchive(coordRoot, owner.instance_id);
|
|
1277
|
+
if (behavior.sessionTelemetrySync) runSessionSyncExtension(coordRoot, true);
|
|
1278
|
+
// Publish the post-cleanup V3 projection so remote machines see this
|
|
1279
|
+
// session disappear without waiting for the stale window.
|
|
1280
|
+
try {
|
|
1281
|
+
publishPresence(coordRoot);
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
logError(coordRoot, err, { phase: "session-end-presence" });
|
|
1209
1284
|
}
|
|
1285
|
+
}
|
|
1210
1286
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
} catch (err) {
|
|
1219
|
-
logError(coordRoot, err, { phase: "subagent-start-context" });
|
|
1220
|
-
}
|
|
1287
|
+
async function handleAgentStarted(run: HookRun): Promise<void> {
|
|
1288
|
+
const { coordRoot, adapter, owner, sessionId, data } = run;
|
|
1289
|
+
try {
|
|
1290
|
+
const childInstanceId = (data.agent_id as string | undefined) ?? owner.instance_id;
|
|
1291
|
+
await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
logError(coordRoot, err, { phase: "subagent-start-context" });
|
|
1221
1294
|
}
|
|
1295
|
+
}
|
|
1222
1296
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
}
|
|
1297
|
+
async function handleAgentCompleted(run: HookRun): Promise<void> {
|
|
1298
|
+
const { coordRoot, payload, owner } = run;
|
|
1299
|
+
try {
|
|
1300
|
+
const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
|
|
1301
|
+
cleanupSessionEnd(coordRoot, childInstanceId);
|
|
1302
|
+
const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.ts");
|
|
1303
|
+
reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
|
|
1233
1306
|
}
|
|
1307
|
+
}
|
|
1234
1308
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1309
|
+
async function handleTurnStarted(run: HookRun): Promise<void> {
|
|
1310
|
+
const {
|
|
1311
|
+
coordRoot,
|
|
1312
|
+
adapter,
|
|
1313
|
+
behavior,
|
|
1314
|
+
payload,
|
|
1315
|
+
owner,
|
|
1316
|
+
sessionId,
|
|
1317
|
+
debugBase,
|
|
1318
|
+
topLevelSession,
|
|
1319
|
+
stopRemediation,
|
|
1320
|
+
v3EventId,
|
|
1321
|
+
} = run;
|
|
1322
|
+
// Effects: reset per-turn sound rate-limit counters + run presence
|
|
1323
|
+
// detection on the prompt, each gated by the behavior.
|
|
1324
|
+
if (behavior.soundEffects) resetSoundCounters(sessionId);
|
|
1325
|
+
if (behavior.promptPresenceDetection) {
|
|
1326
|
+
const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
|
|
1327
|
+
if (prompt) detectPresence(prompt);
|
|
1328
|
+
}
|
|
1329
|
+
// Presence: throttled pull of peer machines' presence refs (default 60s
|
|
1330
|
+
// interval; the systemMessage below reads whatever is locally known).
|
|
1331
|
+
try {
|
|
1332
|
+
fetchPresence(coordRoot);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
|
|
1335
|
+
}
|
|
1336
|
+
let recovery: PreparedContextRecovery | null = null;
|
|
1337
|
+
const continuityState = readContextState(coordRoot, sessionId);
|
|
1338
|
+
if (
|
|
1339
|
+
continuityState?.phase === "checkpointed" &&
|
|
1340
|
+
continuityState.compaction_completed_at !== undefined
|
|
1341
|
+
) {
|
|
1247
1342
|
try {
|
|
1248
|
-
|
|
1343
|
+
recovery = prepareContextRecovery(coordRoot, {
|
|
1344
|
+
instanceId: owner.instance_id,
|
|
1345
|
+
sessionId,
|
|
1346
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
1347
|
+
});
|
|
1249
1348
|
} catch (err) {
|
|
1250
|
-
logError(coordRoot, err, { phase: "user-prompt
|
|
1251
|
-
}
|
|
1252
|
-
let recovery: PreparedContextRecovery | null = null;
|
|
1253
|
-
const continuityState = readContextState(coordRoot, sessionId);
|
|
1254
|
-
if (
|
|
1255
|
-
continuityState?.phase === "checkpointed" &&
|
|
1256
|
-
continuityState.compaction_completed_at !== undefined
|
|
1257
|
-
) {
|
|
1258
|
-
try {
|
|
1259
|
-
recovery = prepareContextRecovery(coordRoot, {
|
|
1260
|
-
instanceId: owner.instance_id,
|
|
1261
|
-
sessionId,
|
|
1262
|
-
cwd: payload?.cwd ?? process.cwd(),
|
|
1263
|
-
});
|
|
1264
|
-
} catch (err) {
|
|
1265
|
-
logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
let hostPromptContext = "";
|
|
1269
|
-
const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
|
|
1270
|
-
if (hostPromptEnabled && topLevelSession && !stopRemediation) {
|
|
1271
|
-
try {
|
|
1272
|
-
const { runPromptContext } = await import("./prompt-context/runner.ts");
|
|
1273
|
-
const result = await runPromptContext({
|
|
1274
|
-
coordRoot,
|
|
1275
|
-
adapter,
|
|
1276
|
-
sessionId,
|
|
1277
|
-
turnId: payload?.turn_id ?? v3EventId ?? sessionId,
|
|
1278
|
-
cwd: payload?.cwd ?? process.cwd(),
|
|
1279
|
-
prompt: payload?.prompt ?? "",
|
|
1280
|
-
});
|
|
1281
|
-
appendDebug(coordRoot, {
|
|
1282
|
-
...debugBase,
|
|
1283
|
-
effect: "prompt-context",
|
|
1284
|
-
...result.audit,
|
|
1285
|
-
});
|
|
1286
|
-
hostPromptContext = result.context ?? "";
|
|
1287
|
-
} catch (err) {
|
|
1288
|
-
logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
|
|
1289
|
-
}
|
|
1349
|
+
logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
|
|
1290
1350
|
}
|
|
1351
|
+
}
|
|
1352
|
+
let hostPromptContext = "";
|
|
1353
|
+
const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
|
|
1354
|
+
if (hostPromptEnabled && topLevelSession && !stopRemediation) {
|
|
1291
1355
|
try {
|
|
1292
|
-
const
|
|
1356
|
+
const { runPromptContext } = await import("./prompt-context/runner.ts");
|
|
1357
|
+
const result = await runPromptContext({
|
|
1293
1358
|
coordRoot,
|
|
1294
|
-
owner.instance_id,
|
|
1295
|
-
sessionId,
|
|
1296
1359
|
adapter,
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1360
|
+
sessionId,
|
|
1361
|
+
turnId: payload?.turn_id ?? v3EventId ?? sessionId,
|
|
1362
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
1363
|
+
prompt: payload?.prompt ?? "",
|
|
1364
|
+
});
|
|
1365
|
+
appendDebug(coordRoot, {
|
|
1366
|
+
...debugBase,
|
|
1367
|
+
effect: "prompt-context",
|
|
1368
|
+
...result.audit,
|
|
1369
|
+
});
|
|
1370
|
+
hostPromptContext = result.context ?? "";
|
|
1304
1371
|
} catch (err) {
|
|
1305
|
-
logError(coordRoot, err, { phase: "user-prompt
|
|
1372
|
+
logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
|
|
1306
1373
|
}
|
|
1307
1374
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
// stop vs stop-failure split.
|
|
1314
|
-
if (norm.event_type === "turn.completed" && eventName === "stop") {
|
|
1315
|
-
// Claude Code session telemetry sync remains an independent side effect.
|
|
1316
|
-
if (adapter === "claude-code") {
|
|
1317
|
-
runSessionSyncExtension(coordRoot, false);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// Publish after the canonical turn event has landed so the blob carries
|
|
1321
|
-
// current task, activity, and claim state. Re-ensure the relay here because
|
|
1322
|
-
// its daemon intentionally exits when a machine has no live sessions.
|
|
1323
|
-
try {
|
|
1324
|
-
publishPresence(coordRoot);
|
|
1325
|
-
ensureRelayDaemon(coordRoot);
|
|
1326
|
-
} catch (err) {
|
|
1327
|
-
logError(coordRoot, err, { phase: "stop-presence" });
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
|
|
1331
|
-
// rule lives in Harnery. agent-hook already emitted this turn.completed
|
|
1332
|
-
// with privacy-safe ritual observations, so the evidence is in the stream.
|
|
1333
|
-
const verdict = evaluateStopHook(coordRoot, {
|
|
1334
|
-
rule: "stop-hook",
|
|
1335
|
-
instance_id: owner.instance_id,
|
|
1336
|
-
session_id: sessionId,
|
|
1375
|
+
try {
|
|
1376
|
+
const injected = await emitUserPromptSubmitSystemMessage(
|
|
1377
|
+
coordRoot,
|
|
1378
|
+
owner.instance_id,
|
|
1379
|
+
sessionId,
|
|
1337
1380
|
adapter,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
// Backstop for sessions whose evidence can never land (e.g. a headless
|
|
1345
|
-
// child whose status command resolves to a different owner): after
|
|
1346
|
-
// `cap` consecutive blocked stops in one cycle, allow the stop instead
|
|
1347
|
-
// of bouncing the model forever. Cycle boundaries come from the
|
|
1348
|
-
// adapter's continuation flag, so adapters without one are unaffected.
|
|
1349
|
-
const { recordRemediationBlock } = await import("./remediation-cap.ts");
|
|
1350
|
-
const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
|
|
1351
|
-
if (remediation.exceeded) {
|
|
1352
|
-
appendDebug(coordRoot, {
|
|
1353
|
-
...debugBase,
|
|
1354
|
-
skipped: "stop-remediation-cap-exhausted",
|
|
1355
|
-
blocked_rule: verdict.rule,
|
|
1356
|
-
blocked_count: remediation.count,
|
|
1357
|
-
session_id: sessionId,
|
|
1358
|
-
remediation_cycle_anchor: verdict.remediation_cycle_anchor,
|
|
1359
|
-
});
|
|
1360
|
-
} else {
|
|
1361
|
-
// Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
|
|
1362
|
-
// as a turn block; Cursor ignores exit codes and re-prompts only via a
|
|
1363
|
-
// `followup_message` it auto-submits.
|
|
1364
|
-
const { emitStopOutcome } = await import("./adapter/output.ts");
|
|
1365
|
-
return emitStopOutcome(adapter, { verdict }, coordRoot);
|
|
1366
|
-
}
|
|
1367
|
-
} else {
|
|
1368
|
-
const { clearRemediationCount } = await import("./remediation-cap.ts");
|
|
1369
|
-
clearRemediationCount(sessionId);
|
|
1381
|
+
payload?.cwd,
|
|
1382
|
+
recovery?.briefing ?? "",
|
|
1383
|
+
hostPromptContext,
|
|
1384
|
+
);
|
|
1385
|
+
if (injected && recovery) {
|
|
1386
|
+
completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
|
|
1370
1387
|
}
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1371
1392
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1386
|
-
if (adapter === "codex") scheduleCodexRuntimeContextRetry(coordRoot, sessionId);
|
|
1393
|
+
async function handleStop(run: HookRun): Promise<number | undefined> {
|
|
1394
|
+
const { coordRoot, adapter, behavior, payload, owner, sessionId, data, debugBase } = run;
|
|
1395
|
+
// Session telemetry sync remains an independent side effect.
|
|
1396
|
+
if (behavior.sessionTelemetrySync) runSessionSyncExtension(coordRoot, false);
|
|
1397
|
+
|
|
1398
|
+
// Publish after the canonical turn event has landed so the blob carries
|
|
1399
|
+
// current task, activity, and claim state. Re-ensure the relay here because
|
|
1400
|
+
// its daemon intentionally exits when a machine has no live sessions.
|
|
1401
|
+
try {
|
|
1402
|
+
publishPresence(coordRoot);
|
|
1403
|
+
ensureRelayDaemon(coordRoot);
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
logError(coordRoot, err, { phase: "stop-presence" });
|
|
1387
1406
|
}
|
|
1388
1407
|
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1408
|
+
// Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
|
|
1409
|
+
// rule lives in Harnery. agent-hook already emitted this turn.completed
|
|
1410
|
+
// with privacy-safe ritual observations, so the evidence is in the stream.
|
|
1411
|
+
const verdict = evaluateStopHook(coordRoot, {
|
|
1412
|
+
rule: "stop-hook",
|
|
1413
|
+
instance_id: owner.instance_id,
|
|
1414
|
+
session_id: sessionId,
|
|
1415
|
+
adapter,
|
|
1416
|
+
stop_hook_active: payload?.stop_hook_active === true,
|
|
1417
|
+
status_box_present_strict: data.status_box_present_strict === true,
|
|
1418
|
+
session_name_observation_unavailable: typeof data.session_name_unavailable_reason === "string",
|
|
1419
|
+
bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
|
|
1420
|
+
workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
|
|
1421
|
+
});
|
|
1422
|
+
if (!verdict.allow) {
|
|
1423
|
+
// Backstop for sessions whose evidence can never land (e.g. a headless
|
|
1424
|
+
// child whose status command resolves to a different owner): after
|
|
1425
|
+
// `cap` consecutive blocked stops in one cycle, allow the stop instead
|
|
1426
|
+
// of bouncing the model forever. Cycle boundaries come from the
|
|
1427
|
+
// adapter's continuation flag, so adapters without one are unaffected.
|
|
1428
|
+
const { recordRemediationBlock } = await import("./remediation-cap.ts");
|
|
1429
|
+
const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
|
|
1430
|
+
if (remediation.exceeded) {
|
|
1431
|
+
appendDebug(coordRoot, {
|
|
1432
|
+
...debugBase,
|
|
1433
|
+
skipped: "stop-remediation-cap-exhausted",
|
|
1434
|
+
blocked_rule: verdict.rule,
|
|
1435
|
+
blocked_count: remediation.count,
|
|
1436
|
+
session_id: sessionId,
|
|
1437
|
+
remediation_cycle_anchor: verdict.remediation_cycle_anchor,
|
|
1438
|
+
});
|
|
1439
|
+
} else {
|
|
1440
|
+
// Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
|
|
1441
|
+
// as a turn block; Cursor ignores exit codes and re-prompts only via a
|
|
1442
|
+
// `followup_message` it auto-submits.
|
|
1443
|
+
const { emitStopOutcome } = await import("./adapter/output.ts");
|
|
1444
|
+
return emitStopOutcome(adapter, { verdict }, coordRoot);
|
|
1409
1445
|
}
|
|
1446
|
+
} else {
|
|
1447
|
+
const { clearRemediationCount } = await import("./remediation-cap.ts");
|
|
1448
|
+
clearRemediationCount(sessionId);
|
|
1449
|
+
}
|
|
1410
1450
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const displayAllowed = await enforcePendingSessionNameDisplay(
|
|
1422
|
-
coordRoot,
|
|
1423
|
-
owner.instance_id,
|
|
1424
|
-
adapter,
|
|
1425
|
-
payload,
|
|
1426
|
-
generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId),
|
|
1427
|
-
);
|
|
1428
|
-
if (!displayAllowed) return 0;
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
|
|
1451
|
+
// An explicit end requested from inside this turn cannot be authoritative
|
|
1452
|
+
// until the adapter has committed turn.completed above. Reconcile only
|
|
1453
|
+
// when such a request exists; any later real work cancels it in the
|
|
1454
|
+
// finalizer instead of being terminated underneath the agent.
|
|
1455
|
+
try {
|
|
1456
|
+
const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import(
|
|
1457
|
+
"../agents/session-finalizer-v3.ts"
|
|
1458
|
+
);
|
|
1459
|
+
if (hasPendingExplicitSessionEndV3(coordRoot)) {
|
|
1460
|
+
reconcileSessionFinalizationV3(coordRoot);
|
|
1431
1461
|
}
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
|
|
1464
|
+
}
|
|
1465
|
+
if (behavior.runtimeContextRetry) scheduleRuntimeContextRetry(coordRoot, adapter, sessionId);
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1432
1468
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1469
|
+
async function handleToolRequested(run: HookRun): Promise<number | undefined> {
|
|
1470
|
+
const { coordRoot, adapter, behavior, payload, owner, sessionId, data, recordedGenerationId } =
|
|
1471
|
+
run;
|
|
1472
|
+
try {
|
|
1473
|
+
// Recorded fork lineage, heal-path flavor. A forked CC conversation
|
|
1474
|
+
// never fires its own session.started (SessionStart fires under the
|
|
1475
|
+
// PARENT's session id with source=resume, before the fork id is
|
|
1476
|
+
// minted; verified 2026-08-05), so the fork's new instance first
|
|
1477
|
+
// materializes right here. Gate detection on "no heartbeat yet" so the
|
|
1478
|
+
// transcript scan runs once per instance lifetime, not per tool call.
|
|
1479
|
+
refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
logError(coordRoot, err, { phase: "pre-tool-use-heal" });
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// The suggested name is a pending display latch. The set-task call itself
|
|
1485
|
+
// runs before a name exists; every later tool waits until the exact block
|
|
1486
|
+
// is the first assistant text after the mint result. Cursor supplies
|
|
1487
|
+
// agent_message directly; Claude Code and Codex are resolved from their
|
|
1488
|
+
// JSONL transcripts. Cursor's current narration can provide positive
|
|
1489
|
+
// evidence, but cannot disprove an earlier response; afterAgentResponse
|
|
1490
|
+
// stamps that durable sighting. Later commentary must not erase an
|
|
1491
|
+
// already-correct display, and unavailable evidence must not deadlock
|
|
1492
|
+
// every tool.
|
|
1493
|
+
try {
|
|
1494
|
+
const displayAllowed = await enforcePendingSessionNameDisplay(
|
|
1495
|
+
coordRoot,
|
|
1496
|
+
owner.instance_id,
|
|
1438
1497
|
adapter,
|
|
1498
|
+
payload,
|
|
1499
|
+
generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId),
|
|
1500
|
+
);
|
|
1501
|
+
if (!displayAllowed) return 0;
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Windows-native Codex + WSL UNC only: block the one cross-shell shape
|
|
1507
|
+
// proven to corrupt argument boundaries. Normal WSL argv calls, literal
|
|
1508
|
+
// bash -s scripts, native Linux/macOS sessions, and every other adapter
|
|
1509
|
+
// pass through. The host instructions name its concrete safe bridge.
|
|
1510
|
+
const unsafeShellReason =
|
|
1511
|
+
behavior.unsafeCrossShellReason?.({
|
|
1439
1512
|
cwd: payload?.cwd,
|
|
1440
1513
|
toolName: payload?.tool_name,
|
|
1441
1514
|
toolInput: payload?.tool_input,
|
|
1442
|
-
});
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1515
|
+
}) ?? null;
|
|
1516
|
+
if (unsafeShellReason) {
|
|
1517
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1518
|
+
emitDeny(adapter, unsafeShellReason);
|
|
1519
|
+
return 0;
|
|
1520
|
+
}
|
|
1448
1521
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
// apply_patch (codex) parses paths from the patch body and runs verdict
|
|
1455
|
-
// per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
|
|
1456
|
-
// (incl. Agent) yield no targets and pass through with no deny.
|
|
1457
|
-
let guardAllowed = true;
|
|
1458
|
-
try {
|
|
1459
|
-
guardAllowed = await runPreToolUseGuard(
|
|
1460
|
-
coordRoot,
|
|
1461
|
-
owner.instance_id,
|
|
1462
|
-
sessionId,
|
|
1463
|
-
data,
|
|
1464
|
-
adapter,
|
|
1465
|
-
);
|
|
1466
|
-
} catch (err) {
|
|
1467
|
-
logError(coordRoot, err, { phase: "pre-tool-use-guard" });
|
|
1468
|
-
}
|
|
1469
|
-
if (!guardAllowed) return 0;
|
|
1522
|
+
const waiterReason = shellWaiterReason(payload?.tool_name, payload?.tool_input);
|
|
1523
|
+
if (waiterReason) {
|
|
1524
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1525
|
+
emitDeny(adapter, waiterReason);
|
|
1526
|
+
return 0;
|
|
1470
1527
|
}
|
|
1471
1528
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1529
|
+
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
1530
|
+
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
1531
|
+
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
1532
|
+
// emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
|
|
1533
|
+
// codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
|
|
1534
|
+
// apply_patch (codex) parses paths from the patch body and runs verdict
|
|
1535
|
+
// per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
|
|
1536
|
+
// (incl. Agent) yield no targets and pass through with no deny.
|
|
1537
|
+
let guardAllowed = true;
|
|
1538
|
+
try {
|
|
1539
|
+
guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
logError(coordRoot, err, { phase: "pre-tool-use-guard" });
|
|
1542
|
+
}
|
|
1543
|
+
if (!guardAllowed) return 0;
|
|
1544
|
+
return undefined;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
async function handlePostToolUse(run: HookRun): Promise<void> {
|
|
1548
|
+
const { coordRoot, adapter, eventName, payload, owner } = run;
|
|
1549
|
+
// Inject the full instruction only at the successful tool boundary that
|
|
1550
|
+
// actually minted or retried a name: the first non-empty set-task, a
|
|
1551
|
+
// pending-name set-task retry, or the transition to lifecycle done. The
|
|
1552
|
+
// coordination row intentionally remains pending until transcript evidence
|
|
1553
|
+
// catches up, so the latch alone cannot drive injection: that would print
|
|
1554
|
+
// the same block after every later tool.
|
|
1555
|
+
//
|
|
1556
|
+
// Between those two extremes sits a bounded reminder. Since ADR 0182 the
|
|
1557
|
+
// PreToolUse gate allows a tool when transcript evidence is unavailable
|
|
1558
|
+
// and says so only on stderr, which the model never reads. A model that
|
|
1559
|
+
// skipped the block after the mint was therefore never asked again
|
|
1560
|
+
// (ADR 0183). While the requested title is still pending, ordinary tool
|
|
1561
|
+
// results carry an idempotent reminder up to a fixed count; coordination
|
|
1562
|
+
// chores (set-task, status, suggest-name) are skipped because a mint or
|
|
1563
|
+
// retry response already carries the full instruction and an end-turn
|
|
1564
|
+
// status is the wrong place to ask.
|
|
1565
|
+
try {
|
|
1566
|
+
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
1567
|
+
const name = sessionNameDisplayPending(row);
|
|
1568
|
+
if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
|
|
1569
|
+
// Record the title before asking for it. The suggestion can still
|
|
1570
|
+
// change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
|
|
1571
|
+
// rebuilt cache), and the agent must not be stranded for displaying
|
|
1572
|
+
// exactly what it was handed.
|
|
1573
|
+
stampSessionNameRequested(coordRoot, owner.instance_id, name);
|
|
1574
|
+
const { emitContext } = await import("./adapter/output.ts");
|
|
1575
|
+
emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
|
|
1576
|
+
} else if (name) {
|
|
1577
|
+
const due = sessionNameDisplayReminderDue(row);
|
|
1578
|
+
const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
|
|
1579
|
+
if (due && !isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) {
|
|
1580
|
+
stampSessionNameReminded(coordRoot, owner.instance_id, due);
|
|
1581
|
+
appendDebug(coordRoot, {
|
|
1582
|
+
ts: new Date().toISOString(),
|
|
1583
|
+
event_name: eventName,
|
|
1584
|
+
adapter,
|
|
1585
|
+
instance_id: owner.instance_id,
|
|
1586
|
+
effect: "session-name-display-reminded",
|
|
1587
|
+
session_name_pending: due,
|
|
1588
|
+
reminders: (row?.session_name_display_reminders ?? 0) + 1,
|
|
1589
|
+
});
|
|
1487
1590
|
const { emitContext } = await import("./adapter/output.ts");
|
|
1488
|
-
emitContext(adapter, "PostToolUse",
|
|
1591
|
+
emitContext(adapter, "PostToolUse", sessionNameDisplayReminder(due));
|
|
1489
1592
|
}
|
|
1490
|
-
} catch (err) {
|
|
1491
|
-
logError(coordRoot, err, { phase: "post-tool-use-session-name" });
|
|
1492
1593
|
}
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
logError(coordRoot, err, { phase: "post-tool-use-session-name" });
|
|
1493
1596
|
}
|
|
1597
|
+
}
|
|
1494
1598
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
|
|
1502
|
-
}
|
|
1599
|
+
function handlePostToolUseFailure(run: HookRun): void {
|
|
1600
|
+
const { coordRoot, payload, owner, data } = run;
|
|
1601
|
+
try {
|
|
1602
|
+
releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
|
|
1503
1605
|
}
|
|
1606
|
+
}
|
|
1504
1607
|
|
|
1608
|
+
async function handleCompletedResponse(run: CompletedResponseRun): Promise<number> {
|
|
1609
|
+
const { coordRoot, adapter, eventName, raw, debugBase, ledgerRoute, hookStartedAt, hookClock } =
|
|
1610
|
+
run;
|
|
1611
|
+
const payload = parsePayload(raw, adapter);
|
|
1612
|
+
const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
|
|
1613
|
+
if (!owner) {
|
|
1614
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
|
|
1615
|
+
return 0;
|
|
1616
|
+
}
|
|
1617
|
+
const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
|
|
1618
|
+
const name = sessionNameDisplayPending(row);
|
|
1619
|
+
const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
|
|
1620
|
+
const sighting = matchSessionNameDisplay(row, text);
|
|
1621
|
+
if (sighting) {
|
|
1622
|
+
stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
|
|
1623
|
+
appendDebug(coordRoot, {
|
|
1624
|
+
...debugBase,
|
|
1625
|
+
effect: "session-name-display-stamped",
|
|
1626
|
+
session_name_pending: sighting.pending,
|
|
1627
|
+
...(sighting.displayed === sighting.pending
|
|
1628
|
+
? {}
|
|
1629
|
+
: { session_name_displayed: sighting.displayed, session_name_drift: true }),
|
|
1630
|
+
});
|
|
1631
|
+
} else if (!name) {
|
|
1632
|
+
// Nothing was owed. Distinct from a miss: conflating the two is what made
|
|
1633
|
+
// a latched Cursor session undiagnosable from this log.
|
|
1634
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
|
|
1635
|
+
} else {
|
|
1636
|
+
appendDebug(coordRoot, {
|
|
1637
|
+
...debugBase,
|
|
1638
|
+
skipped: "session-name-block-absent",
|
|
1639
|
+
session_name_pending: name,
|
|
1640
|
+
reply_bytes: text.length,
|
|
1641
|
+
reply_leads_with_fence: /^\s*`{3,}/.test(text),
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (ledgerRoute.state === "blocked") {
|
|
1645
|
+
appendDebug(coordRoot, {
|
|
1646
|
+
...debugBase,
|
|
1647
|
+
skipped: "v3-control-blocked",
|
|
1648
|
+
reason: ledgerRoute.reason,
|
|
1649
|
+
owner_source: owner.source,
|
|
1650
|
+
});
|
|
1651
|
+
return 0;
|
|
1652
|
+
}
|
|
1653
|
+
const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
|
|
1654
|
+
// The hook parser needs the completed reply long enough to scan it, but
|
|
1655
|
+
// durable hook intake must never retain that body. Keep every other native
|
|
1656
|
+
// field so turn correlation remains available to the producer.
|
|
1657
|
+
const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
|
|
1658
|
+
const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
|
|
1659
|
+
const v3Result = recordLiveHookSignalV3({
|
|
1660
|
+
coordRoot,
|
|
1661
|
+
route: ledgerRoute,
|
|
1662
|
+
eventName,
|
|
1663
|
+
payload: privacySafePayload,
|
|
1664
|
+
adapter,
|
|
1665
|
+
instanceId: owner.instance_id,
|
|
1666
|
+
hook_name: eventName,
|
|
1667
|
+
hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
|
|
1668
|
+
monotonic_ns: hookClock.monotonic_ns,
|
|
1669
|
+
turn_ritual: {
|
|
1670
|
+
status_box_present: statusBoxPresentStrict,
|
|
1671
|
+
status_box_present_strict: statusBoxPresentStrict,
|
|
1672
|
+
session_name_required: name !== undefined,
|
|
1673
|
+
session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
|
|
1674
|
+
},
|
|
1675
|
+
});
|
|
1676
|
+
observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
|
|
1677
|
+
appendDebug(coordRoot, {
|
|
1678
|
+
...debugBase,
|
|
1679
|
+
effect: "cursor-response-ritual-observed",
|
|
1680
|
+
owner_source: owner.source,
|
|
1681
|
+
event_v3_state: v3Result.state,
|
|
1682
|
+
status_box_present_strict: statusBoxPresentStrict,
|
|
1683
|
+
...(v3Result.state === "observed"
|
|
1684
|
+
? {
|
|
1685
|
+
generation_id: v3Result.generation_id,
|
|
1686
|
+
turn_id: v3Result.turn_id,
|
|
1687
|
+
response_observed_at: v3Result.observed_at,
|
|
1688
|
+
}
|
|
1689
|
+
: {}),
|
|
1690
|
+
});
|
|
1505
1691
|
return 0;
|
|
1506
1692
|
}
|
|
1507
1693
|
|
|
1508
|
-
function
|
|
1694
|
+
function scheduleRuntimeContextRetry(
|
|
1695
|
+
coordRoot: string,
|
|
1696
|
+
adapter: Adapter,
|
|
1697
|
+
nativeSessionId: string,
|
|
1698
|
+
): void {
|
|
1509
1699
|
const modulePath = process.argv[1];
|
|
1510
1700
|
if (!modulePath) return;
|
|
1511
1701
|
try {
|
|
1512
1702
|
const child = spawn(
|
|
1513
1703
|
process.execPath,
|
|
1514
|
-
[modulePath, "runtime-context-retry", "--adapter",
|
|
1704
|
+
[modulePath, "runtime-context-retry", "--adapter", adapter, "--session-id", nativeSessionId],
|
|
1515
1705
|
{
|
|
1516
1706
|
cwd: coordRoot,
|
|
1517
1707
|
detached: true,
|
|
@@ -1531,7 +1721,7 @@ async function runRuntimeContextRetryWorker(
|
|
|
1531
1721
|
extra: string[],
|
|
1532
1722
|
): Promise<number> {
|
|
1533
1723
|
const nativeSessionId = extraArg(extra, "--session-id");
|
|
1534
|
-
if (adapter
|
|
1724
|
+
if (!adapter || !adapterBehavior(adapter).runtimeContextRetry || !nativeSessionId) return 0;
|
|
1535
1725
|
for (const [index, delayMs] of [400, 1_000].entries()) {
|
|
1536
1726
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1537
1727
|
const route = resolveLiveEventLedgerRouteV3(coordRoot);
|
|
@@ -1547,6 +1737,27 @@ async function runRuntimeContextRetryWorker(
|
|
|
1547
1737
|
return 0;
|
|
1548
1738
|
}
|
|
1549
1739
|
|
|
1740
|
+
function reportSessionNameObservationUnavailable(
|
|
1741
|
+
coordRoot: string,
|
|
1742
|
+
instanceId: string,
|
|
1743
|
+
adapter: Adapter,
|
|
1744
|
+
eventName: string,
|
|
1745
|
+
reason: string,
|
|
1746
|
+
): void {
|
|
1747
|
+
appendDebug(coordRoot, {
|
|
1748
|
+
ts: new Date().toISOString(),
|
|
1749
|
+
event_name: eventName,
|
|
1750
|
+
adapter,
|
|
1751
|
+
instance_id: instanceId,
|
|
1752
|
+
effect: "session-name-observation-unavailable",
|
|
1753
|
+
reason,
|
|
1754
|
+
action: "fail-open",
|
|
1755
|
+
});
|
|
1756
|
+
process.stderr.write(
|
|
1757
|
+
`Harnery: session-name observation unavailable (${reason}); allowing this ${eventName} without display enforcement. The name remains pending.\n`,
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1550
1761
|
async function enforcePendingSessionNameDisplay(
|
|
1551
1762
|
coordRoot: string,
|
|
1552
1763
|
instanceId: string,
|
|
@@ -1567,11 +1778,13 @@ async function enforcePendingSessionNameDisplay(
|
|
|
1567
1778
|
}
|
|
1568
1779
|
|
|
1569
1780
|
if (inspection.state === "unavailable") {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1781
|
+
reportSessionNameObservationUnavailable(
|
|
1782
|
+
coordRoot,
|
|
1783
|
+
instanceId,
|
|
1784
|
+
adapter,
|
|
1785
|
+
"pre-tool-use",
|
|
1786
|
+
inspection.reason,
|
|
1787
|
+
);
|
|
1575
1788
|
return true;
|
|
1576
1789
|
}
|
|
1577
1790
|
|
|
@@ -1605,21 +1818,20 @@ function inspectDisplayEvidence(
|
|
|
1605
1818
|
payload: ParsedPayload | null,
|
|
1606
1819
|
coordination: Heartbeat | null,
|
|
1607
1820
|
): SessionNameDisplayInspection {
|
|
1608
|
-
|
|
1609
|
-
|
|
1821
|
+
const behavior = adapterBehavior(adapter);
|
|
1822
|
+
const inlineText = behavior.inlineAssistantText?.(payload);
|
|
1823
|
+
if (inlineText !== undefined) {
|
|
1824
|
+
return matchSessionNameDisplay(coordination, inlineText)
|
|
1610
1825
|
? { state: "present" }
|
|
1611
1826
|
: { state: "unavailable", reason: "transcript_not_ready" };
|
|
1612
1827
|
}
|
|
1613
1828
|
|
|
1614
|
-
//
|
|
1615
|
-
// inspection permanently unavailable and the latch never stamped.
|
|
1616
|
-
// the
|
|
1617
|
-
// the latch closes after one success.
|
|
1829
|
+
// Some adapters omit transcript_path on every event, which left this
|
|
1830
|
+
// inspection permanently unavailable and the latch never stamped. The
|
|
1831
|
+
// behavior may discover the transcript by session id; the scan only runs
|
|
1832
|
+
// while a name is pending, so the latch closes after one success.
|
|
1618
1833
|
const transcriptPath =
|
|
1619
|
-
payload?.transcript_path ??
|
|
1620
|
-
(adapter === "codex"
|
|
1621
|
-
? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
|
|
1622
|
-
: undefined);
|
|
1834
|
+
payload?.transcript_path ?? behavior.discoverTranscript?.(payload?.session_id ?? instanceId);
|
|
1623
1835
|
|
|
1624
1836
|
let verdict: SessionNameDisplayInspection = { state: "absent" };
|
|
1625
1837
|
for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
|
|
@@ -1913,7 +2125,7 @@ function releaseClaimOnFailure(
|
|
|
1913
2125
|
coordRoot,
|
|
1914
2126
|
owner: instanceId,
|
|
1915
2127
|
nativeSessionId: before?.session_id ?? instanceId,
|
|
1916
|
-
adapter:
|
|
2128
|
+
adapter: sharedAdapterFromPlatform(before?.platform, { context: "hook-claim-release" }),
|
|
1917
2129
|
operation: "released",
|
|
1918
2130
|
path: canonical,
|
|
1919
2131
|
access: "write",
|
|
@@ -1956,22 +2168,18 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
1956
2168
|
recoveryBriefing = "",
|
|
1957
2169
|
hostPromptContext = "",
|
|
1958
2170
|
): Promise<boolean> {
|
|
2171
|
+
const behavior = adapterBehavior(adapter);
|
|
1959
2172
|
const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
|
|
1960
2173
|
let additionalContext = renderPromptContext({
|
|
1961
2174
|
coordRoot,
|
|
1962
2175
|
instanceId,
|
|
1963
2176
|
sessionId,
|
|
1964
2177
|
sessionNameNudge: true,
|
|
1965
|
-
|
|
1966
|
-
hostPromptReminder: adapter === "codex" || adapter === "cursor",
|
|
1967
|
-
statusFooterNudge: adapter === "codex",
|
|
1968
|
-
turnRitualNudge: adapter === "claude-code" ? adapter : undefined,
|
|
2178
|
+
...behavior.promptContextNudges,
|
|
1969
2179
|
}).trim();
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1974
|
-
}
|
|
2180
|
+
const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
|
|
2181
|
+
if (fileLinkContext) {
|
|
2182
|
+
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1975
2183
|
}
|
|
1976
2184
|
if (recoveryBriefing) {
|
|
1977
2185
|
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
@@ -2022,16 +2230,9 @@ async function emitSubagentStartContext(
|
|
|
2022
2230
|
}
|
|
2023
2231
|
|
|
2024
2232
|
function adapterPlatform(adapter: Adapter): string {
|
|
2025
|
-
if (adapter === "claude-code") return "claude-code";
|
|
2026
2233
|
return adapter;
|
|
2027
2234
|
}
|
|
2028
2235
|
|
|
2029
|
-
function adapterFromPlatform(platform: unknown): Adapter {
|
|
2030
|
-
if (platform === "cursor") return "cursor";
|
|
2031
|
-
if (platform === "codex") return "codex";
|
|
2032
|
-
return "claude-code";
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
2236
|
async function emitSessionStartSystemMessage(
|
|
2036
2237
|
coordRoot: string,
|
|
2037
2238
|
instanceId: string,
|
|
@@ -2041,6 +2242,7 @@ async function emitSessionStartSystemMessage(
|
|
|
2041
2242
|
recoveryBriefing = "",
|
|
2042
2243
|
backupCue = "",
|
|
2043
2244
|
): Promise<boolean> {
|
|
2245
|
+
const behavior = adapterBehavior(adapter);
|
|
2044
2246
|
const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
|
|
2045
2247
|
let additionalContext = "";
|
|
2046
2248
|
// Opportunistic reconciliation makes normal session starts a failsafe for
|
|
@@ -2061,16 +2263,15 @@ async function emitSessionStartSystemMessage(
|
|
|
2061
2263
|
instanceId,
|
|
2062
2264
|
sessionId,
|
|
2063
2265
|
agentName: agentName || undefined,
|
|
2064
|
-
platformLabel:
|
|
2065
|
-
adapter === "claude-code" ? undefined : adapter === "cursor" ? "Cursor" : "Codex",
|
|
2266
|
+
platformLabel: behavior.sessionStartPlatformLabel,
|
|
2066
2267
|
}).trim();
|
|
2067
2268
|
}
|
|
2068
2269
|
|
|
2069
|
-
// Effect (
|
|
2070
|
-
// context. Was a standalone additionalContext emission
|
|
2071
|
-
// journal-on-start adapter; now that agent-hook is the
|
|
2072
|
-
// entry, it folds in here.
|
|
2073
|
-
if (
|
|
2270
|
+
// Effect (journal-maintaining adapters): merge the journal recovery cue into
|
|
2271
|
+
// the session-start context. Was a standalone additionalContext emission
|
|
2272
|
+
// from the previous journal-on-start adapter; now that agent-hook is the
|
|
2273
|
+
// single SessionStart entry, it folds in here.
|
|
2274
|
+
if (behavior.journalMaintenance && !workflowChild) {
|
|
2074
2275
|
const cue = journalRecoveryCue(coordRoot);
|
|
2075
2276
|
if (cue) additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
|
|
2076
2277
|
}
|
|
@@ -2080,19 +2281,14 @@ async function emitSessionStartSystemMessage(
|
|
|
2080
2281
|
if (backupCue && !workflowChild) {
|
|
2081
2282
|
additionalContext = [additionalContext, backupCue].filter(Boolean).join("\n\n");
|
|
2082
2283
|
}
|
|
2083
|
-
if (
|
|
2084
|
-
const fileLinkContext =
|
|
2284
|
+
if (!workflowChild && behavior.bridgeFor(emittedData.cwd)) {
|
|
2285
|
+
const fileLinkContext = behavior.fileLinkContext?.(coordRoot, emittedData.cwd) ?? "";
|
|
2085
2286
|
if (fileLinkContext) {
|
|
2086
2287
|
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
2087
2288
|
}
|
|
2088
|
-
const
|
|
2089
|
-
if (
|
|
2090
|
-
additionalContext = [
|
|
2091
|
-
additionalContext,
|
|
2092
|
-
`Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
|
|
2093
|
-
]
|
|
2094
|
-
.filter(Boolean)
|
|
2095
|
-
.join("\n\n");
|
|
2289
|
+
const warning = behavior.bridgeWarning?.(coordRoot, emittedData.cwd);
|
|
2290
|
+
if (warning) {
|
|
2291
|
+
additionalContext = [additionalContext, warning].filter(Boolean).join("\n\n");
|
|
2096
2292
|
}
|
|
2097
2293
|
}
|
|
2098
2294
|
const { emitContext } = await import("./adapter/output.ts");
|