@phnx-labs/agents-cli 1.22.34 → 1.22.37
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 +672 -2
- package/README.md +82 -35
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.d.ts +15 -0
- package/dist/bootstrap.js +1211 -0
- package/dist/commands/accounts.d.ts +4 -11
- package/dist/commands/accounts.js +159 -27
- package/dist/commands/attach.js +17 -4
- package/dist/commands/audit.d.ts +8 -8
- package/dist/commands/audit.js +57 -47
- package/dist/commands/browser.js +15 -11
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +0 -11
- package/dist/commands/computer-actions.js +33 -0
- package/dist/commands/computer-sessions-picker.d.ts +21 -0
- package/dist/commands/computer-sessions-picker.js +141 -0
- package/dist/commands/computer.d.ts +16 -0
- package/dist/commands/computer.js +37 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +401 -0
- package/dist/commands/cp.d.ts +84 -0
- package/dist/commands/cp.js +231 -0
- package/dist/commands/daemon.js +243 -14
- package/dist/commands/detach-core.d.ts +1 -1
- package/dist/commands/detach-core.js +12 -2
- package/dist/commands/detach.js +1 -1
- package/dist/commands/doctor.js +64 -5
- package/dist/commands/events.d.ts +24 -12
- package/dist/commands/events.js +144 -28
- package/dist/commands/exec.js +85 -41
- package/dist/commands/feed.d.ts +1 -1
- package/dist/commands/feed.js +16 -8
- package/dist/commands/focus.d.ts +33 -1
- package/dist/commands/focus.js +177 -17
- package/dist/commands/funnel.d.ts +1 -1
- package/dist/commands/go.js +9 -8
- package/dist/commands/harness-wizard.d.ts +1 -0
- package/dist/commands/harness-wizard.js +18 -20
- package/dist/commands/harness.d.ts +2 -0
- package/dist/commands/harness.js +59 -26
- package/dist/commands/hooks.js +0 -11
- package/dist/commands/inspect.d.ts +94 -1
- package/dist/commands/inspect.js +866 -40
- package/dist/commands/lease.d.ts +4 -4
- package/dist/commands/lease.js +6 -6
- package/dist/commands/login.js +0 -13
- package/dist/commands/logs.d.ts +2 -2
- package/dist/commands/logs.js +2 -2
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/memory.js +7 -2
- package/dist/commands/message.d.ts +1 -1
- package/dist/commands/message.js +37 -6
- package/dist/commands/models.js +4 -0
- package/dist/commands/monitors.d.ts +0 -1
- package/dist/commands/monitors.js +199 -16
- package/dist/commands/open.d.ts +18 -0
- package/dist/commands/open.js +57 -0
- package/dist/commands/packages.js +2 -107
- package/dist/commands/plugins.js +6 -3
- package/dist/commands/profiles.d.ts +1 -0
- package/dist/commands/profiles.js +16 -160
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +125 -5
- package/dist/commands/reconnect.js +9 -3
- package/dist/commands/repo.js +42 -1
- package/dist/commands/resource-view.d.ts +17 -0
- package/dist/commands/resource-view.js +36 -14
- package/dist/commands/resume.js +9 -2
- package/dist/commands/routines.d.ts +0 -2
- package/dist/commands/routines.js +473 -289
- package/dist/commands/secrets.js +52 -9
- package/dist/commands/sessions-export.d.ts +47 -0
- package/dist/commands/sessions-export.js +116 -6
- package/dist/commands/sessions-import.d.ts +17 -0
- package/dist/commands/sessions-import.js +114 -6
- package/dist/commands/sessions-migrate.d.ts +27 -0
- package/dist/commands/sessions-migrate.js +50 -9
- package/dist/commands/sessions-picker.js +23 -4
- package/dist/commands/sessions-resume.d.ts +5 -3
- package/dist/commands/sessions-resume.js +30 -8
- package/dist/commands/sessions-watch.d.ts +2 -0
- package/dist/commands/sessions-watch.js +53 -0
- package/dist/commands/sessions.d.ts +66 -3
- package/dist/commands/sessions.js +298 -47
- package/dist/commands/set.d.ts +1 -1
- package/dist/commands/set.js +2 -2
- package/dist/commands/setup-browser.js +2 -2
- package/dist/commands/setup-mine.js +0 -1
- package/dist/commands/setup-preferences.js +1 -1
- package/dist/commands/setup-share.js +34 -4
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +26 -0
- package/dist/commands/share.d.ts +85 -0
- package/dist/commands/share.js +379 -8
- package/dist/commands/skills.js +0 -11
- package/dist/commands/snapshot.js +1 -1
- package/dist/commands/ssh.d.ts +18 -0
- package/dist/commands/ssh.js +549 -210
- package/dist/commands/sync.d.ts +5 -0
- package/dist/commands/sync.js +212 -32
- package/dist/commands/teams.d.ts +13 -0
- package/dist/commands/teams.js +187 -30
- package/dist/commands/tickets.d.ts +2 -0
- package/dist/commands/tickets.js +43 -0
- package/dist/commands/update.js +2 -4
- package/dist/commands/usage.js +5 -3
- package/dist/commands/versions.js +11 -5
- package/dist/commands/view.js +15 -7
- package/dist/commands/webhook.d.ts +2 -2
- package/dist/commands/webhook.js +7 -7
- package/dist/index.d.ts +18 -2
- package/dist/index.js +53 -1264
- package/dist/lib/account-catalog.d.ts +18 -0
- package/dist/lib/account-catalog.js +38 -0
- package/dist/lib/account-provider-registry.d.ts +18 -0
- package/dist/lib/account-provider-registry.js +59 -0
- package/dist/lib/account-registry.d.ts +40 -0
- package/dist/lib/account-registry.js +239 -0
- package/dist/lib/account-schema.d.ts +44 -0
- package/dist/lib/account-schema.js +91 -0
- package/dist/lib/account-state-service.d.ts +21 -0
- package/dist/lib/account-state-service.js +60 -0
- package/dist/lib/activity.d.ts +1 -1
- package/dist/lib/activity.js +8 -1
- package/dist/lib/add-dir.d.ts +80 -0
- package/dist/lib/add-dir.js +241 -0
- package/dist/lib/agent-cli-commands.d.ts +11 -0
- package/dist/lib/agent-cli-commands.js +30 -0
- package/dist/lib/agents.js +44 -36
- package/dist/lib/answer-router.js +1 -1
- package/dist/lib/audit/log.d.ts +10 -27
- package/dist/lib/audit/log.js +20 -33
- package/dist/lib/auth-health.d.ts +9 -2
- package/dist/lib/auth-health.js +42 -9
- package/dist/lib/auto-pull-worker.js +7 -3
- package/dist/lib/auto-pull.d.ts +44 -3
- package/dist/lib/auto-pull.js +88 -5
- package/dist/lib/binary-shadow.d.ts +28 -0
- package/dist/lib/binary-shadow.js +121 -0
- package/dist/lib/brand.js +9 -3
- package/dist/lib/browser/ipc.d.ts +30 -0
- package/dist/lib/browser/ipc.js +81 -4
- package/dist/lib/browser/profiles.d.ts +3 -2
- package/dist/lib/browser/profiles.js +65 -22
- package/dist/lib/browser/service.d.ts +4 -0
- package/dist/lib/browser/service.js +30 -3
- package/dist/lib/browser/sessions-list.d.ts +22 -2
- package/dist/lib/browser/sessions-list.js +54 -6
- package/dist/lib/browser/stream.d.ts +2 -0
- package/dist/lib/browser/stream.js +1 -0
- package/dist/lib/browser/types.d.ts +19 -0
- package/dist/lib/byok-usage.d.ts +9 -2
- package/dist/lib/byok-usage.js +133 -31
- package/dist/lib/cli-entry.d.ts +11 -0
- package/dist/lib/cli-entry.js +46 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +2 -2
- package/dist/lib/cloud/registry.js +1 -1
- package/dist/lib/cloud/types.d.ts +1 -1
- package/dist/lib/codex-policy.d.ts +9 -1
- package/dist/lib/codex-policy.js +17 -2
- package/dist/lib/commands.d.ts +2 -0
- package/dist/lib/commands.js +2 -0
- package/dist/lib/computer/sessions-list.d.ts +150 -0
- package/dist/lib/computer/sessions-list.js +363 -0
- package/dist/lib/config-keys.d.ts +86 -0
- package/dist/lib/config-keys.js +193 -0
- package/dist/lib/config-machine-keys.d.ts +12 -0
- package/dist/lib/config-machine-keys.js +17 -0
- package/dist/lib/config-transfer.js +3 -4
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/cli.js +3 -3
- package/dist/lib/daemon-health.d.ts +22 -0
- package/dist/lib/daemon-health.js +33 -3
- package/dist/lib/daemon-services.d.ts +50 -0
- package/dist/lib/daemon-services.js +147 -0
- package/dist/lib/daemon-ticks.d.ts +35 -66
- package/dist/lib/daemon-ticks.js +77 -159
- package/dist/lib/daemon.d.ts +72 -4
- package/dist/lib/daemon.js +792 -202
- package/dist/lib/deeplink/register.d.ts +49 -0
- package/dist/lib/deeplink/register.js +253 -0
- package/dist/lib/deeplink/url.d.ts +34 -0
- package/dist/lib/deeplink/url.js +78 -0
- package/dist/lib/device-config.d.ts +124 -29
- package/dist/lib/device-config.js +346 -141
- package/dist/lib/devices/config-migration.d.ts +42 -0
- package/dist/lib/devices/config-migration.js +203 -0
- package/dist/lib/devices/connect.js +14 -7
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +20 -1
- package/dist/lib/devices/harness-inventory.d.ts +11 -2
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/health.d.ts +1 -1
- package/dist/lib/devices/health.js +1 -1
- package/dist/lib/devices/pending.d.ts +22 -1
- package/dist/lib/devices/pending.js +67 -2
- package/dist/lib/devices/registry.d.ts +0 -25
- package/dist/lib/devices/registry.js +2 -83
- package/dist/lib/devices/resolve-profile.d.ts +20 -0
- package/dist/lib/devices/resolve-profile.js +44 -0
- package/dist/lib/devices/rollout-verify.d.ts +101 -0
- package/dist/lib/devices/rollout-verify.js +193 -0
- package/dist/lib/devices/ssh-config.js +10 -6
- package/dist/lib/event-families.d.ts +28 -0
- package/dist/lib/event-families.js +159 -0
- package/dist/lib/event-stream.d.ts +9 -1
- package/dist/lib/event-stream.js +26 -9
- package/dist/lib/events.d.ts +9 -1
- package/dist/lib/events.js +21 -6
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +220 -122
- package/dist/lib/feed-broadcast.d.ts +25 -0
- package/dist/lib/feed-broadcast.js +33 -0
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +12 -2
- package/dist/lib/fleet/capture.js +15 -0
- package/dist/lib/fleet/manifest.js +9 -0
- package/dist/lib/fleet/types.d.ts +9 -0
- package/dist/lib/fleet-status.js +10 -8
- package/dist/lib/git.d.ts +20 -6
- package/dist/lib/git.js +121 -17
- package/dist/lib/hosts/dispatch.d.ts +8 -3
- package/dist/lib/hosts/dispatch.js +29 -3
- package/dist/lib/hosts/logs.d.ts +2 -2
- package/dist/lib/hosts/logs.js +2 -2
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.d.ts +2 -3
- package/dist/lib/hosts/passthrough.js +10 -24
- package/dist/lib/hosts/progress.js +2 -2
- package/dist/lib/hosts/providers/devices.d.ts +3 -3
- package/dist/lib/hosts/providers/devices.js +18 -12
- package/dist/lib/hosts/ready.d.ts +5 -1
- package/dist/lib/hosts/ready.js +9 -3
- package/dist/lib/hosts/reconcile.d.ts +2 -2
- package/dist/lib/hosts/reconcile.js +2 -2
- package/dist/lib/hosts/registry.js +14 -9
- package/dist/lib/hosts/remote-cmd.js +1 -1
- package/dist/lib/hosts/remote-os.d.ts +9 -6
- package/dist/lib/hosts/remote-os.js +13 -6
- package/dist/lib/hosts/routing-flag.d.ts +23 -0
- package/dist/lib/hosts/routing-flag.js +41 -0
- package/dist/lib/hosts/run-target.js +1 -1
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/hosts/tasks.d.ts +6 -6
- package/dist/lib/hosts/tasks.js +4 -4
- package/dist/lib/hosts/types.js +1 -2
- package/dist/lib/installations/resolve.js +2 -8
- package/dist/lib/isolation-boundary-report.js +0 -1
- package/dist/lib/linear-cache.d.ts +7 -0
- package/dist/lib/linear-cache.js +31 -4
- package/dist/lib/linear-project-counts.d.ts +1 -1
- package/dist/lib/linear-project-counts.js +2 -3
- package/dist/lib/linear-projects.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/mailbox-target.d.ts +1 -1
- package/dist/lib/mailbox-target.js +1 -1
- package/dist/lib/manifest.js +7 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +3 -2
- package/dist/lib/menubar/snapshot.js +6 -2
- package/dist/lib/migrate-fold.d.ts +33 -0
- package/dist/lib/migrate-fold.js +130 -0
- package/dist/lib/migrate.d.ts +34 -34
- package/dist/lib/migrate.js +106 -128
- package/dist/lib/model-tier-overrides.d.ts +1 -1
- package/dist/lib/model-tier-overrides.js +15 -2
- package/dist/lib/monitors/config.d.ts +17 -3
- package/dist/lib/monitors/config.js +52 -18
- package/dist/lib/monitors/engine.d.ts +32 -1
- package/dist/lib/monitors/engine.js +94 -11
- package/dist/lib/monitors/fingerprint.d.ts +46 -0
- package/dist/lib/monitors/fingerprint.js +100 -0
- package/dist/lib/monitors/remote.d.ts +47 -0
- package/dist/lib/monitors/remote.js +81 -0
- package/dist/lib/monitors/state.d.ts +39 -0
- package/dist/lib/monitors/state.js +56 -0
- package/dist/lib/onepassword.d.ts +17 -0
- package/dist/lib/onepassword.js +52 -22
- package/dist/lib/open-url.js +1 -1
- package/dist/lib/picker.d.ts +14 -2
- package/dist/lib/picker.js +35 -13
- package/dist/lib/placement.d.ts +1 -1
- package/dist/lib/placement.js +1 -1
- package/dist/lib/pr-land-detach.d.ts +32 -0
- package/dist/lib/pr-land-detach.js +79 -0
- package/dist/lib/profiles-presets.js +13 -0
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +44 -1
- package/dist/lib/project-focus.d.ts +1 -1
- package/dist/lib/project-focus.js +1 -1
- package/dist/lib/project-key.d.ts +17 -0
- package/dist/lib/project-key.js +26 -0
- package/dist/lib/project-probe.d.ts +4 -1
- package/dist/lib/project-probe.js +5 -4
- package/dist/lib/project-resources.js +2 -18
- package/dist/lib/project-root.d.ts +16 -0
- package/dist/lib/project-root.js +20 -2
- package/dist/lib/projects.d.ts +20 -1
- package/dist/lib/projects.js +78 -1
- package/dist/lib/refresh-coordinator.d.ts +24 -0
- package/dist/lib/refresh-coordinator.js +52 -0
- package/dist/lib/registry.d.ts +2 -2
- package/dist/lib/registry.js +1 -1
- package/dist/lib/resource-aliases.d.ts +13 -0
- package/dist/lib/resource-aliases.js +26 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +66 -10
- package/dist/lib/rotate.d.ts +3 -4
- package/dist/lib/rotate.js +4 -5
- package/dist/lib/routine-activation.d.ts +22 -2
- package/dist/lib/routine-activation.js +63 -16
- package/dist/lib/routine-context.d.ts +9 -2
- package/dist/lib/routine-context.js +21 -6
- package/dist/lib/routines-project.d.ts +58 -48
- package/dist/lib/routines-project.js +190 -159
- package/dist/lib/routines.d.ts +41 -6
- package/dist/lib/routines.js +92 -34
- package/dist/lib/run-defaults.d.ts +5 -0
- package/dist/lib/run-defaults.js +11 -1
- package/dist/lib/runner.d.ts +10 -8
- package/dist/lib/runner.js +122 -39
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +10 -3
- package/dist/lib/secrets/agent.js +43 -8
- package/dist/lib/secrets/bundles.d.ts +1 -0
- package/dist/lib/secrets/bundles.js +92 -80
- package/dist/lib/secrets/index.js +32 -9
- package/dist/lib/secrets/install-helper.d.ts +4 -4
- package/dist/lib/secrets/install-helper.js +4 -4
- package/dist/lib/secrets/push.d.ts +40 -6
- package/dist/lib/secrets/push.js +58 -11
- package/dist/lib/secrets/read-backoff.d.ts +1 -1
- package/dist/lib/secrets/read-backoff.js +1 -1
- package/dist/lib/secrets/remote.d.ts +24 -0
- package/dist/lib/secrets/remote.js +54 -4
- package/dist/lib/self-update.d.ts +108 -0
- package/dist/lib/self-update.js +276 -1
- package/dist/lib/session/active.d.ts +89 -5
- package/dist/lib/session/active.js +165 -10
- package/dist/lib/session/actor-sidecar.d.ts +1 -1
- package/dist/lib/session/actor-sidecar.js +2 -1
- package/dist/lib/session/db.d.ts +176 -2
- package/dist/lib/session/db.js +484 -17
- package/dist/lib/session/discover.js +27 -4
- package/dist/lib/session/hook-sessions.js +2 -2
- package/dist/lib/session/host-link.d.ts +2 -2
- package/dist/lib/session/host-link.js +2 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +57 -0
- package/dist/lib/session/remote-active.js +15 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/session-cache.d.ts +14 -7
- package/dist/lib/session/session-cache.js +51 -9
- package/dist/lib/session/state.d.ts +2 -2
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.d.ts +10 -10
- package/dist/lib/session/sync/config.js +10 -10
- package/dist/lib/session/sync/r2.d.ts +34 -0
- package/dist/lib/session/sync/r2.js +123 -0
- package/dist/lib/session/throughput.d.ts +1 -1
- package/dist/lib/session/throughput.js +1 -1
- package/dist/lib/session/types.d.ts +23 -1
- package/dist/lib/session/types.js +15 -0
- package/dist/lib/session/viewing-in.d.ts +1 -1
- package/dist/lib/session/viewing-in.js +1 -1
- package/dist/lib/session/watch.d.ts +101 -0
- package/dist/lib/session/watch.js +242 -0
- package/dist/lib/share/config.d.ts +4 -0
- package/dist/lib/share/config.js +1 -0
- package/dist/lib/share/delete.d.ts +93 -0
- package/dist/lib/share/delete.js +127 -0
- package/dist/lib/share/provision.d.ts +38 -0
- package/dist/lib/share/provision.js +50 -0
- package/dist/lib/share/publish.d.ts +40 -1
- package/dist/lib/share/publish.js +116 -3
- package/dist/lib/share/worker-template.js +71 -6
- package/dist/lib/shims.d.ts +17 -3
- package/dist/lib/shims.js +136 -27
- package/dist/lib/signin-badge.js +3 -2
- package/dist/lib/skills.js +2 -0
- package/dist/lib/smart-launch.d.ts +8 -8
- package/dist/lib/smart-launch.js +80 -59
- package/dist/lib/ssh-exec.d.ts +1 -1
- package/dist/lib/ssh-exec.js +1 -1
- package/dist/lib/staleness/prune.d.ts +88 -0
- package/dist/lib/staleness/prune.js +69 -0
- package/dist/lib/staleness/registry.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.js +34 -2
- package/dist/lib/staleness/writers/hooks.js +16 -0
- package/dist/lib/staleness/writers/kinds.d.ts +6 -0
- package/dist/lib/staleness/writers/skills.js +19 -0
- package/dist/lib/staleness/writers/subagents.d.ts +2 -3
- package/dist/lib/staleness/writers/subagents.js +0 -4
- package/dist/lib/staleness/writers/types.d.ts +32 -0
- package/dist/lib/startup/command-registry.d.ts +6 -12
- package/dist/lib/startup/command-registry.js +14 -31
- package/dist/lib/startup/dev-build.d.ts +10 -0
- package/dist/lib/startup/dev-build.js +13 -1
- package/dist/lib/startup/spellcheck.d.ts +18 -0
- package/dist/lib/startup/spellcheck.js +45 -0
- package/dist/lib/state.d.ts +36 -9
- package/dist/lib/state.js +96 -46
- package/dist/lib/subagents-registry.d.ts +0 -7
- package/dist/lib/subagents-registry.js +38 -65
- package/dist/lib/subagents.d.ts +0 -33
- package/dist/lib/subagents.js +0 -75
- package/dist/lib/sync-umbrella.js +1 -1
- package/dist/lib/teams/agents.d.ts +41 -1
- package/dist/lib/teams/agents.js +187 -9
- package/dist/lib/teams/api.d.ts +11 -1
- package/dist/lib/teams/api.js +11 -2
- package/dist/lib/teams/delivery.d.ts +41 -0
- package/dist/lib/teams/delivery.js +60 -0
- package/dist/lib/teams/registry.d.ts +14 -0
- package/dist/lib/teams/registry.js +51 -1
- package/dist/lib/teams/remoteWorktree.d.ts +19 -0
- package/dist/lib/teams/remoteWorktree.js +29 -0
- package/dist/lib/teams/scheduler.d.ts +1 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/teams/worktree.d.ts +24 -0
- package/dist/lib/teams/worktree.js +42 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
- package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
- package/dist/lib/terminal/engine.d.ts +4 -0
- package/dist/lib/terminal/engine.js +8 -2
- package/dist/lib/terminal/types.d.ts +21 -2
- package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
- package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
- package/dist/lib/tickets/list.d.ts +53 -0
- package/dist/lib/tickets/list.js +153 -0
- package/dist/lib/tmux/orphan-reap.d.ts +320 -0
- package/dist/lib/tmux/orphan-reap.js +644 -0
- package/dist/lib/tmux/session.d.ts +68 -3
- package/dist/lib/tmux/session.js +95 -5
- package/dist/lib/triggers/handlers.d.ts +20 -0
- package/dist/lib/triggers/handlers.js +6 -1
- package/dist/lib/types.d.ts +46 -34
- package/dist/lib/usage-fleet.d.ts +32 -0
- package/dist/lib/usage-fleet.js +125 -0
- package/dist/lib/usage-refresh.d.ts +3 -3
- package/dist/lib/usage-refresh.js +19 -16
- package/dist/lib/usage.d.ts +51 -47
- package/dist/lib/usage.js +145 -153
- package/dist/lib/version-duplicates.js +2 -2
- package/dist/lib/versions.d.ts +56 -13
- package/dist/lib/versions.js +195 -45
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +3 -3
- package/dist/lib/watchdog/rotate.d.ts +6 -6
- package/dist/lib/watchdog/rotate.js +6 -6
- package/dist/lib/workflows.d.ts +1 -1
- package/dist/lib/yaml-io.d.ts +47 -0
- package/dist/lib/yaml-io.js +55 -0
- package/package.json +2 -1
- package/scripts/install-helper.js +2 -2
- package/scripts/postinstall.js +23 -20
- package/dist/commands/defaults.d.ts +0 -7
- package/dist/commands/defaults.js +0 -107
- package/dist/commands/export.d.ts +0 -11
- package/dist/commands/export.js +0 -215
- package/dist/commands/helper.d.ts +0 -12
- package/dist/commands/helper.js +0 -87
- package/dist/commands/hosts.d.ts +0 -11
- package/dist/commands/hosts.js +0 -330
- package/dist/commands/lock.d.ts +0 -12
- package/dist/commands/lock.js +0 -70
- package/dist/commands/pull.d.ts +0 -17
- package/dist/commands/pull.js +0 -39
- package/dist/commands/push.d.ts +0 -14
- package/dist/commands/push.js +0 -30
- package/dist/commands/wallet.d.ts +0 -20
- package/dist/commands/wallet.js +0 -216
- package/dist/commands/worktree.d.ts +0 -19
- package/dist/commands/worktree.js +0 -272
- package/dist/lib/account-labels.d.ts +0 -24
- package/dist/lib/account-labels.js +0 -72
- package/dist/lib/auto-dispatch-linear.d.ts +0 -18
- package/dist/lib/auto-dispatch-linear.js +0 -107
- package/dist/lib/auto-dispatch-provider.d.ts +0 -10
- package/dist/lib/auto-dispatch-provider.js +0 -30
- package/dist/lib/auto-dispatch.d.ts +0 -93
- package/dist/lib/auto-dispatch.js +0 -128
- package/dist/lib/export.d.ts +0 -72
- package/dist/lib/export.js +0 -269
- package/dist/lib/lock.d.ts +0 -93
- package/dist/lib/lock.js +0 -207
- package/dist/lib/wallet/index.d.ts +0 -78
- package/dist/lib/wallet/index.js +0 -253
package/dist/lib/exec.js
CHANGED
|
@@ -17,6 +17,7 @@ import { isTierToken, resolveTier } from './model-tiers.js';
|
|
|
17
17
|
import { createTimer, redactPrompt, redactArgs } from './events.js';
|
|
18
18
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
|
+
import { expandLocalHome } from './project-root.js';
|
|
20
21
|
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
21
22
|
import { resolveCodexHome } from './codex-home.js';
|
|
22
23
|
import { readCodexConfiguredModel } from './shims.js';
|
|
@@ -31,6 +32,7 @@ import { isTmuxInstalled } from './tmux/binary.js';
|
|
|
31
32
|
import { shellQuote } from './ssh-exec.js';
|
|
32
33
|
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
33
34
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
35
|
+
import { applyAddDirs } from './add-dir.js';
|
|
34
36
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
35
37
|
/**
|
|
36
38
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
@@ -287,43 +289,15 @@ export function buildExecEnv(options) {
|
|
|
287
289
|
const version = options.version
|
|
288
290
|
? resolvedVersion
|
|
289
291
|
: (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
+
// The per-account `claude setup-token` only resolves when there is a version
|
|
293
|
+
// home to key it to; version===null (claude unresolved / not installed) yields
|
|
294
|
+
// null, exactly as the routines path treats it (`runner.ts:1017-1021`). The
|
|
295
|
+
// token decision below runs even then, so an ambient inherited value is stripped
|
|
296
|
+
// on the routines/provisioned path regardless of whether a version resolved.
|
|
297
|
+
const versionHome = version ? getVersionHomePath('claude', version) : null;
|
|
298
|
+
const setupToken = versionHome ? resolveClaudeSetupToken(versionHome) : null;
|
|
299
|
+
if (versionHome) {
|
|
292
300
|
result.CLAUDE_CONFIG_DIR = path.join(versionHome, '.claude');
|
|
293
|
-
const setupToken = resolveClaudeSetupToken(versionHome);
|
|
294
|
-
if (setupToken) {
|
|
295
|
-
// The `auth` bundle's setup-token exists so a run with NO human present
|
|
296
|
-
// authenticates without the Touch-ID-gated login item — usage probes,
|
|
297
|
-
// routines, dispatched runs (claude-account-token.ts). An interactive run
|
|
298
|
-
// has a human at the TTY, and their own per-version login is the credential
|
|
299
|
-
// they established and expect; overriding it made `/status` report
|
|
300
|
-
// `Auth token: CLAUDE_CODE_OAUTH_TOKEN` on a personal machine and took every
|
|
301
|
-
// hand-driven session off that login. macOS cannot cheaply confirm a home's
|
|
302
|
-
// login first (probing the Keychain raises an authorization sheet per
|
|
303
|
-
// installed version on the `agents run` hot path — agents.ts
|
|
304
|
-
// `isClaudeCredentialFileBlank`), so interactive simply defers to Claude
|
|
305
|
-
// Code, which prompts a present human to log in if the login is missing.
|
|
306
|
-
if (resolveInteractive(options)) {
|
|
307
|
-
// Drop an INHERITED copy of the same token too: an interactive launch from
|
|
308
|
-
// inside a headless agent's shell inherits that agent's injected value via
|
|
309
|
-
// sanitizeProcessEnv(process.env) and would keep authenticating as it.
|
|
310
|
-
// Matched by VALUE, so a token the user exported deliberately is a
|
|
311
|
-
// different string and is left alone. This is NARROWER than the routines
|
|
312
|
-
// path, which overwrites-or-deletes unconditionally and never inspects the
|
|
313
|
-
// inherited value (`runner.ts:1020-1021`) — the gap between the two is
|
|
314
|
-
// what RUSH-2360 tracks, including a DIFFERENT account's inherited token,
|
|
315
|
-
// which this equality check lets through.
|
|
316
|
-
if (result.CLAUDE_CODE_OAUTH_TOKEN === setupToken) {
|
|
317
|
-
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
// A token keyed to this version home's own account replaces any ambient
|
|
322
|
-
// shared value inherited from the launcher. options.env still wins below
|
|
323
|
-
// for explicit caller overrides.
|
|
324
|
-
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
301
|
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
328
302
|
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
329
303
|
// half-swapped and broken). Disable it so a pin stays a pin. Honor an
|
|
@@ -333,6 +307,50 @@ export function buildExecEnv(options) {
|
|
|
333
307
|
result.DISABLE_AUTOUPDATER = '1';
|
|
334
308
|
}
|
|
335
309
|
}
|
|
310
|
+
// The `auth` bundle's setup-token exists so a run with NO human present
|
|
311
|
+
// authenticates without the Touch-ID-gated login item — usage probes,
|
|
312
|
+
// routines, dispatched runs (claude-account-token.ts). An interactive run
|
|
313
|
+
// has a human at the TTY, and their own per-version login is the credential
|
|
314
|
+
// they established and expect; overriding it made `/status` report
|
|
315
|
+
// `Auth token: CLAUDE_CODE_OAUTH_TOKEN` on a personal machine and took every
|
|
316
|
+
// hand-driven session off that login. macOS cannot cheaply confirm a home's
|
|
317
|
+
// login first (probing the Keychain raises an authorization sheet per
|
|
318
|
+
// installed version on the `agents run` hot path — agents.ts
|
|
319
|
+
// `isClaudeCredentialFileBlank`), so interactive simply defers to Claude
|
|
320
|
+
// Code, which prompts a present human to log in if the login is missing.
|
|
321
|
+
if (resolveInteractive(options)) {
|
|
322
|
+
// Drop an INHERITED copy of OUR OWN setup-token: an interactive launch from
|
|
323
|
+
// inside a headless agent's shell inherits that agent's injected value via
|
|
324
|
+
// sanitizeProcessEnv(process.env) and would keep authenticating as it.
|
|
325
|
+
// Matched by VALUE, so a token the user exported deliberately is a different
|
|
326
|
+
// string and is left alone (#2383). This is NARROWER than the non-interactive
|
|
327
|
+
// path below, which overwrites-or-deletes unconditionally and never inspects
|
|
328
|
+
// the inherited value — a DIFFERENT account's inherited setup-token passing
|
|
329
|
+
// through this equality check is the adjacent hole RUSH-2360 leaves as
|
|
330
|
+
// follow-up (it does not silently run on a *shared, rotating* token, which is
|
|
331
|
+
// what caused the RUSH-1822 logout storm).
|
|
332
|
+
if (setupToken && result.CLAUDE_CODE_OAUTH_TOKEN === setupToken) {
|
|
333
|
+
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Non-interactive (routines, dispatched, provisioned box): mirror the routines
|
|
338
|
+
// path (`runner.ts:1017-1021`) UNCONDITIONALLY. Inject the per-account
|
|
339
|
+
// setup-token when one resolves — it replaces any ambient shared value
|
|
340
|
+
// inherited from the launcher. When NONE resolves, STRIP the ambient
|
|
341
|
+
// CLAUDE_CODE_OAUTH_TOKEN so a run on a provisioned box can never silently
|
|
342
|
+
// authenticate as the shared, rotating token an earlier version of this path
|
|
343
|
+
// let through — the RUSH-1822 fleet-wide-logout hazard, tracked by RUSH-2360.
|
|
344
|
+
// A missing login then fails loud (401) against this home's own credential
|
|
345
|
+
// instead of quietly borrowing another's. options.env still wins below for an
|
|
346
|
+
// explicit caller override.
|
|
347
|
+
if (setupToken) {
|
|
348
|
+
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
336
354
|
delete result.CODEX_HOME;
|
|
337
355
|
delete result.COPILOT_HOME;
|
|
338
356
|
delete result.KIMI_CODE_HOME;
|
|
@@ -735,22 +753,22 @@ export const AGENT_COMMANDS = {
|
|
|
735
753
|
// `resume` subcommand — special-cased in buildExecCommand.
|
|
736
754
|
resume: { flag: '--session-id' },
|
|
737
755
|
},
|
|
738
|
-
// Warp Agent CLI (`
|
|
739
|
-
// the
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
756
|
+
// Warp Agent CLI (`warp`) — the interactive TUI. It has NO headless one-shot
|
|
757
|
+
// form: the documented flags are --api-key/--auto-approve/--resume <token>/
|
|
758
|
+
// --set-provider-api-key/--clear-provider-api-key/--version/--help — no
|
|
759
|
+
// -p/--prompt, no --model (model is the `/model` picker), no JSON output. So
|
|
760
|
+
// bare `warp` opens the TUI and the single `edit` mode maps to no flags
|
|
761
|
+
// (mirrors hermes); there are no jsonFlags/modelFlag to declare. No `resume`:
|
|
762
|
+
// `warp --resume <token>` reopens a SERVER-SIDE conversation by token, but
|
|
763
|
+
// warp is not session-tracked (absent from SESSION_AGENTS) so agents-cli has
|
|
764
|
+
// no local id to feed — declaring it would make nativeResume(warp) true
|
|
765
|
+
// against an unreachable path.
|
|
746
766
|
warp: {
|
|
747
|
-
base: ['
|
|
748
|
-
promptFlag: '
|
|
767
|
+
base: ['warp'],
|
|
768
|
+
promptFlag: 'positional',
|
|
749
769
|
modeFlags: {
|
|
750
770
|
edit: [],
|
|
751
771
|
},
|
|
752
|
-
jsonFlags: ['--output-format', 'json'],
|
|
753
|
-
modelFlag: '--model',
|
|
754
772
|
},
|
|
755
773
|
};
|
|
756
774
|
/**
|
|
@@ -939,7 +957,10 @@ export function buildExecCommand(options) {
|
|
|
939
957
|
}
|
|
940
958
|
if (options.agent === 'codex') {
|
|
941
959
|
const policyMode = resolvedMode === 'plan' || resolvedMode === 'skip' ? resolvedMode : 'edit';
|
|
942
|
-
const writableRoots = [
|
|
960
|
+
const writableRoots = [
|
|
961
|
+
...codexEditWritableRoots(options.cwd ?? process.cwd()),
|
|
962
|
+
...(options.addDirs ?? []).map(expandLocalHome),
|
|
963
|
+
];
|
|
943
964
|
cmd.push(...codexPolicyArgs(policyMode, writableRoots));
|
|
944
965
|
}
|
|
945
966
|
else if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
@@ -1078,14 +1099,22 @@ export function buildExecCommand(options) {
|
|
|
1078
1099
|
cmd.push(template.promptFlag, options.prompt);
|
|
1079
1100
|
}
|
|
1080
1101
|
}
|
|
1081
|
-
//
|
|
1082
|
-
// resume
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1102
|
+
// Project / --add-dir grants. Codex folds them into the named edit profile
|
|
1103
|
+
// above (workspace_roots); resume rejects the native --add-dir flag.
|
|
1104
|
+
// Everything else is strategy-driven in applyAddDirs:
|
|
1105
|
+
// native-flag — claude, kimi, cursor (`--add-dir`)
|
|
1106
|
+
// grok-sandbox — rules + project sandbox profile when sandboxed
|
|
1107
|
+
// none — ignored (no multi-root surface)
|
|
1108
|
+
//
|
|
1109
|
+
// `~` is expanded in normalizeAddDirs (via expandLocalHome), because no shell
|
|
1110
|
+
// does it for us: a forwarded grant crosses the SSH boundary single-quoted
|
|
1111
|
+
// (`shellQuote` in ssh-exec.ts), so the remote login shell leaves `~/…`
|
|
1112
|
+
// literal and the harness resolves it as a directory actually named `~`.
|
|
1113
|
+
// Expanding on the side that runs the harness is what lets one home-relative
|
|
1114
|
+
// grant re-root per machine.
|
|
1115
|
+
applyAddDirs(options.agent, cmd, options.addDirs, {
|
|
1116
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1117
|
+
});
|
|
1089
1118
|
// Claude-specific: workflow capability scoping. WORKFLOW.md frontmatter
|
|
1090
1119
|
// `tools:` / `mcpServers:` is translated to the headless flags that ACTUALLY
|
|
1091
1120
|
// restrict the run (verified against `claude --help` on the installed CLI):
|
|
@@ -1181,7 +1210,7 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1181
1210
|
// Match the POSIX shim: direct Codex launches default to the safe writable
|
|
1182
1211
|
// profile, while later user arguments can still override native settings.
|
|
1183
1212
|
const launchArgs = agent === 'codex'
|
|
1184
|
-
? ['-c', 'check_for_update_on_startup=false', ...codexPolicyArgs('edit')]
|
|
1213
|
+
? ['-c', 'check_for_update_on_startup=false', ...codexPolicyArgs('edit', codexEditWritableRoots(cwd))]
|
|
1185
1214
|
: [];
|
|
1186
1215
|
// Mint a launch id and export it as AGENT_LAUNCH_ID so the agent's SessionStart
|
|
1187
1216
|
// hook records the same id — the join key that maps this launch to its exact
|
|
@@ -1251,6 +1280,32 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1251
1280
|
export function shouldRecapDeadPane(status, interactive) {
|
|
1252
1281
|
return (status ?? 0) !== 0 || interactive;
|
|
1253
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* The exit code a tmux-wrapped run resolves with (RUSH-2185 / EXEC-23b).
|
|
1285
|
+
*
|
|
1286
|
+
* Three inputs, one rule: report success ONLY for an outcome tmux actually told
|
|
1287
|
+
* us about. A clean user detach (`knownAlive`) is 0; a dead pane reports the
|
|
1288
|
+
* status tmux read off it; everything else — pane unreadable because the server
|
|
1289
|
+
* or session went away, or dead with no status — is UNKNOWN and resolves to
|
|
1290
|
+
* {@link UNKNOWN_OUTCOME_EXIT_CODE}, never 0.
|
|
1291
|
+
*
|
|
1292
|
+
* Why this is not `status ?? 0`: an interactive run whose tmux server died mid-
|
|
1293
|
+
* work landed on exactly those unknown branches and returned 0, so `agents run`
|
|
1294
|
+
* printed a failure banner while handing its caller a success code. The same
|
|
1295
|
+
* "1 if unknown" rule already governs the `--host` follow path
|
|
1296
|
+
* (`docs/specifications.md` §Agent execution, exit-code table).
|
|
1297
|
+
*
|
|
1298
|
+
* @param pane What `paneExitStatus` read back for the agent pane.
|
|
1299
|
+
* @param knownAlive Positive proof the pane is still alive (see
|
|
1300
|
+
* {@link isPaneKnownAliveFromQueryResult}) — a clean detach.
|
|
1301
|
+
*/
|
|
1302
|
+
export function tmuxRunExitCode(pane, knownAlive) {
|
|
1303
|
+
if (knownAlive)
|
|
1304
|
+
return 0;
|
|
1305
|
+
if (pane.dead && pane.status !== undefined)
|
|
1306
|
+
return pane.status;
|
|
1307
|
+
return UNKNOWN_OUTCOME_EXIT_CODE;
|
|
1308
|
+
}
|
|
1254
1309
|
/**
|
|
1255
1310
|
* True only when a `display-message #{pane_dead}` tmux query explicitly returned
|
|
1256
1311
|
* "0" (pane alive). Used to distinguish "pane alive" from "query failed" in
|
|
@@ -1403,13 +1458,105 @@ async function runInTmux(options, executable, args) {
|
|
|
1403
1458
|
const cwd = options.cwd || process.cwd();
|
|
1404
1459
|
const idSeed = (options.sessionId ?? randomUUID()).slice(0, 8);
|
|
1405
1460
|
const name = slugifyName(`ag-${options.agent}-${idSeed}`);
|
|
1461
|
+
const RED = '\x1b[31m', GRAY = '\x1b[90m', OFF = '\x1b[0m';
|
|
1462
|
+
const NO_TMUX_TIP = `${GRAY} Tip: re-run with --no-tmux to launch the agent directly and see its full output.${OFF}\n\n`;
|
|
1463
|
+
// Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
|
|
1464
|
+
// detaches the client the instant the agent exits, so a fast failure (a
|
|
1465
|
+
// gutted install that dies with ENOENT, a bad flag, a crash on startup) would
|
|
1466
|
+
// otherwise leave only a bare `[detached]` with no clue why. Must run BEFORE
|
|
1467
|
+
// killSession — capture-pane needs the session still alive (remain-on-exit
|
|
1468
|
+
// keeps the dead pane readable until we tear it down). Best-effort throughout.
|
|
1469
|
+
const surfacePaneFailure = async (pane, status, headline) => {
|
|
1470
|
+
if (!pane)
|
|
1471
|
+
return;
|
|
1472
|
+
let tail = '';
|
|
1473
|
+
try {
|
|
1474
|
+
const r = await runTmux({ socket, args: ['capture-pane', '-p', '-t', pane, '-S', '-200'], throwOnError: false });
|
|
1475
|
+
if (r.code === 0)
|
|
1476
|
+
tail = formatPaneTail(r.stdout);
|
|
1477
|
+
}
|
|
1478
|
+
catch { /* best-effort — a missing pane just means no recap */ }
|
|
1479
|
+
process.stderr.write(`\n${RED}agents: ${headline} (exit ${status ?? UNKNOWN_OUTCOME_EXIT_CODE}).${OFF}\n`);
|
|
1480
|
+
if (tail) {
|
|
1481
|
+
process.stderr.write(`${GRAY} ── last output from ${options.agent} ──${OFF}\n`);
|
|
1482
|
+
process.stderr.write(tail.replace(/^/gm, ' ') + '\n');
|
|
1483
|
+
process.stderr.write(`${GRAY} ${'─'.repeat(30)}${OFF}\n`);
|
|
1484
|
+
}
|
|
1485
|
+
process.stderr.write(NO_TMUX_TIP);
|
|
1486
|
+
};
|
|
1487
|
+
// F3 (RUSH-2185 / EXEC-23a): paneExitStatus returns {dead:false} for BOTH
|
|
1488
|
+
// "pane is alive" and "tmux query failed (race / pane already gone)". Require
|
|
1489
|
+
// POSITIVE proof before taking the keep-session path — a separate direct query
|
|
1490
|
+
// that only returns true when tmux explicitly confirms pane_dead=0.
|
|
1491
|
+
const checkPaneKnownAlive = async (p) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const r = await runTmux({ socket, args: ['display-message', '-pt', p, '-p', '#{pane_dead}'], throwOnError: false });
|
|
1494
|
+
return isPaneKnownAliveFromQueryResult(r.code, r.stdout);
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
/**
|
|
1501
|
+
* Resolve what an attach client's return actually means, for EVERY path that
|
|
1502
|
+
* attaches (the fresh wrapper below AND the resume-attach above). Asking tmux
|
|
1503
|
+
* is the only way to tell "the user detached" from "the agent exited" from
|
|
1504
|
+
* "the session went away underneath us", so an attach that skips this cannot
|
|
1505
|
+
* do better than assume success — which is exactly the defect EXEC-23b fixes.
|
|
1506
|
+
*/
|
|
1507
|
+
const resolveAfterAttach = async (pane) => {
|
|
1508
|
+
const after = pane ? await paneExitStatus(pane, socket) : { found: false, dead: false, status: undefined };
|
|
1509
|
+
if (after.dead) {
|
|
1510
|
+
// Nonzero exit after attach → the agent crashed rather than the user
|
|
1511
|
+
// detaching cleanly (a clean detach leaves the pane ALIVE, handled below).
|
|
1512
|
+
// F2: for interactive runs, also recap a clean exit-0 — the harness exited
|
|
1513
|
+
// without error but without starting a REPL, which is still a failure.
|
|
1514
|
+
if (shouldRecapDeadPane(after.status, resolveInteractive(options))) {
|
|
1515
|
+
await surfacePaneFailure(pane, after.status, `${options.agent} exited`);
|
|
1516
|
+
}
|
|
1517
|
+
await killSession(name, socket).catch(() => { });
|
|
1518
|
+
// A dead pane whose status tmux never reported is UNKNOWN, not success —
|
|
1519
|
+
// and surfacePaneFailure already printed `exit 1` for it, so the old
|
|
1520
|
+
// `?? 0` made the message and the returned code disagree (EXEC-23b).
|
|
1521
|
+
return { exitCode: tmuxRunExitCode(after, false), stderr: '', stdout: '' };
|
|
1522
|
+
}
|
|
1523
|
+
// after.dead===false, but that could be a stale/unreadable-pane result.
|
|
1524
|
+
// Require positive proof before keeping the session as "user detached".
|
|
1525
|
+
if (pane && await checkPaneKnownAlive(pane)) {
|
|
1526
|
+
// Confirmed alive: the user pressed Ctrl-b d; keep the session for `agents focus`.
|
|
1527
|
+
return { exitCode: tmuxRunExitCode(after, true), stderr: '', stdout: '' };
|
|
1528
|
+
}
|
|
1529
|
+
// F4 (EXEC-23b): the outcome is UNKNOWN — tmux could not tell us whether the
|
|
1530
|
+
// agent finished or was killed. MUST NOT be reported as success: a caller
|
|
1531
|
+
// scripting `agents run` would count a run killed mid-work as a clean
|
|
1532
|
+
// finish. Tear down so no orphan session is left, say so, and exit non-zero
|
|
1533
|
+
// — the same "1 if unknown" rule the `--host` follow path already uses.
|
|
1534
|
+
await killSession(name, socket).catch(() => { });
|
|
1535
|
+
// One computation feeds both the banner and the return value — printing a
|
|
1536
|
+
// code the caller does not receive is the same defect in miniature.
|
|
1537
|
+
const exitCode = tmuxRunExitCode(after, false);
|
|
1538
|
+
// Two distinct causes reach here, and the banner must not assert the wrong
|
|
1539
|
+
// one: a pane we HAD (the session went away under it) versus a run whose
|
|
1540
|
+
// pane id tmux never reported at creation, which had nothing to read from.
|
|
1541
|
+
const cause = pane
|
|
1542
|
+
? 'The tmux session went away before its exit status could be read, so this run may have been killed mid-work.'
|
|
1543
|
+
: 'This run had no readable tmux pane, so its exit status could never be read.';
|
|
1544
|
+
process.stderr.write(`\n${RED}agents: ${options.agent} outcome unknown (exit ${exitCode}).${OFF}\n` +
|
|
1545
|
+
`${GRAY} ${cause}${OFF}\n` + NO_TMUX_TIP);
|
|
1546
|
+
return { exitCode, stderr: '', stdout: '' };
|
|
1547
|
+
};
|
|
1406
1548
|
// A native resume must not create a competing wrapper for a live session.
|
|
1407
1549
|
// A retained dead pane is reaped before the harness resumes normally.
|
|
1408
|
-
|
|
1550
|
+
const resumePrep = options.resume ? await prepareSessionForResume(name, socket) : { decision: 'create' };
|
|
1551
|
+
if (resumePrep.decision === 'attach') {
|
|
1409
1552
|
if (options.sessionId)
|
|
1410
1553
|
writeSessionAliasRecord(options.sessionId, name);
|
|
1411
1554
|
await attachTmux({ socket, args: ['attach-session', '-t', name] });
|
|
1412
|
-
|
|
1555
|
+
// EXEC-23b: a resume-attach is an attach like any other — it must ask tmux
|
|
1556
|
+
// what happened rather than assume 0. Before this it returned a hardcoded
|
|
1557
|
+
// success, so a resumed run whose server died under it reported a clean
|
|
1558
|
+
// finish, the identical defect on the identical shape.
|
|
1559
|
+
return resolveAfterAttach(resumePrep.pane);
|
|
1413
1560
|
}
|
|
1414
1561
|
// SessionStart learns some harness IDs only after launch. Carry the wrapper
|
|
1415
1562
|
// name into that hook so it can bind both identities durably.
|
|
@@ -1490,31 +1637,6 @@ async function runInTmux(options, executable, args) {
|
|
|
1490
1637
|
});
|
|
1491
1638
|
}
|
|
1492
1639
|
}
|
|
1493
|
-
// Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
|
|
1494
|
-
// detaches the client the instant the agent exits, so a fast failure (a
|
|
1495
|
-
// gutted install that dies with ENOENT, a bad flag, a crash on startup) would
|
|
1496
|
-
// otherwise leave only a bare `[detached]` with no clue why. Must run BEFORE
|
|
1497
|
-
// killSession — capture-pane needs the session still alive (remain-on-exit
|
|
1498
|
-
// keeps the dead pane readable until we tear it down). Best-effort throughout.
|
|
1499
|
-
const surfacePaneFailure = async (status, headline) => {
|
|
1500
|
-
if (!pane)
|
|
1501
|
-
return;
|
|
1502
|
-
let tail = '';
|
|
1503
|
-
try {
|
|
1504
|
-
const r = await runTmux({ socket, args: ['capture-pane', '-p', '-t', pane, '-S', '-200'], throwOnError: false });
|
|
1505
|
-
if (r.code === 0)
|
|
1506
|
-
tail = formatPaneTail(r.stdout);
|
|
1507
|
-
}
|
|
1508
|
-
catch { /* best-effort — a missing pane just means no recap */ }
|
|
1509
|
-
const RED = '\x1b[31m', GRAY = '\x1b[90m', OFF = '\x1b[0m';
|
|
1510
|
-
process.stderr.write(`\n${RED}agents: ${headline} (exit ${status ?? 1}).${OFF}\n`);
|
|
1511
|
-
if (tail) {
|
|
1512
|
-
process.stderr.write(`${GRAY} ── last output from ${options.agent} ──${OFF}\n`);
|
|
1513
|
-
process.stderr.write(tail.replace(/^/gm, ' ') + '\n');
|
|
1514
|
-
process.stderr.write(`${GRAY} ${'─'.repeat(30)}${OFF}\n`);
|
|
1515
|
-
}
|
|
1516
|
-
process.stderr.write(`${GRAY} Tip: re-run with --no-tmux to launch the agent directly and see its full output.${OFF}\n\n`);
|
|
1517
|
-
};
|
|
1518
1640
|
// The agent could exit before we attach (fast failure). Don't attach to an
|
|
1519
1641
|
// already-dead pane — surface its output + status directly and tear down.
|
|
1520
1642
|
const before = pane ? await paneExitStatus(pane, socket) : { found: false, dead: false, status: undefined };
|
|
@@ -1524,47 +1646,16 @@ async function runInTmux(options, executable, args) {
|
|
|
1524
1646
|
// user would see only a bare `[detached]` with no clue why. For headless
|
|
1525
1647
|
// runs the old quiet behaviour stands: exit-0 is a successful quick run.
|
|
1526
1648
|
if (shouldRecapDeadPane(before.status, resolveInteractive(options))) {
|
|
1527
|
-
await surfacePaneFailure(before.status, `${options.agent} exited before it could start`);
|
|
1649
|
+
await surfacePaneFailure(pane, before.status, `${options.agent} exited before it could start`);
|
|
1528
1650
|
}
|
|
1529
1651
|
await killSession(name, socket).catch(() => { });
|
|
1530
|
-
|
|
1652
|
+
// A dead pane whose status tmux never reported is an UNKNOWN outcome, not a
|
|
1653
|
+
// success — and the banner one line up already printed `exit 1` for it, so
|
|
1654
|
+
// the old `?? 0` also made the message and the returned code disagree.
|
|
1655
|
+
return { exitCode: tmuxRunExitCode(before, false), stderr: '', stdout: '' };
|
|
1531
1656
|
}
|
|
1532
1657
|
await attachTmux({ socket, args: ['attach-session', '-t', name] });
|
|
1533
|
-
|
|
1534
|
-
// "pane is alive" and "tmux query failed (race / pane already gone)". Require
|
|
1535
|
-
// POSITIVE proof before taking the keep-session path — a separate direct query
|
|
1536
|
-
// that only returns true when tmux explicitly confirms pane_dead=0.
|
|
1537
|
-
const checkPaneKnownAlive = async (p) => {
|
|
1538
|
-
try {
|
|
1539
|
-
const r = await runTmux({ socket, args: ['display-message', '-pt', p, '-p', '#{pane_dead}'], throwOnError: false });
|
|
1540
|
-
return isPaneKnownAliveFromQueryResult(r.code, r.stdout);
|
|
1541
|
-
}
|
|
1542
|
-
catch {
|
|
1543
|
-
return false;
|
|
1544
|
-
}
|
|
1545
|
-
};
|
|
1546
|
-
const after = pane ? await paneExitStatus(pane, socket) : { found: false, dead: false, status: undefined };
|
|
1547
|
-
if (after.dead) {
|
|
1548
|
-
// Nonzero exit after attach → the agent crashed rather than the user
|
|
1549
|
-
// detaching cleanly (a clean detach leaves the pane ALIVE, handled below).
|
|
1550
|
-
// F2: for interactive runs, also recap a clean exit-0 — the harness exited
|
|
1551
|
-
// without error but without starting a REPL, which is still a failure.
|
|
1552
|
-
if (shouldRecapDeadPane(after.status, resolveInteractive(options))) {
|
|
1553
|
-
await surfacePaneFailure(after.status, `${options.agent} exited`);
|
|
1554
|
-
}
|
|
1555
|
-
await killSession(name, socket).catch(() => { });
|
|
1556
|
-
return { exitCode: after.status ?? 0, stderr: '', stdout: '' };
|
|
1557
|
-
}
|
|
1558
|
-
// after.dead===false, but that could be a stale/unreadable-pane result.
|
|
1559
|
-
// Require positive proof before keeping the session as "user detached".
|
|
1560
|
-
if (pane && await checkPaneKnownAlive(pane)) {
|
|
1561
|
-
// Confirmed alive: the user pressed Ctrl-b d; keep the session for `agents focus`.
|
|
1562
|
-
return { exitCode: 0, stderr: '', stdout: '' };
|
|
1563
|
-
}
|
|
1564
|
-
// Ambiguous or unreadable pane (race between pane-died hook and our query) —
|
|
1565
|
-
// tear down rather than leave an orphan session.
|
|
1566
|
-
await killSession(name, socket).catch(() => { });
|
|
1567
|
-
return { exitCode: 0, stderr: '', stdout: '' };
|
|
1658
|
+
return resolveAfterAttach(pane);
|
|
1568
1659
|
}
|
|
1569
1660
|
/**
|
|
1570
1661
|
* Print the run's resolved session id as a one-line stdout sentinel so a `--host`
|
|
@@ -1835,6 +1926,13 @@ async function spawnAgent(options) {
|
|
|
1835
1926
|
}
|
|
1836
1927
|
/** Exit code spawnAgent resolves with when a run is killed for crossing a budget cap. */
|
|
1837
1928
|
export const BUDGET_KILL_EXIT_CODE = 7;
|
|
1929
|
+
/**
|
|
1930
|
+
* Exit code a tmux-wrapped run resolves with when tmux cannot tell us how the
|
|
1931
|
+
* agent finished — the pane is unreadable, or it is dead with no reported status
|
|
1932
|
+
* (EXEC-23b). Never 0: an unknown outcome reported as success is how a run killed
|
|
1933
|
+
* mid-work gets counted as a clean finish by whatever scripted it.
|
|
1934
|
+
*/
|
|
1935
|
+
export const UNKNOWN_OUTCOME_EXIT_CODE = 1;
|
|
1838
1936
|
/**
|
|
1839
1937
|
* Resolve the budget watcher for a run. Returns null (watcher dormant) when no
|
|
1840
1938
|
* caps are configured, so non-budget users pay nothing. When caps exist, builds
|
|
@@ -179,6 +179,31 @@ export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefine
|
|
|
179
179
|
* always wins outright; the fallback never layers on top of it.
|
|
180
180
|
*/
|
|
181
181
|
export declare function effectiveBroadcastConfig(config: FeedBroadcastConfig | undefined, level: FeedPostLevel, meta: Meta): FeedBroadcastConfig | undefined;
|
|
182
|
+
/** Sink name for the ephemeral local banner added by `feed post --notify`. */
|
|
183
|
+
export declare const DESKTOP_NOTIFY_SINK = "notify";
|
|
184
|
+
/**
|
|
185
|
+
* Add a local desktop-banner sink when `feed post --notify` is set — the same
|
|
186
|
+
* `notifyDesktop` banner `run --notify` raises, but for an authored post.
|
|
187
|
+
*
|
|
188
|
+
* `--notify` is a per-post opt-in that ADDS the local banner on top of whatever
|
|
189
|
+
* `feed.broadcast`/owner delivery already runs; it never replaces a configured
|
|
190
|
+
* sink. It carries no `minLevel`, so it fires for any level — a quiet milestone
|
|
191
|
+
* banner on this box does not touch the phone the way an `important` post does.
|
|
192
|
+
* And it routes through the `desktop` channel provider exactly like every other
|
|
193
|
+
* `channel:` sink, so it appears in the outcomes and the `--json` payload rather
|
|
194
|
+
* than a parallel code path the reporting cannot see.
|
|
195
|
+
*
|
|
196
|
+
* The desktop banner is inherently local — `notifyDesktop` reaches whoever is at
|
|
197
|
+
* THIS machine — so a post authored on a headless box notifies that box (a no-op
|
|
198
|
+
* with a stated reason where no notifier exists, per the desktop provider),
|
|
199
|
+
* never the operator's Mac. That is the same locality `run --notify` has; the
|
|
200
|
+
* phone hop stays the job of an `important`-level owner/broadcast sink.
|
|
201
|
+
*
|
|
202
|
+
* The banner is added under `DESKTOP_NOTIFY_SINK`, or the first free
|
|
203
|
+
* `notify-2`/`notify-3`/… when the operator already declared a sink by that
|
|
204
|
+
* name — so `--notify` can never silently overwrite a configured sink; both fire.
|
|
205
|
+
*/
|
|
206
|
+
export declare function withDesktopNotify(config: FeedBroadcastConfig | undefined, notify: boolean): FeedBroadcastConfig | undefined;
|
|
182
207
|
/**
|
|
183
208
|
* Run the planned sinks. A `command:` sink is a direct spawn with a bounded
|
|
184
209
|
* lifetime; a `channel:` sink delivers in-process. Either way a sink that
|
|
@@ -394,6 +394,39 @@ export function effectiveBroadcastConfig(config, level, meta) {
|
|
|
394
394
|
return undefined;
|
|
395
395
|
return { owner: { channel: 'owner' } };
|
|
396
396
|
}
|
|
397
|
+
/** Sink name for the ephemeral local banner added by `feed post --notify`. */
|
|
398
|
+
export const DESKTOP_NOTIFY_SINK = 'notify';
|
|
399
|
+
/**
|
|
400
|
+
* Add a local desktop-banner sink when `feed post --notify` is set — the same
|
|
401
|
+
* `notifyDesktop` banner `run --notify` raises, but for an authored post.
|
|
402
|
+
*
|
|
403
|
+
* `--notify` is a per-post opt-in that ADDS the local banner on top of whatever
|
|
404
|
+
* `feed.broadcast`/owner delivery already runs; it never replaces a configured
|
|
405
|
+
* sink. It carries no `minLevel`, so it fires for any level — a quiet milestone
|
|
406
|
+
* banner on this box does not touch the phone the way an `important` post does.
|
|
407
|
+
* And it routes through the `desktop` channel provider exactly like every other
|
|
408
|
+
* `channel:` sink, so it appears in the outcomes and the `--json` payload rather
|
|
409
|
+
* than a parallel code path the reporting cannot see.
|
|
410
|
+
*
|
|
411
|
+
* The desktop banner is inherently local — `notifyDesktop` reaches whoever is at
|
|
412
|
+
* THIS machine — so a post authored on a headless box notifies that box (a no-op
|
|
413
|
+
* with a stated reason where no notifier exists, per the desktop provider),
|
|
414
|
+
* never the operator's Mac. That is the same locality `run --notify` has; the
|
|
415
|
+
* phone hop stays the job of an `important`-level owner/broadcast sink.
|
|
416
|
+
*
|
|
417
|
+
* The banner is added under `DESKTOP_NOTIFY_SINK`, or the first free
|
|
418
|
+
* `notify-2`/`notify-3`/… when the operator already declared a sink by that
|
|
419
|
+
* name — so `--notify` can never silently overwrite a configured sink; both fire.
|
|
420
|
+
*/
|
|
421
|
+
export function withDesktopNotify(config, notify) {
|
|
422
|
+
if (!notify)
|
|
423
|
+
return config;
|
|
424
|
+
const base = config ?? {};
|
|
425
|
+
let name = DESKTOP_NOTIFY_SINK;
|
|
426
|
+
for (let i = 2; name in base; i++)
|
|
427
|
+
name = `${DESKTOP_NOTIFY_SINK}-${i}`;
|
|
428
|
+
return { ...base, [name]: { channel: 'desktop', to: 'local' } };
|
|
429
|
+
}
|
|
397
430
|
function runCommandSink(name, argv, timeoutMs) {
|
|
398
431
|
const result = spawnSync(argv[0], argv.slice(1), {
|
|
399
432
|
encoding: 'utf-8',
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ export declare function removeBlock(blockId: string, root?: string): boolean;
|
|
|
225
225
|
* Embedded so it ships with the compiled CLI and can be installed to the
|
|
226
226
|
* CLI-writable user hooks dir without a separate file in the npm tarball.
|
|
227
227
|
*/
|
|
228
|
-
export declare const FEED_PUBLISH_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Publish and clear open-block records for `agents feed`.\n\nThe manifest invokes this script for top-level AskUserQuestion calls, waiting\nnotifications, question answers, and session lifecycle events. One atomic file\nper session means a new block replaces the previous block. Answer/resume/stop\nevents remove it so `agents feed` only lists decisions that are still open.\n\nSub-agent gate: when the PreToolUse payload carries `agent_type`, this is a\nTask/Agent subagent -- skip. Only the top-level agent publishes. Verified on\nClaude Code 2.1.170 (2026-07).\n\nFail-open: ANY error is swallowed so a feed hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport sys\nimport json\nimport re\nimport socket\nimport tempfile\nfrom datetime import datetime, timezone\n\nWAITING_NOTIFICATION_TYPES = {\n \"permission_prompt\",\n \"idle_prompt\",\n \"elicitation_dialog\",\n}\nCLEAR_EVENTS = {\n \"PostToolUse\",\n \"Stop\",\n \"SessionEnd\",\n}\n# Codex emits a PermissionRequest event (not Claude's Notification) when it\n# blocks on an approval prompt. Claude never fires PermissionRequest, so the\n# same script handles both: PermissionRequest maps to an approval-class block\n# with a high cost-of-delay so 'agents feed --dispatch' pages it as urgent.\n\n\ndef read_json(path):\n try:\n with open(path) as f:\n return json.load(f)\n except Exception:\n return None\n\n\ndef write_json(path, value):\n dir_name = os.path.dirname(path)\n os.makedirs(dir_name, exist_ok=True)\n fd, tmp = tempfile.mkstemp(dir=dir_name, suffix=\".tmp\")\n try:\n with os.fdopen(fd, \"w\") as f:\n json.dump(value, f, indent=2)\n os.replace(tmp, path)\n except Exception:\n try:\n os.unlink(tmp)\n except Exception:\n pass\n\n\ndef project_from_cwd(cwd):\n \"\"\"Basename of cwd, with worktree paths resolved to their repo name.\"\"\"\n if not cwd:\n return None\n norm = cwd.replace(\"\\\\\", \"/\").rstrip(\"/\")\n if not norm:\n return None\n marker = \"/.agents/worktrees/\"\n idx = norm.find(marker)\n if idx > 0:\n repo_path = norm[:idx]\n base = repo_path[repo_path.rfind(\"/\") + 1:]\n if base:\n return base\n base = norm[norm.rfind(\"/\") + 1:]\n return base or None\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n safe_session_id = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id)\n block_id = f\"block-{safe_session_id}\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n feed_dir = os.path.join(home, \".agents\", \".history\", \"feed\")\n answered_dir = os.path.join(feed_dir, \"answered\")\n asks_dir = os.path.join(feed_dir, \"asks\")\n target = os.path.join(feed_dir, f\"{block_id}.json\")\n hook_event = payload.get(\"hook_event_name\", \"PreToolUse\")\n\n if hook_event in CLEAR_EVENTS:\n # A declared block (`agents feed post --blocked`) is the agent explicitly\n # saying it is stuck. Unlike a question/notification/approval block -- which\n # tracks an in-flight harness prompt that a lifecycle event resolves -- a\n # declared block stays open until it is actually ANSWERED. So while it is\n # still UNANSWERED, Stop/SessionEnd/PostToolUse must never silently drop it:\n # otherwise the needs-you record vanishes the moment the agent parks the block\n # and its turn ends -- exactly when the owner still needs to see and answer it.\n # Once it IS answered (an answered marker exists), it clears like any other\n # block by falling through below -- which frees that marker too, so a later\n # `--blocked` in the same session is not falsely locked as already-answered\n # (recordAnswer creates the marker with O_EXCL).\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n answered = os.path.exists(os.path.join(answered_dir, f\"{block_id}.json\"))\n if existing.get(\"kind\") == \"declared\" and not answered:\n return\n except Exception:\n pass\n # A matcher-less PostToolUse clear (registered for Codex so an approved\n # tool clears its approval card) must NOT wipe an open AskUserQuestion\n # while an unrelated tool runs mid-question -- those are cleared only by\n # the AskUserQuestion-matched PostToolUse. So on PostToolUse, keep a\n # 'question' block; approval/notification blocks clear once the tool runs.\n if hook_event == \"PostToolUse\":\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n if existing.get(\"kind\") == \"question\" and payload.get(\"tool_name\") != \"AskUserQuestion\":\n return\n except Exception:\n pass\n try:\n os.unlink(target)\n except FileNotFoundError:\n pass\n except Exception:\n pass\n # Also clear the answered marker so a future question for this session\n # is not permanently locked.\n try:\n os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n except FileNotFoundError:\n pass\n except Exception:\n pass\n return\n\n # Terminal answers (human typed in the TUI) record an answered marker and\n # remove the block file so the feed stops showing it within one poll cycle.\n # The marker stays behind so a concurrent surface cannot double-answer.\n if hook_event == \"UserPromptSubmit\":\n os.makedirs(answered_dir, exist_ok=True)\n marker = os.path.join(answered_dir, f\"{block_id}.json\")\n try:\n fd = os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)\n record = {\n \"answeredAt\": datetime.now(timezone.utc).isoformat(),\n \"answeredFrom\": \"terminal\",\n }\n with os.fdopen(fd, \"w\") as f:\n json.dump(record, f, indent=2)\n except FileExistsError:\n pass\n except Exception:\n pass\n # Remove the visible block so the feed drops the answered question.\n try:\n os.unlink(target)\n except FileNotFoundError:\n pass\n except Exception:\n pass\n return\n\n notification_type = None\n codex_approval = False\n if hook_event == \"Notification\":\n notification_type = payload.get(\"notification_type\", \"\")\n if notification_type not in WAITING_NOTIFICATION_TYPES:\n return\n # Claude emits a generic permission notification after presenting an\n # AskUserQuestion. Keep the structured questions and options already\n # published for this session instead of replacing them with that less\n # useful notification text.\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n if existing.get(\"kind\") == \"question\":\n return\n except Exception:\n pass\n message = payload.get(\"message\", \"\")\n if not message:\n return\n normalized_questions = [{\n \"text\": message,\n \"header\": payload.get(\"title\") or notification_type.replace(\"_\", \" \").title(),\n \"multiSelect\": False,\n }]\n kind = \"notification\"\n elif hook_event == \"PermissionRequest\":\n # Codex approval prompt. The payload mirrors PreToolUse (tool_name,\n # tool_input) but carries no questions -- Codex is asking to run a tool,\n # not asking the operator a multiple-choice question. Publish it as a\n # notification-kind approval block naming the tool so the feed and the\n # phone notifier can surface it, and so the Factory extension can bridge\n # it to a VS Code notification.\n tool_name = payload.get(\"tool_name\") or \"a tool\"\n tool_input = payload.get(\"tool_input\", {})\n command = \"\"\n if isinstance(tool_input, dict):\n command = (\n tool_input.get(\"command\")\n or tool_input.get(\"cmd\")\n or tool_input.get(\"path\")\n or \"\"\n )\n if isinstance(command, list):\n command = \" \".join(str(c) for c in command)\n detail = f\": {command}\" if command else \"\"\n normalized_questions = [{\n \"text\": f\"Codex needs approval to run {tool_name}{detail}\",\n \"header\": \"Approval needed\",\n \"multiSelect\": False,\n }]\n kind = \"notification\"\n notification_type = \"permission_prompt\"\n codex_approval = True\n else:\n tool_input = payload.get(\"tool_input\", {})\n questions = tool_input.get(\"questions\", [])\n if not questions:\n return\n normalized_questions = []\n for q in questions:\n if not isinstance(q, dict):\n continue\n question = {\n \"text\": q.get(\"question\", q.get(\"header\", \"\")),\n \"header\": q.get(\"header\"),\n \"multiSelect\": q.get(\"multiSelect\", False),\n }\n raw_opts = q.get(\"options\", [])\n if raw_opts:\n question[\"options\"] = [\n {\"label\": o.get(\"label\", \"\"), \"description\": o.get(\"description\")}\n for o in raw_opts\n if isinstance(o, dict)\n ]\n normalized_questions.append(question)\n if not normalized_questions:\n return\n kind = \"question\"\n\n # Identity from env (set by agents-cli at spawn).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n\n now_iso = datetime.now(timezone.utc).isoformat()\n stats_path = os.path.join(asks_dir, f\"{safe_session_id}.json\")\n stats = read_json(stats_path) or {}\n recent = stats.get(\"recentAskTimestamps\") if isinstance(stats, dict) else []\n if not isinstance(recent, list):\n recent = []\n recent.append(now_iso)\n # Keep enough history for rolling one-hour needy detection without unbounded\n # per-session files. The TypeScript reader applies the exact time window.\n recent = recent[-200:]\n write_json(stats_path, {\n \"sessionId\": session_id,\n \"mailboxId\": mailbox_id,\n \"firstAskAt\": stats.get(\"firstAskAt\") or now_iso,\n \"lastAskAt\": now_iso,\n \"totalAskCount\": int(stats.get(\"totalAskCount\") or 0) + 1,\n \"recentAskTimestamps\": recent,\n })\n\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = hostname.split(\".\")[0].strip().lower()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", host) or \"unknown\"\n\n runtime = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n project = project_from_cwd(cwd)\n\n block = {\n \"blockId\": block_id,\n \"sessionId\": session_id,\n \"mailboxId\": mailbox_id,\n \"host\": host,\n \"runtime\": runtime,\n \"ts\": now_iso,\n \"questions\": normalized_questions,\n \"kind\": kind,\n }\n if project:\n block[\"project\"] = project\n if notification_type:\n block[\"notificationType\"] = notification_type\n\n # A Codex PermissionRequest is a real approval gate: mark it approval-class\n # with a high cost-of-delay so 'agents feed --dispatch' classifies it urgent\n # (isPhoneUrgent gates on costOfDelay >= phoneNotifyThreshold, default\n # 'medium') and pages the phone. A plain 'deny' is the safe default.\n if codex_approval:\n block[\"blockClass\"] = \"approval\"\n block[\"costOfDelay\"] = \"high\"\n block[\"safeDefault\"] = \"deny\"\n\n # Optional multi-operator control metadata passed by the agent in the\n # AskUserQuestion tool_input. Defaults keep the existing behavior. A Codex\n # PermissionRequest carries tool ARGS in tool_input (command/path), not\n # operator controls, so it is excluded here -- its class/cost is stamped\n # above from codex_approval.\n controls = payload.get(\"tool_input\", {}) if hook_event not in (\"Notification\", \"PermissionRequest\") else {}\n block_class = controls.get(\"blockClass\") if isinstance(controls, dict) else None\n if block_class in (\"approval\", \"decision\"):\n block[\"blockClass\"] = block_class\n consequence = controls.get(\"consequence\") if isinstance(controls, dict) else None\n if consequence:\n block[\"consequence\"] = consequence\n allowed = controls.get(\"allowedOperators\") if isinstance(controls, dict) else None\n if isinstance(allowed, list):\n block[\"allowedOperators\"] = [str(a) for a in allowed]\n timeout = controls.get(\"timeoutMinutes\") if isinstance(controls, dict) else None\n if isinstance(timeout, (int, float)) and timeout > 0:\n block[\"timeoutMinutes\"] = int(timeout)\n safe_default = controls.get(\"safeDefault\") if isinstance(controls, dict) else None\n if isinstance(safe_default, str):\n block[\"safeDefault\"] = safe_default\n cost = controls.get(\"costOfDelay\") if isinstance(controls, dict) else None\n if cost in (\"low\", \"medium\", \"high\"):\n block[\"costOfDelay\"] = cost\n\n # Publishing a new question clears any stale answered marker from the\n # previous question in this session.\n try:\n os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n except FileNotFoundError:\n pass\n except Exception:\n pass\n\n # Python's expanduser() ignores HOME on Windows, while agents-cli honors a\n # HOME override on every platform. Use the same anchor so hooks and the CLI\n # always read/write one feed store (including temp-home and sandbox runs).\n os.makedirs(feed_dir, exist_ok=True)\n\n fd, tmp = tempfile.mkstemp(dir=feed_dir, suffix=\".tmp\")\n try:\n with os.fdopen(fd, \"w\") as f:\n json.dump(block, f, indent=2)\n os.replace(tmp, target)\n except Exception:\n try:\n os.unlink(tmp)\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
|
|
228
|
+
export declare const FEED_PUBLISH_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Publish and clear open-block records for `agents feed`.\n\nThe manifest invokes this script for top-level AskUserQuestion calls, waiting\nnotifications, question answers, and session lifecycle events. One atomic file\nper session means a new block replaces the previous block. Answer/resume/stop\nevents remove it so `agents feed` only lists decisions that are still open.\n\nSub-agent gate: when the PreToolUse payload carries `agent_type`, this is a\nTask/Agent subagent -- skip. Only the top-level agent publishes. Verified on\nClaude Code 2.1.170 (2026-07).\n\nFail-open: ANY error is swallowed so a feed hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport sys\nimport json\nimport re\nimport socket\nimport tempfile\nfrom datetime import datetime, timezone\n\nWAITING_NOTIFICATION_TYPES = {\n \"permission_prompt\",\n \"idle_prompt\",\n \"elicitation_dialog\",\n}\nCLEAR_EVENTS = {\n \"PostToolUse\",\n \"Stop\",\n \"SessionEnd\",\n}\n# Codex emits a PermissionRequest event (not Claude's Notification) when it\n# blocks on an approval prompt. Claude never fires PermissionRequest, so the\n# same script handles both: PermissionRequest maps to an approval-class block\n# with a high cost-of-delay so 'agents feed --dispatch' pages it as urgent.\n\n\ndef read_json(path):\n try:\n with open(path) as f:\n return json.load(f)\n except Exception:\n return None\n\n\ndef write_json(path, value):\n dir_name = os.path.dirname(path)\n os.makedirs(dir_name, exist_ok=True)\n fd, tmp = tempfile.mkstemp(dir=dir_name, suffix=\".tmp\")\n try:\n with os.fdopen(fd, \"w\") as f:\n json.dump(value, f, indent=2)\n os.replace(tmp, path)\n except Exception:\n try:\n os.unlink(tmp)\n except Exception:\n pass\n\n\ndef project_from_cwd(cwd):\n \"\"\"Basename of cwd, with worktree paths resolved to their repo name.\"\"\"\n if not cwd:\n return None\n norm = cwd.replace(\"\\\\\", \"/\").rstrip(\"/\")\n if not norm:\n return None\n marker = \"/.agents/worktrees/\"\n idx = norm.find(marker)\n if idx > 0:\n repo_path = norm[:idx]\n base = repo_path[repo_path.rfind(\"/\") + 1:]\n if base:\n return base\n base = norm[norm.rfind(\"/\") + 1:]\n return base or None\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n safe_session_id = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id)\n block_id = f\"block-{safe_session_id}\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n feed_dir = os.path.join(home, \".agents\", \".history\", \"feed\")\n answered_dir = os.path.join(feed_dir, \"answered\")\n asks_dir = os.path.join(feed_dir, \"asks\")\n target = os.path.join(feed_dir, f\"{block_id}.json\")\n hook_event = payload.get(\"hook_event_name\", \"PreToolUse\")\n\n if hook_event in CLEAR_EVENTS:\n # A declared block (`agents feed post --blocked`) is the agent explicitly\n # saying it is stuck. Unlike a question/notification/approval block -- which\n # tracks an in-flight harness prompt that a lifecycle event resolves -- a\n # declared block stays open until it is actually ANSWERED. So while it is\n # still UNANSWERED, Stop/SessionEnd/PostToolUse must never silently drop it:\n # otherwise the needs-you record vanishes the moment the agent parks the block\n # and its turn ends -- exactly when the owner still needs to see and answer it.\n # Once it IS answered (an answered marker exists), it clears like any other\n # block by falling through below -- which frees that marker too, so a later\n # `--blocked` in the same session is not falsely locked as already-answered\n # (recordAnswer creates the marker with O_EXCL).\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n answered = os.path.exists(os.path.join(answered_dir, f\"{block_id}.json\"))\n if existing.get(\"kind\") == \"declared\" and not answered:\n return\n except Exception:\n pass\n # A matcher-less PostToolUse clear (registered for Codex so an approved\n # tool clears its approval card) must NOT wipe an open AskUserQuestion\n # while an unrelated tool runs mid-question -- those are cleared only by\n # the AskUserQuestion-matched PostToolUse. So on PostToolUse, keep a\n # 'question' block; approval/notification blocks clear once the tool runs.\n if hook_event == \"PostToolUse\":\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n if existing.get(\"kind\") == \"question\" and payload.get(\"tool_name\") != \"AskUserQuestion\":\n return\n except Exception:\n pass\n try:\n os.unlink(target)\n except FileNotFoundError:\n pass\n except Exception:\n pass\n # Also clear the answered marker so a future question for this session\n # is not permanently locked.\n try:\n os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n except FileNotFoundError:\n pass\n except Exception:\n pass\n return\n\n # Terminal answers (human typed in the TUI) record an answered marker and\n # remove the block file so the feed stops showing it within one poll cycle.\n # The marker stays behind so a concurrent surface cannot double-answer.\n if hook_event == \"UserPromptSubmit\":\n os.makedirs(answered_dir, exist_ok=True)\n marker = os.path.join(answered_dir, f\"{block_id}.json\")\n try:\n fd = os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)\n record = {\n \"answeredAt\": datetime.now(timezone.utc).isoformat(),\n \"answeredFrom\": \"terminal\",\n }\n with os.fdopen(fd, \"w\") as f:\n json.dump(record, f, indent=2)\n except FileExistsError:\n pass\n except Exception:\n pass\n # Remove the visible block so the feed drops the answered question.\n try:\n os.unlink(target)\n except FileNotFoundError:\n pass\n except Exception:\n pass\n return\n\n notification_type = None\n codex_approval = False\n if hook_event == \"Notification\":\n notification_type = payload.get(\"notification_type\", \"\")\n if notification_type not in WAITING_NOTIFICATION_TYPES:\n return\n # Claude emits a generic permission notification after presenting an\n # AskUserQuestion. Keep the structured questions and options already\n # published for this session instead of replacing them with that less\n # useful notification text.\n try:\n with open(target) as existing_file:\n existing = json.load(existing_file)\n if existing.get(\"kind\") == \"question\":\n return\n except Exception:\n pass\n message = payload.get(\"message\", \"\")\n if not message:\n return\n normalized_questions = [{\n \"text\": message,\n \"header\": payload.get(\"title\") or notification_type.replace(\"_\", \" \").title(),\n \"multiSelect\": False,\n }]\n kind = \"notification\"\n elif hook_event == \"PermissionRequest\":\n # Codex approval prompt. The payload mirrors PreToolUse (tool_name,\n # tool_input) but carries no questions -- Codex is asking to run a tool,\n # not asking the operator a multiple-choice question. Publish it as a\n # notification-kind approval block naming the tool so the feed and the\n # phone notifier can surface it, and so AGI EXT can bridge\n # it to a VS Code notification.\n tool_name = payload.get(\"tool_name\") or \"a tool\"\n tool_input = payload.get(\"tool_input\", {})\n command = \"\"\n if isinstance(tool_input, dict):\n command = (\n tool_input.get(\"command\")\n or tool_input.get(\"cmd\")\n or tool_input.get(\"path\")\n or \"\"\n )\n if isinstance(command, list):\n command = \" \".join(str(c) for c in command)\n detail = f\": {command}\" if command else \"\"\n normalized_questions = [{\n \"text\": f\"Codex needs approval to run {tool_name}{detail}\",\n \"header\": \"Approval needed\",\n \"multiSelect\": False,\n }]\n kind = \"notification\"\n notification_type = \"permission_prompt\"\n codex_approval = True\n else:\n tool_input = payload.get(\"tool_input\", {})\n questions = tool_input.get(\"questions\", [])\n if not questions:\n return\n normalized_questions = []\n for q in questions:\n if not isinstance(q, dict):\n continue\n question = {\n \"text\": q.get(\"question\", q.get(\"header\", \"\")),\n \"header\": q.get(\"header\"),\n \"multiSelect\": q.get(\"multiSelect\", False),\n }\n raw_opts = q.get(\"options\", [])\n if raw_opts:\n question[\"options\"] = [\n {\"label\": o.get(\"label\", \"\"), \"description\": o.get(\"description\")}\n for o in raw_opts\n if isinstance(o, dict)\n ]\n normalized_questions.append(question)\n if not normalized_questions:\n return\n kind = \"question\"\n\n # Identity from env (set by agents-cli at spawn).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n\n now_iso = datetime.now(timezone.utc).isoformat()\n stats_path = os.path.join(asks_dir, f\"{safe_session_id}.json\")\n stats = read_json(stats_path) or {}\n recent = stats.get(\"recentAskTimestamps\") if isinstance(stats, dict) else []\n if not isinstance(recent, list):\n recent = []\n recent.append(now_iso)\n # Keep enough history for rolling one-hour needy detection without unbounded\n # per-session files. The TypeScript reader applies the exact time window.\n recent = recent[-200:]\n write_json(stats_path, {\n \"sessionId\": session_id,\n \"mailboxId\": mailbox_id,\n \"firstAskAt\": stats.get(\"firstAskAt\") or now_iso,\n \"lastAskAt\": now_iso,\n \"totalAskCount\": int(stats.get(\"totalAskCount\") or 0) + 1,\n \"recentAskTimestamps\": recent,\n })\n\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = hostname.split(\".\")[0].strip().lower()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", host) or \"unknown\"\n\n runtime = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n project = project_from_cwd(cwd)\n\n block = {\n \"blockId\": block_id,\n \"sessionId\": session_id,\n \"mailboxId\": mailbox_id,\n \"host\": host,\n \"runtime\": runtime,\n \"ts\": now_iso,\n \"questions\": normalized_questions,\n \"kind\": kind,\n }\n if project:\n block[\"project\"] = project\n if notification_type:\n block[\"notificationType\"] = notification_type\n\n # A Codex PermissionRequest is a real approval gate: mark it approval-class\n # with a high cost-of-delay so 'agents feed --dispatch' classifies it urgent\n # (isPhoneUrgent gates on costOfDelay >= phoneNotifyThreshold, default\n # 'medium') and pages the phone. A plain 'deny' is the safe default.\n if codex_approval:\n block[\"blockClass\"] = \"approval\"\n block[\"costOfDelay\"] = \"high\"\n block[\"safeDefault\"] = \"deny\"\n\n # Optional multi-operator control metadata passed by the agent in the\n # AskUserQuestion tool_input. Defaults keep the existing behavior. A Codex\n # PermissionRequest carries tool ARGS in tool_input (command/path), not\n # operator controls, so it is excluded here -- its class/cost is stamped\n # above from codex_approval.\n controls = payload.get(\"tool_input\", {}) if hook_event not in (\"Notification\", \"PermissionRequest\") else {}\n block_class = controls.get(\"blockClass\") if isinstance(controls, dict) else None\n if block_class in (\"approval\", \"decision\"):\n block[\"blockClass\"] = block_class\n consequence = controls.get(\"consequence\") if isinstance(controls, dict) else None\n if consequence:\n block[\"consequence\"] = consequence\n allowed = controls.get(\"allowedOperators\") if isinstance(controls, dict) else None\n if isinstance(allowed, list):\n block[\"allowedOperators\"] = [str(a) for a in allowed]\n timeout = controls.get(\"timeoutMinutes\") if isinstance(controls, dict) else None\n if isinstance(timeout, (int, float)) and timeout > 0:\n block[\"timeoutMinutes\"] = int(timeout)\n safe_default = controls.get(\"safeDefault\") if isinstance(controls, dict) else None\n if isinstance(safe_default, str):\n block[\"safeDefault\"] = safe_default\n cost = controls.get(\"costOfDelay\") if isinstance(controls, dict) else None\n if cost in (\"low\", \"medium\", \"high\"):\n block[\"costOfDelay\"] = cost\n\n # Publishing a new question clears any stale answered marker from the\n # previous question in this session.\n try:\n os.unlink(os.path.join(answered_dir, f\"{block_id}.json\"))\n except FileNotFoundError:\n pass\n except Exception:\n pass\n\n # Python's expanduser() ignores HOME on Windows, while agents-cli honors a\n # HOME override on every platform. Use the same anchor so hooks and the CLI\n # always read/write one feed store (including temp-home and sandbox runs).\n os.makedirs(feed_dir, exist_ok=True)\n\n fd, tmp = tempfile.mkstemp(dir=feed_dir, suffix=\".tmp\")\n try:\n with os.fdopen(fd, \"w\") as f:\n json.dump(block, f, indent=2)\n os.replace(tmp, target)\n except Exception:\n try:\n os.unlink(tmp)\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
|
|
229
229
|
/** Manifest entry for the feed-publish hook, matching the ManifestHook shape. */
|
|
230
230
|
export declare const FEED_PUBLISH_HOOK_MANIFEST: {
|
|
231
231
|
name: string;
|