@caupulican/pi-adaptative 0.81.38 → 0.81.39
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/CHANGELOG.md +79 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/README.md +18 -1
- package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts +131 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts.map +1 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js +196 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js.map +1 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.ts +308 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +22 -2
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +585 -24
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +749 -27
- package/dist/bundled-resources/runtimes/pi-shell-engine/commands/__init__.py +43 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/commands/fs.py +270 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/commands/search.py +252 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/commands/strings.py +399 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/commands/text.py +575 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/context.py +52 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/errors.py +49 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/exec.py +734 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/expand.py +238 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/main.py +132 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/nodes.py +116 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/parser.py +287 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/proc.py +137 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/state.py +100 -0
- package/dist/bundled-resources/runtimes/pi-shell-engine/tokens.py +579 -0
- package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +19 -9
- package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +22 -5
- package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +5 -5
- package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +28 -7
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +15 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-paths.d.ts +49 -0
- package/dist/core/agent-paths.d.ts.map +1 -0
- package/dist/core/agent-paths.js +107 -0
- package/dist/core/agent-paths.js.map +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -3
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +234 -17
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +484 -62
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/contracts.d.ts +9 -0
- package/dist/core/autonomy/contracts.d.ts.map +1 -1
- package/dist/core/autonomy/contracts.js.map +1 -1
- package/dist/core/autonomy/lane-tracker.d.ts +10 -1
- package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tracker.js +5 -1
- package/dist/core/autonomy/lane-tracker.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +122 -6
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +446 -89
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/bash-execution-controller.d.ts +4 -0
- package/dist/core/bash-execution-controller.d.ts.map +1 -1
- package/dist/core/bash-execution-controller.js +7 -1
- package/dist/core/bash-execution-controller.js.map +1 -1
- package/dist/core/compaction-support.d.ts +13 -3
- package/dist/core/compaction-support.d.ts.map +1 -1
- package/dist/core/compaction-support.js +43 -7
- package/dist/core/compaction-support.js.map +1 -1
- package/dist/core/context/context-audit.d.ts +33 -1
- package/dist/core/context/context-audit.d.ts.map +1 -1
- package/dist/core/context/context-audit.js +31 -5
- package/dist/core/context/context-audit.js.map +1 -1
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +16 -1
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/context-pipeline.d.ts +26 -4
- package/dist/core/context-pipeline.d.ts.map +1 -1
- package/dist/core/context-pipeline.js +137 -14
- package/dist/core/context-pipeline.js.map +1 -1
- package/dist/core/cost-guard.d.ts +19 -3
- package/dist/core/cost-guard.d.ts.map +1 -1
- package/dist/core/cost-guard.js +18 -3
- package/dist/core/cost-guard.js.map +1 -1
- package/dist/core/delegation/session-worker-result.d.ts +34 -4
- package/dist/core/delegation/session-worker-result.d.ts.map +1 -1
- package/dist/core/delegation/session-worker-result.js +64 -4
- package/dist/core/delegation/session-worker-result.js.map +1 -1
- package/dist/core/delegation/worker-result.d.ts +42 -1
- package/dist/core/delegation/worker-result.d.ts.map +1 -1
- package/dist/core/delegation/worker-result.js +68 -0
- package/dist/core/delegation/worker-result.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +20 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +51 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/goal-loop-controller.d.ts +17 -1
- package/dist/core/goal-loop-controller.d.ts.map +1 -1
- package/dist/core/goal-loop-controller.js +79 -11
- package/dist/core/goal-loop-controller.js.map +1 -1
- package/dist/core/goals/goal-continuation-controller.d.ts +48 -2
- package/dist/core/goals/goal-continuation-controller.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-controller.js +77 -0
- package/dist/core/goals/goal-continuation-controller.js.map +1 -1
- package/dist/core/goals/goal-continuation-defaults.d.ts +39 -0
- package/dist/core/goals/goal-continuation-defaults.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-defaults.js +42 -0
- package/dist/core/goals/goal-continuation-defaults.js.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.d.ts +4 -0
- package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.js +51 -10
- package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
- package/dist/core/goals/goal-runtime-snapshot.d.ts +101 -2
- package/dist/core/goals/goal-runtime-snapshot.d.ts.map +1 -1
- package/dist/core/goals/goal-runtime-snapshot.js +87 -4
- package/dist/core/goals/goal-runtime-snapshot.js.map +1 -1
- package/dist/core/goals/goal-state.d.ts +89 -1
- package/dist/core/goals/goal-state.d.ts.map +1 -1
- package/dist/core/goals/goal-state.js +71 -5
- package/dist/core/goals/goal-state.js.map +1 -1
- package/dist/core/goals/goal-tool-core.d.ts +56 -2
- package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
- package/dist/core/goals/goal-tool-core.js +111 -5
- package/dist/core/goals/goal-tool-core.js.map +1 -1
- package/dist/core/goals/session-goal-state.d.ts +11 -2
- package/dist/core/goals/session-goal-state.d.ts.map +1 -1
- package/dist/core/goals/session-goal-state.js +32 -17
- package/dist/core/goals/session-goal-state.js.map +1 -1
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +10 -2
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/learning/observation-store.d.ts +12 -3
- package/dist/core/learning/observation-store.d.ts.map +1 -1
- package/dist/core/learning/observation-store.js +30 -15
- package/dist/core/learning/observation-store.js.map +1 -1
- package/dist/core/learning/skill-curator.d.ts +5 -1
- package/dist/core/learning/skill-curator.d.ts.map +1 -1
- package/dist/core/learning/skill-curator.js +21 -19
- package/dist/core/learning/skill-curator.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts +65 -3
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +186 -26
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/memory/providers/file-store.d.ts +1 -1
- package/dist/core/memory/providers/file-store.d.ts.map +1 -1
- package/dist/core/memory/providers/file-store.js +6 -6
- package/dist/core/memory/providers/file-store.js.map +1 -1
- package/dist/core/model-capability.d.ts +34 -0
- package/dist/core/model-capability.d.ts.map +1 -1
- package/dist/core/model-capability.js +42 -1
- package/dist/core/model-capability.js.map +1 -1
- package/dist/core/model-router/tool-escalation.d.ts +15 -0
- package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
- package/dist/core/model-router/tool-escalation.js +23 -1
- package/dist/core/model-router/tool-escalation.js.map +1 -1
- package/dist/core/model-router-controller.d.ts +34 -7
- package/dist/core/model-router-controller.d.ts.map +1 -1
- package/dist/core/model-router-controller.js +95 -16
- package/dist/core/model-router-controller.js.map +1 -1
- package/dist/core/models/adaptation-store.d.ts +7 -0
- package/dist/core/models/adaptation-store.d.ts.map +1 -1
- package/dist/core/models/adaptation-store.js +47 -11
- package/dist/core/models/adaptation-store.js.map +1 -1
- package/dist/core/models/default-model-suggestions.d.ts.map +1 -1
- package/dist/core/models/default-model-suggestions.js +17 -0
- package/dist/core/models/default-model-suggestions.js.map +1 -1
- package/dist/core/models/fitness-store.d.ts +3 -0
- package/dist/core/models/fitness-store.d.ts.map +1 -1
- package/dist/core/models/fitness-store.js +11 -2
- package/dist/core/models/fitness-store.js.map +1 -1
- package/dist/core/models/llamacpp-runtime.d.ts +180 -0
- package/dist/core/models/llamacpp-runtime.d.ts.map +1 -0
- package/dist/core/models/llamacpp-runtime.js +475 -0
- package/dist/core/models/llamacpp-runtime.js.map +1 -0
- package/dist/core/models/local-registration.d.ts +40 -0
- package/dist/core/models/local-registration.d.ts.map +1 -1
- package/dist/core/models/local-registration.js +94 -5
- package/dist/core/models/local-registration.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +6 -5
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/models/model-ref.d.ts +7 -0
- package/dist/core/models/model-ref.d.ts.map +1 -1
- package/dist/core/models/model-ref.js +26 -0
- package/dist/core/models/model-ref.js.map +1 -1
- package/dist/core/models/needle-runtime.d.ts +257 -0
- package/dist/core/models/needle-runtime.d.ts.map +1 -0
- package/dist/core/models/needle-runtime.js +519 -0
- package/dist/core/models/needle-runtime.js.map +1 -0
- package/dist/core/models/prism-llamacpp-lifecycle.d.ts +89 -0
- package/dist/core/models/prism-llamacpp-lifecycle.d.ts.map +1 -0
- package/dist/core/models/prism-llamacpp-lifecycle.js +121 -0
- package/dist/core/models/prism-llamacpp-lifecycle.js.map +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +11 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/process-matrix/codes.d.ts +72 -0
- package/dist/core/process-matrix/codes.d.ts.map +1 -0
- package/dist/core/process-matrix/codes.js +15 -0
- package/dist/core/process-matrix/codes.js.map +1 -0
- package/dist/core/process-matrix/runtime.d.ts +63 -0
- package/dist/core/process-matrix/runtime.d.ts.map +1 -0
- package/dist/core/process-matrix/runtime.js +310 -0
- package/dist/core/process-matrix/runtime.js.map +1 -0
- package/dist/core/process-matrix/store.d.ts +22 -0
- package/dist/core/process-matrix/store.d.ts.map +1 -0
- package/dist/core/process-matrix/store.js +80 -0
- package/dist/core/process-matrix/store.js.map +1 -0
- package/dist/core/process-matrix/supervisor.d.ts +72 -0
- package/dist/core/process-matrix/supervisor.d.ts.map +1 -0
- package/dist/core/process-matrix/supervisor.js +130 -0
- package/dist/core/process-matrix/supervisor.js.map +1 -0
- package/dist/core/profile-registry.d.ts +7 -0
- package/dist/core/profile-registry.d.ts.map +1 -1
- package/dist/core/profile-registry.js +20 -0
- package/dist/core/profile-registry.js.map +1 -1
- package/dist/core/python-runtime.d.ts.map +1 -1
- package/dist/core/python-runtime.js +3 -3
- package/dist/core/python-runtime.js.map +1 -1
- package/dist/core/reflection-controller.d.ts +29 -5
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +215 -126
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/reload-blockers.d.ts +36 -0
- package/dist/core/reload-blockers.d.ts.map +1 -1
- package/dist/core/reload-blockers.js +44 -0
- package/dist/core/reload-blockers.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +8 -7
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +58 -5
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +209 -25
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/scout-controller.d.ts +6 -0
- package/dist/core/scout-controller.d.ts.map +1 -1
- package/dist/core/scout-controller.js +66 -52
- package/dist/core/scout-controller.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-role.d.ts +31 -0
- package/dist/core/session-role.d.ts.map +1 -0
- package/dist/core/session-role.js +52 -0
- package/dist/core/session-role.js.map +1 -0
- package/dist/core/settings-manager.d.ts +42 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +94 -60
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +12 -0
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.js +6 -0
- package/dist/core/system-prompt-builder.js.map +1 -1
- package/dist/core/system-prompt.d.ts +14 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +20 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tasks/session-task-state.d.ts +11 -2
- package/dist/core/tasks/session-task-state.d.ts.map +1 -1
- package/dist/core/tasks/session-task-state.js +27 -11
- package/dist/core/tasks/session-task-state.js.map +1 -1
- package/dist/core/tasks/task-contract-monitor.d.ts +45 -0
- package/dist/core/tasks/task-contract-monitor.d.ts.map +1 -0
- package/dist/core/tasks/task-contract-monitor.js +56 -0
- package/dist/core/tasks/task-contract-monitor.js.map +1 -0
- package/dist/core/tasks/task-state.d.ts +8 -0
- package/dist/core/tasks/task-state.d.ts.map +1 -1
- package/dist/core/tasks/task-state.js +28 -1
- package/dist/core/tasks/task-state.js.map +1 -1
- package/dist/core/tool-gate-controller.d.ts.map +1 -1
- package/dist/core/tool-gate-controller.js +5 -0
- package/dist/core/tool-gate-controller.js.map +1 -1
- package/dist/core/tool-recovery-log-records.d.ts +7 -0
- package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
- package/dist/core/tool-recovery-log-records.js +14 -6
- package/dist/core/tool-recovery-log-records.js.map +1 -1
- package/dist/core/tool-selection/promotion.d.ts +54 -0
- package/dist/core/tool-selection/promotion.d.ts.map +1 -0
- package/dist/core/tool-selection/promotion.js +81 -0
- package/dist/core/tool-selection/promotion.js.map +1 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts +37 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts.map +1 -1
- package/dist/core/tool-selection/tool-performance-store.js +87 -3
- package/dist/core/tool-selection/tool-performance-store.js.map +1 -1
- package/dist/core/tool-selection/tool-selection-controller.d.ts +45 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
- package/dist/core/tool-selection/tool-selection-controller.js +96 -0
- package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
- package/dist/core/tools/bash.d.ts +18 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +95 -16
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/delegate-status.d.ts +14 -0
- package/dist/core/tools/delegate-status.d.ts.map +1 -1
- package/dist/core/tools/delegate-status.js +82 -7
- package/dist/core/tools/delegate-status.js.map +1 -1
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +25 -8
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/goal.d.ts +94 -3
- package/dist/core/tools/goal.d.ts.map +1 -1
- package/dist/core/tools/goal.js +165 -15
- package/dist/core/tools/goal.js.map +1 -1
- package/dist/core/tools/model-fitness.d.ts +7 -0
- package/dist/core/tools/model-fitness.d.ts.map +1 -1
- package/dist/core/tools/model-fitness.js +2 -2
- package/dist/core/tools/model-fitness.js.map +1 -1
- package/dist/core/tools/shell-contract-router.d.ts +6 -1
- package/dist/core/tools/shell-contract-router.d.ts.map +1 -1
- package/dist/core/tools/shell-contract-router.js +23 -2
- package/dist/core/tools/shell-contract-router.js.map +1 -1
- package/dist/core/tools/shell-session.d.ts +89 -0
- package/dist/core/tools/shell-session.d.ts.map +1 -0
- package/dist/core/tools/shell-session.js +432 -0
- package/dist/core/tools/shell-session.js.map +1 -0
- package/dist/core/tools/task-steps.d.ts +4 -0
- package/dist/core/tools/task-steps.d.ts.map +1 -1
- package/dist/core/tools/task-steps.js +63 -8
- package/dist/core/tools/task-steps.js.map +1 -1
- package/dist/core/tools/tmux-dispatch.d.ts +86 -0
- package/dist/core/tools/tmux-dispatch.d.ts.map +1 -0
- package/dist/core/tools/tmux-dispatch.js +91 -0
- package/dist/core/tools/tmux-dispatch.js.map +1 -0
- package/dist/core/tools/windows-shell-engine.d.ts +42 -0
- package/dist/core/tools/windows-shell-engine.d.ts.map +1 -0
- package/dist/core/tools/windows-shell-engine.js +153 -0
- package/dist/core/tools/windows-shell-engine.js.map +1 -0
- package/dist/core/tools/windows-shell-state.d.ts +40 -0
- package/dist/core/tools/windows-shell-state.d.ts.map +1 -0
- package/dist/core/tools/windows-shell-state.js +59 -0
- package/dist/core/tools/windows-shell-state.js.map +1 -0
- package/dist/core/tools/worktree-sync.d.ts +24 -0
- package/dist/core/tools/worktree-sync.d.ts.map +1 -0
- package/dist/core/tools/worktree-sync.js +338 -0
- package/dist/core/tools/worktree-sync.js.map +1 -0
- package/dist/core/trust-manager.d.ts +4 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +20 -2
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/util/atomic-file.d.ts +55 -0
- package/dist/core/util/atomic-file.d.ts.map +1 -0
- package/dist/core/util/atomic-file.js +255 -0
- package/dist/core/util/atomic-file.js.map +1 -0
- package/dist/core/util/minimatch-cache.d.ts +33 -0
- package/dist/core/util/minimatch-cache.d.ts.map +1 -0
- package/dist/core/util/minimatch-cache.js +0 -0
- package/dist/core/util/minimatch-cache.js.map +1 -0
- package/dist/core/worktree-sync/codes.d.ts +227 -0
- package/dist/core/worktree-sync/codes.d.ts.map +1 -0
- package/dist/core/worktree-sync/codes.js +14 -0
- package/dist/core/worktree-sync/codes.js.map +1 -0
- package/dist/core/worktree-sync/git-engine.d.ts +156 -0
- package/dist/core/worktree-sync/git-engine.d.ts.map +1 -0
- package/dist/core/worktree-sync/git-engine.js +1191 -0
- package/dist/core/worktree-sync/git-engine.js.map +1 -0
- package/dist/core/worktree-sync/lane-gate.d.ts +75 -0
- package/dist/core/worktree-sync/lane-gate.d.ts.map +1 -0
- package/dist/core/worktree-sync/lane-gate.js +360 -0
- package/dist/core/worktree-sync/lane-gate.js.map +1 -0
- package/dist/core/worktree-sync/runtime.d.ts +47 -0
- package/dist/core/worktree-sync/runtime.d.ts.map +1 -0
- package/dist/core/worktree-sync/runtime.js +96 -0
- package/dist/core/worktree-sync/runtime.js.map +1 -0
- package/dist/core/worktree-sync/store.d.ts +69 -0
- package/dist/core/worktree-sync/store.d.ts.map +1 -0
- package/dist/core/worktree-sync/store.js +247 -0
- package/dist/core/worktree-sync/store.js.map +1 -0
- package/dist/core/worktree-sync/watcher.d.ts +29 -0
- package/dist/core/worktree-sync/watcher.d.ts.map +1 -0
- package/dist/core/worktree-sync/watcher.js +93 -0
- package/dist/core/worktree-sync/watcher.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +94 -0
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +38 -0
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/auto-learn-controller.d.ts +16 -1
- package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
- package/dist/modes/interactive/auto-learn-controller.js +50 -8
- package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.js +23 -1
- package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts +43 -0
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +290 -3
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-flow-commands.js +24 -4
- package/dist/modes/interactive/session-flow-commands.js.map +1 -1
- package/dist/utils/fs-watch.d.ts +11 -0
- package/dist/utils/fs-watch.d.ts.map +1 -1
- package/dist/utils/fs-watch.js +20 -2
- package/dist/utils/fs-watch.js.map +1 -1
- package/dist/utils/highlight-js-languages.d.ts +4 -0
- package/dist/utils/highlight-js-languages.d.ts.map +1 -0
- package/dist/utils/highlight-js-languages.js +573 -0
- package/dist/utils/highlight-js-languages.js.map +1 -0
- package/dist/utils/shell.d.ts +7 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +21 -6
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +53 -5
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +112 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/process-matrix.md +120 -0
- package/docs/settings.md +3 -0
- package/docs/tmux-agent-manager.md +85 -2
- package/docs/windows.md +52 -3
- package/docs/work-directory.md +29 -0
- package/docs/worktree-sync.md +250 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +10 -4
- package/docs/integration-sweep-builder-blueprint-2026-07-09.md +0 -365
- package/docs/integration-sweep-resume-2026-07-09.md +0 -407
|
@@ -6,7 +6,29 @@ import * as path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { type ExtensionAPI, type ExtensionContext, getProcessWorkRun } from "@caupulican/pi-adaptative";
|
|
8
8
|
import type { AgentToolResult } from "@caupulican/pi-agent-core";
|
|
9
|
+
import type { Usage } from "@caupulican/pi-ai";
|
|
9
10
|
import { Type } from "typebox";
|
|
11
|
+
import {
|
|
12
|
+
buildGrant,
|
|
13
|
+
buildGrantUsageEntry,
|
|
14
|
+
buildLaunchProfileFlags,
|
|
15
|
+
buildTombstone,
|
|
16
|
+
countGrantUsages,
|
|
17
|
+
decodeTmuxWorkerUsageClaim,
|
|
18
|
+
describeGrant,
|
|
19
|
+
GRANT_CUSTOM_TYPE,
|
|
20
|
+
GRANT_USAGE_CUSTOM_TYPE,
|
|
21
|
+
type GrantDispatchParams,
|
|
22
|
+
grantCovers,
|
|
23
|
+
isTmuxDispatchGrant,
|
|
24
|
+
isTmuxDispatchGrantTombstone,
|
|
25
|
+
type LaunchProfileFlag,
|
|
26
|
+
type LaunchProfileSource,
|
|
27
|
+
launchProfileSourceFromGrant,
|
|
28
|
+
ONE_SHOT_LAUNCH_PROFILE_SOURCE,
|
|
29
|
+
type Provider,
|
|
30
|
+
type TmuxDispatchGrant,
|
|
31
|
+
} from "./dispatch-grant.ts";
|
|
10
32
|
|
|
11
33
|
export const piConfig = { tools: ["tmux_agent_manager"] };
|
|
12
34
|
|
|
@@ -20,6 +42,8 @@ type Action =
|
|
|
20
42
|
| "workspace_plan"
|
|
21
43
|
| "launch_workspace"
|
|
22
44
|
| "fire_task"
|
|
45
|
+
| "send_followup"
|
|
46
|
+
| "dismiss"
|
|
23
47
|
| "job_status"
|
|
24
48
|
| "list_jobs"
|
|
25
49
|
| "set_variable"
|
|
@@ -27,10 +51,25 @@ type Action =
|
|
|
27
51
|
| "list_templates"
|
|
28
52
|
| "show_template"
|
|
29
53
|
| "stop_job"
|
|
30
|
-
| "stop_session"
|
|
31
|
-
|
|
54
|
+
| "stop_session"
|
|
55
|
+
| "grant_dispatch"
|
|
56
|
+
| "revoke_grant";
|
|
32
57
|
|
|
33
|
-
type AgentSpec = {
|
|
58
|
+
type AgentSpec = {
|
|
59
|
+
provider?: Provider;
|
|
60
|
+
name?: string;
|
|
61
|
+
command?: string;
|
|
62
|
+
cwd?: string;
|
|
63
|
+
tools?: string[];
|
|
64
|
+
/**
|
|
65
|
+
* Worktree-sync lane key this agent is bound to (core-internal only -- set by the goal->tmux
|
|
66
|
+
* lane-first dispatch adapter, `tmux-dispatch.ts`'s `dispatchTmuxWorker`, never by the model's own
|
|
67
|
+
* `fire_task` params). A `provider:"pi"` agent carrying this launches with `--worktree-lane <key>`
|
|
68
|
+
* appended and one extra lane-doctrine sentence in its scoped `--append-system-prompt` -- see
|
|
69
|
+
* `applyLaunchProfile`/`dispatch-grant.ts`'s `buildLaunchProfileFlags`.
|
|
70
|
+
*/
|
|
71
|
+
worktreeLane?: string;
|
|
72
|
+
};
|
|
34
73
|
type TeamTemplate = {
|
|
35
74
|
name: string;
|
|
36
75
|
description: string;
|
|
@@ -53,12 +92,33 @@ type Params = {
|
|
|
53
92
|
teamTemplate?: string;
|
|
54
93
|
task?: string;
|
|
55
94
|
jobId?: string;
|
|
95
|
+
agentId?: string;
|
|
56
96
|
variableName?: string;
|
|
57
97
|
variableValue?: string;
|
|
58
98
|
deadlineSeconds?: number;
|
|
59
99
|
dryRun?: boolean;
|
|
60
100
|
force?: boolean;
|
|
61
101
|
confirm?: string;
|
|
102
|
+
/** Goal this dispatch/grant is scoped to. Optional today (no caller wires it yet); accepted so a
|
|
103
|
+
* future goal-loop dispatch can pass it through to grant scope/coverage checks without a shape change. */
|
|
104
|
+
goalId?: string;
|
|
105
|
+
/** grant_dispatch: target agent for the standing grant. revoke_grant: unused (the active grant's own
|
|
106
|
+
* agent is used for its tombstone; this field is not required to revoke). */
|
|
107
|
+
agent?: Provider;
|
|
108
|
+
/** grant_dispatch: allowlisted tool names pushed into the child `pi`'s own --tools flag. */
|
|
109
|
+
allowedTools?: string[];
|
|
110
|
+
/** grant_dispatch: resource profile name pushed into the child `pi`'s own --resource-profile flag. */
|
|
111
|
+
resourceProfile?: string;
|
|
112
|
+
/** grant_dispatch: write paths the grant's scoped system prompt tells the child worker to stay within. */
|
|
113
|
+
writePaths?: string[];
|
|
114
|
+
/** grant_dispatch: number of real launches (fire_task/send_followup dispatches) this grant authorizes. */
|
|
115
|
+
maxLaunches?: number;
|
|
116
|
+
/** grant_dispatch: grant validity window from creation time. Omit for a grant that never expires (until revoke_grant). */
|
|
117
|
+
expiresInMinutes?: number;
|
|
118
|
+
/** grant_dispatch: advisory USD ceiling. Never enforced across the process boundary — a claim to review only. */
|
|
119
|
+
maxUsdAdvisory?: number;
|
|
120
|
+
/** revoke_grant: grantId to revoke. Optional — omitted means "revoke whichever grant is currently active". */
|
|
121
|
+
grantId?: string;
|
|
62
122
|
};
|
|
63
123
|
|
|
64
124
|
type RunResult = { ok: boolean; status: number | null; stdout: string; stderr: string; error?: string; args: string[] };
|
|
@@ -92,6 +152,16 @@ type FireAgentPlan = {
|
|
|
92
152
|
resultPath: string;
|
|
93
153
|
paneId?: string;
|
|
94
154
|
result?: AgentResult;
|
|
155
|
+
/** Last turn dispatched to this agent's pane via fire_task (1) or send_followup (>=2). Absent means 1. */
|
|
156
|
+
currentTurn?: number;
|
|
157
|
+
/** Last turn whose terminal handoff was delivered to the parent. Absent means 0 (never notified). */
|
|
158
|
+
notifiedTurn?: number;
|
|
159
|
+
/** A follow-up turn durably reserved before its prompt is injected. Reconciliation may re-arm its
|
|
160
|
+
* watcher, but never re-injects it: a retry must not duplicate an uncertain external side effect. */
|
|
161
|
+
pendingTurn?: number;
|
|
162
|
+
/** Mirrors `AgentSpec.worktreeLane` -- carried onto the plan so `applyLaunchProfile` (per-agent
|
|
163
|
+
* flags) and the dispatch-phase `reportManagedLane` call can both read it after `buildFireTaskPlan`. */
|
|
164
|
+
worktreeLane?: string;
|
|
95
165
|
};
|
|
96
166
|
type FireTaskPlan = {
|
|
97
167
|
id: string;
|
|
@@ -111,7 +181,50 @@ type FireTaskPlan = {
|
|
|
111
181
|
agents: FireAgentPlan[];
|
|
112
182
|
launchCommands: string[];
|
|
113
183
|
variables?: VariableState;
|
|
184
|
+
/** Mirrors the follow-up target agent's currentTurn, for external observability (job_status). */
|
|
185
|
+
currentTurn?: number;
|
|
186
|
+
/** Set when session-level reconcile finds the tmux session gone while the job was not terminal. Informational only; nothing is ever killed to produce this state. */
|
|
187
|
+
orphanedAt?: string;
|
|
188
|
+
/** Set by action=dismiss: the manager stops tracking/re-arming this job. The tmux session is left running. */
|
|
189
|
+
dismissedAt?: string;
|
|
190
|
+
};
|
|
191
|
+
/** The subset of FireTaskPlan the pane watcher generator actually reads — narrow on purpose so a
|
|
192
|
+
* synthesized single-agent, single-turn spec (send_followup, reconcile resume) can reuse it without
|
|
193
|
+
* fabricating unrelated FireTaskPlan fields (createdAt, jobPath, task, ...). */
|
|
194
|
+
type PaneWatcherJobSpec = Pick<FireTaskPlan, "id" | "sessionName" | "deadlineSeconds" | "agents">;
|
|
195
|
+
type ManagedLaneBridgeEvent = {
|
|
196
|
+
laneId: string;
|
|
197
|
+
phase: "dispatch" | "terminal";
|
|
198
|
+
goalId?: string;
|
|
199
|
+
status?: string;
|
|
200
|
+
reasonCode?: string;
|
|
201
|
+
changedFiles?: string[];
|
|
202
|
+
worktreeLaneKey?: string;
|
|
203
|
+
request?: unknown;
|
|
204
|
+
usage?: Usage;
|
|
114
205
|
};
|
|
206
|
+
/** The subset of `ExtensionAPI` this file calls through a narrow local type instead of the full
|
|
207
|
+
* `ExtensionAPI`, so a lightweight test double only needs to implement what a given test path actually
|
|
208
|
+
* exercises. `reportManagedLane`/`reportSpawnedUsage` are wired live host-side (the lane/usage bridge)
|
|
209
|
+
* but stay optional-chained here — best-effort reporting, never a gate: a host/test double that omits
|
|
210
|
+
* them degrades to "the lane/usage claim was not reported," never a crash or a silently-skipped launch.
|
|
211
|
+
* `appendEntry`/`getFlag`/`registerFlag` back the STANDING GRANT itself (session persistence + the
|
|
212
|
+
* non-interactive opt-in flag); `registerFlag` is called unconditionally at extension load so it stays
|
|
213
|
+
* optional-chained too, while `appendEntry`/`getFlag` are asserted present at the point a grant action
|
|
214
|
+
* actually needs them (a host that can't persist a grant should fail that action clearly, not pretend). */
|
|
215
|
+
type HostBridge = {
|
|
216
|
+
reportManagedLane?: (event: ManagedLaneBridgeEvent) => void;
|
|
217
|
+
reportSpawnedUsage?: (usage: Usage, opts?: { label?: string; sourceSessionId?: string; reportId?: string }) => void;
|
|
218
|
+
appendEntry?: (customType: string, data?: unknown) => void;
|
|
219
|
+
getFlag?: (name: string) => boolean | string | undefined;
|
|
220
|
+
registerFlag?: (
|
|
221
|
+
name: string,
|
|
222
|
+
options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
|
|
223
|
+
) => void;
|
|
224
|
+
};
|
|
225
|
+
function agentLaneId(jobId: string, agentId: string): string {
|
|
226
|
+
return `tmux:${jobId}:${agentId}`;
|
|
227
|
+
}
|
|
115
228
|
|
|
116
229
|
const EXTENSION_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
117
230
|
export function getTmuxAgentManagerDataRoot(): string {
|
|
@@ -431,7 +544,7 @@ function normalizeAgents(params: Params): AgentSpec[] {
|
|
|
431
544
|
if (!baseCommand) throw new Error(`agent ${name} needs command when provider=custom`);
|
|
432
545
|
const tools = Array.isArray(agent.tools) && agent.tools.length > 0 ? agent.tools : undefined;
|
|
433
546
|
const command = tools ? `${baseCommand} --tools ${tools.join(",")}` : baseCommand;
|
|
434
|
-
return { provider, name, command, cwd: agent.cwd, tools };
|
|
547
|
+
return { provider, name, command, cwd: agent.cwd, tools, worktreeLane: agent.worktreeLane };
|
|
435
548
|
});
|
|
436
549
|
}
|
|
437
550
|
function defaultProviderInvocation(provider: Provider): string {
|
|
@@ -500,7 +613,7 @@ function buildWorkspacePlan(ctx: ExtensionContext, params: Params) {
|
|
|
500
613
|
);
|
|
501
614
|
return { workspaceName, sessionName, cwd, agents, launchCommands };
|
|
502
615
|
}
|
|
503
|
-
export function makePaneWatcherScript(job:
|
|
616
|
+
export function makePaneWatcherScript(job: PaneWatcherJobSpec): string {
|
|
504
617
|
const agentCases = job.agents.flatMap((agent) => [
|
|
505
618
|
` ${agent.id})`,
|
|
506
619
|
` agent_name=${quoteShell(agent.name)}`,
|
|
@@ -639,6 +752,7 @@ function buildFireTaskPlan(ctx: ExtensionContext, params: Params): FireTaskPlan
|
|
|
639
752
|
promptPath: path.join(jobDir, `${slug}.prompt.md`),
|
|
640
753
|
logPath: path.join(jobDir, `${slug}.log`),
|
|
641
754
|
resultPath: path.join(jobDir, `${slug}.result.json`),
|
|
755
|
+
worktreeLane: agent.worktreeLane,
|
|
642
756
|
};
|
|
643
757
|
});
|
|
644
758
|
const job: FireTaskPlan = {
|
|
@@ -731,6 +845,59 @@ function targetDisplayName(agent: AgentSpec, index: number): string {
|
|
|
731
845
|
function tmuxSessionExists(name: string): boolean {
|
|
732
846
|
return runTmux(["has-session", "-t", name], 2_000).ok;
|
|
733
847
|
}
|
|
848
|
+
function tmuxPaneExists(sessionName: string, paneId: string): boolean {
|
|
849
|
+
const result = runTmux(["list-panes", "-t", sessionName, "-F", "#{pane_id}"], 2_000);
|
|
850
|
+
if (!result.ok) return false;
|
|
851
|
+
return result.stdout
|
|
852
|
+
.split(/\s+/)
|
|
853
|
+
.map((line) => line.trim())
|
|
854
|
+
.includes(paneId);
|
|
855
|
+
}
|
|
856
|
+
/** True when tmux already has a pipe-pane consumer attached to this pane (tmux's own `#{pane_pipe}`
|
|
857
|
+
* format flag). Session-level reconcile MUST check this before re-arming: a pane that still has an
|
|
858
|
+
* active pipe already has a live watcher; replacing it would close that watcher's stdin and race its
|
|
859
|
+
* own EOF-triggered `finish(failed, pane-stream-ended)` against the real marker that may be about to
|
|
860
|
+
* arrive, possibly recording a false failure for a job that was about to succeed on its own. Re-arming
|
|
861
|
+
* is only safe (and only useful) when tmux reports no pipe is currently attached. */
|
|
862
|
+
function tmuxPaneHasPipe(paneId: string): boolean {
|
|
863
|
+
const result = runTmux(["display-message", "-p", "-t", paneId, "#{pane_pipe}"], 2_000);
|
|
864
|
+
return result.ok && result.stdout.trim() === "1";
|
|
865
|
+
}
|
|
866
|
+
function turnMarkers(
|
|
867
|
+
job: PaneWatcherJobSpec,
|
|
868
|
+
agent: FireAgentPlan,
|
|
869
|
+
turn: number,
|
|
870
|
+
): { doneMarker: string; blockedMarker: string } {
|
|
871
|
+
if (turn <= 1) return { doneMarker: agent.doneMarker, blockedMarker: agent.blockedMarker };
|
|
872
|
+
const base = `TMUX_${crypto.createHash("sha1").update(`${job.id}:${agent.id}:${turn}`).digest("hex").slice(0, 10).toUpperCase()}`;
|
|
873
|
+
return { doneMarker: `${base}_DONE`, blockedMarker: `${base}_BLOCKED` };
|
|
874
|
+
}
|
|
875
|
+
function turnResultPath(jobDir: string, agentId: string, turn: number, baseResultPath: string): string {
|
|
876
|
+
return turn <= 1 ? baseResultPath : path.join(jobDir, `${agentId}.turn-${turn}.result.json`);
|
|
877
|
+
}
|
|
878
|
+
function turnPromptPath(jobDir: string, agentId: string, turn: number, basePromptPath: string): string {
|
|
879
|
+
return turn <= 1 ? basePromptPath : path.join(jobDir, `${agentId}.turn-${turn}.prompt.md`);
|
|
880
|
+
}
|
|
881
|
+
function turnWatcherPath(job: FireTaskPlan, turn: number): string {
|
|
882
|
+
return turn <= 1 ? job.watcherPath : path.join(job.jobDir, `pane-watcher.turn-${turn}.sh`);
|
|
883
|
+
}
|
|
884
|
+
/** The result-file path the CURRENT turn's watcher will write to for this agent (turn 1 keeps the
|
|
885
|
+
* fixed back-compat path). Stop handling must target this path, not the fixed one, or a stop signal
|
|
886
|
+
* issued mid-follow-up would land on a stale turn's file that nothing is watching anymore. */
|
|
887
|
+
function currentResultPath(job: FireTaskPlan, agent: FireAgentPlan): string {
|
|
888
|
+
return turnResultPath(job.jobDir, agent.id, agent.currentTurn ?? 1, agent.resultPath);
|
|
889
|
+
}
|
|
890
|
+
/** Atomically read-modify-write job.json, always from a FRESH read (never from a caller's possibly
|
|
891
|
+
* stale in-memory copy), mirroring the temp+rename pattern the original notify path used. */
|
|
892
|
+
function persistJobPatch(jobId: string, mutate: (current: FireTaskPlan) => FireTaskPlan | undefined): FireTaskPlan {
|
|
893
|
+
const jobPath = path.join(jobsRoot(), jobId, "job.json");
|
|
894
|
+
const current = JSON.parse(fs.readFileSync(jobPath, "utf8")) as FireTaskPlan;
|
|
895
|
+
const mutated = mutate(current) ?? current;
|
|
896
|
+
const temporaryPath = `${jobPath}.tmp-${process.pid}-${Date.now()}`;
|
|
897
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(mutated, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
898
|
+
fs.renameSync(temporaryPath, jobPath);
|
|
899
|
+
return mutated;
|
|
900
|
+
}
|
|
734
901
|
function sendCommandToPane(paneId: string, command: string): void {
|
|
735
902
|
runTmux(["send-keys", "-t", paneId, "-l", command]);
|
|
736
903
|
runTmux(["send-keys", "-t", paneId, "Enter"]);
|
|
@@ -785,6 +952,70 @@ function injectPromptToPane(paneId: string, prompt: string, provider: Provider):
|
|
|
785
952
|
runTmux(["send-keys", "-t", paneId, "-l", text], 10_000);
|
|
786
953
|
runTmux(["send-keys", "-t", paneId, provider === "agy" ? "C-m" : "Enter"], 3_000);
|
|
787
954
|
}
|
|
955
|
+
/** Write a fresh per-turn prompt + single-agent watcher script and arm `pipe-pane -O` for it. Used by
|
|
956
|
+
* send_followup to open a NEW turn on an already-live pane. Does not inject the prompt text into the
|
|
957
|
+
* pane itself — callers do that separately via injectPromptToPane, after this has armed the watcher,
|
|
958
|
+
* so the completion contract is listening before the worker can possibly reply. */
|
|
959
|
+
function dispatchAgentTurn(
|
|
960
|
+
job: FireTaskPlan,
|
|
961
|
+
agent: FireAgentPlan,
|
|
962
|
+
turn: number,
|
|
963
|
+
promptText: string,
|
|
964
|
+
): { promptPath: string; resultPath: string; watcherPath: string } {
|
|
965
|
+
if (!agent.paneId) throw new Error(`tmux agent ${agent.name} has no recorded pane id`);
|
|
966
|
+
const markers = turnMarkers(job, agent, turn);
|
|
967
|
+
const resultPath = turnResultPath(job.jobDir, agent.id, turn, agent.resultPath);
|
|
968
|
+
const promptPath = turnPromptPath(job.jobDir, agent.id, turn, agent.promptPath);
|
|
969
|
+
const watcherPath = turnWatcherPath(job, turn);
|
|
970
|
+
fs.writeFileSync(promptPath, `${promptText}\n`, { mode: 0o600 });
|
|
971
|
+
const turnSpec: PaneWatcherJobSpec = {
|
|
972
|
+
id: job.id,
|
|
973
|
+
sessionName: job.sessionName,
|
|
974
|
+
deadlineSeconds: job.deadlineSeconds,
|
|
975
|
+
agents: [
|
|
976
|
+
{ ...agent, doneMarker: markers.doneMarker, blockedMarker: markers.blockedMarker, resultPath, promptPath },
|
|
977
|
+
],
|
|
978
|
+
};
|
|
979
|
+
fs.writeFileSync(watcherPath, makePaneWatcherScript(turnSpec), { mode: 0o700 });
|
|
980
|
+
fs.chmodSync(watcherPath, 0o700);
|
|
981
|
+
const armed = runTmux([
|
|
982
|
+
"pipe-pane",
|
|
983
|
+
"-O",
|
|
984
|
+
"-t",
|
|
985
|
+
agent.paneId,
|
|
986
|
+
`sh ${quoteShell(watcherPath)} ${quoteShell(agent.id)}`,
|
|
987
|
+
]);
|
|
988
|
+
if (!armed.ok)
|
|
989
|
+
throw new Error(
|
|
990
|
+
`Failed to arm completion watcher for turn ${turn}: ${armed.error || armed.stderr || armed.stdout}`,
|
|
991
|
+
);
|
|
992
|
+
return { promptPath, resultPath, watcherPath };
|
|
993
|
+
}
|
|
994
|
+
/** Session-level reconcile's resume step: re-arm `pipe-pane` for an agent whose current turn is still
|
|
995
|
+
* pending on a pane tmux confirms is alive AND has no pipe already attached (see tmuxPaneHasPipe). Only
|
|
996
|
+
* regenerates the watcher SCRIPT file if it is missing (e.g. this is the first reconcile after a reload
|
|
997
|
+
* whose disk state predates a crash) — the prompt was already injected once and is not re-sent. */
|
|
998
|
+
function rearmAgentWatcher(job: FireTaskPlan, agent: FireAgentPlan): void {
|
|
999
|
+
if (!agent.paneId) return;
|
|
1000
|
+
const turn = agent.currentTurn ?? 1;
|
|
1001
|
+
const markers = turnMarkers(job, agent, turn);
|
|
1002
|
+
const resultPath = turnResultPath(job.jobDir, agent.id, turn, agent.resultPath);
|
|
1003
|
+
const promptPath = turnPromptPath(job.jobDir, agent.id, turn, agent.promptPath);
|
|
1004
|
+
const watcherPath = turnWatcherPath(job, turn);
|
|
1005
|
+
if (!fs.existsSync(watcherPath)) {
|
|
1006
|
+
const turnSpec: PaneWatcherJobSpec = {
|
|
1007
|
+
id: job.id,
|
|
1008
|
+
sessionName: job.sessionName,
|
|
1009
|
+
deadlineSeconds: job.deadlineSeconds,
|
|
1010
|
+
agents: [
|
|
1011
|
+
{ ...agent, doneMarker: markers.doneMarker, blockedMarker: markers.blockedMarker, resultPath, promptPath },
|
|
1012
|
+
],
|
|
1013
|
+
};
|
|
1014
|
+
fs.writeFileSync(watcherPath, makePaneWatcherScript(turnSpec), { mode: 0o700 });
|
|
1015
|
+
fs.chmodSync(watcherPath, 0o700);
|
|
1016
|
+
}
|
|
1017
|
+
runTmux(["pipe-pane", "-O", "-t", agent.paneId, `sh ${quoteShell(watcherPath)} ${quoteShell(agent.id)}`]);
|
|
1018
|
+
}
|
|
788
1019
|
function stopTmuxSession(
|
|
789
1020
|
sessionName: string,
|
|
790
1021
|
dryRun: boolean,
|
|
@@ -804,9 +1035,10 @@ function isAlreadyExistsError(error: unknown): boolean {
|
|
|
804
1035
|
}
|
|
805
1036
|
function markStopRequested(job: FireTaskPlan): void {
|
|
806
1037
|
for (const agent of job.agents) {
|
|
807
|
-
|
|
1038
|
+
const resultPath = currentResultPath(job, agent);
|
|
1039
|
+
if (fs.existsSync(resultPath)) continue;
|
|
808
1040
|
try {
|
|
809
|
-
fs.writeFileSync(`${
|
|
1041
|
+
fs.writeFileSync(`${resultPath}.stop-requested`, `${new Date().toISOString()}\n`, {
|
|
810
1042
|
encoding: "utf8",
|
|
811
1043
|
mode: 0o600,
|
|
812
1044
|
flag: "wx",
|
|
@@ -817,11 +1049,12 @@ function markStopRequested(job: FireTaskPlan): void {
|
|
|
817
1049
|
}
|
|
818
1050
|
}
|
|
819
1051
|
function clearStopRequested(job: FireTaskPlan): void {
|
|
820
|
-
for (const agent of job.agents) fs.rmSync(`${agent
|
|
1052
|
+
for (const agent of job.agents) fs.rmSync(`${currentResultPath(job, agent)}.stop-requested`, { force: true });
|
|
821
1053
|
}
|
|
822
1054
|
function persistStoppedResults(job: FireTaskPlan): void {
|
|
823
1055
|
for (const agent of job.agents) {
|
|
824
|
-
|
|
1056
|
+
const resultPath = currentResultPath(job, agent);
|
|
1057
|
+
if (!fs.existsSync(resultPath)) {
|
|
825
1058
|
const result = {
|
|
826
1059
|
jobId: job.id,
|
|
827
1060
|
agentId: agent.id,
|
|
@@ -834,7 +1067,7 @@ function persistStoppedResults(job: FireTaskPlan): void {
|
|
|
834
1067
|
notifiedBy: "stop-action-event",
|
|
835
1068
|
};
|
|
836
1069
|
try {
|
|
837
|
-
fs.writeFileSync(
|
|
1070
|
+
fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, {
|
|
838
1071
|
encoding: "utf8",
|
|
839
1072
|
mode: 0o600,
|
|
840
1073
|
flag: "wx",
|
|
@@ -843,7 +1076,7 @@ function persistStoppedResults(job: FireTaskPlan): void {
|
|
|
843
1076
|
if (!isAlreadyExistsError(error)) throw error;
|
|
844
1077
|
}
|
|
845
1078
|
}
|
|
846
|
-
fs.rmSync(`${
|
|
1079
|
+
fs.rmSync(`${resultPath}.stop-requested`, { force: true });
|
|
847
1080
|
}
|
|
848
1081
|
}
|
|
849
1082
|
function stopTmuxSessionWithJobSignals(
|
|
@@ -888,8 +1121,10 @@ function loadJob(id: string): FireTaskPlan {
|
|
|
888
1121
|
const parsed = JSON.parse(fs.readFileSync(jobPath, "utf8")) as FireTaskPlan;
|
|
889
1122
|
if (!Array.isArray(parsed.agents)) throw new Error(`tmux job has invalid agents: ${id}`);
|
|
890
1123
|
for (const agent of parsed.agents) {
|
|
1124
|
+
// Read from the CURRENT turn's result path (turn 1 is the fixed back-compat path) so a job
|
|
1125
|
+
// mid-follow-up is correctly seen as non-terminal until the NEW turn settles.
|
|
891
1126
|
try {
|
|
892
|
-
agent.result = JSON.parse(fs.readFileSync(agent
|
|
1127
|
+
agent.result = JSON.parse(fs.readFileSync(currentResultPath(parsed, agent), "utf8")) as AgentResult;
|
|
893
1128
|
} catch {
|
|
894
1129
|
delete agent.result;
|
|
895
1130
|
}
|
|
@@ -930,7 +1165,13 @@ function listJobs(): unknown[] {
|
|
|
930
1165
|
workspaceName: job.workspaceName,
|
|
931
1166
|
sessionName: job.sessionName,
|
|
932
1167
|
createdAt: job.createdAt,
|
|
933
|
-
|
|
1168
|
+
orphaned: Boolean(job.orphanedAt),
|
|
1169
|
+
dismissed: Boolean(job.dismissedAt),
|
|
1170
|
+
agents: job.agents.map((agent) => ({
|
|
1171
|
+
name: agent.name,
|
|
1172
|
+
status: agent.result?.status || "pending",
|
|
1173
|
+
turn: agent.currentTurn ?? 1,
|
|
1174
|
+
})),
|
|
934
1175
|
};
|
|
935
1176
|
} catch (error: unknown) {
|
|
936
1177
|
return { id: d.name, error: errorMessage(error) };
|
|
@@ -957,12 +1198,126 @@ function setVariable(jobId: string, name: string, value: string) {
|
|
|
957
1198
|
fs.writeFileSync(job.varsPath, `${JSON.stringify(current, null, 2)}\n`, { mode: 0o600 });
|
|
958
1199
|
return { varsPath: job.varsPath, variables: current.variables, updatedAt: current.updatedAt };
|
|
959
1200
|
}
|
|
1201
|
+
// ---------------------------------------------------------------------------
|
|
1202
|
+
// STANDING GRANT — approval-gated tmux dispatch. The pure decode/decision logic lives in
|
|
1203
|
+
// dispatch-grant.ts; the functions below are the session-access GLUE: read the latest grant custom
|
|
1204
|
+
// entry (skipping past malformed payloads, but stopping DEAD on a tombstone — never resurrecting an
|
|
1205
|
+
// older grant beneath a revocation), count spend, and gate a real launch.
|
|
1206
|
+
// ---------------------------------------------------------------------------
|
|
1207
|
+
|
|
1208
|
+
/** Most recent VALID `tmux-dispatch-grant` entry on the active branch, or undefined when there is
|
|
1209
|
+
* none, it was revoked (a tombstone is a hard stop — never skipped to find an older grant), or every
|
|
1210
|
+
* entry found fails to decode. Mirrors the branch-walk idiom used by goal/task session state. */
|
|
1211
|
+
function resolveLatestGrantEntry(ctx: ExtensionContext): TmuxDispatchGrant | undefined {
|
|
1212
|
+
let fromId: string | undefined;
|
|
1213
|
+
for (;;) {
|
|
1214
|
+
const entry = ctx.sessionManager.getLatestCustomEntryOnBranch(GRANT_CUSTOM_TYPE, fromId);
|
|
1215
|
+
if (!entry) return undefined;
|
|
1216
|
+
if (isTmuxDispatchGrantTombstone(entry.data)) return undefined;
|
|
1217
|
+
if (isTmuxDispatchGrant(entry.data)) return entry.data;
|
|
1218
|
+
if (!entry.parentId) return undefined;
|
|
1219
|
+
fromId = entry.parentId;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/** Decoded `data` payloads of every custom entry of `customType` on the active branch (root→leaf
|
|
1223
|
+
* order), for spend-counting — a single `getLatestCustomEntryOnBranch` call only ever returns ONE
|
|
1224
|
+
* entry, but budget spend requires ALL of them. */
|
|
1225
|
+
function collectCustomEntryData(ctx: ExtensionContext, customType: string): unknown[] {
|
|
1226
|
+
const result: unknown[] = [];
|
|
1227
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1228
|
+
const loose = entry as unknown as { type?: string; customType?: string; data?: unknown };
|
|
1229
|
+
if (loose.type === "custom" && loose.customType === customType) result.push(loose.data);
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
/** Gate a REAL tmux dispatch (fire_task non-dryRun, send_followup) per the standing-grant doctrine:
|
|
1234
|
+
* a valid covering grant dispatches UNATTENDED (spending one usage); absent that, an
|
|
1235
|
+
* interactive host may approve a ONE-SHOT launch; absent BOTH, this REFUSES — never a silent launch.
|
|
1236
|
+
* Returns the covering grant when one authorized this launch (so the caller can derive launch-profile
|
|
1237
|
+
* flags from its envelope), or `{}` for a one-shot approval (no envelope to derive from — the caller
|
|
1238
|
+
* falls back to the conservative default profile). */
|
|
1239
|
+
async function authorizeLaunch(
|
|
1240
|
+
bridge: HostBridge,
|
|
1241
|
+
ctx: ExtensionContext,
|
|
1242
|
+
request: { agents: readonly Provider[]; goalId?: string; jobId: string; description: string },
|
|
1243
|
+
): Promise<{ grant?: TmuxDispatchGrant }> {
|
|
1244
|
+
if (request.agents.length === 0) throw new Error("tmux dispatch refused: launch has no agents");
|
|
1245
|
+
const grant = resolveLatestGrantEntry(ctx);
|
|
1246
|
+
// A standing grant names one provider. It must cover every child and spend one unit for every
|
|
1247
|
+
// child process; a matching first pane must never authorize an unrestricted mixed team.
|
|
1248
|
+
if (grant && request.agents.every((agent) => grantCovers(grant, { agent, goalId: request.goalId }))) {
|
|
1249
|
+
const used = countGrantUsages(grant.grantId, collectCustomEntryData(ctx, GRANT_USAGE_CUSTOM_TYPE));
|
|
1250
|
+
if (grant.budget.maxLaunches - used >= request.agents.length) {
|
|
1251
|
+
if (!bridge.appendEntry)
|
|
1252
|
+
throw new Error("this host does not support session custom entries; cannot spend the tmux dispatch grant.");
|
|
1253
|
+
for (let index = 0; index < request.agents.length; index++) {
|
|
1254
|
+
bridge.appendEntry(
|
|
1255
|
+
GRANT_USAGE_CUSTOM_TYPE,
|
|
1256
|
+
buildGrantUsageEntry(grant.grantId, `${request.jobId}:${index}`),
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
return { grant };
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (ctx.hasUI) {
|
|
1263
|
+
const approved = await ctx.ui.confirm(
|
|
1264
|
+
"tmux dispatch approval",
|
|
1265
|
+
[
|
|
1266
|
+
`No standing grant currently authorizes every child of: ${request.description}.`,
|
|
1267
|
+
`Agents: ${request.agents.join(", ")}${request.goalId ? `, goal: ${request.goalId}` : ""}.`,
|
|
1268
|
+
"Approve this ONE-SHOT launch? Run tmux_agent_manager action=grant_dispatch to authorize future launches without this prompt.",
|
|
1269
|
+
].join("\n"),
|
|
1270
|
+
);
|
|
1271
|
+
if (!approved) throw new Error(`tmux dispatch declined by the owner: ${request.description}`);
|
|
1272
|
+
return {};
|
|
1273
|
+
}
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
`no standing grant for tmux dispatch; run grant_dispatch first: every child must be covered. ${request.description}. Refusing to launch without a grant or interactive approval.`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
/** Render grant/one-shot-derived launch-profile flags into a pi child's start command. Values
|
|
1279
|
+
* are shell-quoted; flags are not (they're fixed literals, never user input). */
|
|
1280
|
+
function appendLaunchProfileFlags(command: string, flags: LaunchProfileFlag[]): string {
|
|
1281
|
+
const rendered = flags.map((flag) =>
|
|
1282
|
+
flag.value !== undefined ? `${flag.flag} ${quoteShell(flag.value)}` : flag.flag,
|
|
1283
|
+
);
|
|
1284
|
+
return [command, ...rendered].join(" ");
|
|
1285
|
+
}
|
|
1286
|
+
/** Apply the launch profile to every provider="pi" agent in the job (fire_task only — send_followup
|
|
1287
|
+
* reuses an already-launched pane, so there is no new child command to configure). Non-pi agents are
|
|
1288
|
+
* bounded by the grant only at the launch layer (agent/budget/count); their internal tool-loop
|
|
1289
|
+
* enforcement is the target CLI's own responsibility (documented limitation, not a hidden gap). */
|
|
1290
|
+
function applyLaunchProfile(job: FireTaskPlan, source: LaunchProfileSource): void {
|
|
1291
|
+
for (const agent of job.agents) {
|
|
1292
|
+
if (agent.provider !== "pi") continue;
|
|
1293
|
+
// Per-agent flags: `worktreeLane` is a per-agent property (only a lane-first goal->tmux
|
|
1294
|
+
// dispatch sets it), so the shared `source` envelope is extended with it just for this agent's
|
|
1295
|
+
// flag build -- every other agent in a multi-agent team keeps the plain grant-derived profile.
|
|
1296
|
+
const flags = buildLaunchProfileFlags(
|
|
1297
|
+
agent.worktreeLane ? { ...source, worktreeLane: agent.worktreeLane } : source,
|
|
1298
|
+
);
|
|
1299
|
+
agent.command = appendLaunchProfileFlags(agent.command || defaultProviderInvocation(agent.provider), flags);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/** Read an OPTIONAL, cooperative worker-reported usage claim for the turn that just went terminal (a
|
|
1303
|
+
* sibling `<result-path>.usage.json` file next to the watcher's own result file). Absent file ⇒ no
|
|
1304
|
+
* claim was offered ⇒ nothing is reported (never fabricated). */
|
|
1305
|
+
function readWorkerUsageClaim(job: FireTaskPlan, agent: FireAgentPlan): Usage | undefined {
|
|
1306
|
+
const usagePath = `${currentResultPath(job, agent)}.usage.json`;
|
|
1307
|
+
try {
|
|
1308
|
+
return decodeTmuxWorkerUsageClaim(JSON.parse(fs.readFileSync(usagePath, "utf8")));
|
|
1309
|
+
} catch {
|
|
1310
|
+
return undefined;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
960
1314
|
async function executeTool(
|
|
961
1315
|
_toolCallId: string,
|
|
962
1316
|
params: Params,
|
|
963
1317
|
_signal: AbortSignal | undefined,
|
|
964
1318
|
_onUpdate: unknown,
|
|
965
1319
|
ctx: ExtensionContext,
|
|
1320
|
+
bridge: HostBridge,
|
|
966
1321
|
): Promise<AgentToolResult<unknown>> {
|
|
967
1322
|
const action = params.action || "status";
|
|
968
1323
|
const detection = detectTmux();
|
|
@@ -1015,6 +1370,62 @@ async function executeTool(
|
|
|
1015
1370
|
details: { action, jobId: params.jobId, variableName: params.variableName, result },
|
|
1016
1371
|
};
|
|
1017
1372
|
}
|
|
1373
|
+
if (action === "grant_dispatch") {
|
|
1374
|
+
if (!params.agent) throw new Error("grant_dispatch requires agent");
|
|
1375
|
+
if (!Object.hasOwn(PROVIDER_COMMANDS, params.agent))
|
|
1376
|
+
throw new Error(`grant_dispatch: unsupported agent: ${params.agent}`);
|
|
1377
|
+
const grantParams: GrantDispatchParams = {
|
|
1378
|
+
agent: params.agent,
|
|
1379
|
+
goalId: params.goalId,
|
|
1380
|
+
allowedTools: params.allowedTools,
|
|
1381
|
+
resourceProfile: params.resourceProfile,
|
|
1382
|
+
writePaths: params.writePaths,
|
|
1383
|
+
maxLaunches: typeof params.maxLaunches === "number" ? params.maxLaunches : Number.NaN,
|
|
1384
|
+
expiresInMinutes: params.expiresInMinutes,
|
|
1385
|
+
maxUsdAdvisory: params.maxUsdAdvisory,
|
|
1386
|
+
};
|
|
1387
|
+
const grant = buildGrant(grantParams);
|
|
1388
|
+
const summary = describeGrant(grant);
|
|
1389
|
+
// OWNER-AUTHORIZED, NEVER SILENT: an interactive host must explicitly confirm the exact
|
|
1390
|
+
// grant details; a non-interactive host (print/rpc, or the unattended goal loop) has no confirm
|
|
1391
|
+
// surface, so it needs an explicit opt-in flag instead — absent either, refuse to create the grant.
|
|
1392
|
+
if (ctx.hasUI) {
|
|
1393
|
+
const approved = await ctx.ui.confirm("Authorize tmux dispatch grant", summary);
|
|
1394
|
+
if (!approved) throw new Error("grant_dispatch was declined by the owner.");
|
|
1395
|
+
} else if (bridge.getFlag?.("allow-tmux-dispatch") !== true) {
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
"grant_dispatch requires interactive approval; no UI is available in this mode. Pass --allow-tmux-dispatch to authorize grant creation non-interactively.",
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
if (!bridge.appendEntry)
|
|
1401
|
+
throw new Error("this host does not support session custom entries; cannot persist a tmux dispatch grant.");
|
|
1402
|
+
bridge.appendEntry(GRANT_CUSTOM_TYPE, grant);
|
|
1403
|
+
return {
|
|
1404
|
+
content: [{ type: "text", text: `Created tmux dispatch grant ${grant.grantId}.\n${summary}` }],
|
|
1405
|
+
details: { action, grant },
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
if (action === "revoke_grant") {
|
|
1409
|
+
const activeGrant = resolveLatestGrantEntry(ctx);
|
|
1410
|
+
const targetGrantId = firstString(params.grantId) || activeGrant?.grantId;
|
|
1411
|
+
if (!targetGrantId) throw new Error("revoke_grant: no active tmux dispatch grant to revoke.");
|
|
1412
|
+
if (activeGrant && activeGrant.grantId !== targetGrantId)
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`revoke_grant: grantId ${targetGrantId} is not the active grant (active: ${activeGrant.grantId}).`,
|
|
1415
|
+
);
|
|
1416
|
+
if (!bridge.appendEntry)
|
|
1417
|
+
throw new Error("this host does not support session custom entries; cannot revoke a tmux dispatch grant.");
|
|
1418
|
+
bridge.appendEntry(GRANT_CUSTOM_TYPE, buildTombstone(targetGrantId));
|
|
1419
|
+
return {
|
|
1420
|
+
content: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: `Revoked tmux dispatch grant ${targetGrantId}. Future launches need a new grant_dispatch or interactive/opt-in approval.`,
|
|
1424
|
+
},
|
|
1425
|
+
],
|
|
1426
|
+
details: { action, grantId: targetGrantId },
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1018
1429
|
const guard = await guardTmux(ctx, detection, `tmux ${action}`);
|
|
1019
1430
|
if (!guard.allowed)
|
|
1020
1431
|
return { content: [{ type: "text", text: guard.text }], details: { action, detection, guard, skipped: true } };
|
|
@@ -1079,6 +1490,22 @@ async function executeTool(
|
|
|
1079
1490
|
throw new Error(
|
|
1080
1491
|
`tmux session already exists: ${job.sessionName}. Use stop_job/stop_session first or choose a different workspaceName.`,
|
|
1081
1492
|
);
|
|
1493
|
+
// APPROVAL-GATED LAUNCH: resolved before any tmux/FS side effect. A standing grant must
|
|
1494
|
+
// authorize every child provider and spend its budget per child process.
|
|
1495
|
+
const authorization = await authorizeLaunch(bridge, ctx, {
|
|
1496
|
+
agents: job.agents.map((agent) => agent.provider),
|
|
1497
|
+
goalId: params.goalId,
|
|
1498
|
+
jobId: job.id,
|
|
1499
|
+
description: `fire_task launch of job ${job.id} (${job.agents.length} child process${job.agents.length === 1 ? "" : "es"}: ${job.agents.map((agent) => `${agent.name}/${agent.provider}`).join(", ")})`,
|
|
1500
|
+
});
|
|
1501
|
+
applyLaunchProfile(job, {
|
|
1502
|
+
...(authorization.grant ? launchProfileSourceFromGrant(authorization.grant) : ONE_SHOT_LAUNCH_PROFILE_SOURCE),
|
|
1503
|
+
// Process-matrix parent identity: the dispatched child self-registers as a worker of THIS
|
|
1504
|
+
// session and winds down gracefully if this session disappears (see dispatch-grant.ts's
|
|
1505
|
+
// `LaunchProfileSource.parentPid`/`parentSession` doc).
|
|
1506
|
+
parentPid: process.pid,
|
|
1507
|
+
parentSession: ctx.sessionManager.getSessionId?.() ?? ctx.sessionManager.getSessionFile(),
|
|
1508
|
+
});
|
|
1082
1509
|
const archivedJobDir = prepareJobDirForLaunch(job, params.force);
|
|
1083
1510
|
const panes = job.agents.map((agent) => ({
|
|
1084
1511
|
title: agent.name,
|
|
@@ -1094,6 +1521,13 @@ async function executeTool(
|
|
|
1094
1521
|
for (const agent of job.agents) {
|
|
1095
1522
|
if (!agent.paneId) continue;
|
|
1096
1523
|
injectPromptToPane(agent.paneId, fs.readFileSync(agent.promptPath, "utf8"), agent.provider);
|
|
1524
|
+
bridge.reportManagedLane?.({
|
|
1525
|
+
laneId: agentLaneId(job.id, agent.id),
|
|
1526
|
+
phase: "dispatch",
|
|
1527
|
+
status: "launched",
|
|
1528
|
+
goalId: params.goalId,
|
|
1529
|
+
worktreeLaneKey: agent.worktreeLane,
|
|
1530
|
+
});
|
|
1097
1531
|
}
|
|
1098
1532
|
return {
|
|
1099
1533
|
content: [
|
|
@@ -1114,6 +1548,128 @@ async function executeTool(
|
|
|
1114
1548
|
details: { action, detection, job, runs: launch.runs, paneIds: launch.paneIds, watcherPanes, archivedJobDir },
|
|
1115
1549
|
};
|
|
1116
1550
|
}
|
|
1551
|
+
if (action === "send_followup") {
|
|
1552
|
+
if (!params.jobId) throw new Error("send_followup requires jobId");
|
|
1553
|
+
const followupTask = firstString(params.task, params.body);
|
|
1554
|
+
if (!followupTask) throw new Error("send_followup requires task (or body) with the follow-up objective");
|
|
1555
|
+
const job = loadJob(params.jobId);
|
|
1556
|
+
if (job.dismissedAt)
|
|
1557
|
+
throw new Error(`tmux job ${job.id} was dismissed; use fire_task to relaunch instead of send_followup`);
|
|
1558
|
+
if (job.agents.length === 0) throw new Error(`tmux job has no agents: ${job.id}`);
|
|
1559
|
+
const targetAgent = params.agentId ? job.agents.find((agent) => agent.id === params.agentId) : job.agents[0];
|
|
1560
|
+
if (!targetAgent) throw new Error(`tmux job ${job.id} has no agent ${params.agentId}`);
|
|
1561
|
+
if (!targetAgent.paneId)
|
|
1562
|
+
throw new Error(`tmux agent ${targetAgent.name} has no recorded pane id; cannot send a follow-up`);
|
|
1563
|
+
if (targetAgent.pendingTurn !== undefined)
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
`send_followup refused: turn ${targetAgent.pendingTurn} for ${targetAgent.id} is pending reconciliation; refusing to risk a duplicate prompt`,
|
|
1566
|
+
);
|
|
1567
|
+
const turn = (targetAgent.currentTurn ?? 1) + 1;
|
|
1568
|
+
const markers = turnMarkers(job, targetAgent, turn);
|
|
1569
|
+
const promptText = managedPrompt(
|
|
1570
|
+
followupTask,
|
|
1571
|
+
job.id,
|
|
1572
|
+
targetAgent,
|
|
1573
|
+
markers.doneMarker,
|
|
1574
|
+
markers.blockedMarker,
|
|
1575
|
+
job.varsPath,
|
|
1576
|
+
);
|
|
1577
|
+
if (params.dryRun === true)
|
|
1578
|
+
return {
|
|
1579
|
+
content: [
|
|
1580
|
+
{
|
|
1581
|
+
type: "text",
|
|
1582
|
+
text: `DRY RUN tmux send_followup\nJob: ${job.id}\nAgent: ${targetAgent.name}\nTurn: ${turn}`,
|
|
1583
|
+
},
|
|
1584
|
+
],
|
|
1585
|
+
details: { action, detection, dryRun: true, jobId: job.id, agentId: targetAgent.id, turn },
|
|
1586
|
+
};
|
|
1587
|
+
if (!tmuxSessionExists(job.sessionName))
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
`tmux session is gone: ${job.sessionName}. The worker pane cannot receive a follow-up; use fire_task to relaunch.`,
|
|
1590
|
+
);
|
|
1591
|
+
if (!tmuxPaneExists(job.sessionName, targetAgent.paneId))
|
|
1592
|
+
throw new Error(
|
|
1593
|
+
`tmux pane ${targetAgent.paneId} for ${targetAgent.name} is gone. The worker cannot receive a follow-up; use fire_task to relaunch.`,
|
|
1594
|
+
);
|
|
1595
|
+
// APPROVAL-GATED LAUNCH (doctrine-regression mandatory): a follow-up dispatches a fresh
|
|
1596
|
+
// turn into an already-running child, so there is no new child command to profile — only the
|
|
1597
|
+
// grant/one-shot authorization is resolved here (no applyLaunchProfile call).
|
|
1598
|
+
await authorizeLaunch(bridge, ctx, {
|
|
1599
|
+
agents: [targetAgent.provider],
|
|
1600
|
+
goalId: params.goalId,
|
|
1601
|
+
jobId: `${job.id}:turn:${turn}`,
|
|
1602
|
+
description: `send_followup turn ${turn} to ${targetAgent.name} in job ${job.id}`,
|
|
1603
|
+
});
|
|
1604
|
+
// Reserve before watcher/prompt side effects. Reconcile re-arms a reserved turn but never
|
|
1605
|
+
// re-injects it, so crash recovery cannot duplicate a provider prompt.
|
|
1606
|
+
const reserved = persistJobPatch(job.id, (current) => {
|
|
1607
|
+
const agent = current.agents.find((entry) => entry.id === targetAgent.id);
|
|
1608
|
+
if (!agent || agent.pendingTurn !== undefined) return undefined;
|
|
1609
|
+
agent.currentTurn = turn;
|
|
1610
|
+
agent.pendingTurn = turn;
|
|
1611
|
+
agent.result = undefined;
|
|
1612
|
+
current.currentTurn = turn;
|
|
1613
|
+
return current;
|
|
1614
|
+
});
|
|
1615
|
+
const reservedAgent = reserved.agents.find((agent) => agent.id === targetAgent.id);
|
|
1616
|
+
if (!reservedAgent || reservedAgent.pendingTurn !== turn)
|
|
1617
|
+
throw new Error(`send_followup refused: failed to reserve turn ${turn} for ${targetAgent.id}`);
|
|
1618
|
+
const written = dispatchAgentTurn(reserved, reservedAgent, turn, promptText);
|
|
1619
|
+
injectPromptToPane(reservedAgent.paneId as string, promptText, reservedAgent.provider);
|
|
1620
|
+
persistJobPatch(job.id, (current) => {
|
|
1621
|
+
const agent = current.agents.find((entry) => entry.id === reservedAgent.id);
|
|
1622
|
+
if (!agent || agent.pendingTurn !== turn) return undefined;
|
|
1623
|
+
delete agent.pendingTurn;
|
|
1624
|
+
return current;
|
|
1625
|
+
});
|
|
1626
|
+
bridge.reportManagedLane?.({
|
|
1627
|
+
laneId: agentLaneId(job.id, targetAgent.id),
|
|
1628
|
+
phase: "dispatch",
|
|
1629
|
+
status: "follow-up",
|
|
1630
|
+
goalId: params.goalId,
|
|
1631
|
+
request: { turn },
|
|
1632
|
+
worktreeLaneKey: targetAgent.worktreeLane,
|
|
1633
|
+
});
|
|
1634
|
+
return {
|
|
1635
|
+
content: [
|
|
1636
|
+
{
|
|
1637
|
+
type: "text",
|
|
1638
|
+
text: [
|
|
1639
|
+
`Sent follow-up turn ${turn} to ${targetAgent.name} in job ${job.id}.`,
|
|
1640
|
+
`Session: ${job.sessionName}`,
|
|
1641
|
+
`Prompt: ${written.promptPath}`,
|
|
1642
|
+
`Completion: event-driven pane watcher re-armed for turn ${turn} (no polling).`,
|
|
1643
|
+
].join("\n"),
|
|
1644
|
+
},
|
|
1645
|
+
],
|
|
1646
|
+
details: { action, detection, jobId: job.id, agentId: targetAgent.id, turn, ...written },
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (action === "dismiss") {
|
|
1650
|
+
if (!params.jobId) throw new Error("dismiss requires jobId");
|
|
1651
|
+
const job = loadJob(params.jobId);
|
|
1652
|
+
if (job.dismissedAt)
|
|
1653
|
+
return {
|
|
1654
|
+
content: [{ type: "text", text: `tmux job ${job.id} was already dismissed.` }],
|
|
1655
|
+
details: { action, jobId: job.id, alreadyDismissed: true },
|
|
1656
|
+
};
|
|
1657
|
+
persistJobPatch(job.id, (current) => {
|
|
1658
|
+
current.dismissedAt = new Date().toISOString();
|
|
1659
|
+
return current;
|
|
1660
|
+
});
|
|
1661
|
+
for (const agent of job.agents)
|
|
1662
|
+
bridge.reportManagedLane?.({ laneId: agentLaneId(job.id, agent.id), phase: "terminal", status: "dismissed" });
|
|
1663
|
+
return {
|
|
1664
|
+
content: [
|
|
1665
|
+
{
|
|
1666
|
+
type: "text",
|
|
1667
|
+
text: `Dismissed tmux job ${job.id}. Session '${job.sessionName}' left running (not killed); attach with: tmux attach -t ${job.sessionName}. Use stop_session/stop_job to terminate it.`,
|
|
1668
|
+
},
|
|
1669
|
+
],
|
|
1670
|
+
details: { action, jobId: job.id, sessionName: job.sessionName },
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1117
1673
|
if (action === "notify") {
|
|
1118
1674
|
const title = firstString(params.title, "Pi") || "Pi";
|
|
1119
1675
|
const body = firstString(params.body, "Pi needs attention") || "Pi needs attention";
|
|
@@ -1193,19 +1749,76 @@ function formatFireTaskHandoff(job: FireTaskPlan): string {
|
|
|
1193
1749
|
].join("\n");
|
|
1194
1750
|
}
|
|
1195
1751
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1752
|
+
/** Diff live tmux sessions against this session's job records. Mirrors the invariant behind
|
|
1753
|
+
* `LocalRuntimeController.reconcile` (local-runtime-controller.ts): reconcile only ever OBSERVES —
|
|
1754
|
+
* it never kills a session it did not provably start. A job whose tmux session is gone is marked
|
|
1755
|
+
* orphaned (informational, one-shot); a job whose session is alive but current turn is still
|
|
1756
|
+
* pending is resumed by re-arming its watcher, but ONLY when tmux confirms no pipe is already
|
|
1757
|
+
* attached to that pane (see tmuxPaneHasPipe) — otherwise a live watcher would be raced and could
|
|
1758
|
+
* be made to report a false failure. */
|
|
1759
|
+
function reconcileTmuxSessions(ctx: ExtensionContext, bridge: HostBridge): void {
|
|
1760
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1761
|
+
const jobs = loadJobPlans().filter((job) => job.parentSessionFile === sessionFile && !job.dismissedAt);
|
|
1762
|
+
if (jobs.length === 0) return;
|
|
1763
|
+
const detection = detectTmux();
|
|
1764
|
+
if (!detection.cliAvailable) return;
|
|
1765
|
+
const liveSessions = new Set(detection.sessions);
|
|
1766
|
+
for (const job of jobs) {
|
|
1767
|
+
if (!liveSessions.has(job.sessionName)) {
|
|
1768
|
+
if (!isFireTaskTerminal(job) && !job.orphanedAt) {
|
|
1769
|
+
persistJobPatch(job.id, (current) => {
|
|
1770
|
+
current.orphanedAt = new Date().toISOString();
|
|
1771
|
+
return current;
|
|
1772
|
+
});
|
|
1773
|
+
if (ctx.hasUI)
|
|
1774
|
+
ctx.ui.notify(
|
|
1775
|
+
`tmux job ${job.id} is orphaned: session '${job.sessionName}' is gone and the job never reached a terminal state. Nothing was killed; this is informational only.`,
|
|
1776
|
+
"warning",
|
|
1777
|
+
);
|
|
1778
|
+
// Nothing is killed here — only the LANE record is released. A dead session means the
|
|
1779
|
+
// worker itself can no longer report its own terminal state, so a goal bound to it would
|
|
1780
|
+
// wait forever without this: release the managed lane per non-terminal agent so the goal's
|
|
1781
|
+
// continuation stops waiting on a worker that will never report back.
|
|
1782
|
+
for (const agent of job.agents) {
|
|
1783
|
+
if (agent.result !== undefined) continue;
|
|
1784
|
+
bridge.reportManagedLane?.({
|
|
1785
|
+
laneId: agentLaneId(job.id, agent.id),
|
|
1786
|
+
phase: "terminal",
|
|
1787
|
+
status: "orphaned",
|
|
1788
|
+
reasonCode: "tmux_session_orphaned",
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (isFireTaskTerminal(job)) continue;
|
|
1795
|
+
for (const agent of job.agents) {
|
|
1796
|
+
if (agent.result !== undefined) continue;
|
|
1797
|
+
if (!agent.paneId || !tmuxPaneExists(job.sessionName, agent.paneId)) continue;
|
|
1798
|
+
if (tmuxPaneHasPipe(agent.paneId)) continue;
|
|
1799
|
+
rearmAgentWatcher(job, agent);
|
|
1800
|
+
bridge.reportManagedLane?.({
|
|
1801
|
+
laneId: agentLaneId(job.id, agent.id),
|
|
1802
|
+
phase: "dispatch",
|
|
1803
|
+
status: "resumed",
|
|
1804
|
+
worktreeLaneKey: agent.worktreeLane,
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1203
1808
|
}
|
|
1204
1809
|
|
|
1205
1810
|
export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
1206
1811
|
let handoffContext: ExtensionContext | undefined;
|
|
1207
1812
|
let handoffTail = Promise.resolve();
|
|
1208
1813
|
const jobWatchers = new Map<string, fs.FSWatcher>();
|
|
1814
|
+
const bridge: HostBridge = pi;
|
|
1815
|
+
// Non-interactive opt-in for grant_dispatch: optional-chained so a lightweight test double
|
|
1816
|
+
// that doesn't implement registerFlag still loads the extension; a real host always has it.
|
|
1817
|
+
bridge.registerFlag?.("allow-tmux-dispatch", {
|
|
1818
|
+
type: "boolean",
|
|
1819
|
+
description:
|
|
1820
|
+
"Authorize tmux_agent_manager grant_dispatch to create a standing tmux dispatch grant without interactive approval (print/rpc/non-interactive mode only).",
|
|
1821
|
+
});
|
|
1209
1822
|
|
|
1210
1823
|
const closeJobWatchers = () => {
|
|
1211
1824
|
for (const watcher of jobWatchers.values()) watcher.close();
|
|
@@ -1214,9 +1827,16 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1214
1827
|
|
|
1215
1828
|
const refreshJobHandoffs = async (ctx: ExtensionContext): Promise<void> => {
|
|
1216
1829
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1217
|
-
const jobs = loadJobPlans().filter(
|
|
1830
|
+
const jobs = loadJobPlans().filter(
|
|
1831
|
+
(job) => (!job.parentSessionFile || job.parentSessionFile === sessionFile) && !job.dismissedAt,
|
|
1832
|
+
);
|
|
1218
1833
|
for (const job of jobs) {
|
|
1219
|
-
if (!isFireTaskTerminal(job)
|
|
1834
|
+
if (!isFireTaskTerminal(job)) continue;
|
|
1835
|
+
// Per-turn notify: a job is not permanently "closed" after its first terminal turn — a
|
|
1836
|
+
// follow-up reopens it, so the gate is per-agent (notifiedTurn < currentTurn), not a
|
|
1837
|
+
// single whole-job notifiedAt flag.
|
|
1838
|
+
const pendingAgents = job.agents.filter((agent) => (agent.notifiedTurn ?? 0) < (agent.currentTurn ?? 1));
|
|
1839
|
+
if (pendingAgents.length === 0) continue;
|
|
1220
1840
|
pi.sendMessage(
|
|
1221
1841
|
{
|
|
1222
1842
|
customType: "tmux-background-completion",
|
|
@@ -1230,7 +1850,31 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1230
1850
|
},
|
|
1231
1851
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1232
1852
|
);
|
|
1233
|
-
|
|
1853
|
+
persistJobPatch(job.id, (current) => {
|
|
1854
|
+
current.notifiedAt = new Date().toISOString();
|
|
1855
|
+
for (const agent of current.agents) {
|
|
1856
|
+
const inMemoryAgent = job.agents.find((entry) => entry.id === agent.id);
|
|
1857
|
+
agent.notifiedTurn = inMemoryAgent?.currentTurn ?? agent.currentTurn ?? 1;
|
|
1858
|
+
}
|
|
1859
|
+
return current;
|
|
1860
|
+
});
|
|
1861
|
+
for (const agent of pendingAgents) {
|
|
1862
|
+
// Advisory-only, and only ever reported when the worker itself chose to write the
|
|
1863
|
+
// claim file — never fabricated, never a hard cross-process cap.
|
|
1864
|
+
const usage = readWorkerUsageClaim(job, agent);
|
|
1865
|
+
bridge.reportManagedLane?.({
|
|
1866
|
+
laneId: agentLaneId(job.id, agent.id),
|
|
1867
|
+
phase: "terminal",
|
|
1868
|
+
status: agent.result?.status,
|
|
1869
|
+
usage,
|
|
1870
|
+
});
|
|
1871
|
+
if (usage) {
|
|
1872
|
+
bridge.reportSpawnedUsage?.(usage, {
|
|
1873
|
+
label: "tmux-worker",
|
|
1874
|
+
reportId: `tmux-worker:${job.sessionName}:${job.id}:${agent.currentTurn ?? 1}`,
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1234
1878
|
if (ctx.hasUI) ctx.ui.notify(`tmux background task ${job.id} completed.`, "info");
|
|
1235
1879
|
}
|
|
1236
1880
|
|
|
@@ -1277,6 +1921,7 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1277
1921
|
pi.on("session_start", async (_event, ctx) => {
|
|
1278
1922
|
handoffContext = ctx;
|
|
1279
1923
|
closeJobWatchers();
|
|
1924
|
+
reconcileTmuxSessions(ctx, bridge);
|
|
1280
1925
|
await queueJobHandoffRefresh(ctx);
|
|
1281
1926
|
});
|
|
1282
1927
|
pi.on("session_shutdown", async () => {
|
|
@@ -1297,9 +1942,14 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1297
1942
|
"Prefer action=fire_task for interactive worker batches. It returns after event watchers and prompts are armed; do not wait, poll, or peek for completion.",
|
|
1298
1943
|
"Use action=list_templates/show_template before assembling repeated teams; pass teamTemplate when a built-in team fits.",
|
|
1299
1944
|
"Use action=workspace_plan before launch_workspace when designing a pane layout.",
|
|
1945
|
+
"Use action=send_followup to re-inject a new prompt into a live fire_task job's pane mid-run, without relaunching. It re-arms the completion watcher for a fresh turn and reuses the same event-driven handoff.",
|
|
1946
|
+
"Use action=dismiss to stop tracking a job without killing its tmux session; the pane keeps running and can still be attached to or stopped later with stop_job/stop_session.",
|
|
1300
1947
|
"Use stop_job/stop_session dry-run first, then confirm=yes-tmux-stop for real cleanup.",
|
|
1301
1948
|
"Do not put secrets in task text or command strings; prompts, commands, and logs persist under ~/.pi/agent/work/background/tmux-agent-manager/state/jobs.",
|
|
1302
1949
|
"tmux display/status actions are metadata only; validate worker result/log files before claiming task completion.",
|
|
1950
|
+
"fire_task and send_followup are APPROVAL-GATED: a real (non-dryRun) dispatch needs either a standing grant (action=grant_dispatch, once per session/goal) or a one-shot interactive approval. With no grant and no UI available, the dispatch is refused, never silent — run grant_dispatch first.",
|
|
1951
|
+
"Use action=grant_dispatch to authorize repeated unattended dispatch: set agent, maxLaunches, and optionally goalId/allowedTools/resourceProfile/writePaths/expiresInMinutes. Requires interactive confirmation, or the --allow-tmux-dispatch flag when no UI is available. Use action=revoke_grant to end it early.",
|
|
1952
|
+
"A grant-covered pi child launches with a RESTRICTED profile (--tools/--resource-profile or --no-extensions --no-skills, plus a scoped --append-system-prompt naming the grant and its hard stops) — the envelope lives in the CHILD's own launch config, not an in-process sandbox. Non-pi agents (agy/claude/codex/opencode/custom) are bounded by the grant at launch only; their internal tool loop is that CLI's own responsibility.",
|
|
1303
1953
|
],
|
|
1304
1954
|
parameters: Type.Object({
|
|
1305
1955
|
action: Type.Optional(
|
|
@@ -1314,6 +1964,8 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1314
1964
|
Type.Literal("workspace_plan"),
|
|
1315
1965
|
Type.Literal("launch_workspace"),
|
|
1316
1966
|
Type.Literal("fire_task"),
|
|
1967
|
+
Type.Literal("send_followup"),
|
|
1968
|
+
Type.Literal("dismiss"),
|
|
1317
1969
|
Type.Literal("job_status"),
|
|
1318
1970
|
Type.Literal("list_jobs"),
|
|
1319
1971
|
Type.Literal("set_variable"),
|
|
@@ -1322,10 +1974,12 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1322
1974
|
Type.Literal("show_template"),
|
|
1323
1975
|
Type.Literal("stop_job"),
|
|
1324
1976
|
Type.Literal("stop_session"),
|
|
1977
|
+
Type.Literal("grant_dispatch"),
|
|
1978
|
+
Type.Literal("revoke_grant"),
|
|
1325
1979
|
],
|
|
1326
1980
|
{
|
|
1327
1981
|
description:
|
|
1328
|
-
"status, setup_help, guard, notify, set_status, clear_status, workspace_plan, launch_workspace, fire_task, job_status, list_jobs, set_variable, list_variables, list_templates, show_template, stop_job, or
|
|
1982
|
+
"status, setup_help, guard, notify, set_status, clear_status, workspace_plan, launch_workspace, fire_task, send_followup, dismiss, job_status, list_jobs, set_variable, list_variables, list_templates, show_template, stop_job, stop_session, grant_dispatch, or revoke_grant. Default status.",
|
|
1329
1983
|
},
|
|
1330
1984
|
),
|
|
1331
1985
|
),
|
|
@@ -1367,12 +2021,20 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1367
2021
|
),
|
|
1368
2022
|
task: Type.Optional(
|
|
1369
2023
|
Type.String({
|
|
1370
|
-
description:
|
|
2024
|
+
description:
|
|
2025
|
+
"Fire-and-forget worker objective. Required for fire_task unless body is provided. Also used as the follow-up objective for send_followup.",
|
|
1371
2026
|
}),
|
|
1372
2027
|
),
|
|
1373
2028
|
jobId: Type.Optional(
|
|
1374
2029
|
Type.String({
|
|
1375
|
-
description:
|
|
2030
|
+
description:
|
|
2031
|
+
"Job id for fire_task (optional), job_status/list_variables/set_variable/send_followup/dismiss (required).",
|
|
2032
|
+
}),
|
|
2033
|
+
),
|
|
2034
|
+
agentId: Type.Optional(
|
|
2035
|
+
Type.String({
|
|
2036
|
+
description:
|
|
2037
|
+
"Target agent id for send_followup. Defaults to the job's first (primary) agent. Multi-agent-per-turn follow-up is not supported; only one agent's pane receives each follow-up.",
|
|
1376
2038
|
}),
|
|
1377
2039
|
),
|
|
1378
2040
|
variableName: Type.Optional(
|
|
@@ -1432,9 +2094,69 @@ export default function tmuxAgentManagerExtension(pi: ExtensionAPI) {
|
|
|
1432
2094
|
confirm: Type.Optional(
|
|
1433
2095
|
Type.String({ description: "Required for real stop_job/stop_session: yes-tmux-stop." }),
|
|
1434
2096
|
),
|
|
2097
|
+
goalId: Type.Optional(
|
|
2098
|
+
Type.String({
|
|
2099
|
+
description:
|
|
2100
|
+
"Goal this dispatch/grant is scoped to. For grant_dispatch: an unscoped grant (omitted) covers any goal; a scoped grant covers only launches naming the same goalId. For fire_task/send_followup: tags the request for grant-coverage matching.",
|
|
2101
|
+
}),
|
|
2102
|
+
),
|
|
2103
|
+
agent: Type.Optional(
|
|
2104
|
+
Type.Union(
|
|
2105
|
+
[
|
|
2106
|
+
Type.Literal("pi"),
|
|
2107
|
+
Type.Literal("codex"),
|
|
2108
|
+
Type.Literal("agy"),
|
|
2109
|
+
Type.Literal("claude"),
|
|
2110
|
+
Type.Literal("opencode"),
|
|
2111
|
+
Type.Literal("custom"),
|
|
2112
|
+
],
|
|
2113
|
+
{ description: "grant_dispatch: the provider this standing grant authorizes." },
|
|
2114
|
+
),
|
|
2115
|
+
),
|
|
2116
|
+
allowedTools: Type.Optional(
|
|
2117
|
+
Type.Array(Type.String(), {
|
|
2118
|
+
description:
|
|
2119
|
+
"grant_dispatch: tool allowlist pushed into the child pi's own --tools flag. Defaults to a read-biased safe set (read, grep, find, ls) when omitted.",
|
|
2120
|
+
}),
|
|
2121
|
+
),
|
|
2122
|
+
resourceProfile: Type.Optional(
|
|
2123
|
+
Type.String({
|
|
2124
|
+
description:
|
|
2125
|
+
"grant_dispatch: resource profile name pushed into the child pi's own --resource-profile flag. When omitted, the child launches with --no-extensions --no-skills instead.",
|
|
2126
|
+
}),
|
|
2127
|
+
),
|
|
2128
|
+
writePaths: Type.Optional(
|
|
2129
|
+
Type.Array(Type.String(), {
|
|
2130
|
+
description: "grant_dispatch: write paths named in the child's scoped --append-system-prompt role text.",
|
|
2131
|
+
}),
|
|
2132
|
+
),
|
|
2133
|
+
maxLaunches: Type.Optional(
|
|
2134
|
+
Type.Number({
|
|
2135
|
+
description: "grant_dispatch: number of real fire_task/send_followup dispatches this grant authorizes.",
|
|
2136
|
+
minimum: 1,
|
|
2137
|
+
}),
|
|
2138
|
+
),
|
|
2139
|
+
expiresInMinutes: Type.Optional(
|
|
2140
|
+
Type.Number({
|
|
2141
|
+
description:
|
|
2142
|
+
"grant_dispatch: grant validity window from creation. Omit for a grant that never expires (until revoke_grant).",
|
|
2143
|
+
minimum: 1,
|
|
2144
|
+
}),
|
|
2145
|
+
),
|
|
2146
|
+
maxUsdAdvisory: Type.Optional(
|
|
2147
|
+
Type.Number({
|
|
2148
|
+
description:
|
|
2149
|
+
"grant_dispatch: advisory USD ceiling. Never enforced across the process boundary (the child bills under its own auth) — a claim to review only.",
|
|
2150
|
+
}),
|
|
2151
|
+
),
|
|
2152
|
+
grantId: Type.Optional(
|
|
2153
|
+
Type.String({
|
|
2154
|
+
description: "revoke_grant: grant id to revoke. Omit to revoke whichever grant is currently active.",
|
|
2155
|
+
}),
|
|
2156
|
+
),
|
|
1435
2157
|
}),
|
|
1436
2158
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1437
|
-
const result = await executeTool(toolCallId, params as Params, signal, onUpdate, ctx);
|
|
2159
|
+
const result = await executeTool(toolCallId, params as Params, signal, onUpdate, ctx, bridge);
|
|
1438
2160
|
await queueJobHandoffRefresh(ctx);
|
|
1439
2161
|
return result;
|
|
1440
2162
|
},
|