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
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harnery OpenCode plugin: bridges OpenCode V2 in-process lifecycle hooks to
|
|
3
|
+
* `agent-hook <subcommand> --adapter opencode`, so an interactive OpenCode
|
|
4
|
+
* session (TUI, `opencode run`, the desktop app) produces the same Event Ledger
|
|
5
|
+
* V3 evidence as a Claude Code, Cursor, or Codex session.
|
|
6
|
+
*
|
|
7
|
+
* OpenCode V2 has no settings-file command hooks; its only interception point
|
|
8
|
+
* is a plugin loaded into the shared background service. `harn init --adapter
|
|
9
|
+
* opencode` copies this file, verbatim under a hash-stamped ownership header,
|
|
10
|
+
* to `.opencode/plugins/harnery/index.ts` with a sibling `harnery.json` that
|
|
11
|
+
* names the `agent-hook` launcher. OpenCode auto-discovers that directory, so
|
|
12
|
+
* no `opencode.json` edit is needed.
|
|
13
|
+
*
|
|
14
|
+
* Contract this file keeps, because it is copied into consumer repositories:
|
|
15
|
+
* - self-contained: `node:*` imports only, no relative imports, no Harnery
|
|
16
|
+
* package imports (the consumer may not have Harnery resolvable from here);
|
|
17
|
+
* - fail-open: a hook that cannot reach `agent-hook` logs and returns, so a
|
|
18
|
+
* broken bridge never breaks the operator's OpenCode session;
|
|
19
|
+
* - observe-only at turn end: OpenCode has no blocking Stop channel, so the
|
|
20
|
+
* `stop` verdict is recorded but never re-prompts.
|
|
21
|
+
*
|
|
22
|
+
* Event map (OpenCode → agent-hook subcommand), kept in sync with
|
|
23
|
+
* `OPENCODE_EVENTS` in `src/core/hooks/adapter/events.ts`:
|
|
24
|
+
* event session.created → session-start (child → sub-agent-start)
|
|
25
|
+
* session.hook("prompt") → user-prompt-submit
|
|
26
|
+
* tool.hook("execute.before") → pre-tool-use (deny → throw)
|
|
27
|
+
* tool.hook("execute.after") → post-tool-use | post-tool-use-failure
|
|
28
|
+
* permission.hook("evaluate") → permission-request
|
|
29
|
+
* session.hook("compaction") → pre-compact
|
|
30
|
+
* event session.execution.succeeded → stop (child → sub-agent-stop)
|
|
31
|
+
* event session.execution.failed → stop-failure (child → sub-agent-stop)
|
|
32
|
+
* event session.execution.interrupted → stop-failure (child → sub-agent-stop)
|
|
33
|
+
* event session.deleted → session-end
|
|
34
|
+
* shell.hook("create.before") → stamps OPENCODE_SESSION_ID into the tool shell
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { spawn } from "node:child_process";
|
|
38
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
39
|
+
import { homedir } from "node:os";
|
|
40
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
|
|
43
|
+
export const HARNERY_OPENCODE_PLUGIN_ID = "harnery";
|
|
44
|
+
export const HARNERY_OPENCODE_CONFIG_FILE = "harnery.json";
|
|
45
|
+
export const HARNERY_OPENCODE_CONFIG_SCHEMA = "harnery-opencode-plugin/v1";
|
|
46
|
+
/** Env var the shell hook stamps so a coord CLI run as a tool recovers its session. */
|
|
47
|
+
export const OPENCODE_SESSION_ENV = "OPENCODE_SESSION_ID";
|
|
48
|
+
const ADAPTER = "opencode";
|
|
49
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
50
|
+
const CONTEXT_DESCRIPTION = "Harnery coordination context";
|
|
51
|
+
|
|
52
|
+
// ── OpenCode plugin API (the subset this plugin touches) ─────────────────────
|
|
53
|
+
|
|
54
|
+
export interface OpenCodeRegistration {
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface OpenCodeToolBefore {
|
|
58
|
+
tool: string;
|
|
59
|
+
readonly sessionID: string;
|
|
60
|
+
readonly agent?: string;
|
|
61
|
+
readonly messageID?: string;
|
|
62
|
+
readonly id: string;
|
|
63
|
+
input: unknown;
|
|
64
|
+
}
|
|
65
|
+
export type OpenCodeToolAfter = {
|
|
66
|
+
readonly tool: string;
|
|
67
|
+
readonly sessionID: string;
|
|
68
|
+
readonly agent?: string;
|
|
69
|
+
readonly messageID?: string;
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly input: unknown;
|
|
72
|
+
} & (
|
|
73
|
+
| { readonly status: "completed"; result: unknown }
|
|
74
|
+
| { readonly status: "error"; error: unknown }
|
|
75
|
+
);
|
|
76
|
+
export interface OpenCodeSessionPrompt {
|
|
77
|
+
readonly sessionID: string;
|
|
78
|
+
readonly messageID?: string;
|
|
79
|
+
prompt: { text?: string; [k: string]: unknown };
|
|
80
|
+
metadata?: Record<string, unknown>;
|
|
81
|
+
delivery?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface OpenCodeSystemPart {
|
|
84
|
+
type: "text";
|
|
85
|
+
text: string;
|
|
86
|
+
[k: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
export interface OpenCodeSessionRequest {
|
|
89
|
+
readonly sessionID: string;
|
|
90
|
+
readonly agent?: string;
|
|
91
|
+
readonly model?: { providerID?: string; id?: string; variant?: string };
|
|
92
|
+
system: OpenCodeSystemPart[];
|
|
93
|
+
[k: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
export interface OpenCodePermissionEvaluation {
|
|
96
|
+
readonly sessionID: string;
|
|
97
|
+
readonly agent?: string;
|
|
98
|
+
readonly action: string;
|
|
99
|
+
readonly resources: ReadonlyArray<string>;
|
|
100
|
+
readonly metadata?: Record<string, unknown>;
|
|
101
|
+
readonly source?: { type?: string; messageID?: string; id?: string };
|
|
102
|
+
effect: "allow" | "ask" | "deny";
|
|
103
|
+
message?: string;
|
|
104
|
+
}
|
|
105
|
+
export interface OpenCodeShellCreateBefore {
|
|
106
|
+
command: string;
|
|
107
|
+
cwd: string;
|
|
108
|
+
timeout?: number;
|
|
109
|
+
shell?: string;
|
|
110
|
+
env: Record<string, string | undefined>;
|
|
111
|
+
}
|
|
112
|
+
export interface OpenCodeBusEvent {
|
|
113
|
+
id?: string;
|
|
114
|
+
created?: number;
|
|
115
|
+
type: string;
|
|
116
|
+
location?: { directory?: string };
|
|
117
|
+
data?: Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
type Hook<T> = (
|
|
120
|
+
name: string,
|
|
121
|
+
callback: (event: T) => Promise<void> | void,
|
|
122
|
+
) => Promise<OpenCodeRegistration>;
|
|
123
|
+
export interface OpenCodePluginContext {
|
|
124
|
+
readonly location: { directory: string; project?: { id?: string; directory?: string } };
|
|
125
|
+
readonly options?: Record<string, unknown>;
|
|
126
|
+
readonly session: {
|
|
127
|
+
hook: Hook<OpenCodeSessionPrompt | OpenCodeSessionRequest>;
|
|
128
|
+
synthetic(input: { sessionID: string; text: string; description?: string }): Promise<unknown>;
|
|
129
|
+
};
|
|
130
|
+
readonly tool: { hook: Hook<OpenCodeToolBefore | OpenCodeToolAfter> };
|
|
131
|
+
readonly permission: { hook: Hook<OpenCodePermissionEvaluation> };
|
|
132
|
+
readonly shell: { hook: Hook<OpenCodeShellCreateBefore> };
|
|
133
|
+
readonly event: {
|
|
134
|
+
subscribe(options?: { signal?: AbortSignal }): AsyncIterable<OpenCodeBusEvent>;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface HarneryOpenCodeConfig {
|
|
141
|
+
schema: string;
|
|
142
|
+
/** `agent-hook` launcher: repo-relative path (run through `bash`) or the bare `agent-hook` on PATH. */
|
|
143
|
+
agentHook: string;
|
|
144
|
+
/** Project root that `agent-hook` resolves its coord root from (its cwd). */
|
|
145
|
+
projectRoot: string;
|
|
146
|
+
timeoutMs: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the plugin's config from the sibling `harnery.json`. The plugin lives
|
|
151
|
+
* at `<root>/.opencode/plugins/harnery/index.ts`, so the project root is three
|
|
152
|
+
* directories up; `agentHook` is relative to that root, as `harn init` writes it.
|
|
153
|
+
*/
|
|
154
|
+
export function loadHarneryOpenCodeConfig(pluginDir: string): HarneryOpenCodeConfig {
|
|
155
|
+
const projectRoot = resolve(pluginDir, "..", "..", "..");
|
|
156
|
+
let agentHook = "agent-hook";
|
|
157
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
158
|
+
const path = resolve(pluginDir, HARNERY_OPENCODE_CONFIG_FILE);
|
|
159
|
+
if (existsSync(path)) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
162
|
+
agentHook?: unknown;
|
|
163
|
+
timeoutMs?: unknown;
|
|
164
|
+
};
|
|
165
|
+
if (typeof parsed.agentHook === "string" && parsed.agentHook.length > 0) {
|
|
166
|
+
agentHook = parsed.agentHook;
|
|
167
|
+
}
|
|
168
|
+
if (typeof parsed.timeoutMs === "number" && parsed.timeoutMs > 0) {
|
|
169
|
+
timeoutMs = parsed.timeoutMs;
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
/* unreadable config → PATH launcher, default timeout */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { schema: HARNERY_OPENCODE_CONFIG_SCHEMA, agentHook, projectRoot, timeoutMs };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** argv for one bridge call, mirroring `hookCommand` in `src/core/hooks/adapter/wiring.ts`. */
|
|
179
|
+
export function agentHookArgv(config: HarneryOpenCodeConfig, subcommand: string): string[] {
|
|
180
|
+
const tail = [subcommand, "--adapter", ADAPTER];
|
|
181
|
+
if (config.agentHook === "agent-hook") return ["agent-hook", ...tail];
|
|
182
|
+
const launcher = isAbsolute(config.agentHook)
|
|
183
|
+
? config.agentHook
|
|
184
|
+
: resolve(config.projectRoot, config.agentHook);
|
|
185
|
+
return ["bash", launcher, ...tail];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Directories a tool shell needs so the project's bare launchers resolve: its
|
|
190
|
+
* own `bin/`, plus whichever Bun install directory exists (a project launcher is
|
|
191
|
+
* commonly a shim that execs `bun`). Only existing directories are returned, so
|
|
192
|
+
* a PATH is never padded with dead entries.
|
|
193
|
+
*/
|
|
194
|
+
export function shellPathDirs(projectRoot: string, home = homedir()): string[] {
|
|
195
|
+
const dirs = [join(projectRoot, "bin")];
|
|
196
|
+
for (const bin of [join(home, ".bun", "bin"), "/usr/local/bin", "/opt/homebrew/bin"]) {
|
|
197
|
+
if (existsSync(join(bin, "bun"))) dirs.push(bin);
|
|
198
|
+
}
|
|
199
|
+
return dirs.filter((dir) => existsSync(dir));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Prepend {@link shellPathDirs} to a tool shell's PATH, preserving order and
|
|
204
|
+
* skipping entries already present. An OpenCode server can start from a
|
|
205
|
+
* GUI-launched environment whose PATH lacks the project's own tools, which would
|
|
206
|
+
* make an otherwise-valid `bin/<launcher>` call fail as "command not found".
|
|
207
|
+
*/
|
|
208
|
+
export function prependShellPath(
|
|
209
|
+
env: Record<string, string | undefined>,
|
|
210
|
+
projectRoot: string,
|
|
211
|
+
home = homedir(),
|
|
212
|
+
): void {
|
|
213
|
+
const current = env.PATH ?? process.env.PATH ?? "";
|
|
214
|
+
const parts = current.split(delimiter).filter((part) => part.length > 0);
|
|
215
|
+
const additions = shellPathDirs(projectRoot, home).filter((dir) => !parts.includes(dir));
|
|
216
|
+
env.PATH = [...additions, ...parts].join(delimiter);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Payload translation (pure; unit-tested) ──────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export type HookPayload = Record<string, unknown>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* OpenCode names tools in lowercase (`shell`, `read`, `edit`); Harnery's
|
|
225
|
+
* parsers key command extraction on `Bash`/`Shell` and target hashing on the
|
|
226
|
+
* capitalized Claude Code names, so capitalize and map `shell` → `Shell`.
|
|
227
|
+
*/
|
|
228
|
+
export function harneryToolName(tool: string | undefined): string {
|
|
229
|
+
if (!tool) return "unknown";
|
|
230
|
+
if (tool === "shell" || tool === "bash") return tool === "shell" ? "Shell" : "Bash";
|
|
231
|
+
return tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function modelLabel(model: unknown): string | undefined {
|
|
235
|
+
if (!model || typeof model !== "object") return undefined;
|
|
236
|
+
const m = model as { providerID?: unknown; id?: unknown; variant?: unknown };
|
|
237
|
+
if (typeof m.id !== "string") return undefined;
|
|
238
|
+
const base = typeof m.providerID === "string" ? `${m.providerID}/${m.id}` : m.id;
|
|
239
|
+
return typeof m.variant === "string" && m.variant ? `${base}#${m.variant}` : base;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function str(value: unknown): string | undefined {
|
|
243
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function sessionStartPayload(data: Record<string, unknown>, cwd: string): HookPayload {
|
|
247
|
+
return {
|
|
248
|
+
hook_event_name: "SessionStart",
|
|
249
|
+
session_id: data.sessionID,
|
|
250
|
+
cwd: str(data.directory) ?? cwd,
|
|
251
|
+
source: "startup",
|
|
252
|
+
...(str(data.parentID) ? { parent_session_id: data.parentID } : {}),
|
|
253
|
+
...(modelLabel(data.model) ? { model: modelLabel(data.model) } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function subagentStartPayload(data: Record<string, unknown>, cwd: string): HookPayload {
|
|
258
|
+
return {
|
|
259
|
+
hook_event_name: "SubagentStart",
|
|
260
|
+
session_id: data.parentID,
|
|
261
|
+
parent_session_id: data.parentID,
|
|
262
|
+
subagent_id: data.sessionID,
|
|
263
|
+
agent_type: str(data.agent) ?? "subagent",
|
|
264
|
+
cwd: str(data.directory) ?? cwd,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function promptPayload(event: OpenCodeSessionPrompt, cwd: string): HookPayload {
|
|
269
|
+
return {
|
|
270
|
+
hook_event_name: "UserPromptSubmit",
|
|
271
|
+
session_id: event.sessionID,
|
|
272
|
+
cwd,
|
|
273
|
+
...(event.messageID ? { turn_id: event.messageID } : {}),
|
|
274
|
+
...(typeof event.prompt?.text === "string" ? { prompt: event.prompt.text } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function toolBeforePayload(event: OpenCodeToolBefore, cwd: string): HookPayload {
|
|
279
|
+
return {
|
|
280
|
+
hook_event_name: "PreToolUse",
|
|
281
|
+
session_id: event.sessionID,
|
|
282
|
+
cwd,
|
|
283
|
+
...(event.messageID ? { turn_id: event.messageID } : {}),
|
|
284
|
+
tool_name: harneryToolName(event.tool),
|
|
285
|
+
tool_input: event.input ?? null,
|
|
286
|
+
tool_use_id: event.id,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function toolAfterPayload(
|
|
291
|
+
event: OpenCodeToolAfter,
|
|
292
|
+
cwd: string,
|
|
293
|
+
): { subcommand: "post-tool-use" | "post-tool-use-failure"; payload: HookPayload } {
|
|
294
|
+
const base = {
|
|
295
|
+
session_id: event.sessionID,
|
|
296
|
+
cwd,
|
|
297
|
+
...(event.messageID ? { turn_id: event.messageID } : {}),
|
|
298
|
+
tool_name: harneryToolName(event.tool),
|
|
299
|
+
tool_input: event.input ?? null,
|
|
300
|
+
tool_use_id: event.id,
|
|
301
|
+
};
|
|
302
|
+
if (event.status === "error") {
|
|
303
|
+
return {
|
|
304
|
+
subcommand: "post-tool-use-failure",
|
|
305
|
+
payload: {
|
|
306
|
+
hook_event_name: "PostToolUseFailure",
|
|
307
|
+
...base,
|
|
308
|
+
tool_response: errorText(event.error),
|
|
309
|
+
reason: errorText(event.error),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
subcommand: "post-tool-use",
|
|
315
|
+
payload: {
|
|
316
|
+
hook_event_name: "PostToolUse",
|
|
317
|
+
...base,
|
|
318
|
+
tool_response: toolResultText(event.result),
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Flatten an OpenCode `Tool.Result` to the text agent-hook summarizes. */
|
|
324
|
+
export function toolResultText(result: unknown): unknown {
|
|
325
|
+
if (!result || typeof result !== "object") return result;
|
|
326
|
+
const r = result as { output?: unknown; content?: unknown };
|
|
327
|
+
if (typeof r.output === "string") return r.output;
|
|
328
|
+
if (typeof r.content === "string") return r.content;
|
|
329
|
+
if (Array.isArray(r.content)) {
|
|
330
|
+
const texts = r.content
|
|
331
|
+
.map((part) =>
|
|
332
|
+
part && typeof part === "object" ? (part as { text?: unknown }).text : undefined,
|
|
333
|
+
)
|
|
334
|
+
.filter((t): t is string => typeof t === "string");
|
|
335
|
+
if (texts.length > 0) return texts.join("\n");
|
|
336
|
+
}
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function errorText(error: unknown): string {
|
|
341
|
+
if (error instanceof Error) return error.message;
|
|
342
|
+
if (
|
|
343
|
+
error &&
|
|
344
|
+
typeof error === "object" &&
|
|
345
|
+
typeof (error as { message?: unknown }).message === "string"
|
|
346
|
+
) {
|
|
347
|
+
return (error as { message: string }).message;
|
|
348
|
+
}
|
|
349
|
+
return typeof error === "string" ? error : JSON.stringify(error ?? null);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function permissionPayload(event: OpenCodePermissionEvaluation, cwd: string): HookPayload {
|
|
353
|
+
return {
|
|
354
|
+
hook_event_name: "PermissionRequest",
|
|
355
|
+
session_id: event.sessionID,
|
|
356
|
+
cwd,
|
|
357
|
+
tool_name: harneryToolName(event.action),
|
|
358
|
+
tool_input: {
|
|
359
|
+
resources: [...event.resources],
|
|
360
|
+
...(event.message ? { description: event.message } : {}),
|
|
361
|
+
effect: event.effect,
|
|
362
|
+
},
|
|
363
|
+
...(event.source?.id ? { tool_use_id: event.source.id } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function compactionPayload(event: OpenCodeSessionRequest, cwd: string): HookPayload {
|
|
368
|
+
return {
|
|
369
|
+
hook_event_name: "PreCompact",
|
|
370
|
+
session_id: event.sessionID,
|
|
371
|
+
cwd,
|
|
372
|
+
trigger: "auto",
|
|
373
|
+
...(modelLabel(event.model) ? { model: modelLabel(event.model) } : {}),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function turnEndPayload(
|
|
378
|
+
type:
|
|
379
|
+
| "session.execution.succeeded"
|
|
380
|
+
| "session.execution.failed"
|
|
381
|
+
| "session.execution.interrupted",
|
|
382
|
+
data: Record<string, unknown>,
|
|
383
|
+
cwd: string,
|
|
384
|
+
child: boolean,
|
|
385
|
+
): { subcommand: "stop" | "stop-failure" | "sub-agent-stop"; payload: HookPayload } {
|
|
386
|
+
const ok = type === "session.execution.succeeded";
|
|
387
|
+
const status = ok ? "ok" : type === "session.execution.failed" ? "error" : "interrupted";
|
|
388
|
+
const reason = ok ? undefined : (str(data.reason) ?? errorText(data.error));
|
|
389
|
+
if (child) {
|
|
390
|
+
return {
|
|
391
|
+
subcommand: "sub-agent-stop",
|
|
392
|
+
payload: {
|
|
393
|
+
hook_event_name: "SubagentStop",
|
|
394
|
+
session_id: data.sessionID,
|
|
395
|
+
cwd,
|
|
396
|
+
exit_status: status,
|
|
397
|
+
...(reason ? { reason } : {}),
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (ok) {
|
|
402
|
+
return {
|
|
403
|
+
subcommand: "stop",
|
|
404
|
+
payload: {
|
|
405
|
+
hook_event_name: "Stop",
|
|
406
|
+
session_id: data.sessionID,
|
|
407
|
+
cwd,
|
|
408
|
+
stop_hook_active: false,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
subcommand: "stop-failure",
|
|
414
|
+
payload: {
|
|
415
|
+
hook_event_name: "StopFailure",
|
|
416
|
+
session_id: data.sessionID,
|
|
417
|
+
cwd,
|
|
418
|
+
...(reason ? { reason } : {}),
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function sessionEndPayload(data: Record<string, unknown>, cwd: string): HookPayload {
|
|
424
|
+
return {
|
|
425
|
+
hook_event_name: "SessionEnd",
|
|
426
|
+
session_id: data.sessionID,
|
|
427
|
+
cwd,
|
|
428
|
+
clean_exit: true,
|
|
429
|
+
reason: "deleted",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── agent-hook stdout interpretation (pure; unit-tested) ─────────────────────
|
|
434
|
+
|
|
435
|
+
export interface HookOutcome {
|
|
436
|
+
/** `additionalContext` text agent-hook asked to inject, if any. */
|
|
437
|
+
context?: string;
|
|
438
|
+
/** A PreToolUse deny reason, if agent-hook blocked the call. */
|
|
439
|
+
deny?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Read the Claude-shaped `hookSpecificOutput` envelope agent-hook writes for this adapter. */
|
|
443
|
+
export function interpretHookStdout(stdout: string): HookOutcome {
|
|
444
|
+
const outcome: HookOutcome = {};
|
|
445
|
+
for (const line of stdout.split("\n")) {
|
|
446
|
+
const trimmed = line.trim();
|
|
447
|
+
if (!trimmed.startsWith("{")) continue;
|
|
448
|
+
let json: { hookSpecificOutput?: Record<string, unknown> };
|
|
449
|
+
try {
|
|
450
|
+
json = JSON.parse(trimmed) as typeof json;
|
|
451
|
+
} catch {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const out = json.hookSpecificOutput;
|
|
455
|
+
if (!out) continue;
|
|
456
|
+
if (typeof out.additionalContext === "string" && out.additionalContext) {
|
|
457
|
+
outcome.context = outcome.context
|
|
458
|
+
? `${outcome.context}\n\n${out.additionalContext}`
|
|
459
|
+
: out.additionalContext;
|
|
460
|
+
}
|
|
461
|
+
if (out.permissionDecision === "deny") {
|
|
462
|
+
outcome.deny =
|
|
463
|
+
typeof out.permissionDecisionReason === "string" && out.permissionDecisionReason
|
|
464
|
+
? out.permissionDecisionReason
|
|
465
|
+
: "Blocked by Harnery coordination guard.";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return outcome;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Bridge runtime ───────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
export interface BridgeResult {
|
|
474
|
+
exitCode: number | null;
|
|
475
|
+
stdout: string;
|
|
476
|
+
stderr: string;
|
|
477
|
+
timedOut: boolean;
|
|
478
|
+
error?: string;
|
|
479
|
+
}
|
|
480
|
+
export type Bridge = (subcommand: string, payload: HookPayload) => Promise<BridgeResult>;
|
|
481
|
+
|
|
482
|
+
/** Spawn `agent-hook` with the payload on stdin. Never throws. */
|
|
483
|
+
export function createAgentHookBridge(config: HarneryOpenCodeConfig): Bridge {
|
|
484
|
+
return (subcommand, payload) =>
|
|
485
|
+
new Promise<BridgeResult>((resolvePromise) => {
|
|
486
|
+
const [bin, ...args] = agentHookArgv(config, subcommand);
|
|
487
|
+
let stdout = "";
|
|
488
|
+
let stderr = "";
|
|
489
|
+
let settled = false;
|
|
490
|
+
let timedOut = false;
|
|
491
|
+
const finish = (result: BridgeResult): void => {
|
|
492
|
+
if (settled) return;
|
|
493
|
+
settled = true;
|
|
494
|
+
clearTimeout(timer);
|
|
495
|
+
resolvePromise(result);
|
|
496
|
+
};
|
|
497
|
+
let child: ReturnType<typeof spawn>;
|
|
498
|
+
try {
|
|
499
|
+
child = spawn(bin!, args, {
|
|
500
|
+
cwd: config.projectRoot,
|
|
501
|
+
env: { ...process.env, HARNERY_AGENT_COORD_PLATFORM: ADAPTER },
|
|
502
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
finish({ exitCode: null, stdout, stderr, timedOut, error: errorText(error) });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const timer = setTimeout(() => {
|
|
509
|
+
timedOut = true;
|
|
510
|
+
try {
|
|
511
|
+
child.kill("SIGKILL");
|
|
512
|
+
} catch {
|
|
513
|
+
/* already gone */
|
|
514
|
+
}
|
|
515
|
+
}, config.timeoutMs);
|
|
516
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
517
|
+
stdout += chunk.toString("utf8");
|
|
518
|
+
});
|
|
519
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
520
|
+
stderr += chunk.toString("utf8");
|
|
521
|
+
});
|
|
522
|
+
child.on("error", (error) =>
|
|
523
|
+
finish({ exitCode: null, stdout, stderr, timedOut, error: errorText(error) }),
|
|
524
|
+
);
|
|
525
|
+
child.on("close", (code) => finish({ exitCode: code, stdout, stderr, timedOut }));
|
|
526
|
+
try {
|
|
527
|
+
child.stdin?.on("error", () => undefined);
|
|
528
|
+
child.stdin?.end(JSON.stringify(payload));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
finish({ exitCode: null, stdout, stderr, timedOut, error: errorText(error) });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface PluginDependencies {
|
|
536
|
+
bridge?: Bridge;
|
|
537
|
+
config?: HarneryOpenCodeConfig;
|
|
538
|
+
log?: (event: string, detail?: Record<string, unknown>) => void;
|
|
539
|
+
/** Override the plugin directory (tests); defaults to this file's directory. */
|
|
540
|
+
pluginDir?: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function defaultPluginDir(): string {
|
|
544
|
+
try {
|
|
545
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
546
|
+
} catch {
|
|
547
|
+
return process.cwd();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function fileLogger(
|
|
552
|
+
projectRoot: string,
|
|
553
|
+
): (event: string, detail?: Record<string, unknown>) => void {
|
|
554
|
+
const dir = resolve(projectRoot, ".harnery", "debug");
|
|
555
|
+
const file = resolve(dir, "opencode-plugin.ndjson");
|
|
556
|
+
return (event, detail) => {
|
|
557
|
+
try {
|
|
558
|
+
mkdirSync(dir, { recursive: true });
|
|
559
|
+
appendFileSync(
|
|
560
|
+
file,
|
|
561
|
+
`${JSON.stringify({ ts: new Date().toISOString(), event, ...detail })}\n`,
|
|
562
|
+
);
|
|
563
|
+
} catch {
|
|
564
|
+
/* logging is best-effort */
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Build the plugin definition. `deps` exist for tests: a fake bridge records
|
|
571
|
+
* the payloads instead of spawning `agent-hook`.
|
|
572
|
+
*/
|
|
573
|
+
export function createHarneryOpenCodePlugin(deps: PluginDependencies = {}) {
|
|
574
|
+
return {
|
|
575
|
+
id: HARNERY_OPENCODE_PLUGIN_ID,
|
|
576
|
+
async setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>> {
|
|
577
|
+
const pluginDir = deps.pluginDir ?? defaultPluginDir();
|
|
578
|
+
const config = deps.config ?? loadHarneryOpenCodeConfig(pluginDir);
|
|
579
|
+
// agent-hook resolves its coord root from cwd, so the bridge runs from
|
|
580
|
+
// the project root. Prefer OpenCode's own location for the payload cwd.
|
|
581
|
+
const cwd = ctx.location?.directory || config.projectRoot;
|
|
582
|
+
const log = deps.log ?? fileLogger(config.projectRoot);
|
|
583
|
+
const bridge = deps.bridge ?? createAgentHookBridge(config);
|
|
584
|
+
/** Child (subagent) sessions seen on the bus, so turn ends map to sub-agent-stop. */
|
|
585
|
+
const children = new Set<string>();
|
|
586
|
+
/** Per-turn context agent-hook asked to inject, pushed into every model call's system prompt. */
|
|
587
|
+
const turnContext = new Map<string, string>();
|
|
588
|
+
/** shell command → session id, so the shell hook (which carries no session) can stamp the env. */
|
|
589
|
+
const pendingShells = new Map<string, string>();
|
|
590
|
+
let lastToolSession: string | undefined;
|
|
591
|
+
|
|
592
|
+
const run = async (subcommand: string, payload: HookPayload): Promise<HookOutcome> => {
|
|
593
|
+
const result = await bridge(subcommand, payload);
|
|
594
|
+
if (result.error || result.timedOut || (result.exitCode !== 0 && result.exitCode !== 2)) {
|
|
595
|
+
log("bridge_failure", {
|
|
596
|
+
subcommand,
|
|
597
|
+
exit_code: result.exitCode,
|
|
598
|
+
timed_out: result.timedOut,
|
|
599
|
+
error: result.error,
|
|
600
|
+
stderr: result.stderr.slice(0, 2000),
|
|
601
|
+
});
|
|
602
|
+
return {};
|
|
603
|
+
}
|
|
604
|
+
// Exit 2 is Claude Code's Stop-block channel. OpenCode cannot block a
|
|
605
|
+
// turn, so the verdict is recorded (agent-hook already wrote it) and
|
|
606
|
+
// the reason is kept in the plugin log only.
|
|
607
|
+
if (result.exitCode === 2) {
|
|
608
|
+
log("stop_verdict_observed", { subcommand, stderr: result.stderr.slice(0, 2000) });
|
|
609
|
+
}
|
|
610
|
+
return interpretHookStdout(result.stdout);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const inject = async (sessionID: string, text: string): Promise<void> => {
|
|
614
|
+
if (!text) return;
|
|
615
|
+
try {
|
|
616
|
+
await ctx.session.synthetic({ sessionID, text, description: CONTEXT_DESCRIPTION });
|
|
617
|
+
} catch (error) {
|
|
618
|
+
log("synthetic_failure", { session_id: sessionID, error: errorText(error) });
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const registrations: OpenCodeRegistration[] = [];
|
|
623
|
+
const register = async <T>(
|
|
624
|
+
hook: Hook<T>,
|
|
625
|
+
name: string,
|
|
626
|
+
cb: (event: T) => Promise<void> | void,
|
|
627
|
+
) => {
|
|
628
|
+
registrations.push(
|
|
629
|
+
await hook(name, async (event) => {
|
|
630
|
+
try {
|
|
631
|
+
await cb(event);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
if (error instanceof HarneryDeny) throw error;
|
|
634
|
+
log("handler_failure", { hook: name, error: errorText(error) });
|
|
635
|
+
}
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
await register<OpenCodeSessionPrompt>(
|
|
641
|
+
ctx.session.hook as Hook<OpenCodeSessionPrompt>,
|
|
642
|
+
"prompt",
|
|
643
|
+
async (event) => {
|
|
644
|
+
const outcome = await run("user-prompt-submit", promptPayload(event, cwd));
|
|
645
|
+
if (outcome.context) turnContext.set(event.sessionID, outcome.context);
|
|
646
|
+
else turnContext.delete(event.sessionID);
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
await register<OpenCodeSessionRequest>(
|
|
650
|
+
ctx.session.hook as Hook<OpenCodeSessionRequest>,
|
|
651
|
+
"context",
|
|
652
|
+
(event) => {
|
|
653
|
+
const text = turnContext.get(event.sessionID);
|
|
654
|
+
if (text && Array.isArray(event.system)) event.system.push({ type: "text", text });
|
|
655
|
+
},
|
|
656
|
+
);
|
|
657
|
+
await register<OpenCodeSessionRequest>(
|
|
658
|
+
ctx.session.hook as Hook<OpenCodeSessionRequest>,
|
|
659
|
+
"compaction",
|
|
660
|
+
async (event) => {
|
|
661
|
+
await run("pre-compact", compactionPayload(event, cwd));
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
await register<OpenCodeToolBefore>(
|
|
665
|
+
ctx.tool.hook as Hook<OpenCodeToolBefore>,
|
|
666
|
+
"execute.before",
|
|
667
|
+
async (event) => {
|
|
668
|
+
lastToolSession = event.sessionID;
|
|
669
|
+
const command =
|
|
670
|
+
event.input && typeof event.input === "object"
|
|
671
|
+
? (event.input as { command?: unknown }).command
|
|
672
|
+
: undefined;
|
|
673
|
+
if (typeof command === "string") pendingShells.set(command, event.sessionID);
|
|
674
|
+
const outcome = await run("pre-tool-use", toolBeforePayload(event, cwd));
|
|
675
|
+
if (outcome.deny) throw new HarneryDeny(outcome.deny);
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
await register<OpenCodeToolAfter>(
|
|
679
|
+
ctx.tool.hook as Hook<OpenCodeToolAfter>,
|
|
680
|
+
"execute.after",
|
|
681
|
+
async (event) => {
|
|
682
|
+
const { subcommand, payload } = toolAfterPayload(event, cwd);
|
|
683
|
+
await run(subcommand, payload);
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
await register<OpenCodePermissionEvaluation>(
|
|
687
|
+
ctx.permission.hook,
|
|
688
|
+
"evaluate",
|
|
689
|
+
async (event) => {
|
|
690
|
+
await run("permission-request", permissionPayload(event, cwd));
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
await register<OpenCodeShellCreateBefore>(ctx.shell.hook, "create.before", (event) => {
|
|
694
|
+
const sessionID = pendingShells.get(event.command) ?? lastToolSession;
|
|
695
|
+
if (typeof event.command === "string") pendingShells.delete(event.command);
|
|
696
|
+
if (!event.env) return;
|
|
697
|
+
// PATH is session-independent, so stamp it even for a shell the plugin
|
|
698
|
+
// cannot attribute to a session.
|
|
699
|
+
prependShellPath(event.env, config.projectRoot);
|
|
700
|
+
if (!sessionID) return;
|
|
701
|
+
event.env[OPENCODE_SESSION_ENV] = sessionID;
|
|
702
|
+
event.env.HARNERY_AGENT_COORD_PLATFORM = ADAPTER;
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const controller = new AbortController();
|
|
706
|
+
const handleBus = async (event: OpenCodeBusEvent): Promise<void> => {
|
|
707
|
+
if (
|
|
708
|
+
event.location?.directory &&
|
|
709
|
+
ctx.location?.directory &&
|
|
710
|
+
event.location.directory !== ctx.location.directory
|
|
711
|
+
) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const data = event.data ?? {};
|
|
715
|
+
const sessionID = str(data.sessionID);
|
|
716
|
+
if (!sessionID) return;
|
|
717
|
+
switch (event.type) {
|
|
718
|
+
case "session.created": {
|
|
719
|
+
if (str(data.parentID)) {
|
|
720
|
+
children.add(sessionID);
|
|
721
|
+
const outcome = await run("sub-agent-start", subagentStartPayload(data, cwd));
|
|
722
|
+
if (outcome.context) await inject(sessionID, outcome.context);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const outcome = await run("session-start", sessionStartPayload(data, cwd));
|
|
726
|
+
if (outcome.context) await inject(sessionID, outcome.context);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
case "session.execution.succeeded":
|
|
730
|
+
case "session.execution.failed":
|
|
731
|
+
case "session.execution.interrupted": {
|
|
732
|
+
turnContext.delete(sessionID);
|
|
733
|
+
const { subcommand, payload } = turnEndPayload(
|
|
734
|
+
event.type,
|
|
735
|
+
data,
|
|
736
|
+
cwd,
|
|
737
|
+
children.has(sessionID),
|
|
738
|
+
);
|
|
739
|
+
await run(subcommand, payload);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
case "session.deleted": {
|
|
743
|
+
turnContext.delete(sessionID);
|
|
744
|
+
children.delete(sessionID);
|
|
745
|
+
await run("session-end", sessionEndPayload(data, cwd));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
default:
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
void (async () => {
|
|
753
|
+
try {
|
|
754
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
755
|
+
try {
|
|
756
|
+
await handleBus(event);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
log("bus_handler_failure", { type: event.type, error: errorText(error) });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (!controller.signal.aborted)
|
|
763
|
+
log("bus_subscription_ended", { error: errorText(error) });
|
|
764
|
+
}
|
|
765
|
+
})();
|
|
766
|
+
|
|
767
|
+
log("plugin_ready", { project_root: config.projectRoot, agent_hook: config.agentHook, cwd });
|
|
768
|
+
|
|
769
|
+
return async () => {
|
|
770
|
+
controller.abort();
|
|
771
|
+
for (const registration of registrations.splice(0)) {
|
|
772
|
+
try {
|
|
773
|
+
await registration.dispose();
|
|
774
|
+
} catch {
|
|
775
|
+
/* disposing on shutdown */
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Thrown from `execute.before` to reject a tool call; OpenCode surfaces the message to the model. */
|
|
784
|
+
export class HarneryDeny extends Error {
|
|
785
|
+
constructor(reason: string) {
|
|
786
|
+
super(reason);
|
|
787
|
+
this.name = "HarneryDeny";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export default createHarneryOpenCodePlugin();
|