switchroom 0.7.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse hook — detect a wedged persistent-bash session.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code's Bash tool uses a persistent `bash` subprocess for state
|
|
6
|
+
* continuity (so `cd /foo` in one call survives to the next). When that
|
|
7
|
+
* subprocess's IO state desyncs — typically after a long-running or
|
|
8
|
+
* interrupted command leaves stdin in mid-heredoc, or after sentinel
|
|
9
|
+
* parsing breaks — every subsequent Bash call returns exit-1 with empty
|
|
10
|
+
* stdout and empty stderr. Even `true` returns exit 1. The wedge is
|
|
11
|
+
* sticky for the session; `switchroom agent restart <self>` is the only
|
|
12
|
+
* reliable recovery (it spawns a fresh `claude` → fresh persistent bash).
|
|
13
|
+
*
|
|
14
|
+
* This hook watches PostToolUse events for the wedge signature and,
|
|
15
|
+
* after N consecutive matches, writes a sentinel + logs to stderr so
|
|
16
|
+
* the operator (via `docker logs`) or the gateway (via a future card)
|
|
17
|
+
* can prompt for restart. The hook itself can NEVER fix the wedge —
|
|
18
|
+
* PostToolUse fires after the tool already ran. It's a detection +
|
|
19
|
+
* surfacing surface, not a recovery surface.
|
|
20
|
+
*
|
|
21
|
+
* Claude Code PostToolUse protocol:
|
|
22
|
+
* stdin: JSON { tool_name, tool_use_id, tool_input, tool_response, ... }
|
|
23
|
+
* stdout: optional JSON (hookSpecificOutput.additionalContext for next
|
|
24
|
+
* turn). We use this to nudge the model toward KillBash +
|
|
25
|
+
* self-restart guidance once the wedge is detected.
|
|
26
|
+
* exit: 0 always. Hook failures must never block the tool flow.
|
|
27
|
+
*
|
|
28
|
+
* State:
|
|
29
|
+
* $TELEGRAM_STATE_DIR/wedge-counter.txt — integer, consecutive empty Bash
|
|
30
|
+
* results. Reset to 0 on any non-Bash event or any non-empty Bash
|
|
31
|
+
* result. Incremented on each empty Bash result.
|
|
32
|
+
* $TELEGRAM_STATE_DIR/wedge-detected.json — JSON sentinel written when
|
|
33
|
+
* counter reaches THRESHOLD. Contains { ts, session_id, agent,
|
|
34
|
+
* consecutive }. Gateway can poll for this and surface a card; for
|
|
35
|
+
* now its presence is informational only.
|
|
36
|
+
*
|
|
37
|
+
* Threshold: 3. Picked to balance false positives (some real commands
|
|
38
|
+
* legitimately produce no output and exit non-zero, e.g. `test -f
|
|
39
|
+
* /nonexistent`) against latency-to-detect. Three in a row is rare
|
|
40
|
+
* outside genuine wedge.
|
|
41
|
+
*
|
|
42
|
+
* Detection is shape-based not exit-code-based because the tool_response
|
|
43
|
+
* shape varies by Claude Code version. We match on:
|
|
44
|
+
* - tool_name === "Bash"
|
|
45
|
+
* - stringified response contains BOTH empty stdout marker AND empty
|
|
46
|
+
* stderr marker. Marker patterns covered: <bash-stdout></bash-stdout>,
|
|
47
|
+
* "stdout":"" + "stderr":"", and the bare "(no output)" string some
|
|
48
|
+
* versions emit.
|
|
49
|
+
*
|
|
50
|
+
* If detection markers change in a future Claude Code release, this hook
|
|
51
|
+
* silently misses the wedge — that's the right failure mode (better than
|
|
52
|
+
* false-firing).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
56
|
+
import { join, dirname } from 'node:path'
|
|
57
|
+
|
|
58
|
+
// Higher than the original 3 to avoid false-firing on legitimate
|
|
59
|
+
// empty-output command sequences (a sed, then two greps with no matches,
|
|
60
|
+
// is a normal refactor pattern and shouldn't trigger). PR #1188 review
|
|
61
|
+
// found 3 was guaranteed-FP. 5 + the noOutputExpected /
|
|
62
|
+
// returnCodeInterpretation skip below should keep real wedges detectable
|
|
63
|
+
// while staying quiet during normal grep/find/sed chains.
|
|
64
|
+
const THRESHOLD = 5
|
|
65
|
+
|
|
66
|
+
// node:fs operations on the counter / sentinel files are read-modify-write
|
|
67
|
+
// without explicit locking. Safe because Claude Code serializes tool calls
|
|
68
|
+
// per session — there is at most one PostToolUse fire in flight per agent
|
|
69
|
+
// at any time. Documented so a future caller doesn't introduce parallelism
|
|
70
|
+
// and silently lose counts.
|
|
71
|
+
|
|
72
|
+
function readStdin() {
|
|
73
|
+
try {
|
|
74
|
+
return readFileSync(0, 'utf8')
|
|
75
|
+
} catch {
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stateDir() {
|
|
81
|
+
return process.env.TELEGRAM_STATE_DIR || null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function counterPath() {
|
|
85
|
+
const dir = stateDir()
|
|
86
|
+
return dir ? join(dir, 'wedge-counter.txt') : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sentinelPath() {
|
|
90
|
+
const dir = stateDir()
|
|
91
|
+
return dir ? join(dir, 'wedge-detected.json') : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readCounter() {
|
|
95
|
+
const p = counterPath()
|
|
96
|
+
if (!p || !existsSync(p)) return 0
|
|
97
|
+
try {
|
|
98
|
+
const raw = readFileSync(p, 'utf8').trim()
|
|
99
|
+
const n = Number.parseInt(raw, 10)
|
|
100
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
101
|
+
} catch {
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeCounter(n) {
|
|
107
|
+
const p = counterPath()
|
|
108
|
+
if (!p) return
|
|
109
|
+
try {
|
|
110
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
111
|
+
writeFileSync(p, String(n), 'utf8')
|
|
112
|
+
} catch {
|
|
113
|
+
// fail-silent; counter loss just delays detection by a couple of cycles
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeSentinel(payload) {
|
|
118
|
+
const p = sentinelPath()
|
|
119
|
+
if (!p) return
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
122
|
+
writeFileSync(p, JSON.stringify(payload, null, 2), 'utf8')
|
|
123
|
+
} catch {
|
|
124
|
+
// fail-silent
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function clearSentinel() {
|
|
129
|
+
const p = sentinelPath()
|
|
130
|
+
if (!p) return
|
|
131
|
+
try {
|
|
132
|
+
rmSync(p, { force: true })
|
|
133
|
+
} catch {
|
|
134
|
+
// fail-silent
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resetCounter() {
|
|
139
|
+
// Counter reset means we're back in healthy territory — clear the
|
|
140
|
+
// sentinel too so a future operator-side surface that polls for
|
|
141
|
+
// `wedge-detected.json` doesn't see stale state from a long-cleared
|
|
142
|
+
// wedge. Per PR #1188 review B2.
|
|
143
|
+
writeCounter(0)
|
|
144
|
+
clearSentinel()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Test whether a Bash tool_response matches the wedge signature.
|
|
149
|
+
*
|
|
150
|
+
* The wedge produces: empty stdout AND empty stderr AND no
|
|
151
|
+
* Claude-Code-supplied "no output is expected here" annotation AND not
|
|
152
|
+
* interrupted by the user.
|
|
153
|
+
*
|
|
154
|
+
* The benign empty-output cases that PR #1188 review B1 called out
|
|
155
|
+
* (grep/find/sed/test with no matches or in-place mutation) are
|
|
156
|
+
* disambiguated by:
|
|
157
|
+
* - `noOutputExpected: true` — Claude Code annotates Bash calls whose
|
|
158
|
+
* command pattern legitimately produces no output.
|
|
159
|
+
* - `returnCodeInterpretation: "..."` — present when Claude Code has
|
|
160
|
+
* a human-readable explanation for the exit code (e.g. "No matches
|
|
161
|
+
* found" for grep). Its presence means "this empty result is
|
|
162
|
+
* understood, not a desync."
|
|
163
|
+
* - `interrupted: true` — user pressed `!` mid-command. Not a wedge.
|
|
164
|
+
*
|
|
165
|
+
* Defensive: response shape varies across Claude Code versions and
|
|
166
|
+
* across plain-string vs structured-object representations. We check
|
|
167
|
+
* each known marker and fail-no-match on anything else.
|
|
168
|
+
*/
|
|
169
|
+
function isEmptyBashResponse(toolResponse) {
|
|
170
|
+
if (toolResponse == null) return false
|
|
171
|
+
|
|
172
|
+
// Structured-object path. Most reliable — read the fields directly
|
|
173
|
+
// and consult the annotations.
|
|
174
|
+
if (typeof toolResponse === 'object') {
|
|
175
|
+
const r = toolResponse
|
|
176
|
+
// Interruption is user-initiated, not a desync. Don't count.
|
|
177
|
+
if (r.interrupted === true) return false
|
|
178
|
+
// Claude Code already knows this command's empty output is expected.
|
|
179
|
+
if (r.noOutputExpected === true) return false
|
|
180
|
+
// Claude Code has a human-readable explanation — the empty result is
|
|
181
|
+
// accounted for, not a parse failure.
|
|
182
|
+
if (typeof r.returnCodeInterpretation === 'string' && r.returnCodeInterpretation.length > 0) {
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
// Real empty-result check. Both streams empty (or missing).
|
|
186
|
+
const stdout = typeof r.stdout === 'string' ? r.stdout : ''
|
|
187
|
+
const stderr = typeof r.stderr === 'string' ? r.stderr : ''
|
|
188
|
+
if (stdout === '' && stderr === '') return true
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// String path — older Claude Code versions, or when the response was
|
|
193
|
+
// wrapped before reaching the hook. We can't read structured fields,
|
|
194
|
+
// so we rely on substring shape and accept slightly higher FP risk on
|
|
195
|
+
// this path (covered by THRESHOLD raise + skill-side recovery being
|
|
196
|
+
// cheap).
|
|
197
|
+
let body
|
|
198
|
+
try {
|
|
199
|
+
body = String(toolResponse)
|
|
200
|
+
} catch {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
if (body.length > 4096) return false
|
|
204
|
+
|
|
205
|
+
// If the string form contains noOutputExpected:true or a
|
|
206
|
+
// returnCodeInterpretation, treat as accounted-for.
|
|
207
|
+
if (/"noOutputExpected"\s*:\s*true/.test(body)) return false
|
|
208
|
+
if (/"interrupted"\s*:\s*true/.test(body)) return false
|
|
209
|
+
if (/"returnCodeInterpretation"\s*:\s*"[^"]+"/.test(body)) return false
|
|
210
|
+
|
|
211
|
+
// XML-style tags: <bash-stdout></bash-stdout><bash-stderr></bash-stderr>
|
|
212
|
+
const hasEmptyStdoutTag = /<bash-stdout>\s*<\/bash-stdout>/i.test(body)
|
|
213
|
+
const hasEmptyStderrTag = /<bash-stderr>\s*<\/bash-stderr>/i.test(body)
|
|
214
|
+
if (hasEmptyStdoutTag && hasEmptyStderrTag) return true
|
|
215
|
+
|
|
216
|
+
// JSON-stringified shape from older serializers.
|
|
217
|
+
const hasEmptyStdoutJson = /"stdout"\s*:\s*""/.test(body)
|
|
218
|
+
const hasEmptyStderrJson = /"stderr"\s*:\s*""/.test(body)
|
|
219
|
+
if (hasEmptyStdoutJson && hasEmptyStderrJson) return true
|
|
220
|
+
|
|
221
|
+
// Literal zero-info bodies.
|
|
222
|
+
if (body === '{}' || body === '""' || body === '') return true
|
|
223
|
+
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function emitWedgeContext(consecutive) {
|
|
228
|
+
// PostToolUse can prepend additionalContext to the model's next turn.
|
|
229
|
+
// Use it to surface a single-line nudge once the wedge is suspected
|
|
230
|
+
// so the agent knows to try recovery rather than retrying the same
|
|
231
|
+
// command in a loop.
|
|
232
|
+
const text =
|
|
233
|
+
`[wedge-detect] ${consecutive} consecutive empty-result Bash calls — ` +
|
|
234
|
+
`your persistent shell is likely wedged. Try \`KillBash\` to drop ` +
|
|
235
|
+
`the wedged session, OR ask the user for \`switchroom agent restart ${process.env.SWITCHROOM_AGENT_NAME || '<self>'}\` ` +
|
|
236
|
+
`if KillBash doesn't recover. Don't retry the same command.`
|
|
237
|
+
const payload = {
|
|
238
|
+
hookSpecificOutput: {
|
|
239
|
+
hookEventName: 'PostToolUse',
|
|
240
|
+
additionalContext: text,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
process.stdout.write(JSON.stringify(payload) + '\n')
|
|
245
|
+
} catch {
|
|
246
|
+
// fail-silent
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function main() {
|
|
251
|
+
const raw = readStdin()
|
|
252
|
+
if (!raw) return
|
|
253
|
+
let evt
|
|
254
|
+
try {
|
|
255
|
+
evt = JSON.parse(raw)
|
|
256
|
+
} catch {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Non-Bash events reset the counter (the wedge is specific to the
|
|
261
|
+
// persistent shell; other tools succeeding doesn't tell us anything
|
|
262
|
+
// about Bash, but a different tool firing means we're at least not in
|
|
263
|
+
// a tight loop of Bash retries — safe to reset).
|
|
264
|
+
if (evt.tool_name !== 'Bash') {
|
|
265
|
+
resetCounter()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!isEmptyBashResponse(evt.tool_response)) {
|
|
270
|
+
// Bash call returned real output → not wedged → reset.
|
|
271
|
+
resetCounter()
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Empty Bash result. Increment.
|
|
276
|
+
const next = readCounter() + 1
|
|
277
|
+
writeCounter(next)
|
|
278
|
+
|
|
279
|
+
if (next >= THRESHOLD) {
|
|
280
|
+
const sentinel = {
|
|
281
|
+
ts: new Date().toISOString(),
|
|
282
|
+
session_id: evt.session_id || null,
|
|
283
|
+
agent: process.env.SWITCHROOM_AGENT_NAME || null,
|
|
284
|
+
consecutive: next,
|
|
285
|
+
// Capture the last tool_use_id so an operator-side investigator
|
|
286
|
+
// can pin which tool calls triggered the threshold.
|
|
287
|
+
last_tool_use_id: evt.tool_use_id || null,
|
|
288
|
+
}
|
|
289
|
+
writeSentinel(sentinel)
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
`wedge-detect: ${next} consecutive empty-result Bash calls; ` +
|
|
292
|
+
`sentinel at ${sentinelPath()}; recommend KillBash or ` +
|
|
293
|
+
`switchroom agent restart\n`,
|
|
294
|
+
)
|
|
295
|
+
emitWedgeContext(next)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
main()
|
|
301
|
+
} catch {
|
|
302
|
+
// PostToolUse must never block the tool flow.
|
|
303
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inbound-classifier.ts — cheap regex classifier for inbound user text.
|
|
3
|
+
*
|
|
4
|
+
* Today the only signal we emit is `status_query` — short user messages
|
|
5
|
+
* asking "are you still working / are you there / status?" — because that's
|
|
6
|
+
* the primary KPI for the conversational turn UX redesign (see issue #1122):
|
|
7
|
+
* if the user has to ask, the design has failed.
|
|
8
|
+
*
|
|
9
|
+
* Strictly read-only: the classifier never alters routing. Inbound text
|
|
10
|
+
* still reaches the agent unchanged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Patterns that mean "are you still working / do you remember me." Kept
|
|
15
|
+
* conservative on purpose — false positives are worse than misses, because
|
|
16
|
+
* a wrong-positive would noise up the very KPI we're trying to measure.
|
|
17
|
+
*
|
|
18
|
+
* All patterns match the entire trimmed message body (anchored), so longer
|
|
19
|
+
* messages that happen to contain "status" don't trip them.
|
|
20
|
+
*/
|
|
21
|
+
const STATUS_QUERY_PATTERNS: readonly RegExp[] = [
|
|
22
|
+
/^\?+$/,
|
|
23
|
+
/^status\s*\??$/i,
|
|
24
|
+
/^update\s*\??$/i,
|
|
25
|
+
/^any\s+update\s*\??$/i,
|
|
26
|
+
/^still\s+there\s*\??$/i,
|
|
27
|
+
/^still\s+working\s*\??$/i,
|
|
28
|
+
/^are\s+you\s+there\s*\??$/i,
|
|
29
|
+
/^you\s+there\s*\??$/i,
|
|
30
|
+
/^hello\s*\?+$/i,
|
|
31
|
+
/^hey\s*\?+$/i,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export interface InboundClassification {
|
|
35
|
+
isStatusQuery: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function classifyInbound(text: string | null | undefined): InboundClassification {
|
|
39
|
+
if (text == null) return { isStatusQuery: false }
|
|
40
|
+
const trimmed = text.trim()
|
|
41
|
+
if (trimmed === '') return { isStatusQuery: false }
|
|
42
|
+
// Cap length — a long message that starts with "status?" isn't a status
|
|
43
|
+
// query; the user's appending real content. Keep the classifier focused
|
|
44
|
+
// on standalone "ping" messages.
|
|
45
|
+
if (trimmed.length > 40) return { isStatusQuery: false }
|
|
46
|
+
for (const pat of STATUS_QUERY_PATTERNS) {
|
|
47
|
+
if (pat.test(trimmed)) return { isStatusQuery: true }
|
|
48
|
+
}
|
|
49
|
+
return { isStatusQuery: false }
|
|
50
|
+
}
|
|
@@ -164,3 +164,139 @@ export function validateAndWrapAgentKeyboard(
|
|
|
164
164
|
const wrapped = wrapAgentCallbacks(keyboard)
|
|
165
165
|
return { ok: true, wrapped }
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
// ─── finalizeCallback (#1150 + audit follow-up) ──────────────────────────
|
|
169
|
+
//
|
|
170
|
+
// Centralized "the user tapped a terminal button" helper. Every callback
|
|
171
|
+
// handler that resolves a decision (Approve / Deny / Pick option / Confirm
|
|
172
|
+
// revoke / Always-allow / Dismiss / etc.) MUST route through this helper
|
|
173
|
+
// so the three button-UX invariants are uniformly enforced:
|
|
174
|
+
//
|
|
175
|
+
// 1. Visible press feedback — `answerCallbackQuery` with `text:` so
|
|
176
|
+
// Telegram shows a toast. Operators who tap and see nothing within
|
|
177
|
+
// ~200ms double-tap; the toast IS the press-feedback affordance.
|
|
178
|
+
// 2. Keyboard collapses with clarity — the message is edited in place
|
|
179
|
+
// to strip `reply_markup` AND append a status line that describes
|
|
180
|
+
// what the operator selected. One atomic edit, not two: the user
|
|
181
|
+
// must be able to scroll back later and see the resolved decision
|
|
182
|
+
// next to the original prompt.
|
|
183
|
+
// 3. Side effect (typically: synthesize an inbound back to the model
|
|
184
|
+
// so the agent's turn continues) — runs AFTER the message edit
|
|
185
|
+
// lands so the model never sees "I'm being woken up" before the
|
|
186
|
+
// operator sees the visual confirmation.
|
|
187
|
+
//
|
|
188
|
+
// Multi-step wizards (vault grant wizard, etc.) should NOT use this
|
|
189
|
+
// helper for intermediate-step transitions — those swap one keyboard
|
|
190
|
+
// for the next via `editMessageText` + new `reply_markup`. Use this
|
|
191
|
+
// helper for the WIZARD-FINAL step (Generate, Cancel) so the success
|
|
192
|
+
// card collapses correctly and isn't re-tappable.
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Minimal callback-context shape the helper needs. Real grammy
|
|
196
|
+
* `Context` satisfies this; tests can implement a lightweight fake
|
|
197
|
+
* without dragging the grammy types in.
|
|
198
|
+
*/
|
|
199
|
+
export interface FinalizeCallbackContext {
|
|
200
|
+
answerCallbackQuery: (
|
|
201
|
+
opts?: { text?: string; show_alert?: boolean },
|
|
202
|
+
) => Promise<unknown>
|
|
203
|
+
editMessageText: (text: string, opts?: Record<string, unknown>) => Promise<unknown>
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface FinalizeCallbackOptions {
|
|
207
|
+
/**
|
|
208
|
+
* Toast text shown to the operator via `answerCallbackQuery`. Telegram
|
|
209
|
+
* caps this at 200 chars; pass a short verb-phrase ("Approved",
|
|
210
|
+
* "Saved", "Switching to slot 2"). Required — the toast IS invariant 1.
|
|
211
|
+
*/
|
|
212
|
+
ackText: string
|
|
213
|
+
/**
|
|
214
|
+
* When true, the toast renders as a full modal alert instead of the
|
|
215
|
+
* bottom-bar toast. Default false. Use for destructive or one-way
|
|
216
|
+
* decisions (e.g. "Vault grant revoked") where the operator needs
|
|
217
|
+
* stronger acknowledgement.
|
|
218
|
+
*/
|
|
219
|
+
alert?: boolean
|
|
220
|
+
/**
|
|
221
|
+
* The new body text for the message AFTER the keyboard is stripped.
|
|
222
|
+
* Build this yourself from `<original prompt>\n\n<status line>` so the
|
|
223
|
+
* scrollback preserves the question alongside the answer. The keyboard
|
|
224
|
+
* is stripped unconditionally regardless of `newText`.
|
|
225
|
+
*/
|
|
226
|
+
newText: string
|
|
227
|
+
/**
|
|
228
|
+
* Parse mode for `newText`. Match the original message's parse mode —
|
|
229
|
+
* mixing modes mid-edit silently breaks formatting. Optional; omitted
|
|
230
|
+
* means plain text.
|
|
231
|
+
*/
|
|
232
|
+
parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'
|
|
233
|
+
/**
|
|
234
|
+
* Side effect invoked AFTER `editMessageText` resolves. Use for
|
|
235
|
+
* synthesizing the `<channel source="...">` inbound that wakes the
|
|
236
|
+
* agent's session. Errors are caught + logged via the `log` seam;
|
|
237
|
+
* they do NOT propagate (a failed inbound synthesis must not regress
|
|
238
|
+
* to "operator's tap visually un-applied"). The model-visible flow
|
|
239
|
+
* is allowed to fail loudly via separate mechanisms (the supervisor
|
|
240
|
+
* watchdog, the silence-poke ladder); this helper's job is to keep
|
|
241
|
+
* invariants 1+2 strict and best-effort the rest.
|
|
242
|
+
*
|
|
243
|
+
* Skip for surfaces with no model in the loop (auth dashboard
|
|
244
|
+
* actions that shell to the host CLI, operator-event dismiss, etc).
|
|
245
|
+
*/
|
|
246
|
+
synthInbound?: () => void | Promise<void>
|
|
247
|
+
/** Logger seam for tests. Defaults to stderr. */
|
|
248
|
+
log?: (line: string) => void
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Apply the three-invariant finalize pattern. See module docstring
|
|
253
|
+
* above for design rationale.
|
|
254
|
+
*
|
|
255
|
+
* Order: ack → edit → synth. The ack is fired-and-forgotten (so a slow
|
|
256
|
+
* Telegram API doesn't delay the visible state change), but the edit
|
|
257
|
+
* is awaited so `synthInbound` doesn't race ahead of the operator's
|
|
258
|
+
* visual confirmation. Each step's error is logged + swallowed —
|
|
259
|
+
* partial success is preferred to "tap looked dead AND the model
|
|
260
|
+
* stayed stuck" full failure.
|
|
261
|
+
*/
|
|
262
|
+
export async function finalizeCallback(
|
|
263
|
+
ctx: FinalizeCallbackContext,
|
|
264
|
+
opts: FinalizeCallbackOptions,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const log = opts.log ?? ((line: string) => process.stderr.write(line))
|
|
267
|
+
// Invariant 1 — toast. Fire-and-forget; we don't want a slow
|
|
268
|
+
// answerCallbackQuery round-trip to delay the message edit.
|
|
269
|
+
void ctx.answerCallbackQuery({
|
|
270
|
+
text: opts.ackText,
|
|
271
|
+
...(opts.alert ? { show_alert: true } : {}),
|
|
272
|
+
}).catch((err: unknown) => {
|
|
273
|
+
log(`finalizeCallback: answerCallbackQuery failed: ${(err as Error).message}\n`)
|
|
274
|
+
})
|
|
275
|
+
// Invariant 2 — strip keyboard + append status line, atomic edit.
|
|
276
|
+
try {
|
|
277
|
+
await ctx.editMessageText(opts.newText, {
|
|
278
|
+
reply_markup: { inline_keyboard: [] },
|
|
279
|
+
...(opts.parseMode ? { parse_mode: opts.parseMode } : {}),
|
|
280
|
+
// Default link_preview_options off — most finalized cards don't
|
|
281
|
+
// benefit from preview cards, and a stale preview survives the
|
|
282
|
+
// edit otherwise.
|
|
283
|
+
link_preview_options: { is_disabled: true },
|
|
284
|
+
})
|
|
285
|
+
} catch (err) {
|
|
286
|
+
// MESSAGE_NOT_MODIFIED (text didn't change) and MESSAGE_TO_EDIT_NOT_FOUND
|
|
287
|
+
// (operator already deleted the card) are both benign. Other failures
|
|
288
|
+
// log + continue — we still want synthInbound to run.
|
|
289
|
+
log(`finalizeCallback: editMessageText failed: ${(err as Error).message}\n`)
|
|
290
|
+
}
|
|
291
|
+
// Invariant 3 — model wake-up (when applicable).
|
|
292
|
+
if (opts.synthInbound != null) {
|
|
293
|
+
try {
|
|
294
|
+
const r = opts.synthInbound()
|
|
295
|
+
if (r != null && typeof (r as Promise<unknown>).then === 'function') {
|
|
296
|
+
await (r as Promise<unknown>)
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
log(`finalizeCallback: synthInbound threw: ${(err as Error).message}\n`)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"3.2.4","results":[[":tests/progress-card-driver.test.ts",{"duration":82.55005700000004,"failed":false}],[":tests/progress-card.test.ts",{"duration":50.04072000000002,"failed":false}],[":tests/telegram-format.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-handler.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-harness.test.ts",{"duration":4955.245276000001,"failed":false}],[":tests/streaming-orchestration.test.ts",{"duration":0,"failed":false}],[":tests/races.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher.test.ts",{"duration":0,"failed":false}],[":tests/gateway-bridge.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-manager.test.ts",{"duration":25.365711999999974,"failed":false}],[":tests/pty-tail.test.ts",{"duration":0,"failed":false}],[":tests/turn-end-regressions.test.ts",{"duration":0,"failed":false}],[":tests/session-tail.test.ts",{"duration":0,"failed":false}],[":tests/gateway-clean-shutdown-marker.test.ts",{"duration":0,"failed":true}],[":tests/e2e.test.ts",{"duration":0,"failed":false}],[":tests/setup-flow.test.ts",{"duration":0,"failed":false}],[":tests/tool-labels.test.ts",{"duration":0,"failed":false}],[":tests/registry-turns.test.ts",{"duration":0,"failed":true}],[":tests/welcome-text.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-oauth-code.test.ts",{"duration":0,"failed":false}],[":tests/draft-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-stuck-warning.test.ts",{"duration":21.23773799999998,"failed":false}],[":tests/history.test.ts",{"duration":0,"failed":false}],[":tests/streaming-e2e.test.ts",{"duration":0,"failed":false}],[":tests/foreman-handlers.test.ts",{"duration":0,"failed":false}],[":tests/foreman-create-flow.test.ts",{"duration":0,"failed":false}],[":tests/operator-events.test.ts",{"duration":0,"failed":false}],[":tests/vault-grants-revoke.test.ts",{"duration":0,"failed":false}],[":tests/boot-probes.test.ts",{"duration":0,"failed":true}],[":tests/pty-partial-handler.test.ts",{"duration":0,"failed":false}],[":tests/auth-slot-commands.test.ts",{"duration":0,"failed":false}],[":tests/vault-subcommands.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard.test.ts",{"duration":0,"failed":false}],[":tests/active-pins-sweep.test.ts",{"duration":0,"failed":false}],[":tests/turns-writer.test.ts",{"duration":0,"failed":true}],[":tests/retry-api-call.test.ts",{"duration":0,"failed":false}],[":tests/steering.test.ts",{"duration":0,"failed":false}],[":tests/outbound-ordering.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-client.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-mutex.test.ts",{"duration":0,"failed":true}],[":tests/auth-dashboard-edge-cases.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/foreman-write-ops.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-render.test.ts",{"duration":0,"failed":false}],[":tests/handoff-continuity.test.ts",{"duration":0,"failed":false}],[":tests/false-restart-banner.test.ts",{"duration":0,"failed":false}],[":tests/answer-stream-silent-markers.test.ts",{"duration":0,"failed":false}],[":tests/ipc-validator.test.ts",{"duration":0,"failed":false}],[":tests/stream-controller.test.ts",{"duration":0,"failed":false}],[":tests/gateway-409-retry-banner.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-error-paths.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-race.test.ts",{"duration":0,"failed":false}],[":tests/progress-update.test.ts",{"duration":0,"failed":true}],[":tests/restart-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-cross-turn.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-probe-target.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions.test.ts",{"duration":0,"failed":false}],[":tests/quota-cache.test.ts",{"duration":0,"failed":true}],[":tests/streaming-metrics.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-session-tail.test.ts",{"duration":0,"failed":false}],[":tests/foreman-state.test.ts",{"duration":0,"failed":true}],[":tests/ipc-protocol.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/telegram-button-constraints.test.ts",{"duration":0,"failed":false}],[":tests/bot-runtime.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-network-retry.test.ts",{"duration":0,"failed":false}],[":tests/attachment-path.test.ts",{"duration":0,"failed":false}],[":tests/active-pins.test.ts",{"duration":0,"failed":false}],[":tests/subagent-tracker-hooks.test.ts",{"duration":0,"failed":true}],[":tests/fake-bot-api.test.ts",{"duration":0,"failed":false}],[":tests/quota-check.test.ts",{"duration":0,"failed":false}],[":tests/parse-mode-rotation.test.ts",{"duration":0,"failed":false}],[":tests/gateway-secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-safety.test.ts",{"duration":0,"failed":false}],[":tests/multi-turn-continuity.test.ts",{"duration":0,"failed":false}],[":tests/status-accent.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-auto-capture.test.ts",{"duration":0,"failed":false}],[":tests/auth-login-url-button.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard-restart-flow.test.ts",{"duration":0,"failed":false}],[":tests/setup-state.test.ts",{"duration":0,"failed":true}],[":tests/progress-card-golden.test.ts",{"duration":6.658348999999987,"failed":false}],[":tests/unhandled-rejection-policy.test.ts",{"duration":0,"failed":true}],[":tests/typing-wrap.test.ts",{"duration":0,"failed":false}],[":tests/auth-account-identity-surface.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-secretlint.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-pipeline.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-history.test.ts",{"duration":0,"failed":false}],[":tests/gateway-message-validator.test.ts",{"duration":0,"failed":false}],[":tests/silent-reply-guard.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions-sweep.test.ts",{"duration":0,"failed":false}],[":tests/pin-event-log.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-fail-closed.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-operator.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer-wiring.test.ts",{"duration":0,"failed":true}],[":tests/boot-card-reason.test.ts",{"duration":0,"failed":true}],[":tests/plugin-logger.test.ts",{"duration":0,"failed":false}],[":tests/vault-grant-wizard.test.ts",{"duration":0,"failed":false}],[":tests/turn-signal-tracker.test.ts",{"duration":0,"failed":false}],[":tests/context-exhaustion.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-reset.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer.test.ts",{"duration":0,"failed":false}],[":tests/poll-health.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-prose-recovery.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-suppressor-no-silent-allow.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-dedupe.test.ts",{"duration":0,"failed":true}],[":tests/protocol-fixtures.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-staging.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-audit.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-gitleaks.test.ts",{"duration":0,"failed":false}],[":tests/subagent-registry-bugs.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-driver-eviction.test.ts",{"duration":22.822707999999977,"failed":false}],[":tests/progress-card-driver-fleet-shadow.test.ts",{"duration":7.463677000000018,"failed":false}],[":tests/two-zone-card-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-concurrent-turns-isolation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-survives-next-turn.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-snapshot.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-edit-throttle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-done-when-all-terminal.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-html-balance.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-detection.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-draft-flag.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-per-member.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-header-phases.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-recovery.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-header-escalation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-fleet-row.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-cap.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-sanitise.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-close-paths-converge.test.ts",{"duration":10.167681000000016,"failed":false}],[":registry/subagents.test.ts",{"duration":0,"failed":true}],[":tests/issues-card.test.ts",{"duration":0,"failed":false}],[":registry/subagents-bugs.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream-dedup.test.ts",{"duration":0,"failed":false}],[":tests/model-unavailable.test.ts",{"duration":0,"failed":false}],[":tests/preamble-suppressor.test.ts",{"duration":0,"failed":false}],[":tests/harness-ordering-invariants.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-spec.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-ipc-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-i6-turn-flush-replay-dedup.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner-driver.e2e.test.ts",{"duration":0,"failed":false}],[":tests/waiting-ux.e2e.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-parent-marker.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-redact.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-stall-notification.test.ts",{"duration":0,"failed":false}],[":tests/telegraph.test.ts",{"duration":0,"failed":true}],[":tests/recent-outbound-dedup.test.ts",{"duration":0,"failed":true}],[":tests/turn-flush-card-takeover.test.ts",{"duration":0,"failed":false}],[":tests/resolve-calling-subagent.test.ts",{"duration":0,"failed":true}],[":tests/secret-guard-pretool.test.ts",{"duration":0,"failed":true}],[":tests/first-paint.test.ts",{"duration":0,"failed":false}],[":tests/ask-user.test.ts",{"duration":0,"failed":true}],[":registry/api-registry.test.ts",{"duration":0,"failed":true}],[":tests/credits-watch.test.ts",{"duration":0,"failed":false}],[":admin-commands/dispatch.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state.test.ts",{"duration":0,"failed":false}],[":tests/voice-transcribe.test.ts",{"duration":0,"failed":true}],[":tests/turn-active-marker.test.ts",{"duration":0,"failed":false}],[":tests/inline-keyboard-callbacks.test.ts",{"duration":0,"failed":false}],[":tests/issues-watcher.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f1-ladder-integrity.test.ts",{"duration":0,"failed":false}],[":tests/gateway-disconnect-flush.test.ts",{"duration":0,"failed":false}],[":tests/reply-terminal-reaction.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-dedup-controller.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f3-late-card.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions-allowed-filter.test.ts",{"duration":0,"failed":false}],[":tests/pty-tail-real-fixture.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway.smoke.test.ts",{"duration":0,"failed":false}],[":tests/harness-parse-mode-validation.test.ts",{"duration":0,"failed":false}],[":tests/inbound-coalesce.test.ts",{"duration":0,"failed":false}],[":tests/gateway-update-placeholder-dispatch.test.ts",{"duration":0,"failed":true}],[":tests/interrupt-marker.test.ts",{"duration":0,"failed":true}],[":tests/dm-command-gate.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback-dispatcher.e2e.test.ts",{"duration":0,"failed":false}],[":tests/sync-chat-running-subagents.test.ts",{"duration":0,"failed":false}],[":tests/subagents-schema-init-order.test.ts",{"duration":0,"failed":true}],[":tests/draft-transport.test.ts",{"duration":0,"failed":false}],[":gateway/access-validator.test.ts",{"duration":0,"failed":false}],[":registry/turns-schema.test.ts",{"duration":0,"failed":true}],[":tests/update-factory-edited-and-reactions.test.ts",{"duration":0,"failed":false}],[":tests/gateway-no-reply-single-emit.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f2-instant-draft.test.ts",{"duration":0,"failed":false}],[":tests/gateway-boot-marker-clear.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state-watcher.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-update-placeholder.test.ts",{"duration":0,"failed":false}],[":tests/permission-title.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner.test.ts",{"duration":0,"failed":false}],[":tests/sticker-aliases.test.ts",{"duration":0,"failed":true}],[":tests/ipc-server-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-pty-partial.test.ts",{"duration":0,"failed":false}],[":channel-envelope-safety.test.ts",{"duration":0,"failed":false}],[":tests/bridge-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/send-typing-action-validation.test.ts",{"duration":0,"failed":false}],[":tests/spawn-detached-cgroup-escape.test.ts",{"duration":0,"failed":false}],[":gateway/boot-sweep-filter.test.ts",{"duration":0,"failed":false}],[":tests/finalize-callback.test.ts",{"duration":17.317307,"failed":false}]]}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"build": "node scripts/build.mjs",
|
|
22
22
|
"prepublishOnly": "npm run build",
|
|
23
23
|
"test:uat": "vitest run --config ../vitest.uat.config.ts",
|
|
24
|
-
"uat:login": "bun uat/login.ts"
|
|
24
|
+
"uat:login": "bun uat/login.ts",
|
|
25
|
+
"uat:driver-info": "bun uat/driver-info.ts"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@grammyjs/runner": "^2.0.3",
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
"@secretlint/secretlint-rule-preset-recommend": "^12.2.0",
|
|
32
33
|
"@secretlint/types": "^12.2.0",
|
|
33
34
|
"@xterm/headless": "^6.0.0",
|
|
34
|
-
"grammy": "^1.21.0"
|
|
35
|
+
"grammy": "^1.21.0",
|
|
36
|
+
"posthog-node": "^5.29.2"
|
|
35
37
|
},
|
|
36
38
|
"engines": {
|
|
37
39
|
"node": ">=20.11.0"
|
|
@@ -131,3 +131,54 @@ function skillBasenameFromPath(input: Record<string, unknown>): string | null {
|
|
|
131
131
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
132
132
|
return basename(trimmed) || null;
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Inverse of `resolveAlwaysAllowRule` — does a stored allow-rule cover a
|
|
137
|
+
* fresh `permission_request`? Used by the bridge's session-scoped
|
|
138
|
+
* always-allow cache (issue #1138) to short-circuit prompts when the
|
|
139
|
+
* operator has already tapped "🔁 Always allow" for an equivalent tool
|
|
140
|
+
* call earlier in the same session.
|
|
141
|
+
*
|
|
142
|
+
* Matching rules (mirrors what `resolveAlwaysAllowRule` produces):
|
|
143
|
+
*
|
|
144
|
+
* - Bare tool name (`Edit`, `Bash`, `Write`, …) ⇒ matches any
|
|
145
|
+
* invocation of that tool. This is consistent with how
|
|
146
|
+
* `tools.allow: [Edit]` works in `.claude/settings.json` — there's
|
|
147
|
+
* no arg matching at this layer.
|
|
148
|
+
* - `Skill(<name>)` ⇒ matches only `Skill` invocations whose resolved
|
|
149
|
+
* skill name (via the same field-fallback chain as the resolver)
|
|
150
|
+
* equals `<name>`.
|
|
151
|
+
* - `mcp__<server>__<tool>` ⇒ matches the exact namespaced MCP tool
|
|
152
|
+
* name.
|
|
153
|
+
*
|
|
154
|
+
* Returns `false` for any malformed rule rather than throwing — the
|
|
155
|
+
* caller (bridge) is on the hot permission path and should fall through
|
|
156
|
+
* to the gateway prompt on bad input.
|
|
157
|
+
*/
|
|
158
|
+
export function matchesAllowRule(
|
|
159
|
+
rule: string,
|
|
160
|
+
toolName: string,
|
|
161
|
+
inputPreview: string | undefined,
|
|
162
|
+
): boolean {
|
|
163
|
+
if (!rule || !toolName) return false;
|
|
164
|
+
|
|
165
|
+
// Skill(name) — extract the parenthesized argument and compare against
|
|
166
|
+
// the resolved skill identifier from the request.
|
|
167
|
+
const skillMatch = /^Skill\(([^)]+)\)$/.exec(rule);
|
|
168
|
+
if (skillMatch) {
|
|
169
|
+
if (toolName !== "Skill") return false;
|
|
170
|
+
const ruleSkill = skillMatch[1];
|
|
171
|
+
const input = parseInput(inputPreview);
|
|
172
|
+
if (!input) return false;
|
|
173
|
+
const reqSkill =
|
|
174
|
+
readString(input, "skill") ??
|
|
175
|
+
readString(input, "skill_name") ??
|
|
176
|
+
readString(input, "skillName") ??
|
|
177
|
+
readString(input, "name") ??
|
|
178
|
+
skillBasenameFromPath(input);
|
|
179
|
+
return reqSkill === ruleSkill;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Bare tool name or namespaced MCP tool — exact string compare.
|
|
183
|
+
return rule === toolName;
|
|
184
|
+
}
|