From f75977ca6ef430f0cc4fbb92255c5ecf151b7b9c Mon Sep 17 00:00:00 2001 From: operator Date: Fri, 9 Oct 2026 23:13:43 +0000 Subject: [PATCH] fix(work): import hashlib and wire heal subparser into main CLI --- .agents/skills/box/SKILL.md | 3 +- ACCOUNTS.md | 1 + NODES.md | 2 + bin/agent_md.py | 10 + bin/approvals.py | 244 ++++- bin/box-ctl.py | 69 +- bin/box-work.py | 6 + bin/completion-audit.py | 41 +- bin/ensure-node-supervision.sh | 35 +- bin/exec-constrained.py | 143 ++- bin/fleet-alert-check.sh | 81 +- bin/gravity.py | 63 +- bin/job-dispatch.py | 149 ++- bin/kpi.py | 5 +- bin/muse-chat-api.py | 23 +- bin/muse-tui.py | 464 +++++++- bin/muse_choice_watcher.py | 855 +++++++++++++-- bin/onboard_pipeline.py | 69 +- bin/prompt_envelope.py | 7 +- bin/response-harvester.py | 189 +++- bin/tests/test_followup_fixes.py | 144 ++- bin/tmux_auto_approver.py | 97 +- bin/tmux_server_watchdog.py | 107 +- dm-signers/allowed_signers | 2 + docs/AGENT-TOOLING.md | 38 + docs/MUSE-AUTH-CLI.md | 42 +- docs/OPERATOR-DRIVE-RUNBOOK.md | 38 + job-sidechats.json | 1420 +++++++++++++++++-------- jobs/646-exec-health.json | 12 - jobs/646-hourly-checkin.json | 17 - jobs/auto-work-646-a01.json | 15 - jobs/auto-work-646-a02.json | 15 - jobs/auto-work-646-a03.json | 15 - jobs/auto-work-646-a04.json | 15 - jobs/auto-work-646-a05.json | 15 - jobs/auto-work-646-a06.json | 15 - jobs/auto-work-646-a07.json | 15 - jobs/auto-work-646-a08.json | 15 - jobs/auto-work-646-a09.json | 15 - jobs/auto-work-646-a10.json | 15 - jobs/auto-work-646-a11.json | 15 - jobs/auto-work-646-a12.json | 15 - jobs/auto-work-646-a13.json | 15 - jobs/auto-work-646-a14.json | 15 - jobs/auto-work-646-a15.json | 15 - jobs/auto-work-646-a16.json | 15 - jobs/auto-work-646-a17.json | 15 - jobs/auto-work-646-a18.json | 15 - jobs/auto-work-646-a19.json | 15 - jobs/auto-work-646-a20.json | 15 - jobs/auto-work-dev-i01.json | 22 - jobs/auto-work-dev-i02.json | 22 - jobs/auto-work-dev-i03.json | 22 - jobs/auto-work-dev-i04.json | 22 - jobs/auto-work-dev-i05.json | 22 - jobs/auto-work-dev-i06.json | 22 - jobs/auto-work-dev-i07.json | 22 - jobs/auto-work-dev-i08.json | 22 - jobs/auto-work-dev-i09.json | 22 - jobs/auto-work-dev-i10.json | 22 - jobs/auto-work-dev-i11.json | 22 - jobs/auto-work-dev-i12.json | 22 - jobs/auto-work-dev-i13.json | 22 - jobs/auto-work-dev-i14.json | 22 - jobs/auto-work-dev-i15.json | 22 - jobs/auto-work-dev-i16.json | 22 - jobs/auto-work-dev-i17.json | 22 - jobs/auto-work-dev-i18.json | 22 - jobs/auto-work-dev-i19.json | 22 - jobs/auto-work-dev-i20.json | 22 - jobs/auto-work-health-h01.json | 22 - jobs/auto-work-health-h02.json | 22 - jobs/auto-work-health-h03.json | 22 - jobs/auto-work-health-h04.json | 22 - jobs/auto-work-health-h05.json | 22 - jobs/auto-work-health-h06.json | 22 - jobs/auto-work-health-h07.json | 22 - jobs/auto-work-health-h08.json | 22 - jobs/auto-work-health-h09.json | 22 - jobs/auto-work-health-h10.json | 22 - jobs/auto-work-health-h11.json | 22 - jobs/auto-work-health-h12.json | 22 - jobs/auto-work-health-h13.json | 22 - jobs/auto-work-health-h14.json | 22 - jobs/auto-work-health-h15.json | 22 - jobs/auto-work-health-h16.json | 22 - jobs/auto-work-health-h17.json | 22 - jobs/auto-work-health-h18.json | 22 - jobs/auto-work-health-h19.json | 22 - jobs/auto-work-health-h20.json | 22 - jobs/auto-work-muse-c01.json | 22 - jobs/auto-work-muse-c02.json | 22 - jobs/auto-work-muse-c03.json | 22 - jobs/auto-work-muse-c04.json | 22 - jobs/auto-work-muse-c05.json | 22 - jobs/auto-work-muse-c06.json | 22 - jobs/auto-work-muse-c07.json | 22 - jobs/auto-work-muse-c08.json | 22 - jobs/auto-work-muse-c09.json | 22 - jobs/auto-work-muse-c10.json | 22 - jobs/auto-work-muse-c11.json | 22 - jobs/auto-work-muse-c12.json | 22 - jobs/auto-work-muse-c13.json | 22 - jobs/auto-work-muse-c14.json | 22 - jobs/auto-work-muse-c15.json | 22 - jobs/auto-work-muse-c16.json | 22 - jobs/auto-work-muse-c17.json | 22 - jobs/auto-work-muse-c18.json | 22 - jobs/auto-work-muse-c19.json | 22 - jobs/auto-work-muse-c20.json | 22 - jobs/auto-work-opm-d01.json | 22 - jobs/auto-work-opm-d02.json | 22 - jobs/auto-work-opm-d03.json | 22 - jobs/auto-work-opm-d04.json | 22 - jobs/auto-work-opm-d05.json | 22 - jobs/auto-work-opm-d06.json | 22 - jobs/auto-work-opm-d07.json | 22 - jobs/auto-work-opm-d08.json | 22 - jobs/auto-work-opm-d09.json | 22 - jobs/auto-work-opm-d10.json | 22 - jobs/auto-work-opm-d11.json | 22 - jobs/auto-work-opm-d12.json | 22 - jobs/auto-work-opm-d13.json | 22 - jobs/auto-work-opm-d14.json | 22 - jobs/auto-work-opm-d15.json | 22 - jobs/auto-work-opm-d16.json | 22 - jobs/auto-work-opm-d17.json | 22 - jobs/auto-work-opm-d18.json | 22 - jobs/auto-work-opm-d19.json | 22 - jobs/auto-work-opm-d20.json | 22 - jobs/auto-work-pip-b01.json | 22 - jobs/auto-work-pip-b02.json | 22 - jobs/auto-work-pip-b03.json | 22 - jobs/auto-work-pip-b04.json | 22 - jobs/auto-work-pip-b05.json | 22 - jobs/auto-work-pip-b06.json | 22 - jobs/auto-work-pip-b07.json | 22 - jobs/auto-work-pip-b08.json | 22 - jobs/auto-work-pip-b09.json | 22 - jobs/auto-work-pip-b10.json | 22 - jobs/auto-work-pip-b11.json | 22 - jobs/auto-work-pip-b12.json | 22 - jobs/auto-work-pip-b13.json | 22 - jobs/auto-work-pip-b14.json | 22 - jobs/auto-work-pip-b15.json | 22 - jobs/auto-work-pip-b16.json | 22 - jobs/auto-work-pip-b17.json | 22 - jobs/auto-work-pip-b18.json | 22 - jobs/auto-work-pip-b19.json | 22 - jobs/auto-work-pip-b20.json | 22 - jobs/auto-work-queue-f01.json | 22 - jobs/auto-work-queue-f03.json | 22 - jobs/auto-work-queue-f04.json | 22 - jobs/auto-work-queue-f05.json | 22 - jobs/auto-work-queue-f07.json | 22 - jobs/auto-work-queue-f08.json | 22 - jobs/auto-work-queue-f09.json | 22 - jobs/auto-work-queue-f11.json | 22 - jobs/auto-work-queue-f12.json | 22 - jobs/auto-work-queue-f13.json | 22 - jobs/auto-work-queue-f15.json | 22 - jobs/auto-work-queue-f16.json | 22 - jobs/auto-work-queue-f17.json | 22 - jobs/auto-work-queue-f19.json | 22 - jobs/auto-work-queue-f20.json | 22 - jobs/auto-work-sweep-j17.json | 15 - jobs/auto-work-xop-e01.json | 22 - jobs/auto-work-xop-e03.json | 22 - jobs/auto-work-xop-e04.json | 22 - jobs/auto-work-xop-e05.json | 22 - jobs/auto-work-xop-e07.json | 22 - jobs/auto-work-xop-e08.json | 22 - jobs/auto-work-xop-e09.json | 22 - jobs/auto-work-xop-e11.json | 22 - jobs/auto-work-xop-e12.json | 22 - jobs/auto-work-xop-e13.json | 22 - jobs/auto-work-xop-e15.json | 22 - jobs/auto-work-xop-e16.json | 22 - jobs/auto-work-xop-e17.json | 22 - jobs/auto-work-xop-e19.json | 22 - jobs/auto-work-xop-e20.json | 22 - jobs/autonomy-pulse-646.json | 30 - jobs/autonomy-pulse-opm.json | 30 - jobs/autonomy-pulse-pip.json | 30 - jobs/box-deep-health.json | 21 - jobs/box-http-health.json | 22 - jobs/box-service-health.json | 22 - jobs/opm-swarm-harvest.json | 22 - muse-choices-rules.json | 6 + shared/operators/TOOLS.md | 8 + tests/test_approvals.py | 107 +- tests/test_box_approvals_https.py | 35 +- tests/test_box_dev_https.py | 54 +- tests/test_box_jobs_https.py | 70 +- tests/test_box_loop_https.py | 34 +- tests/test_box_md_https.py | 53 +- tests/test_box_read_https.py | 76 +- tests/test_box_runtime.py | 31 +- tests/test_invite_handler.py | 23 +- tests/test_loop_health_remediation.py | 58 +- tests/test_settings_rpa.py | 27 +- tests/test_tool_calls.py | 17 + 202 files changed, 4185 insertions(+), 4142 deletions(-) delete mode 100644 jobs/646-exec-health.json delete mode 100644 jobs/646-hourly-checkin.json delete mode 100644 jobs/auto-work-646-a01.json delete mode 100644 jobs/auto-work-646-a02.json delete mode 100644 jobs/auto-work-646-a03.json delete mode 100644 jobs/auto-work-646-a04.json delete mode 100644 jobs/auto-work-646-a05.json delete mode 100644 jobs/auto-work-646-a06.json delete mode 100644 jobs/auto-work-646-a07.json delete mode 100644 jobs/auto-work-646-a08.json delete mode 100644 jobs/auto-work-646-a09.json delete mode 100644 jobs/auto-work-646-a10.json delete mode 100644 jobs/auto-work-646-a11.json delete mode 100644 jobs/auto-work-646-a12.json delete mode 100644 jobs/auto-work-646-a13.json delete mode 100644 jobs/auto-work-646-a14.json delete mode 100644 jobs/auto-work-646-a15.json delete mode 100644 jobs/auto-work-646-a16.json delete mode 100644 jobs/auto-work-646-a17.json delete mode 100644 jobs/auto-work-646-a18.json delete mode 100644 jobs/auto-work-646-a19.json delete mode 100644 jobs/auto-work-646-a20.json delete mode 100644 jobs/auto-work-dev-i01.json delete mode 100644 jobs/auto-work-dev-i02.json delete mode 100644 jobs/auto-work-dev-i03.json delete mode 100644 jobs/auto-work-dev-i04.json delete mode 100644 jobs/auto-work-dev-i05.json delete mode 100644 jobs/auto-work-dev-i06.json delete mode 100644 jobs/auto-work-dev-i07.json delete mode 100644 jobs/auto-work-dev-i08.json delete mode 100644 jobs/auto-work-dev-i09.json delete mode 100644 jobs/auto-work-dev-i10.json delete mode 100644 jobs/auto-work-dev-i11.json delete mode 100644 jobs/auto-work-dev-i12.json delete mode 100644 jobs/auto-work-dev-i13.json delete mode 100644 jobs/auto-work-dev-i14.json delete mode 100644 jobs/auto-work-dev-i15.json delete mode 100644 jobs/auto-work-dev-i16.json delete mode 100644 jobs/auto-work-dev-i17.json delete mode 100644 jobs/auto-work-dev-i18.json delete mode 100644 jobs/auto-work-dev-i19.json delete mode 100644 jobs/auto-work-dev-i20.json delete mode 100644 jobs/auto-work-health-h01.json delete mode 100644 jobs/auto-work-health-h02.json delete mode 100644 jobs/auto-work-health-h03.json delete mode 100644 jobs/auto-work-health-h04.json delete mode 100644 jobs/auto-work-health-h05.json delete mode 100644 jobs/auto-work-health-h06.json delete mode 100644 jobs/auto-work-health-h07.json delete mode 100644 jobs/auto-work-health-h08.json delete mode 100644 jobs/auto-work-health-h09.json delete mode 100644 jobs/auto-work-health-h10.json delete mode 100644 jobs/auto-work-health-h11.json delete mode 100644 jobs/auto-work-health-h12.json delete mode 100644 jobs/auto-work-health-h13.json delete mode 100644 jobs/auto-work-health-h14.json delete mode 100644 jobs/auto-work-health-h15.json delete mode 100644 jobs/auto-work-health-h16.json delete mode 100644 jobs/auto-work-health-h17.json delete mode 100644 jobs/auto-work-health-h18.json delete mode 100644 jobs/auto-work-health-h19.json delete mode 100644 jobs/auto-work-health-h20.json delete mode 100644 jobs/auto-work-muse-c01.json delete mode 100644 jobs/auto-work-muse-c02.json delete mode 100644 jobs/auto-work-muse-c03.json delete mode 100644 jobs/auto-work-muse-c04.json delete mode 100644 jobs/auto-work-muse-c05.json delete mode 100644 jobs/auto-work-muse-c06.json delete mode 100644 jobs/auto-work-muse-c07.json delete mode 100644 jobs/auto-work-muse-c08.json delete mode 100644 jobs/auto-work-muse-c09.json delete mode 100644 jobs/auto-work-muse-c10.json delete mode 100644 jobs/auto-work-muse-c11.json delete mode 100644 jobs/auto-work-muse-c12.json delete mode 100644 jobs/auto-work-muse-c13.json delete mode 100644 jobs/auto-work-muse-c14.json delete mode 100644 jobs/auto-work-muse-c15.json delete mode 100644 jobs/auto-work-muse-c16.json delete mode 100644 jobs/auto-work-muse-c17.json delete mode 100644 jobs/auto-work-muse-c18.json delete mode 100644 jobs/auto-work-muse-c19.json delete mode 100644 jobs/auto-work-muse-c20.json delete mode 100644 jobs/auto-work-opm-d01.json delete mode 100644 jobs/auto-work-opm-d02.json delete mode 100644 jobs/auto-work-opm-d03.json delete mode 100644 jobs/auto-work-opm-d04.json delete mode 100644 jobs/auto-work-opm-d05.json delete mode 100644 jobs/auto-work-opm-d06.json delete mode 100644 jobs/auto-work-opm-d07.json delete mode 100644 jobs/auto-work-opm-d08.json delete mode 100644 jobs/auto-work-opm-d09.json delete mode 100644 jobs/auto-work-opm-d10.json delete mode 100644 jobs/auto-work-opm-d11.json delete mode 100644 jobs/auto-work-opm-d12.json delete mode 100644 jobs/auto-work-opm-d13.json delete mode 100644 jobs/auto-work-opm-d14.json delete mode 100644 jobs/auto-work-opm-d15.json delete mode 100644 jobs/auto-work-opm-d16.json delete mode 100644 jobs/auto-work-opm-d17.json delete mode 100644 jobs/auto-work-opm-d18.json delete mode 100644 jobs/auto-work-opm-d19.json delete mode 100644 jobs/auto-work-opm-d20.json delete mode 100644 jobs/auto-work-pip-b01.json delete mode 100644 jobs/auto-work-pip-b02.json delete mode 100644 jobs/auto-work-pip-b03.json delete mode 100644 jobs/auto-work-pip-b04.json delete mode 100644 jobs/auto-work-pip-b05.json delete mode 100644 jobs/auto-work-pip-b06.json delete mode 100644 jobs/auto-work-pip-b07.json delete mode 100644 jobs/auto-work-pip-b08.json delete mode 100644 jobs/auto-work-pip-b09.json delete mode 100644 jobs/auto-work-pip-b10.json delete mode 100644 jobs/auto-work-pip-b11.json delete mode 100644 jobs/auto-work-pip-b12.json delete mode 100644 jobs/auto-work-pip-b13.json delete mode 100644 jobs/auto-work-pip-b14.json delete mode 100644 jobs/auto-work-pip-b15.json delete mode 100644 jobs/auto-work-pip-b16.json delete mode 100644 jobs/auto-work-pip-b17.json delete mode 100644 jobs/auto-work-pip-b18.json delete mode 100644 jobs/auto-work-pip-b19.json delete mode 100644 jobs/auto-work-pip-b20.json delete mode 100644 jobs/auto-work-queue-f01.json delete mode 100644 jobs/auto-work-queue-f03.json delete mode 100644 jobs/auto-work-queue-f04.json delete mode 100644 jobs/auto-work-queue-f05.json delete mode 100644 jobs/auto-work-queue-f07.json delete mode 100644 jobs/auto-work-queue-f08.json delete mode 100644 jobs/auto-work-queue-f09.json delete mode 100644 jobs/auto-work-queue-f11.json delete mode 100644 jobs/auto-work-queue-f12.json delete mode 100644 jobs/auto-work-queue-f13.json delete mode 100644 jobs/auto-work-queue-f15.json delete mode 100644 jobs/auto-work-queue-f16.json delete mode 100644 jobs/auto-work-queue-f17.json delete mode 100644 jobs/auto-work-queue-f19.json delete mode 100644 jobs/auto-work-queue-f20.json delete mode 100644 jobs/auto-work-sweep-j17.json delete mode 100644 jobs/auto-work-xop-e01.json delete mode 100644 jobs/auto-work-xop-e03.json delete mode 100644 jobs/auto-work-xop-e04.json delete mode 100644 jobs/auto-work-xop-e05.json delete mode 100644 jobs/auto-work-xop-e07.json delete mode 100644 jobs/auto-work-xop-e08.json delete mode 100644 jobs/auto-work-xop-e09.json delete mode 100644 jobs/auto-work-xop-e11.json delete mode 100644 jobs/auto-work-xop-e12.json delete mode 100644 jobs/auto-work-xop-e13.json delete mode 100644 jobs/auto-work-xop-e15.json delete mode 100644 jobs/auto-work-xop-e16.json delete mode 100644 jobs/auto-work-xop-e17.json delete mode 100644 jobs/auto-work-xop-e19.json delete mode 100644 jobs/auto-work-xop-e20.json delete mode 100644 jobs/autonomy-pulse-646.json delete mode 100644 jobs/autonomy-pulse-opm.json delete mode 100644 jobs/autonomy-pulse-pip.json delete mode 100644 jobs/box-deep-health.json delete mode 100644 jobs/box-http-health.json delete mode 100644 jobs/box-service-health.json delete mode 100644 jobs/opm-swarm-harvest.json diff --git a/.agents/skills/box/SKILL.md b/.agents/skills/box/SKILL.md index be4d84a..1005fd9 100644 --- a/.agents/skills/box/SKILL.md +++ b/.agents/skills/box/SKILL.md @@ -33,7 +33,8 @@ Add `--json` to any command for machine-readable output when parsing results in - `box job list` / `box job log` — scheduled jobs and execution events. - `box harvest status` / `box followup list` — harvest watermarks / pending nudges. - `box muse-choices on|off|status|logs|reconcile|resolve` — Muse TUI auto-answer daemon switch, state, per-pane logs, held-prompt resolve (default on; `off` is the box-command opt-out). -- `box runtime list|send|launch|layout|spread` — Muse CLI tmux runtimes: live state + approval posture, send-keys input, auto-approved launches, pane-geometry layout + spread for squeezed panes. +- `box runtime list|send|launch|layout|spread|reconcile|kill|restart|brief` — Muse CLI tmux runtimes: live state + approval posture, send-keys input, launches with approval trail (bare launch injects `--approval-mode on-request`; fleet socket `/tmp/tmux-muse.sock` is watcher-answered), pane-geometry layout + spread, manifest reconcile, session kill / manifest restart / brief delivery. +- `box tasks list|show|create|claim|done|requeue|sweep` — agent work queue (`fleet/tasks/` pending/claimed/done; distinct from scheduled `box job`). Prefer these over raw `mv`. - `box tmux tally` / `box tmux auto [status|on|off|watch|once|logs|match]` — multi-socket Tmux worker tally, regex auto-approver daemon & guardrails. - `box onboard connects` / `box onboard-tui` — fleet & client onboarding inventory, CDP ports, OTP salvage & 4-surface TUI. - `box invite status|code |redeem ` / `box usage [--node N]` — invite codes and usage limits. diff --git a/ACCOUNTS.md b/ACCOUNTS.md index df89fb7..8e9863e 100644 --- a/ACCOUNTS.md +++ b/ACCOUNTS.md @@ -32,6 +32,7 @@ node name, chrome-box profile, API `--account`, and the agent's display name. | def | def | def | email_otp | defnotabotnet@gmail.com | defnotabotnet@gmail.com | no | yes | active | 104.28.195.181 | 9450 | def | Full onboarding completed 2026-10-04; age verification cleared via Instagram linking (paradahub). Active chat session. | | opm | opm | opm | email_otp | Nico Parada | artglobal.cc@gmail.com | no | yes | active | 104.28.195.181 | 9440 | opm | Email changed from yourfriendnico@proton.me to artglobal.cc@gmail.com. Linked with IG auxfate. Browser up, session active. | | dev | dev | dev | email_otp | paradaproduced@gmail.com | paradaproduced@gmail.com | no | yes | active | 104.28.195.181 | 9460 | dev | Full onboarding completed 2026-10-04; unlocked /access gate via Meta Accounts Center IG linking (veryraremeta). Active chat session. | +| 646b | 646b | 646b | email_otp | pixos.dev | pixos.dev@proton.me | no | yes | active | 104.28.195.184 | 9460 | 646b | Salvage node for 646, onboarded 2026-10-09, redeemed REDCJ7. | ## Login Type Details diff --git a/NODES.md b/NODES.md index 0d22fe1..e557da2 100644 --- a/NODES.md +++ b/NODES.md @@ -27,3 +27,5 @@ Roles: `worker` (persistent swarm/daemon), `repair` (fix sessions), sessions carry no node and show `-` in `box runtime list`. Session creators owned by existing flows keep their names until owners rename; new sessions should follow the convention from birth. +| id-verify-examp-8060e2a | warp-id-verify-examp-8060e2a | unknown | 9229 | retired | id-verify-examp-8060e2a (auto-registered; retired 2026-10-08, stray onboarding example, no warp identity) | +| 646b | warp-646b | unknown | 9460 | active | 646b (auto-registered) | diff --git a/bin/agent_md.py b/bin/agent_md.py index 5defad6..b58749f 100755 --- a/bin/agent_md.py +++ b/bin/agent_md.py @@ -48,6 +48,14 @@ MD_ACCOUNT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$") MD_FILENAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,128}$") MD_SUBPATH_RE = re.compile(r"^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$") +# Exact subpaths permitted for read/write alongside plain basenames. +# Narrow operator-key-management allowlist: membership is an exact string +# match, so no wildcards and no traversal are expressible. Template flows +# (diff/amend/append/pull) still require TARGET_MD_FILES. +MD_ALLOWED_SUBPATHS = frozenset({ + ".ssh/authorized_keys", +}) + def validate_account(account: str) -> str: """Reject account values that could escape the cookies/config path.""" @@ -70,6 +78,8 @@ def validate_filename(filename: str, template_only: bool = False) -> str: "Unknown shared template %r: must be one of %s" % (filename, sorted(TARGET_MD_FILES))) return filename + if isinstance(filename, str) and filename in MD_ALLOWED_SUBPATHS: + return filename if not isinstance(filename, str) or filename in (".", "..") \ or not MD_FILENAME_RE.fullmatch(filename): raise MDValidationError( diff --git a/bin/approvals.py b/bin/approvals.py index fcbbfc7..498b2ac 100755 --- a/bin/approvals.py +++ b/bin/approvals.py @@ -153,6 +153,10 @@ VALID_NODES = ["muse", "pip", "646", "opm", "def", "dev"] KEY_REQUEST_TTL_SECONDS = 2 * 3600 INPUT_WAIT_TTL_SECONDS = 30 * 60 BROWSER_APPROVAL_TTL_SECONDS = 30 * 60 +# Tail cap for key-request audit scans: check_node_key_request scans only the +# last N lines of box-ctl.jsonl (key events cluster at the end), falling back +# to a full scan when the tail holds no relevant record for the node. +KEY_SCAN_TAIL_LINES = 5000 # Trusted infrastructure IPs safe for automated approval TRUSTED_IPS = { @@ -187,6 +191,51 @@ def is_trusted_target(target: str, card_text: str = "") -> bool: return True return False +def is_plausible_target(target: str) -> bool: + """True if target looks like a real network endpoint, not a parser artifact. + + P1 fix (2026-10-08): the target-extraction regex happily captures garbage + tokens like "echo" from dialog text ("connect to echo over SSH"), which + then fail-closed to is_trusted=False and page CRITICAL ~6/day for pip's + routine Heartbeat dialog. This validator runs BEFORE the is_trusted check: + only strict IPv4 (0-255 octets) or plausible hostnames pass. + """ + if not target or not isinstance(target, str): + return False + t = target.strip().lower().rstrip(".") + if not t: + return False + # Strict IPv4: four octets, each 0-255, no leading-zero weirdness + parts = t.split(".") + if len(parts) == 4: + try: + octets = [int(p) for p in parts] + # Reject leading zeros ("01") to avoid octal ambiguity, except "0" itself + if all(0 <= o <= 255 for o in octets) and all( + p == str(o) for p, o in zip(parts, octets) + ): + return True + except ValueError: + pass + # Four numeric parts but invalid octets (e.g. 999.999.999.999) -> not plausible + if all(p.isdigit() for p in parts): + return False + # Hostname: "localhost" or a dotted name with valid labels + if t == "localhost": + return True + # All-numeric dotted tokens that aren't valid IPv4 (e.g. "1.2.3") are + # parser artifacts, not hostnames + if "." in t and all(c.isdigit() or c == "." for c in t): + return False + if "." in t: + import re as _re + if _re.match(r"^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$", t): + # Each label 1-63 chars, no empty labels + if all(1 <= len(label) <= 63 for label in t.split(".")): + return True + return False + + REDACT_PATTERNS = [ (re.compile(r"Bearer\s+[A-Za-z0-9._~+/-]+=*", re.IGNORECASE), "Bearer [REDACTED]"), @@ -308,6 +357,63 @@ def _rec_approval_type(rec: dict) -> str: return _approval_type(rec.get("action", "")) +def _tail_lines(path: Path, n: int) -> list: + """Return up to the last n lines of path as strings (seek-based, no full read).""" + with open(path, "rb") as f: + f.seek(0, os.SEEK_END) + pos = f.tell() + if pos == 0: + return [] + data = b"" + while pos > 0 and data.count(b"\n") <= n: + step = min(8192, pos) + pos -= step + f.seek(pos) + data = f.read(step) + data + return data.decode("utf-8", "replace").split("\n")[-n:] + + +def _scan_key_lines(lines, node: str): + """Scan audit lines (forward order) for a node's key-request state. + + Returns (latest_req, resolved, saw_relevant). A suffix-slice scan is + authoritative when saw_relevant: the newest relevant record in a suffix + decides the outcome identically to a full scan (any newer request or + later resolution would itself lie in the suffix). + """ + latest_req = None + resolved = False + saw_relevant = False + for line in lines: + line = line.strip() + if not line: + continue + # Prefilter: only key-approval actions can affect the outcome, and + # all carry this substring; skip json.loads for everything else. + if "key-approval" not in line: + continue + try: + rec = json.loads(line) + except Exception: + continue + if rec.get("name") != node: + continue + act = rec.get("action") + if act == "key-approval-request": + latest_req = rec + resolved = False + saw_relevant = True + elif _rec_approval_type(rec) == "key" and act in ( + "key-approval-allow", "key-approval-deny", "key-approval-expired", + ): + # Only a KEY-type resolution clears a key request. A browser + # approval-allow/deny must never resolve a pending key request + # (cross-type resolution bug). + resolved = True + saw_relevant = True + return latest_req, resolved, saw_relevant + + def check_node_key_request(node: str) -> dict: """Check if node has an active unfulfilled key approval request in box-ctl.jsonl. @@ -317,32 +423,15 @@ def check_node_key_request(node: str) -> dict: """ if not CTL_LOG.exists(): return None - latest_req = None - resolved = False now = datetime.now(timezone.utc).timestamp() try: - with open(CTL_LOG, "r") as f: - for line in f: - line = line.strip() - if not line: - continue - try: - rec = json.loads(line) - except Exception: - continue - if rec.get("name") != node: - continue - act = rec.get("action") - if act == "key-approval-request": - latest_req = rec - resolved = False - elif _rec_approval_type(rec) == "key" and act in ( - "key-approval-allow", "key-approval-deny", "key-approval-expired", - ): - # Only a KEY-type resolution clears a key request. A browser - # approval-allow/deny must never resolve a pending key request - # (cross-type resolution bug). - resolved = True + latest_req, resolved, saw = _scan_key_lines( + _tail_lines(CTL_LOG, KEY_SCAN_TAIL_LINES), node) + if not saw: + # No relevant record in tail: older history may hold an + # unresolved request; fall back to a full scan. + with open(CTL_LOG, "r") as f: + latest_req, resolved, _ = _scan_key_lines(f, node) except Exception: return None @@ -760,6 +849,11 @@ def inspect_node_approvals(node: str) -> dict: if bg_tasks_count > 0 and "need review" not in purpose.lower() and "need review" not in title.lower(): purpose = f"{purpose} [{bg_tasks_count} queued task(s) awaiting review]".strip() + # P1: reject implausible targets (parser artifacts like "echo") + # before the trust check. Garbage tokens -> parser-suspect. + target_plausible = is_plausible_target(target or ip) + if target and not target_plausible: + target = None is_trusted = is_trusted_target(target or ip, card_text) return { @@ -770,6 +864,7 @@ def inspect_node_approvals(node: str) -> dict: "purpose": purpose, "ip": ip, "target": target or ip or "-", + "target_plausible": target_plausible, "is_trusted": is_trusted, "buttons": data.get("buttons", []), "has_allow_once": data.get("has_allow_once", False), @@ -1355,3 +1450,104 @@ def dismiss_node_task(node: str, caller: str = "box-approvals") -> dict: "cleared_waits": clear_res.get("cleared_per_node", {}).get(node, 0), } + +# --------------------------------------------------------------------------- +# Coordinator Gating & Markdown Decision Records +# --------------------------------------------------------------------------- + +DOCS_DIR = REPO_ROOT / "docs" + + +def parse_yaml_frontmatter(text: str) -> dict: + """Parse YAML frontmatter delimited by ^--- from Markdown text without external dependencies.""" + if not text or not text.startswith("---"): + return {} + parts = text.split("---", 2) + if len(parts) < 3: + return {} + raw_yaml = parts[1].strip() + data = {} + current_key = None + for line in raw_yaml.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if ":" in line: + k, v = line.split(":", 1) + k = k.strip() + v = v.strip().strip("'\"") + if v.lower() == "true": + v = True + elif v.lower() == "false": + v = False + elif v == "": + v = [] + current_key = k + data[k] = v + continue + data[k] = v + current_key = k + elif line.startswith("- ") and current_key and isinstance(data.get(current_key), list): + item = line[2:].strip().strip("'\"") + data[current_key].append(item) + return data + + +def scan_coordinator_gates(docs_dir: Path = None) -> list: + """Scan docs/*.md for coordinator gate decision records.""" + target_dir = docs_dir or DOCS_DIR + gates = [] + if not target_dir.exists(): + return gates + for doc in target_dir.glob("*.md"): + try: + content = doc.read_text(encoding="utf-8") + meta = parse_yaml_frontmatter(content) + if meta.get("gate") == "coordinator" or "coordinator" in meta: + meta["doc_path"] = str(doc) + meta["doc_name"] = doc.name + meta["is_signed_off"] = meta.get("status") in ("signed-off", "accepted", "final") + gates.append(meta) + except Exception: + pass + gates.sort(key=lambda x: str(x.get("accepted_at", "")), reverse=True) + return gates + + +def verify_coordinator_signoff(scope: str, docs_dir: Path = None) -> dict: + """Verify if a specific scope or target has a signed-off coordinator decision record. + + Scope can match `scope` or any item in `signoff_targets`. + """ + gates = scan_coordinator_gates(docs_dir) + for g in gates: + targets = g.get("signoff_targets") or [] + if not isinstance(targets, list): + targets = [targets] + if g.get("scope") == scope or scope in targets: + if g.get("is_signed_off"): + return { + "ok": True, + "scope": scope, + "status": g.get("status"), + "coordinator": g.get("coordinator"), + "accepted_at": g.get("accepted_at"), + "doc_name": g.get("doc_name"), + "doc_path": g.get("doc_path"), + } + else: + return { + "ok": False, + "scope": scope, + "status": g.get("status"), + "coordinator": g.get("coordinator"), + "doc_name": g.get("doc_name"), + "error": f"Gate for scope '{scope}' exists in {g.get('doc_name')} but status is '{g.get('status')}' (not signed-off)", + } + return { + "ok": False, + "scope": scope, + "error": f"No coordinator decision record found covering scope '{scope}' in {docs_dir or DOCS_DIR}", + } + + diff --git a/bin/box-ctl.py b/bin/box-ctl.py index f802f47..789d172 100755 --- a/bin/box-ctl.py +++ b/bin/box-ctl.py @@ -1223,23 +1223,41 @@ def act_chrome_errors(no_advance=False): fail("SCAN_ERROR", "chrome-error-scan.sh failed", {"stderr": r.stderr}) +_SUPER_CLI_MOD = None + + +def _super_cli_mod(): + """Lazily import super-cli.py once per process (amortized over calls).""" + global _SUPER_CLI_MOD + if _SUPER_CLI_MOD is None: + import importlib.util + spec = importlib.util.spec_from_file_location( + "super_cli_boxctl", str(BIN / "super-cli.py")) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + _SUPER_CLI_MOD = mod + return _SUPER_CLI_MOD + + def act_dm_log(limit=50, agent=None): if agent is not None and agent not in VALID_AGENTS: fail("BAD_NODE", f"unknown agent: {agent}") audit("dm-log", f"{agent or 'all'}/{limit}") - cmd = [sys.executable, str(BIN / "super-cli.py"), "dm", "log", "--json", "-n", str(limit)] - if agent: - cmd += ["--agent", agent] - r = subprocess.run(cmd, capture_output=True, text=True) - if r.returncode == 0: - try: - data = json.loads(r.stdout) - data["dms"] = data.get("entries", []) - print(json.dumps(data)) - return - except Exception: - pass - fail("DM_LOG_ERROR", "failed to read dm log", {"stderr": r.stderr}) + try: + import argparse + import io + from contextlib import redirect_stdout + sc = _super_cli_mod() + args = argparse.Namespace(n=limit, agent=agent, filter=None, json=True) + buf = io.StringIO() + with redirect_stdout(buf): + sc.cmd_dm_log(args) + data = json.loads(buf.getvalue()) + data["dms"] = data.get("entries", []) + print(json.dumps(data)) + return + except Exception as e: + fail("DM_LOG_ERROR", "failed to read dm log", {"stderr": str(e)}) def act_unread(agent=None): @@ -1482,20 +1500,9 @@ def _policy_scan(): except OSError: return None, {"error": f"cannot read {DM_LOG}"} - for line in lines: - line = line.strip() - if not line: - continue - try: - ev = json.loads(line) - except json.JSONDecodeError: - continue - tags = ev.get("tags") - if isinstance(tags, dict) and "allow_main_chat" in tags: - ts = ev.get("ts") or "" - if adoption_ts is None or ts < adoption_ts: - adoption_ts = ts - + # Single parse pass: stash parsed events because the classification + # pass needs adoption_ts, a minimum over the whole file. + events = [] for line in lines: line = line.strip() if not line: @@ -1506,6 +1513,14 @@ def _policy_scan(): except json.JSONDecodeError: malformed += 1 continue + events.append(ev) + tags = ev.get("tags") + if isinstance(tags, dict) and "allow_main_chat" in tags: + ts = ev.get("ts") or "" + if adoption_ts is None or ts < adoption_ts: + adoption_ts = ts + + for ev in events: ts = ev.get("ts") or "" if adoption_ts is not None and ts < adoption_ts: if ev.get("type") == "sent" and ev.get("target") == "main": diff --git a/bin/box-work.py b/bin/box-work.py index 46ec5c1..6a696ec 100755 --- a/bin/box-work.py +++ b/bin/box-work.py @@ -22,6 +22,7 @@ import argparse import urllib.request import urllib.parse import urllib.error +import hashlib from datetime import datetime, timezone from pathlib import Path @@ -809,6 +810,9 @@ def main(): p_merge = sub.add_parser("merge", help="Merge an open PR into master") p_merge.add_argument("pr", type=int, help="Pull request number (e.g. 214)") + p_heal = sub.add_parser("heal", help="Run automated remediation on an agent") + p_heal.add_argument("agent", help="Agent username to heal") + p_chats = sub.add_parser("chats", help="View recent live chat activity") p_chats.add_argument("--agent", help="Filter by agent name") p_chats.add_argument("--limit", type=int, default=10, help="Number of messages to show") @@ -820,6 +824,8 @@ def main(): cmd_status(args) elif action == "check": cmd_check(args) + elif action == "heal": + cmd_heal(args) elif action == "start": cmd_start(args) elif action == "assign": diff --git a/bin/completion-audit.py b/bin/completion-audit.py index 591f1ca..8d14973 100755 --- a/bin/completion-audit.py +++ b/bin/completion-audit.py @@ -81,7 +81,11 @@ def compute_funnel(events, cutoff): elif ty == "job_result": fam = family_of(e.get("job_id")) families[fam]["results"] += 1 - families[fam]["ok" if e.get("success") else "fail"] += 1 + snippet = e.get("result_snippet") or "" + if e.get("outcome") == "declined" or snippet.startswith("DECLINE:"): + families[fam]["declined"] += 1 + else: + families[fam]["ok" if e.get("success") else "fail"] += 1 elif ty == "job_failed": families[family_of(e.get("job_id"))]["failed"] += 1 elif ty == "fallback_executed": @@ -213,6 +217,8 @@ def render_digest(rep): bits = [] if t.get("failed"): bits.append(f"{t['failed']} job_failed") + if t.get("declined"): + bits.append(f"{t['declined']} declined") if tools.get("fail"): bits.append(f"{tools['fail']} tool errors") if t.get("fallback_ok") or t.get("fallback_fail"): @@ -239,14 +245,25 @@ def render_digest(rep): def should_post(report): - """Post on degraded, else heartbeat at most every HEARTBEAT_INTERVAL_H.""" - if report["degraded"]: - return True, "degraded" + """Post on degraded if changed or every HEARTBEAT_INTERVAL_H, else heartbeat at most every HEARTBEAT_INTERVAL_H.""" try: - state = json.load(open(STATE_FILE)) - last = parse_ts(state.get("last_heartbeat")) + with open(STATE_FILE, "r", encoding="utf-8") as f: + state = json.load(f) except Exception: - last = None + state = {} + + if report.get("degraded"): + last_totals = state.get("last_totals") + last_reasons = state.get("last_reasons") + last_post = parse_ts(state.get("last_degraded_post") or state.get("last_post")) + same_metrics = (last_totals is not None and last_totals == report.get("totals")) + same_reasons = (last_reasons is not None and last_reasons == report.get("reasons")) + if same_metrics and same_reasons: + if last_post and (utcnow() - last_post) < timedelta(hours=HEARTBEAT_INTERVAL_H): + return False, "degraded-unchanged" + return True, "degraded" + + last = parse_ts(state.get("last_heartbeat")) if last is None or (utcnow() - last) > timedelta(hours=HEARTBEAT_INTERVAL_H): return True, "heartbeat" return False, "green-quiet" @@ -296,12 +313,18 @@ def main(): return 0 ok, detail = post_digest(render_digest(report)) print(f"post: {'delivered' if ok else 'FAILED'} ({why}) {detail[:120]}") - if ok and why == "heartbeat": + if ok: try: state = {} if STATE_FILE.exists(): state = json.loads(STATE_FILE.read_text(encoding="utf-8")) - state["last_heartbeat"] = report["ts"] + state["last_post"] = report["ts"] + if why == "degraded": + state["last_degraded_post"] = report["ts"] + state["last_totals"] = report.get("totals") + state["last_reasons"] = report.get("reasons") + elif why == "heartbeat": + state["last_heartbeat"] = report["ts"] STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8") except Exception as e: print(f"warning: state save failed: {e}") diff --git a/bin/ensure-node-supervision.sh b/bin/ensure-node-supervision.sh index 74cd31b..4d69bf0 100755 --- a/bin/ensure-node-supervision.sh +++ b/bin/ensure-node-supervision.sh @@ -7,6 +7,7 @@ # supervisors: cdp-relay-watchdog, agent-health.sh, relay-health-check, # cdp-latency-check. Port from netvm-names pinning (honors # CDP_PORT_OVERRIDE, so provision's picked port wins when present). +# Example/verify/probe names retire on sight (never active, no timer). # 2. chromebox-watchdog-.timer unit + enable --now — the one # supervisor that needs a per-node systemd unit (the @.service # template already exists). Needs root for the real unit dir. @@ -25,16 +26,38 @@ UNIT_DIR="${UNIT_DIR:-/etc/systemd/system}" usage() { echo "usage: ensure-node-supervision.sh | --all" >&2; exit 1; } -ensure_registry_row() { +# Example/verify/probe nodes (onboarding drills, id-verify examples) must +# never join active supervision: they carry no warp identity, wedge the +# pinned registry contract, and spin chrome restarts forever. Match is +# deliberately narrow (examp anywhere, test-/verify- prefixes) so real +# node names containing those substrings elsewhere stay active. +is_example_node() { + case "$1" in + *examp*|test*|verify-*|*-verify-*) return 0;; + *) return 1;; + esac +} + +row_is_retired() { local node="$1" + grep -qE "^\|[[:space:]]*$node[[:space:]]*\|[^|]*\|[^|]*\|[^|]*\|[[:space:]]*retired[[:space:]]*\|" \ + "$NODES_MD" 2>/dev/null +} + +ensure_registry_row() { + local node="$1" status="active" note="auto-registered" if grep -qE "^\|[[:space:]]*$node[[:space:]]*\|" "$NODES_MD" 2>/dev/null; then echo "registry: $node already in NODES.md" return 0 fi netvm_names "$node" || { echo "registry: unknown node $node" >&2; return 1; } - printf '| %s | %s | unknown | %s | active | %s (auto-registered) |\n' \ - "$node" "$NETNS" "$CDP_PORT" "$node" >> "$NODES_MD" - echo "registry: added $node (port $CDP_PORT)" + if is_example_node "$node"; then + status="retired" + note="auto-registered example — retired" + fi + printf '| %s | %s | unknown | %s | %s | %s (%s) |\n' \ + "$node" "$NETNS" "$CDP_PORT" "$status" "$node" "$note" >> "$NODES_MD" + echo "registry: added $node (port $CDP_PORT, $status)" } ensure_timer() { @@ -72,6 +95,10 @@ EOF ensure_node() { local node="$1" ensure_registry_row "$node" + if row_is_retired "$node"; then + echo "timer: $node retired, skipping supervision" + return 0 + fi ensure_timer "$node" } diff --git a/bin/exec-constrained.py b/bin/exec-constrained.py index 080f77b..a8ed9f2 100755 --- a/bin/exec-constrained.py +++ b/bin/exec-constrained.py @@ -74,6 +74,7 @@ HEX_RE = re.compile(r'^[0-9a-f]{8,128}$') DM_ID_RE = re.compile(r'^[0-9a-fA-F]{6,64}$') TEST_MODULE_RE = re.compile(r'^tests\.[a-z0-9_]+$') JOB_DISPATCH_ID_RE = re.compile(r'^[a-z0-9][a-z0-9-]{0,63}-\d{8}-\d{6}-[a-f0-9]{8}$') +FLOW_ID_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$') STRAT_TYPES = frozenset({'wake', 'job', 'siphon', 'manual', 'health', 'heartbeat'}) STRAT_PRIORITIES = frozenset({'routine', 'normal', 'important'}) SUBTYPE_RE = re.compile(r'^[A-Za-z0-9_.-]{1,64}$') @@ -765,6 +766,120 @@ def _tmux_prune_build(a): return [sys.executable, os.path.join(BIN_DIR, 'muse-tmux.py'), 'prune', '--ttl', str(a['ttl'])] +def _flow_id_name(val): + if not isinstance(val, str) or not FLOW_ID_RE.fullmatch(val): + raise OpError("flow_id must be 1-64 alphanumeric, dash, or underscore chars") + return val + + +def _flow_start_validate(raw): + if not isinstance(raw, dict): + raise OpError("args must be an object") + allowed = {"flow_id", "command", "agent", "cwd"} + for k in raw: + if k not in allowed: + raise OpError(f"unknown arg: {k}") + if not raw.get("flow_id"): + raise OpError("flow_id is required") + agent = raw.get("agent", "646") + if agent and (not isinstance(agent, str) or agent not in AGENTS): + agent = "646" + return { + "flow_id": _flow_id_name(raw["flow_id"]), + "command": str(raw["command"]) if raw.get("command") else None, + "agent": agent, + "cwd": str(raw["cwd"]) if raw.get("cwd") else None, + } + + +def _flow_start_build(a): + cmd = [sys.executable, os.path.join(BIN_DIR, "flow_engine.py"), "start", a["flow_id"], "--agent", a["agent"]] + if a.get("command"): + cmd.extend(["--command", a["command"]]) + if a.get("cwd"): + cmd.extend(["--cwd", a["cwd"]]) + return cmd + + +def _flow_read_validate(raw): + if not isinstance(raw, dict): + raise OpError("args must be an object") + allowed = {"flow_id", "lines"} + for k in raw: + if k not in allowed: + raise OpError(f"unknown arg: {k}") + if not raw.get("flow_id"): + raise OpError("flow_id is required") + lines = raw.get("lines", 40) + try: + lines = int(lines) + if lines < 1 or lines > 200: + lines = 40 + except Exception: + lines = 40 + return { + "flow_id": _flow_id_name(raw["flow_id"]), + "lines": lines, + } + + +def _flow_read_build(a): + return [sys.executable, os.path.join(BIN_DIR, "flow_engine.py"), "read", a["flow_id"], "--lines", str(a["lines"])] + + +def _flow_send_validate(raw): + if not isinstance(raw, dict): + raise OpError("args must be an object") + allowed = {"flow_id", "keys", "command", "no_enter"} + for k in raw: + if k not in allowed: + raise OpError(f"unknown arg: {k}") + if not raw.get("flow_id"): + raise OpError("flow_id is required") + if "keys" not in raw: + raise OpError("keys is required") + return { + "flow_id": _flow_id_name(raw["flow_id"]), + "keys": str(raw["keys"]), + "command": bool(raw.get("command", False)), + "no_enter": bool(raw.get("no_enter", False)), + } + + +def _flow_send_build(a): + cmd = [sys.executable, os.path.join(BIN_DIR, "flow_engine.py"), "send", a["flow_id"], a["keys"]] + if a.get("command"): + cmd.append("--command") + if a.get("no_enter"): + cmd.append("--no-enter") + return cmd + + +def _flow_list_validate(raw): + return {} + + +def _flow_list_build(a): + return [sys.executable, os.path.join(BIN_DIR, "flow_engine.py"), "list"] + + +def _flow_stop_validate(raw): + if not isinstance(raw, dict): + raise OpError("args must be an object") + allowed = {"flow_id"} + for k in raw: + if k not in allowed: + raise OpError(f"unknown arg: {k}") + if not raw.get("flow_id"): + raise OpError("flow_id is required") + return {"flow_id": _flow_id_name(raw["flow_id"])} + + +def _flow_stop_build(a): + return [sys.executable, os.path.join(BIN_DIR, "flow_engine.py"), "stop", a["flow_id"]] + + + def _vars_list_validate(raw): if raw not in ({}, None): raise OpError('vars.list takes no required args') @@ -2279,6 +2394,31 @@ OPS = { 'timeout': 15, 'side_effecting': True, 'desc': 'Reap stale unattached sessions inactive for >TTL (default 2h)', }, + 'flow.start': { + 'validate': _flow_start_validate, 'build': _flow_start_build, + 'timeout': 15, 'side_effecting': True, + 'desc': 'Start an agentic workflow in a persistent tmux pane with output logging', + }, + 'flow.read': { + 'validate': _flow_read_validate, 'build': _flow_read_build, + 'timeout': 15, 'side_effecting': False, + 'desc': 'Read output delta and execution state (working/idle/waiting_prompt/finished) from a flow pane', + }, + 'flow.send': { + 'validate': _flow_send_validate, 'build': _flow_send_build, + 'timeout': 15, 'side_effecting': True, + 'desc': 'Send keystrokes or advance command in a flow tmux pane', + }, + 'flow.list': { + 'validate': _flow_list_validate, 'build': _flow_list_build, + 'timeout': 10, 'side_effecting': False, + 'desc': 'List all active agentic flow sessions and their statuses', + }, + 'flow.stop': { + 'validate': _flow_stop_validate, 'build': _flow_stop_build, + 'timeout': 15, 'side_effecting': True, + 'desc': 'Stop and terminate a flow tmux pane session', + }, 'exec.ping': { 'validate': _health_validate, 'build': lambda a: ['/bin/echo', 'PONG'], @@ -2377,7 +2517,8 @@ DEFAULT_PERMS = {'dm.read', 'dm.log', 'chat.messages', 'health.check', 'fleet.un 'thread.list', 'thread.view', 'exec.ping', 'git.status', 'git.diff', 'git.log', 'job.next', 'md.audit', 'md.list', 'md.read', 'md.diff', - 'approval.check', 'tmux.tally', 'tmux.auto_status', 'onboard.connects'} + 'approval.check', 'tmux.tally', 'tmux.auto_status', 'onboard.connects', + 'flow.read', 'flow.list'} def permitted(ident, op): diff --git a/bin/fleet-alert-check.sh b/bin/fleet-alert-check.sh index a88c3dc..9490139 100755 --- a/bin/fleet-alert-check.sh +++ b/bin/fleet-alert-check.sh @@ -42,6 +42,15 @@ REALERT_MIN="${FLEET_ALERT_REALERT_MIN:-30}" # forever. Overridable per environment. INPUT_WAIT_TTL="${FLEET_ALERT_INPUT_WAIT_TTL:-1800}" BROWSER_APPROVAL_TTL="${FLEET_ALERT_BROWSER_APPROVAL_TTL:-1800}" +# Routine input_wait task patterns (2026-10-08, P4): scheduled-task +# confirmations matching these (case-insensitive) are noise-grade +# housekeeping that auto-dismisses at TTL. They go to the digest +# (kind=DIGEST in the outbox; the #lobby relay ignores non-ALERT/ +# RECOVERY kinds) instead of paging CRITICAL. Anything NOT matching +# stays CRITICAL (fail-closed). Pipe-separated; overridable per +# environment. ALL of a node's waits must match for the node to +# classify as routine. +INPUT_WAIT_ROUTINE_PATTERNS="${FLEET_ALERT_INPUT_WAIT_ROUTINE:-scavenger|background worker|daily checkin|auto-work-queue}" QUIET_HOURS="${FLEET_ALERT_QUIET_HOURS:-}" DRY_RUN="${FLEET_ALERT_DRY_RUN:-0}" INJECT_FAIL="${FLEET_ALERT_INJECT_FAIL:-}" @@ -196,6 +205,48 @@ notify_input_wait() { fi } +input_wait_routine() { # -> prints 1 if ALL waits match routine patterns, else 0 + # P4 (2026-10-08): classify a node's input waits as routine (digest) + # or novel (CRITICAL). Fail-closed: empty/unparseable waits, empty + # patterns, regex errors, or ANY non-matching wait -> 0 (page it). + INPUT_WAIT_ROUTINE_PATTERNS="$INPUT_WAIT_ROUTINE_PATTERNS" python3 - "$1" <<'PYEOF' +import json, os, re, sys +pats = [p.strip() for p in os.environ.get("INPUT_WAIT_ROUTINE_PATTERNS", "").split("|") if p.strip()] +try: + waits = json.loads(sys.argv[1]).get("waits", []) +except Exception: + waits = [] +if not waits or not pats: + print(0) + sys.exit() +for w in waits: + task = w.get("task") or "" + try: + matched = any(re.search(p, task, re.I) for p in pats) + except re.error: + matched = False + if not matched: + print(0) + sys.exit() +print(1) +PYEOF +} + +target_plausible_false() { # -> prints 1 if target_plausible is explicitly false, else 0 + # P1 follow-up (2026-10-08): the approval target parser flags garbage + # tokens (e.g. "echo", "true") as target_plausible=false. Implausible + # targets go to the digest instead of paging CRITICAL. Fail-closed: + # missing field, null, non-boolean, or unparseable JSON -> 0 (page it). + python3 - "$1" <<'PYEOF_INNER' +import json, sys +try: + v = json.loads(sys.argv[1]).get("target_plausible") +except Exception: + v = None +print(1 if v is False else 0) +PYEOF_INNER +} + injected() { # cond -> 0 if injected-fail case ",$INJECT_FAIL," in *,"$1,"*) return 0;; *) return 1;; esac } @@ -241,6 +292,7 @@ info = approvals.inspect_node_approvals('$node') out = { 'has_pending': info.get('has_pending', False), 'target': info.get('target') or info.get('ip') or 'unknown', + 'target_plausible': info.get('target_plausible'), 'title': info.get('title') or '', 'waits': info.get('input_waits') or [] } @@ -262,8 +314,20 @@ print(json.dumps(out)) read -r action fails < <(state_machine "$cond" "$failing" "$BROWSER_APPROVAL_TTL") case "$action" in ALERT_FIRST|ALERT_REALERT) - emit_record "ALERT" "$cond" "$detail" "$fails" - echo "$cond" >> "$STATE_DIR/.alerts.tmp" + if [ "$(target_plausible_false "$node_data")" = "1" ]; then + # P1 follow-up (2026-10-08): implausible approval target + # (parser artifact, target_plausible=false) -> digest, don't + # page. kind=DIGEST is ignored by the #lobby relay; the + # triage digest consumer batches these. No .alerts.tmp + # entry, so no box_notify broadcast either — the digest is + # the only output. Missing/unparseable field -> CRITICAL + # (fail-closed; handled inside target_plausible_false). + emit_record "DIGEST" "$cond" "implausible target: $detail" "$fails" + log "$cond implausible target x$fails — digested, not paged" + else + emit_record "ALERT" "$cond" "$detail" "$fails" + echo "$cond" >> "$STATE_DIR/.alerts.tmp" + fi ;; RECOVERY) emit_record "RECOVERY" "$cond" "$detail" "$fails" @@ -308,8 +372,17 @@ if w: read -r action_in fails_in < <(state_machine "$cond_in" "$failing_in" "$INPUT_WAIT_TTL") case "$action_in" in ALERT_FIRST|ALERT_REALERT) - emit_record "ALERT" "$cond_in" "$detail_in" "$fails_in" - echo "$cond_in|$detail_in" >> "$STATE_DIR/.alerts.tmp" + if [ "$(input_wait_routine "$node_data")" = "1" ]; then + # P4 (2026-10-08): routine housekeeping -> digest, don't page. + # kind=DIGEST is ignored by the #lobby relay; the triage + # digest consumer batches these. No .alerts.tmp entry, so + # no targeted DM either — the digest is the only output. + emit_record "DIGEST" "$cond_in" "routine: $detail_in" "$fails_in" + log "$cond_in routine input_wait x$fails_in — digested, not paged" + else + emit_record "ALERT" "$cond_in" "$detail_in" "$fails_in" + echo "$cond_in|$detail_in" >> "$STATE_DIR/.alerts.tmp" + fi ;; RECOVERY) emit_record "RECOVERY" "$cond_in" "$detail_in" "$fails_in" diff --git a/bin/gravity.py b/bin/gravity.py index e399010..f26e094 100644 --- a/bin/gravity.py +++ b/bin/gravity.py @@ -364,12 +364,11 @@ DM_LOG_FILE = os.path.join(NETVM_ROOT, "dm-log.jsonl") JOB_LOG_FILE = os.path.join(NETVM_ROOT, "job-log.jsonl") -def reconstruct_loops(limit=50, agent=None, status_filter=None) -> list: - """Reconstruct active and recent loops from followups.json and dm-log.jsonl. +def _load_loop_candidates() -> dict: + """Parse followups.json + dm-log.jsonl into a loop_id -> dict map. - Returns a list of dicts: - loop_id, agent, sender, target, purpose, state, sent_at, deadline, - nudges_sent, nudges_allowed, escalate_to, tags, summary + Pure parse phase of reconstruct_loops, extracted so diagnose_breaks and + remediate_breaks can share one parse instead of re-reading the logs. """ loops = {} # loop_id -> dict @@ -499,6 +498,11 @@ def reconstruct_loops(limit=50, agent=None, status_filter=None) -> list: "source": "dm-log.jsonl", } + return loops + + +def _select_loops(loops: dict, limit=50, agent=None, status_filter=None) -> list: + """Filter/sort/limit a candidate map from _load_loop_candidates.""" # Filter and sort result = list(loops.values()) if agent: @@ -517,6 +521,17 @@ def reconstruct_loops(limit=50, agent=None, status_filter=None) -> list: return result[:limit] +def reconstruct_loops(limit=50, agent=None, status_filter=None) -> list: + """Reconstruct active and recent loops from followups.json and dm-log.jsonl. + + Returns a list of dicts: + loop_id, agent, sender, target, purpose, state, sent_at, deadline, + nudges_sent, nudges_allowed, escalate_to, tags, summary + """ + return _select_loops(_load_loop_candidates(), limit=limit, agent=agent, + status_filter=status_filter) + + def get_fleet_loop_health(threshold=None) -> dict: """Calculate fleet loop health per agent and overall verdict.""" if threshold is None: @@ -570,8 +585,14 @@ def get_fleet_loop_health(threshold=None) -> dict: } -def diagnose_breaks() -> list: - """Diagnose break taxonomy across intrinsic loops and support services.""" +def diagnose_breaks(_fleet_cache=None, _loops_cache=None) -> list: + """Diagnose break taxonomy across intrinsic loops and support services. + + _fleet_cache: optional list; when given, the fleet approval scan result + is appended so callers (remediate_breaks) can reuse it instead of + re-scanning (each scan fans 6 nodes over the full audit log). + _loops_cache: optional list; when given, the parsed loop-candidate map + is appended for the same single-parse sharing.""" import subprocess breaks = [] @@ -609,7 +630,10 @@ def diagnose_breaks() -> list: }) # 3. Active follow-up loops check - active_loops = reconstruct_loops(limit=20, status_filter="pending") + _loops_map = _load_loop_candidates() + if _loops_cache is not None: + _loops_cache.append(_loops_map) + active_loops = _select_loops(_loops_map, limit=20, status_filter="pending") now_ts = time.time() for l in active_loops: nudges_sent = l.get("nudges_sent", 0) @@ -628,6 +652,8 @@ def diagnose_breaks() -> list: try: import approvals fleet_apps = approvals.check_fleet_approvals() + if _fleet_cache is not None: + _fleet_cache.append(fleet_apps) for app in fleet_apps: if app.get("has_pending"): node = app["node"] @@ -734,7 +760,9 @@ def remediate_breaks(dry_run=False) -> dict: escalated = [] # 1. Check diagnosed hard breaks first - breaks = diagnose_breaks() + _fleet_cache = [] + _loops_cache = [] + breaks = diagnose_breaks(_fleet_cache=_fleet_cache, _loops_cache=_loops_cache) for b in breaks: if b.get("severity") in ("CRITICAL", "WARNING"): escalated.append(b) @@ -753,8 +781,13 @@ def remediate_breaks(dry_run=False) -> dict: now_iso = datetime.now(timezone.utc).isoformat() - # Build answer map from reconstruct_loops - loops = reconstruct_loops(limit=200) + # Build answer map from reconstruct_loops (reuse diagnose's parse: + # nothing between the parses writes the loop logs in-process, and a + # concurrently landed reply is picked up on the next cycle). + if _loops_cache: + loops = _select_loops(_loops_cache[0], limit=200) + else: + loops = reconstruct_loops(limit=200) answered_dms = { l["loop_id"]: l for l in loops if l.get("state") in ("ANSWERED", "CLOSED") } @@ -836,7 +869,13 @@ def remediate_breaks(dry_run=False) -> dict: # Auto-remediate trusted approval blocks try: import approvals - fleet_apps = approvals.check_fleet_approvals() + # Reuse the diagnose_breaks scan: nothing between the scans touches + # browser-approval state, and this block only reads it. Fall back to + # a fresh scan if the first one failed. + if _fleet_cache: + fleet_apps = _fleet_cache[0] + else: + fleet_apps = approvals.check_fleet_approvals() for app in fleet_apps: if app.get("has_pending") and app.get("is_trusted") and app.get("status") != "KEY_APPROVAL": node = app["node"] diff --git a/bin/job-dispatch.py b/bin/job-dispatch.py index 374f603..8e088b2 100755 --- a/bin/job-dispatch.py +++ b/bin/job-dispatch.py @@ -41,6 +41,13 @@ NETVM_EXEC = "/home/super/Projects/NetVM/bin/netvm-exec.sh" JOB_LOG = NETVM_ROOT / "job-log.jsonl" SIDECHAT_STATE = NETVM_ROOT / "job-sidechats.json" +# Sidechat rotation: persistent reuse_key threads accumulate full history +# and every dispatch re-sends it (cloud context), so a stale thread burns +# full-thread tokens per nod. Cap counted threads by dispatch budget and +# flush uncounted legacy threads past the age cap. +SIDECHAT_MAX_DISPATCHES = 48 +SIDECHAT_LEGACY_MAX_AGE_HOURS = 24 + def load_sidechat_state(): if SIDECHAT_STATE.exists(): try: @@ -54,6 +61,40 @@ def save_sidechat_state(state): tmp.write_text(json.dumps(state, indent=2)) tmp.replace(SIDECHAT_STATE) + +def should_rotate_sidechat(record, current_title, now=None, + max_dispatches=SIDECHAT_MAX_DISPATCHES, + legacy_max_age_hours=SIDECHAT_LEGACY_MAX_AGE_HOURS): + """Decide whether a reused sidechat must rotate to a fresh thread. + + Returns (rotate, reason). Rotates when the dispatch budget is spent, + the rendered title moved on (daily {date} templates), or an + uncounted legacy record is past the age cap. Anything unassessable + (plain-UUID records, missing/unparseable age) fails open to reuse. + """ + now = now or datetime.now(timezone.utc) + if not isinstance(record, dict): + return False, "unrecorded" + count = record.get("dispatch_count") + if isinstance(count, int) and count >= max_dispatches: + return True, f"dispatch budget spent ({count}/{max_dispatches})" + stored_title = record.get("title") or "" + ALLOW_SIDECHAT_TITLE_ROTATION = False + if ALLOW_SIDECHAT_TITLE_ROTATION and stored_title and current_title and stored_title != current_title: + return True, f"title rolled over ({stored_title} -> {current_title})" + if count is None: + created = record.get("created_at") + if created: + try: + age_h = (now - datetime.fromisoformat( + str(created).replace("Z", "+00:00"))).total_seconds() / 3600 + except Exception: + return False, "unparseable age" + if age_h > legacy_max_age_hours: + return True, (f"predates counting, age {age_h:.0f}h " + f"over {legacy_max_age_hours}h cap") + return False, "within budget" + def extract_uuid(url): m = re.search(r"/thread/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", url or "") return m.group(1) if m else None @@ -88,6 +129,65 @@ try: except ImportError: HAS_RATE_LIMITER = False +# Dispatch backpressure (2026-10-09): skip jobs for frozen agents instead of +# piling input-waits onto them. See tests/test_dispatch_hold.py. +DISPATCH_HOLD_FILE = JOBS_DIR / "dispatch-hold.json" +HOLD_WAIT_THRESHOLD = 3 +HOLD_WAIT_WINDOW_MIN = 60 + + +def dispatch_hold_reason(agent, now=None, hold_path=None, job_log_path=None): + # Hold reason if dispatch to agent must be skipped, else None. + # Explicit operator holds win; otherwise auto-hold after repeated waits. + from datetime import timedelta + now = now or datetime.now(timezone.utc) + try: + with open(hold_path or DISPATCH_HOLD_FILE) as f: + holds = json.load(f) + except (OSError, ValueError): + holds = {} + entry = holds.get(agent) if isinstance(holds, dict) else None + if isinstance(entry, dict): + until = entry.get("until") + if until: + try: + exp = datetime.fromisoformat(until) + if exp.tzinfo is None: + exp = exp.replace(tzinfo=timezone.utc) + except ValueError: + exp = None + if exp is not None and exp <= now: + entry = None + if entry is not None: + return "explicit hold (%s)" % entry.get("reason", "operator") + try: + cutoff = now - timedelta(minutes=HOLD_WAIT_WINDOW_MIN) + n = 0 + with open(job_log_path or JOB_LOG) as f: + for line in f: + try: + r = json.loads(line) + except ValueError: + continue + if r.get("type") != "job_dispatch_agent_input_wait": + continue + if r.get("agent") != agent: + continue + try: + ts = datetime.fromisoformat(r.get("ts", "")) + except ValueError: + continue + if ts.tzinfo is None: + ts = ts.replace(tzinfo=timezone.utc) + if ts >= cutoff: + n += 1 + if n >= HOLD_WAIT_THRESHOLD: + return "auto-hold (%d input-waits in last %dm)" % (n, HOLD_WAIT_WINDOW_MIN) + except OSError: + pass + return None + + def log_event(event_type, data): """Append event to job-log.jsonl""" entry = { @@ -395,6 +495,13 @@ def main(): # Load job job = load_job(job_name) + # Backpressure: skip frozen agents before arming follow-ups or sending. + _hold = dispatch_hold_reason(job.get("agent")) + if _hold: + print("Held: job %s for %s skipped (%s)." % (job_name, job.get("agent"), _hold), file=sys.stderr) + log_event("job_dispatch_held", {"job_name": job_name, "agent": job.get("agent"), "reason": _hold}) + sys.exit(0) + # Generate job_id job_id = f"{job_name}-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}-{uuid.uuid4().hex[:8]}" @@ -461,19 +568,33 @@ def main(): # Check if reuse_key exists in job-sidechats.json and thread is still alive sc_state = load_sidechat_state() reused_uuid = None + rotated_from = None if reuse_key and reuse_key in sc_state: val = sc_state[reuse_key] cand_uuid = val.get("thread_uuid") if isinstance(val, dict) else val if cand_uuid: - try: - import muse_hybrid - threads, err = muse_hybrid.get_threads(agent) - if not err and threads: - thread_ids = [t.get("session_id") for t in threads] - if cand_uuid in thread_ids: - reused_uuid = cand_uuid - except Exception: - pass + rotate, reason = should_rotate_sidechat(val, sc_name) + if rotate: + print(f"Rotating sidechat '{reuse_key}': {reason}") + log_event("job_sidechat_rotate", { + "job_name": job_name, "job_id": job_id, + "reuse_key": reuse_key, "old_thread": cand_uuid, + "reason": reason, + }) + rotated_from = cand_uuid + else: + try: + import muse_hybrid + threads, err = muse_hybrid.get_threads(agent) + if not err and threads: + thread_ids = [t.get("session_id") for t in threads] + if cand_uuid in thread_ids: + reused_uuid = cand_uuid + except Exception: + pass + if reused_uuid and isinstance(val, dict): + val["dispatch_count"] = val.get("dispatch_count", 0) + 1 + save_sidechat_state(sc_state) if reused_uuid: target = reused_uuid @@ -487,13 +608,19 @@ def main(): new_uuid = res.get("session_id") key_to_save = reuse_key or sc_name is_persistent = bool(reuse_key) - sc_state[key_to_save] = { + new_record = { "thread_uuid": new_uuid, "agent": agent, "title": channel_title, "type": "persistent" if is_persistent else "ephemeral", - "created_at": datetime.now(timezone.utc).isoformat() + "created_at": datetime.now(timezone.utc).isoformat(), + "dispatch_count": 1, } + if rotated_from: + new_record["rotated_from"] = rotated_from + new_record["rotated_at"] = datetime.now( + timezone.utc).isoformat() + sc_state[key_to_save] = new_record save_sidechat_state(sc_state) target = new_uuid print(f"Spawned new sidechat channel '{channel_title}' ({new_uuid}) for {agent}") diff --git a/bin/kpi.py b/bin/kpi.py index 86ec224..acc49dd 100755 --- a/bin/kpi.py +++ b/bin/kpi.py @@ -281,8 +281,9 @@ def generate_preservation_advisory( tips = [] pct = weekly_used_pct or 0 - if pct >= 95 or "0 tokens left" in extra_tokens_remaining: - return "CRITICAL: Quota exhausted. Do NOT send chat messages. Salvage via 'box onboard start --for %s'." % node + is_bonus_empty = ("0 tokens left" in extra_tokens_remaining) or (not extra_tokens_remaining) + if pct >= 95 and is_bonus_empty: + return "CRITICAL: Quota exhausted. Salvage via 'box onboard start --for %s'." % node if pct >= 70: tips.append("Quota > 70%%: Cease prose chatter; offload tasks to background tmux workers.") diff --git a/bin/muse-chat-api.py b/bin/muse-chat-api.py index 2a0288d..58a86d0 100755 --- a/bin/muse-chat-api.py +++ b/bin/muse-chat-api.py @@ -117,6 +117,27 @@ def ev(ws, expr, await_p=False): print(f"CDP evaluate failed: {type(e).__name__}: {e}", file=sys.stderr) return None + +def _is_valid_ipv4(ip: str) -> bool: + """Strict IPv4 validation: four octets, each 0-255, no leading zeros. + + P1 fix (2026-10-08): the old \d{1,3} pattern matched invalid IPs like + 999.999.999.999 and version strings. Only strict IPv4 passes. + """ + if not ip or not isinstance(ip, str): + return False + parts = ip.split(".") + if len(parts) != 4: + return False + try: + return all( + 0 <= int(part) <= 255 and part == str(int(part)) + for part in parts + ) + except ValueError: + return False + + def check_approvals(ws): """ Check for browser permission dialogs. @@ -175,7 +196,7 @@ def check_approvals(ws): for d in dialogs: # Extract IP if present import re - ips = re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', d) + ips = [ip for ip in re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', d) if _is_valid_ipv4(ip)] # Check trust: if IP present, must be in TRUSTED_IPS; if no IP, untrusted approval dialog if ips: is_trusted = any(ip in TRUSTED_IPS for ip in ips) diff --git a/bin/muse-tui.py b/bin/muse-tui.py index 56e3052..cb4268a 100755 --- a/bin/muse-tui.py +++ b/bin/muse-tui.py @@ -16,6 +16,7 @@ Dual-mode interface: - Job Scheduler & Dispatch trigger - Background Tmux sessions & Swarm worker monitor - Live Event & DM log tailer + - Container SSH tunnel health & tmux pop-out dialer """ import sys @@ -27,6 +28,7 @@ import threading import subprocess import hashlib import select +import shlex import signal import textwrap import urllib.request @@ -169,6 +171,85 @@ def format_recency(ts: float) -> str: return "never" +# --------------------------------------------------------------------------- +# SSH / Container Tunnel Management Subsystem +# --------------------------------------------------------------------------- +SSH_JUMP_HOST = os.environ.get("SSH_JUMP_HOST", "34.139.37.135") +SSH_OPERATOR_USER = os.environ.get("OPERATOR_USER", "super") +SSH_IDENTITY_FILE = os.environ.get("SSH_IDENTITY_FILE", "") + +try: + from agent_md import TUNNEL_PORTS as SSH_TUNNEL_PORTS +except Exception: + SSH_TUNNEL_PORTS = { + "muse-main": {"port": 2224, "terminal": 7681, "user": "muse"}, + "muse": {"port": 2225, "terminal": 7682, "user": "hatch"}, + "646": {"port": 2226, "terminal": 7683, "user": "hatch"}, + "pip": {"port": 2227, "terminal": 7684, "user": "hatch"}, + "opm": {"port": 2228, "terminal": 7685, "user": "hatch"}, + "def": {"port": 2229, "terminal": 7686, "user": "hatch"}, + "dev": {"port": 2230, "terminal": 7687, "user": "hatch"}, + } + + +def build_ssh_dial_command(account: str, port=None, user=None, jump_host=None, + operator_user=None, identity_file=None, + ssh_options=None, remote_command=None) -> list: + """Build the jump-host dial argv for an agent container. + + ssh_options are inserted before the destination; remote_command (str or + list) is appended after it for non-interactive probes. + """ + info = SSH_TUNNEL_PORTS.get(account, {}) + port = port or info.get("port") + user = user or info.get("user", "hatch") + jump_host = jump_host or SSH_JUMP_HOST + operator_user = operator_user or SSH_OPERATOR_USER + if identity_file is None: + identity_file = SSH_IDENTITY_FILE + cmd = ["ssh", "-o", "StrictHostKeyChecking=no"] + if identity_file: + cmd += ["-o", "IdentitiesOnly=yes", "-i", identity_file] + if ssh_options: + cmd += list(ssh_options) + cmd += ["-J", f"{operator_user}@{jump_host}", "-p", str(port), f"{user}@localhost"] + if remote_command: + cmd += [remote_command] if isinstance(remote_command, str) else list(remote_command) + return cmd + + +def build_ssh_dial_string(account: str, **kwargs) -> str: + """Shell-quoted dial command for display, clipboard copy, and pop-out.""" + return " ".join(shlex.quote(p) for p in build_ssh_dial_command(account, **kwargs)) + + +def build_ssh_popout_shell(account: str, **kwargs) -> str: + """Interactive shell line for the pop-out window: ssh, then keep a shell.""" + dial = build_ssh_dial_string(account, **kwargs) + return f"{dial}; echo '[ssh exited ($?) — window kept open, exit to close]'; exec \"${{SHELL:-/bin/bash}}\"" + + +def build_tmux_popout_command(label: str, shell_command: str, socket_path: str = None) -> list: + """Build `tmux new-window` argv opening shell_command in a fresh window.""" + safe_label = re.sub(r"[^A-Za-z0-9_.-]", "-", label)[:32] or "ssh" + cmd = ["tmux"] + if socket_path: + cmd += ["-S", socket_path] + return cmd + ["new-window", "-n", safe_label, shell_command] + + +def ssh_row_order(nodes: list, extra_accounts=()) -> list: + """Fleet nodes first, then any extra tunnel accounts (e.g. muse-main).""" + rows = list(nodes) + for acct in extra_accounts: + if acct not in rows: + rows.append(acct) + for acct in SSH_TUNNEL_PORTS: + if acct not in rows: + rows.append(acct) + return rows + + # --------------------------------------------------------------------------- # Prompt & Skill Library Subsystem # --------------------------------------------------------------------------- @@ -337,6 +418,31 @@ class PromptManager: return False +# --------------------------------------------------------------------------- +# Box Mode Tab Bar (single source of truth for renderer + click handler) +# --------------------------------------------------------------------------- +BOX_TABS = [ + "1: Agent Chat", + "2: Fleet Status", + "3: Approvals", + "4: Jobs Scheduler", + "5: Tmux / Swarms", + "6: DM Logs", + "7: SSH / Boxes", +] + + +def box_tab_bounds(tabs=None, x: int = 0) -> list: + """Clickable x-ranges for the Box tab bar, mirroring _render_box_tabs.""" + bounds = [] + cur_x = x + 1 + for tab_name in (tabs if tabs is not None else BOX_TABS): + label = f" [{tab_name}] " + bounds.append((cur_x, cur_x + len(label) - 1)) + cur_x += len(label) + 1 + return bounds + + # --------------------------------------------------------------------------- # Data Layer & Async Poller # --------------------------------------------------------------------------- @@ -368,6 +474,13 @@ class FleetDataManager: self.tmux_cache = [] self.dm_logs_cache = [] + # SSH / container tunnel health (Box tab 7) + self.ssh_cache = {} # account -> health dict from ssh-check + state_since + self.ssh_jump_reachable = None # None = never checked + self.ssh_checked_at = 0.0 + self.ssh_check_latency_ms = None + self.ssh_check_error = "" + # Interaction ranking: node -> float timestamp of last true input / chat [insert] self.agent_interactions = {n: 0.0 for n in self.nodes} self._load_agent_interactions() @@ -405,6 +518,8 @@ class FleetDataManager: self.preload_priority_chats(self.active_node, sidechat_limit=0) self.poller_thread = threading.Thread(target=self._worker_loop, daemon=True) self.poller_thread.start() + # First SSH sweep in background so Box tab 7 is warm on open + threading.Thread(target=self._fetch_ssh_health, daemon=True).start() else: self.poller_thread = None @@ -888,6 +1003,7 @@ class FleetDataManager: last_med = 0.0 last_slow = 0.0 last_fleet_approvals = 0.0 + last_ssh = 0.0 # Initial fetch of active node main chat only with self.lock: @@ -950,6 +1066,11 @@ class FleetDataManager: self._fetch_dm_logs() last_slow = now + # 5. SSH tunnel health (every 30s): single VM-side sweep + if (now - last_ssh >= 30.0): + self._fetch_ssh_health() + last_ssh = now + # Sleep in short increments to allow prompt wakeup on user actions for _ in range(10): if not self.running or (hasattr(self, 'user_poll_trigger') and self.user_poll_trigger.is_set()): @@ -1181,6 +1302,68 @@ class FleetDataManager: except Exception: pass + def _apply_ssh_check_result(self, data: dict, now: float = None): + """Merge one ssh-check payload into ssh_cache with flap tracking. + + state_since records the last (ssh_up, term_up) transition per + account so the SSH view can show uptime/downtime durations. + """ + now = now if now is not None else time.time() + if not isinstance(data, dict): + return + accounts = data.get("accounts", {}) + if not isinstance(accounts, dict): + accounts = {} + with self.lock: + self.ssh_jump_reachable = data.get("jump_reachable") + self.ssh_checked_at = now + self.ssh_check_latency_ms = data.get("latency_ms") + self.ssh_check_error = "" if data.get("jump_reachable") else str(data.get("error", "")) + for acct, info in accounts.items(): + if not isinstance(info, dict): + continue + prev = self.ssh_cache.get(acct, {}) + entry = dict(info) + prev_state = (prev.get("ssh_up"), prev.get("term_up")) + new_state = (entry.get("ssh_up"), entry.get("term_up")) + if prev_state != new_state or "state_since" not in prev: + entry["state_since"] = now + entry["prev_ssh_up"] = prev.get("ssh_up") + else: + entry["state_since"] = prev.get("state_since", now) + entry["prev_ssh_up"] = prev.get("prev_ssh_up") + self.ssh_cache[acct] = entry + + def _fetch_ssh_health(self): + """Run box-ctl ssh-check (single VM-side sweep) and merge results.""" + try: + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-check"] + res = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if res.returncode == 0: + try: + data = json.loads(res.stdout) + except Exception: + return + if isinstance(data, dict) and data.get("ok"): + self._apply_ssh_check_result(data) + except Exception: + pass + + def probe_container_uptime(self, account: str) -> tuple[bool, str]: + """On-demand end-to-end probe: run `uptime` inside the container.""" + if account not in SSH_TUNNEL_PORTS: + return False, f"No tunnel registered for '{account}'" + cmd = build_ssh_dial_command( + account, + ssh_options=["-o", "BatchMode=yes", "-o", "ConnectTimeout=12"], + remote_command="uptime", + ) + rc, stdout, stderr = run_command_isolated(cmd, timeout=25.0) + if rc == 0 and (stdout or "").strip(): + return True, stdout.strip() + err_lines = (stderr or stdout or f"exit {rc}").strip().splitlines() + return False, (err_lines[-1] if err_lines else f"exit {rc}")[:200] + def _fetch_dm_logs(self): log_path = REPO_ROOT / "dm-log.jsonl" if not log_path.exists(): @@ -1362,10 +1545,13 @@ class FleetDataManager: class MuseTUI: """Full-terminal curses application supporting Muse and Box operational modes.""" - def __init__(self, stdscr, initial_mode="muse", initial_node=None, initial_thread=None): + def __init__(self, stdscr, initial_mode="muse", initial_node=None, initial_thread=None, initial_tab=None): self.stdscr = stdscr self.mode = initial_mode # "muse" or "box" - self.box_tab = 0 # 0: Chat, 1: Fleet, 2: Approvals, 3: Jobs, 4: Tmux, 5: Logs + self.box_tab = 0 # 0: Chat, 1: Fleet, 2: Approvals, 3: Jobs, 4: Tmux, 5: Logs, 6: SSH + if initial_mode == "box" and initial_tab is not None: + tab_map = {"chat": 0, "fleet": 1, "approvals": 2, "jobs": 3, "tmux": 4, "logs": 5, "ssh": 6} + self.box_tab = tab_map.get(str(initial_tab).lower(), 0) self.data = FleetDataManager() # Selection state: default to top of sorted list (highest unread / most recently interacted) @@ -1408,6 +1594,8 @@ class MuseTUI: self.jobs_sel_idx = 0 self.jobs_scroll_start = 0 self.tmux_sel_idx = 0 + self.ssh_sel_idx = 0 + self.ssh_scroll_idx = 0 self.table_scroll_idx = 0 # Input buffer @@ -1674,6 +1862,8 @@ class MuseTUI: self._render_tmux_view(content_y, 0, content_h, w) elif self.box_tab == 5: self._render_dm_logs_view(content_y, 0, content_h, w) + elif self.box_tab == 6: + self._render_ssh_view(content_y, 0, content_h, w) # 4. Bottom Input Bar & Toast self._render_bottom_bar(h - bottom_bar_h, 0, bottom_bar_h, w) @@ -1764,14 +1954,7 @@ class MuseTUI: self.safe_addstr(self.stdscr, y, w - len(clock) - 2, clock, self._attr("header")) def _render_box_tabs(self, y: int, x: int, w: int): - tabs = [ - "1: Agent Chat", - "2: Fleet Status", - "3: Approvals", - "4: Jobs Scheduler", - "5: Tmux / Swarms", - "6: DM Logs", - ] + tabs = BOX_TABS self.safe_addstr(self.stdscr, y, x, " " * w, self._attr("dim")) cur_x = x + 1 for idx, tab_name in enumerate(tabs): @@ -2839,6 +3022,106 @@ class MuseTUI: self.safe_addstr(self.stdscr, row_y, x + 2, line_str, color) row_y += 1 + def get_ssh_rows(self) -> list: + """SSH view row order: fleet nodes first, then extra tunnel accounts.""" + with self.data.lock: + extras = list(self.data.ssh_cache.keys()) + nodes = list(self.data.nodes) + return ssh_row_order(nodes, extras) + + def _render_ssh_view(self, y: int, x: int, h: int, w: int): + self.safe_addstr(self.stdscr, y, x + 1, f"CONTAINER SSH TUNNEL HEALTH (jump: {SSH_OPERATOR_USER}@{SSH_JUMP_HOST})", self._attr("bold")) + + with self.data.lock: + jump = self.data.ssh_jump_reachable + checked_at = self.data.ssh_checked_at + sweep_ms = self.data.ssh_check_latency_ms + check_err = self.data.ssh_check_error + ssh_cache = dict(self.data.ssh_cache) + + if jump is None: + jump_txt, jump_attr = "sweep pending…", self._attr("dim") + elif jump: + jump_txt = f"jump OK (sweep {sweep_ms}ms, checked {format_recency(checked_at)})" + jump_attr = self._attr("success") + else: + jump_txt = f"jump UNREACHABLE ({(check_err or 'unknown')[:w - 24]})" + jump_attr = self._attr("danger") + self.safe_addstr(self.stdscr, y + 1, x + 1, jump_txt[:w - 2], jump_attr) + self.safe_addstr(self.stdscr, y + 2, x + 1, " ACCOUNT SSH PORT SSH STATE LAT SSH BANNER / HOSTKEY TERM PORT TERM STATE SINCE", self._attr("dim")) + self.safe_addstr(self.stdscr, y + 3, x + 1, "─" * (w - 2), self._attr("dim")) + + rows = self.get_ssh_rows() + if self.ssh_sel_idx >= len(rows): + self.ssh_sel_idx = max(0, len(rows) - 1) + + visible_rows = max(3, h - 6) + scroll_start = getattr(self, "ssh_scroll_idx", 0) + if self.ssh_sel_idx < scroll_start: + scroll_start = self.ssh_sel_idx + elif self.ssh_sel_idx >= scroll_start + visible_rows: + scroll_start = self.ssh_sel_idx - visible_rows + 1 + scroll_start = max(0, min(scroll_start, max(0, len(rows) - visible_rows))) + self.ssh_scroll_idx = scroll_start + + row_y = y + 4 + for row_i in range(visible_rows): + idx = scroll_start + row_i + if idx >= len(rows): + break + acct = rows[idx] + is_sel = (idx == self.ssh_sel_idx) + row_attr = self._attr("selected") if is_sel else self._attr("normal") + info = SSH_TUNNEL_PORTS.get(acct, {}) + health = ssh_cache.get(acct, {}) + sport = info.get("port", "?") + tport = info.get("terminal", "?") + + ssh_up = health.get("ssh_up") + term_up = health.get("term_up") + if ssh_up is True: + ssh_txt, ssh_attr = "UP ", self._attr("success") + elif ssh_up is False: + ssh_txt, ssh_attr = "DOWN", self._attr("danger") + else: + ssh_txt, ssh_attr = "?? ", self._attr("dim") + if term_up is True: + term_txt, term_attr = "UP ", self._attr("success") + elif term_up is False: + term_txt, term_attr = "DOWN", self._attr("danger") + else: + term_txt, term_attr = "?? ", self._attr("dim") + if is_sel: + ssh_attr = row_attr + term_attr = row_attr + + lat = health.get("ssh_latency_ms") + lat_txt = f"{lat}ms" if lat is not None else "--" + banner = (health.get("ssh_banner") or health.get("term_http") or "-").strip() or "-" + since_ts = health.get("state_since", 0.0) + if ssh_up is None and term_up is None: + since_txt = "never checked" if jump is None else "unknown" + else: + since_txt = f"{'up' if ssh_up else 'down'} {format_recency(since_ts)}" + + head = "▶ " if is_sel else " " + name_attr = row_attr if is_sel else self._attr("bold") + self.safe_addstr(self.stdscr, row_y, x + 1, f"{head}{acct:<10}"[:12], name_attr) + self.safe_addstr(self.stdscr, row_y, x + 13, f":{sport:<8}", row_attr if is_sel else self._attr("dim")) + self.safe_addstr(self.stdscr, row_y, x + 23, ssh_txt, ssh_attr) + self.safe_addstr(self.stdscr, row_y, x + 32, f"{lat_txt:<7}", row_attr if is_sel else self._attr("dim")) + self.safe_addstr(self.stdscr, row_y, x + 40, banner[:26].ljust(26), row_attr if is_sel else self._attr("normal")) + self.safe_addstr(self.stdscr, row_y, x + 67, f":{tport:<8}", row_attr if is_sel else self._attr("dim")) + self.safe_addstr(self.stdscr, row_y, x + 77, term_txt, term_attr) + self.safe_addstr(self.stdscr, row_y, x + 83, since_txt[:w - 84 - 8], row_attr if is_sel else self._attr("dim")) + self.safe_addstr(self.stdscr, row_y, max(x + 90, w - 8), "[SSH]", self._attr("wo_badge") if is_sel else self._attr("dim")) + row_y += 1 + + hint_y = y + h - 1 + sel_acct = rows[self.ssh_sel_idx].upper() if rows else "-" + hints = f"Selected: [{sel_acct}] [Enter/s]: SSH pop-out (tmux) [c]: Copy dial [u]: Container uptime [r]: Refresh [j/k]: Nav" + self.safe_addstr(self.stdscr, hint_y, x + 1, hints[:w - 2], self._attr("dim")) + # ----------------------------------------------------------------------- # Chat History Sends Search & Prompts Subsystem # ----------------------------------------------------------------------- @@ -3210,6 +3493,8 @@ class MuseTUI: ("[w] or [/wo]", "Compose and cryptographically sign a Work Order"), ("[a] or [F2]", "Open Approvals Resolution Drawer (Allow, Always, Deny)"), ("[F5] or [m]", "Toggle between Muse Chat TUI and Box Fleet Command TUI"), + ("[1]-[7] (Box)", "Switch Box tabs: Chat/Fleet/Approvals/Jobs/Tmux/Logs/SSH"), + ("[Tab 7: SSH]", "Enter/s: tmux pop-out c: copy dial u: container uptime r: refresh"), ("[g] / [G] / [Home/End]", "Jump to oldest message / follow live latest message"), ("[q]", "Quit TUI (in NORMAL mode)"), ] @@ -4247,6 +4532,30 @@ class MuseTUI: self.toggle_transcript_style() return True + # Box mode Fast Actions: Tab 6 (SSH) — placed before the 's'/'c'/'y' + # globals below so SSH keys win on this tab. + if self.mode == "box" and self.box_tab == 6: + if ch in (curses.KEY_ENTER, 10, 13): + self._ssh_popout_selected() + return True + elif ch in (ord('s'), ord('S')): + self._ssh_popout_selected() + return True + elif ch in (ord('c'), ord('C')): + self._ssh_copy_dial_selected() + return True + elif ch in (ord('u'), ord('U')): + rows = self.get_ssh_rows() + if rows and 0 <= self.ssh_sel_idx < len(rows): + acct = rows[self.ssh_sel_idx] + threading.Thread(target=self._async_ssh_uptime, args=(acct,), daemon=True).start() + self.set_toast(f"Probing container uptime on {acct}...", "info") + return True + elif ch in (ord('r'), ord('R')): + threading.Thread(target=self.data._fetch_ssh_health, daemon=True).start() + self.set_toast("Probing SSH tunnels via jump host...", "info") + return True + # Open Context Menu for active sidebar thread, fleet agent, or message: 'x', 'c', or Space if ch in (ord('x'), ord('X'), ord('c'), ord('C'), ord(' ')) and not (self.mode == "box" and self.box_tab == 2): if self.focus_pane == "fleet": @@ -4564,7 +4873,7 @@ class MuseTUI: threading.Thread(target=self._async_kill_tmux, args=(sess_name,), daemon=True).start() return True elif ch in (ord('r'), ord('R')): - threading.Thread(target=self.data._fetch_tmux, daemon=True).start() + threading.Thread(target=self.data._fetch_tmux_sessions, daemon=True).start() self.set_toast("Refreshed tmux background sessions.", "info") return True @@ -4613,29 +4922,29 @@ class MuseTUI: self.set_toast(f"Switched to agent: {node.upper()}", "success") return True - # Box mode tab selection: '1' - '6' (when in box mode, except 1-3 on Tab 2) + # Box mode tab selection: '1' - '7' (when in box mode, except 1-3 on Tab 2) if self.mode == "box": if self.box_tab == 2: - if ord('4') <= ch <= ord('6'): + if ord('4') <= ch <= ord('7'): self.box_tab = ch - ord('1') return True - elif ord('1') <= ch <= ord('6'): + elif ord('1') <= ch <= ord('7'): self.box_tab = ch - ord('1') return True # Box mode tab navigation (when not in Chat tab 0): '[' / ']' / Tab / Shift-Tab if self.mode == "box" and self.box_tab != 0: if ch in (ord('['), curses.KEY_LEFT): - self.box_tab = (self.box_tab - 1) % 6 + self.box_tab = (self.box_tab - 1) % 7 return True elif ch in (ord(']'), curses.KEY_RIGHT): - self.box_tab = (self.box_tab + 1) % 6 + self.box_tab = (self.box_tab + 1) % 7 return True elif ch == ord('\t'): - self.box_tab = (self.box_tab + 1) % 6 + self.box_tab = (self.box_tab + 1) % 7 return True elif ch == curses.KEY_BTAB: - self.box_tab = (self.box_tab - 1) % 6 + self.box_tab = (self.box_tab - 1) % 7 return True # Muse View / Chat Tab: Direct Conversation Cycling & Pane Switching @@ -4722,6 +5031,8 @@ class MuseTUI: self.jobs_sel_idx = max(0, self.jobs_sel_idx - 1) elif self.box_tab == 4: self.tmux_sel_idx = max(0, self.tmux_sel_idx - 1) + elif self.box_tab == 6: + self.ssh_sel_idx = max(0, self.ssh_sel_idx - 1) else: self.table_scroll_idx = max(0, self.table_scroll_idx - 1) return True @@ -4764,6 +5075,10 @@ class MuseTUI: sessions = list(self.data.tmux_cache) if sessions: self.tmux_sel_idx = min(len(sessions) - 1, self.tmux_sel_idx + 1) + elif self.box_tab == 6: + rows = self.get_ssh_rows() + if rows: + self.ssh_sel_idx = min(len(rows) - 1, self.ssh_sel_idx + 1) else: self.table_scroll_idx += 1 return True @@ -4784,6 +5099,8 @@ class MuseTUI: self.jobs_sel_idx = max(0, self.jobs_sel_idx - 5) elif self.box_tab == 4: self.tmux_sel_idx = max(0, self.tmux_sel_idx - 5) + elif self.box_tab == 6: + self.ssh_sel_idx = max(0, self.ssh_sel_idx - 5) else: self.table_scroll_idx = max(0, self.table_scroll_idx - 5) return True @@ -4812,6 +5129,10 @@ class MuseTUI: sessions = list(self.data.tmux_cache) if sessions: self.tmux_sel_idx = min(len(sessions) - 1, self.tmux_sel_idx + 5) + elif self.box_tab == 6: + rows = self.get_ssh_rows() + if rows: + self.ssh_sel_idx = min(len(rows) - 1, self.ssh_sel_idx + 5) else: self.table_scroll_idx += 5 return True @@ -4837,6 +5158,8 @@ class MuseTUI: self.jobs_sel_idx = 0 elif self.box_tab == 4: self.tmux_sel_idx = 0 + elif self.box_tab == 6: + self.ssh_sel_idx = 0 else: self.table_scroll_idx = 0 return True @@ -4876,6 +5199,10 @@ class MuseTUI: sessions = list(self.data.tmux_cache) if sessions: self.tmux_sel_idx = max(0, len(sessions) - 1) + elif self.box_tab == 6: + rows = self.get_ssh_rows() + if rows: + self.ssh_sel_idx = max(0, len(rows) - 1) else: self.table_scroll_idx = max(0, len(self.data.nodes) - 5) return True @@ -4971,6 +5298,8 @@ class MuseTUI: self.jobs_sel_idx = max(0, self.jobs_sel_idx - 1) elif self.box_tab == 4: self.tmux_sel_idx = max(0, self.tmux_sel_idx - 1) + elif self.box_tab == 6: + self.ssh_sel_idx = max(0, self.ssh_sel_idx - 1) else: self.table_scroll_idx = max(0, self.table_scroll_idx - 1) elif mx >= sidebar_w: @@ -5020,6 +5349,10 @@ class MuseTUI: sessions = list(self.data.tmux_cache) if sessions: self.tmux_sel_idx = min(len(sessions) - 1, self.tmux_sel_idx + 1) + elif self.box_tab == 6: + rows = self.get_ssh_rows() + if rows: + self.ssh_sel_idx = min(len(rows) - 1, self.ssh_sel_idx + 1) else: self.table_scroll_idx += 1 elif mx >= sidebar_w: @@ -5273,8 +5606,7 @@ class MuseTUI: # Box Tabs click (my == 1 and self.mode == "box") if my == 1 and self.mode == "box": - tab_bounds = [(1, 18), (19, 37), (38, 53), (54, 74), (75, 94), (95, 108)] - for idx, (start, end) in enumerate(tab_bounds): + for idx, (start, end) in enumerate(box_tab_bounds()): if start <= mx <= end: self.box_tab = idx return True @@ -5669,7 +6001,7 @@ class MuseTUI: self.focus_pane = "transcript" return True - # Box View clicks (Tabs 1, 2, 3, 4) + # Box View clicks (Tabs 1, 2, 3, 4, 6) elif self.mode == "box": self.editor_mode = "NORMAL" row_idx = my - (content_y + 3) @@ -5763,6 +6095,21 @@ class MuseTUI: self.set_toast(f"Selected session '{sess_name}'. Click [Kill] or [Attach].", "info") return True + elif self.box_tab == 6: + # Tab 6: SSH / Boxes (rows start one line lower: jump-status line) + rows = self.get_ssh_rows() + scroll_start = getattr(self, "ssh_scroll_idx", 0) + clicked_idx = scroll_start + row_idx - 1 + if 0 <= clicked_idx < len(rows): + self.ssh_sel_idx = clicked_idx + if mx >= w - 8: + # [SSH] pop-out + self._ssh_popout_selected() + else: + acct = rows[clicked_idx] + self.set_toast(f"Selected [{acct.upper()}]. Click [SSH] or press Enter to pop out.", "info") + return True + return True def _handle_insert_key(self, ch: int) -> bool: @@ -6348,6 +6695,77 @@ class MuseTUI: else: self.set_toast(f"Kill failed: {msg[:40]}", "error") + def _ssh_popout_selected(self): + """Open the selected container SSH session in a new tmux window.""" + rows = self.get_ssh_rows() + if not rows or not (0 <= self.ssh_sel_idx < len(rows)): + self.set_toast("No SSH row selected.", "warn") + return + acct = rows[self.ssh_sel_idx] + if acct not in SSH_TUNNEL_PORTS: + self.set_toast(f"No tunnel registered for '{acct}'.", "warn") + return + if not os.environ.get("TMUX"): + # Refuse to spawn into an invisible server: new-window would + # create a detached server the operator cannot see. + try: + probe = subprocess.run(["tmux", "ls"], capture_output=True, text=True, timeout=3) + server_up = probe.returncode == 0 + except Exception: + server_up = False + if not server_up: + copy_to_clipboard(build_ssh_dial_string(acct)) + self.set_toast("Not inside tmux and no server running; dial copied to clipboard.", "warn") + return + shell_cmd = build_ssh_popout_shell(acct) + pop_cmd = build_tmux_popout_command(f"ssh-{acct}", shell_cmd) + try: + curses.def_prog_mode() + curses.endwin() + try: + res = subprocess.run(pop_cmd, capture_output=True, text=True, timeout=5) + finally: + try: + curses.reset_prog_mode() + self.stdscr.refresh() + except Exception: + pass + self.need_full_redraw = True + if res.returncode == 0: + self.set_toast(f"Opened SSH to {acct} in tmux window ssh-{acct}.", "success") + else: + copy_to_clipboard(build_ssh_dial_string(acct)) + err = (res.stderr or "").strip().splitlines() + hint = err[-1][:60] if err else f"exit {res.returncode}" + self.set_toast(f"tmux pop-out failed ({hint}); dial copied.", "error") + except Exception as e: + copy_to_clipboard(build_ssh_dial_string(acct)) + self.set_toast(f"Pop-out failed ({e}); dial copied to clipboard.", "error") + + def _ssh_copy_dial_selected(self): + """Copy the selected container's dial command to the clipboard.""" + rows = self.get_ssh_rows() + if not rows or not (0 <= self.ssh_sel_idx < len(rows)): + self.set_toast("No SSH row selected.", "warn") + return + acct = rows[self.ssh_sel_idx] + if acct not in SSH_TUNNEL_PORTS: + self.set_toast(f"No tunnel registered for '{acct}'.", "warn") + return + dial = build_ssh_dial_string(acct) + if copy_to_clipboard(dial): + self.set_toast(f"Copied dial for {acct}: {dial[:80]}", "success") + else: + self.set_toast(f"Dial for {acct}: {dial}", "info") + + def _async_ssh_uptime(self, account: str): + ok, out_text = self.data.probe_container_uptime(account) + if ok: + first = (out_text or "").strip().splitlines() + self.set_toast(f"{account} uptime: {first[0][:90]}" if first else f"{account}: uptime probe empty.", "success" if first else "warn") + else: + self.set_toast(f"{account} uptime failed: {out_text[:90]}", "error") + def _execute_chat_action(self, action_id: str): """Execute selected contextual action on the targeted chat thread.""" chat_info = getattr(self, "context_chat", None) or {} @@ -7039,6 +7457,7 @@ def main(): parser.add_argument("--account", "-a", choices=VALID_NODES, default=None, help="Initial agent account (default: top of list)") parser.add_argument("--thread", "-t", help="Initial thread UUID to open") parser.add_argument("--mode", "-m", choices=["muse", "box"], default="muse", help="TUI mode (default: muse)") + parser.add_argument("--tab", choices=["chat", "fleet", "approvals", "jobs", "tmux", "logs", "ssh"], default=None, help="Initial Box tab (box mode only)") args = parser.parse_args() try: @@ -7046,7 +7465,8 @@ def main(): stdscr, initial_mode=args.mode, initial_node=args.account, - initial_thread=args.thread + initial_thread=args.thread, + initial_tab=args.tab ).run()) except KeyboardInterrupt: pass diff --git a/bin/muse_choice_watcher.py b/bin/muse_choice_watcher.py index 28767da..75eee1e 100755 --- a/bin/muse_choice_watcher.py +++ b/bin/muse_choice_watcher.py @@ -3,8 +3,11 @@ Kinds (prompt -> key): native Muse approval menu -> "1", agent interview menu -> "1", explicit-phrase request -> captured TOKEN, -lettered A/B/C choice -> "A", numbered (1)/(2) menu -> "1", y/n -line-end prompt -> "y". +lettered A/B/C choice -> "A", numbered (1)/(2) menu -> "1", +cursorless dotted 1./2./3. grill question -> "1" (rule-held for +coordinator sign-off, never expires to approve), y/n +line-end prompt -> "y", /permissions screens -> builtin-hold (never +auto-answered; the operator drives mode changes by hand). Each prompt is answered at most once (after stability + re-verify), with an hourly cap as backstop. @@ -33,6 +36,8 @@ import hashlib import json import os import re +import shlex +import shutil import signal import subprocess import sys @@ -50,21 +55,51 @@ KNOWN_SOCKETS = [ "/tmp/tmux-muse.sock", ] +# Sockets whose sole purpose is self-running fleet agents. Prompts on +# fleet sockets are answered (and logged) regardless of approval-mode +# launch flags, and get a higher hourly answer budget: a headless +# builder under on-request approvals burns many answers per hour. +FLEET_SOCKETS = [ + "/tmp/tmux-muse.sock", +] + POLL_INTERVAL = 0.5 STABILITY_POLLS = 2 MAX_ANSWERS_PER_HOUR = 20 +FLEET_MAX_ANSWERS_PER_HOUR = 200 ANSWERED_TTL_SECONDS = 600 # identical prompt back after 10m => stuck, allow one recovery answer CLAIM_TTL_SECONDS = 30 # concurrent-claim window: bounds wedge if winner dies pre-send RULES_FILE = os.path.join(REPO_ROOT, "muse-choices-rules.json") HOLD_WINDOW_SECONDS = 120 # D3: short hold window, then expire to approve NEGATIVE_KEYS = {"muse-approval": "2", "yn": "n"} # D2 deny keys -QUESTION_KINDS = frozenset({"interview", "letter", "numbered", "explicit-phrase"}) +QUESTION_KINDS = frozenset({"interview", "letter", "numbered", "explicit-phrase", + "numbered-plain", "permissions"}) +HOLD_FOREVER_KINDS = frozenset({"numbered-plain"}) # coordinator-gated: +# holds renew instead of expiring to approve; resolve or manual answer only +AGENT_PANE_HINTS = ("muse-bin", "muse-code", "agy.bin") +# pane_current_command substrings: every agent harness is watched by +# default. muse-code is the launch shim: a fresh pane shows it until +# the muse-bin exec lands, so omitting it loses the launch race (no +# watcher starts and none is reported). +HARNESS_HINTS = (("muse", ("muse-bin", "muse-code")), + ("agy", ("agy.bin",))) # argv[0] substrings per harness +AGY_HOLD_RULE = "agy-hold-all" # builtin: agy sends unproven, hold all _RULES_CACHE = {"key": None, "rules": []} LOG_MAX_BYTES = 1_000_000 HEARTBEAT_SECONDS = 60 +SUBMIT_GAP_SECONDS = 0.4 # text..Enter gap: the muse composer treats a +# back-to-back burst as paste and inserts a newline instead of submitting +COMPOSER_KINDS = frozenset({"letter", "numbered", "yn", "explicit-phrase", + "numbered-plain"}) +VERIFY_POLL_SECONDS = 0.1 +VERIFY_TEXT_TIMEOUT = 1.5 # unchanged past this: fail open to blind send +POST_ENTER_SETTLE = 0.4 # submit processing before the post-verify capture +COMPOSER_TAIL_WINDOW = 10 # trailing lines scanned for the live input row TAIL_WINDOW = 25 # only prompts in the last N lines count (no stale scrollback) OPTION_SPAN = 12 # A..C option lines must fit within N lines (wrapped lines ok) MAX_OPTION_LEN = 160 # option lines longer than this are ignored (prose guard) +WRAPPED_TAIL_CAP = 4 # wrapped continuation rows skipped past the last option +CUE_BELOW_ROWS = 3 # cue may sit this far below the option block end # A. text / B) text / C: text / C - text (single capital letter + delimiter) OPTION_RE = re.compile(r"^\s*([A-Z])\s*[.\)\-:]\s+\S") @@ -121,6 +156,18 @@ MUSE_COLLAPSED_KEY_RE = re.compile(r"ctrl\s*\+\s*o\b", re.IGNORECASE) INTERVIEW_OPT_RE = re.compile("^\\s*([>\\u203a])?\\s*(\\d+)\\.\\s+\\S") INTERVIEW_CUE_ABOVE = 8 # question must sit within N lines above option 1 +# Cursorless dotted 1./2./3. grill question (observed live on a policy +# grill): ordered dotted options, a ?-ended question above, and a strong +# pick-cue nearby. Both the question and the cue are required: bare +# prose lists must never match. Any cursor-prefixed dotted line vetoes +# (cursor menus belong to the interview matcher). +NUMBERED_PLAIN_OPT_RE = re.compile(r"^\s*(\d+)\.\s+\S") +NUMBERED_PLAIN_CURSOR_RE = re.compile("^\\s*[>\\u203a]\\s*\\d+\\.\\s+\\S") +NUMBERED_PLAIN_CUE_RE = re.compile( + r"(?i)\bpick\s+(one|[123]|a number)\b|\breply\s+[123]\b" + r"|\bchoose\s+(one|[123])\b|\benter\s+[123]\b" + r"|\bselect\s+an?\s*(option|one|number)\b|\byour choice\b") + # Explicit-phrase request (model asks the user to reply a magic word; # observed live): "Reply ACCEPT to approve this text as written ..." # Only a single ALL-CAPS token qualifies -- lowercase/prose after Reply @@ -128,6 +175,37 @@ INTERVIEW_CUE_ABOVE = 8 # question must sit within N lines above option 1 EXPLICIT_RE = re.compile(r"^\s*(?i:Reply)\s+([A-Z][A-Z0-9_-]{1,11})\b") EXPLICIT_WINDOW = 8 # request must sit in the last N content lines +# /permissions mode picker (pasted verbatim from a live session): +# Permissions +# Selections are saved as your default for new sessions and exec runs. +# 1. Read only Reads files only. +# 2. Ask me Edits workspace ... +# 3. Auto-review Same access as Ask me; ... +# › 4. Unrestricted (current) No filesystem sandbox ... +# A mode change persists as the default for new sessions and exec runs, +# so the picker is builtin-hold (operator drives the TUI by hand) and +# is never auto-answered. The (current) marker names the live mode. +PERMISSIONS_HEADER_RE = re.compile(r"^\s*Permissions\s*$") +PERMISSIONS_OPT_RE = re.compile( + "^\\s*([>\\u203a])?\\s*([1-4])\\.\\s+" + "(Read only|Ask me|Auto-review|Unrestricted)\\b") +PERMISSIONS_CURRENT_RE = re.compile(r"\(current\)") +PERMISSIONS_SPAN = 18 # header..option 4 (wrapped descriptions ok) + +# /permissions enable confirmation (pasted verbatim from live): +# Enable unrestricted permissions? +# Profile: Unrestricted +# ... +# Cancel +# › Enable Unrestricted +# Confirming flips the session (and the saved default) out of the +# logged-approval path, so it is builtin-hold like the picker. +PERMISSIONS_CONFIRM_TITLE_RE = re.compile( + r"^\s*Enable\s+(.+?)\s+permissions\?\s*$", re.IGNORECASE) +PERMISSIONS_CONFIRM_OPT_RE = re.compile( + "^\\s*([>\\u203a])?\\s*(Cancel|Enable\\s+\\S.*?)\\s*$") +PERMISSIONS_CONFIRM_SPAN = 14 # title..options span + # Runtime state sensing for external agents driving panes via send-keys. # A working pane shows a running indicator ("- running (Ns ...", "Calling # tools (...", or the "esc to interrupt" tail, often wrapped/edge-cut); @@ -320,6 +398,23 @@ def _sig_for(kind, parts): return hashlib.sha1(src.encode()).hexdigest()[:16] +def _option_block_end(window, end, marker_re): + """Last row of an option block: skip its wrapped continuation rows. + + Captures are width-chunked, not joined, so an option's text may wrap + onto several display rows past its marker line. Cue distance is + measured past the block, not the marker. Stops at blank rows, at the + next marker, and at WRAPPED_TAIL_CAP rows past the marker (bounding + the net: a cue far below a long tail still rejects). + """ + tail = end + while (tail + 1 < len(window) and tail + 1 - end <= WRAPPED_TAIL_CAP + and window[tail + 1].strip() + and not marker_re.match(window[tail + 1])): + tail += 1 + return tail + + def _find_letter(window): """A/B/C lettered choice block -> {"sig", "kind", "key", ...} or None.""" run = _ordered_run(_option_markers(window)) @@ -327,7 +422,8 @@ def _find_letter(window): return None start, end = run[0][0], run[-1][0] lo = max(0, start - 4) - hi = min(len(window), end + 5) + tail = _option_block_end(window, end, OPTION_RE) + hi = min(len(window), tail + 1 + CUE_BELOW_ROWS) cue = None for i in range(lo, hi): if CUE_RE.search(window[i]) or QUESTION_RE.search(window[i]): @@ -360,7 +456,8 @@ def _find_numbered(window): if end is None or end - start > OPTION_SPAN: return None lo = max(0, start - 2) - hi = min(len(window), end + 4) + tail = _option_block_end(window, end, NUMBERED_OPT_RE) + hi = min(len(window), tail + 1 + CUE_BELOW_ROWS) cue = None for i in range(lo, hi): if NUMBERED_CUE_RE.search(window[i]): @@ -522,6 +619,72 @@ def _find_interview(window): "cue": question, "start": start, "end": run[-1]} +def _ordered_int_run(markers): + """Complete 1,2[,3...] runs; returns the NEWEST (last) one or None. + + Unlike _ordered_run (first on ties), grill windows stack an answered + block above the live one: the live prompt is the lower run. + """ + runs = [] + for start in range(len(markers)): + if markers[start][1] != 1: + continue + run = [markers[start]] + for idx in range(start + 1, len(markers)): + if markers[idx][1] == len(run) + 1: + run.append(markers[idx]) + else: + break + if len(run) >= 2 and run[-1][0] - run[0][0] <= OPTION_SPAN: + runs.append(run) + return runs[-1] if runs else None + + +def _find_numbered_plain(window): + """Cursorless dotted 1./2./3. grill question -> match or None. + + Ordered dotted options starting at 1, a ?-ended question above, and + a strong pick-cue nearby. Both the question and the cue are + required: bare prose lists must never match. Any cursor-prefixed + dotted line vetoes (cursor menus belong to _find_interview). + """ + for line in window: + if NUMBERED_PLAIN_CURSOR_RE.match(line): + return None + markers = [] + for i, line in enumerate(window): + if len(line) > MAX_OPTION_LEN: + continue + m = NUMBERED_PLAIN_OPT_RE.match(line) + if m: + markers.append((i, int(m.group(1)))) + run = _ordered_int_run(markers) + if not run: + return None + start, end = run[0][0], run[-1][0] + question = None + for qi in range(max(0, start - INTERVIEW_CUE_ABOVE), start): + if QUESTION_RE.search(window[qi]): + question = window[qi].strip() + if question is None: + return None + lo = max(0, start - 4) + tail = _option_block_end(window, end, NUMBERED_PLAIN_OPT_RE) + hi = min(len(window), tail + 1 + CUE_BELOW_ROWS) + cue = None + for i in range(lo, hi): + if NUMBERED_PLAIN_CUE_RE.search(window[i]): + cue = window[i].strip() + break + if cue is None: + return None + options = [window[i].strip()[:120] for i, _ in run] + question = question[:200] + return {"sig": _sig_for("numbered-plain", options + [question]), + "kind": "numbered-plain", "key": "1", "options": options, + "cue": question, "start": start, "end": end} + + def _find_explicit(window): """Explicit-phrase request ("Reply ACCEPT to ...") or None. @@ -546,15 +709,113 @@ def _find_explicit(window): return None +def _find_permissions_picker(window): + """/permissions mode picker -> match or None. + + Requires the exact "Permissions" header, all four mode options in + 1..4 order, a (current) marker naming the live mode, and a cursor + on an option line (proves a live selectable menu). The sig covers + labels + current mode but not cursor position, so operator + navigation doesn't churn holds while an actual mode change does. + Key is None: the picker is builtin-hold, never auto-answered. + """ + header = None + for i, line in enumerate(window): + if len(line) > MAX_OPTION_LEN: + continue + if PERMISSIONS_HEADER_RE.match(line): + header = i + break + if header is None: + return None + found = {} + cursor = False + end = header + for i in range(header + 1, min(len(window), header + 1 + PERMISSIONS_SPAN)): + line = window[i] + if len(line) > MAX_OPTION_LEN: + continue + m = PERMISSIONS_OPT_RE.match(line) + if not m: + continue + if m.group(1): + cursor = True + found[int(m.group(2))] = (i, m.group(3), + bool(PERMISSIONS_CURRENT_RE.search(line))) + end = i + if sorted(found) != [1, 2, 3, 4] or not cursor: + return None + current = next((label for _, label, is_cur in found.values() if is_cur), + None) + if current is None: + return None + options = ["%d. %s" % (n, found[n][1]) for n in (1, 2, 3, 4)] + cue = "Permissions (current: %s)" % current + return {"sig": _sig_for("permissions", options + [current]), + "kind": "permissions", "key": None, "options": options, + "cue": cue, "current": current, + "start": header, "end": end} + + +def _find_permissions_confirm(window): + """/permissions enable confirmation -> match or None. + + Requires an "Enable permissions?" title plus Cancel and + Enable-... option lines with a cursor on one. Sig excludes cursor + position (navigation churn, see picker). Key None: builtin-hold. + """ + title = None + profile = None + for i, line in enumerate(window): + if len(line) > MAX_OPTION_LEN: + continue + m = PERMISSIONS_CONFIRM_TITLE_RE.match(line) + if m: + title = i + profile = m.group(1).strip() + break + if title is None: + return None + seen_cancel = seen_enable = cursor = False + end = title + for i in range(title + 1, min(len(window), title + 1 + PERMISSIONS_CONFIRM_SPAN)): + line = window[i] + if len(line) > MAX_OPTION_LEN: + continue + m = PERMISSIONS_CONFIRM_OPT_RE.match(line) + if not m: + continue + if m.group(1): + cursor = True + opt = m.group(2) + if opt == "Cancel": + seen_cancel = True + elif opt.startswith("Enable"): + seen_enable = True + end = i + if not (seen_cancel and seen_enable and cursor): + return None + cue = window[title].strip()[:200] + options = ["Cancel", "Enable %s" % profile] if profile else ["Cancel"] + return {"sig": _sig_for("permissions", [cue]), + "kind": "permissions", "key": None, "options": options, + "cue": cue, "current": None, + "start": title, "end": end} + + def find_choice_prompt(text, tail_window=TAIL_WINDOW): """Detect a Muse prompt awaiting reply. - Kinds (priority order): muse-approval (native Would-you-like - menu -> key "1"), muse-approval-collapsed (collapsed long command - -> bare Enter, expand-or-accept), interview (cursor + ordered - 1./2. menu + question -> key "1"), explicit-phrase ("Reply TOKEN - to ..." -> key TOKEN), letter (A/B/C -> key "A"), numbered - ((1)/(2) menu -> key "1"), yn (y/n line-end -> key "y"). + Kinds (priority order): permissions (/permissions picker or + enable confirmation -> builtin-hold, never auto-answered), + muse-approval (native Would-you-like menu -> key "1"), + muse-approval-collapsed (collapsed long command -> bare Enter, + expand-or-accept), interview (cursor + ordered 1./2. menu + + question -> key "1"), numbered-plain (cursorless dotted 1./2./3. + grill question + pick-cue -> key "1", coordinator-held), + explicit-phrase ("Reply TOKEN to ..." -> + key TOKEN), letter (A/B/C -> key "A"), numbered ((1)/(2) menu -> + key "1"), yn (y/n line-end -> key "y"). Returns {"sig", "kind", "key", "options", "cue", "start", "end"} or None. Only blocks in the last `tail_window` content lines are eligible, so answered/stale prompts in scrollback never re-trigger. @@ -569,8 +830,10 @@ def find_choice_prompt(text, tail_window=TAIL_WINDOW): window = lines[-tail_window:] if not window: return None - return (_find_muse_approval(window) or _find_collapsed_approval(window) - or _find_interview(window) or _find_explicit(window) + return (_find_permissions_picker(window) or _find_permissions_confirm(window) + or _find_muse_approval(window) or _find_collapsed_approval(window) + or _find_interview(window) or _find_numbered_plain(window) + or _find_explicit(window) or _find_letter(window) or _find_numbered(window) or _find_yn(window)) @@ -697,8 +960,143 @@ def launch_opt_out(cmd_argv): return False -def pane_muse_argv(socket_path, pane_id): - """Argv of the muse process in a pane (empty when not found).""" +def pane_holds_for_optout(socket_path, cmd_argv): + """True when the watcher must hold prompts for launch-flag opt-out. + + Fleet sockets host self-running agents: prompts there are answered + (and logged) regardless of approval-mode flags, since nobody is + watching to answer by hand. Elsewhere an explicit --approval-mode + other than never declares human-in-the-loop intent -> hold. + """ + if socket_path in FLEET_SOCKETS: + return False + return launch_opt_out(cmd_argv) + + +def hourly_cap_for(socket_path): + """Hourly answer budget for a pane loop on socket_path.""" + if socket_path in FLEET_SOCKETS: + return FLEET_MAX_ANSWERS_PER_HOUR + return MAX_ANSWERS_PER_HOUR + + +def approval_determined(cmd_argv): + """True when argv already determines approval behavior. + + yolo / disable-approval / approval-mode / permission-profile all + settle it; sandbox and workspace-trust flags do not (they ride + along with whatever approval posture applies). + """ + posture = muse_approval_flags(cmd_argv) + if posture["profile"] is not None: + return True + flags = posture["flags"] + if "yolo" in flags or "disable-approval" in flags: + return True + return any(f.startswith("approval-mode=") for f in flags) + + +def muse_launch_cmdline(muse_args): + """Build a fleet launch command line. + + Strips a leading "--" separator; bare launches (no + approval-determining flags) get on-request approvals injected so + prompts render for the watcher trail. Returns (cmdline, injected). + Shared by `box runtime launch` and the runtime reconciler so both + spawn identical sessions. + """ + args = list(muse_args or []) + if args[:1] == ["--"]: + args = args[1:] + injected = ([] if approval_determined(args) + else ["--approval-mode", "on-request"]) + launcher = (shutil.which("muse-code") + or "/home/super/.local/bin/muse-code") + return (shlex.join([launcher] + injected + args), injected) + + +def saved_default_profile(settings_path=None): + """Saved default permission profile id, or None. + + Reads permissions.default_profile from the muse settings file + (e.g. ":unrestricted" after driving the /permissions picker). The + app applies this to sessions launched without explicit approval + flags; launch flags override it ("Launch overrides" footer). + Never raises. + """ + path = settings_path or os.path.expanduser( + "~/.config/muse/settings.json") + try: + with open(path) as f: + data = json.load(f) + except Exception: + return None + try: + return data.get("permissions", {}).get("default_profile") + except Exception: + return None + + +def resolve_socket(sock): + """Resolve a user-supplied tmux socket alias to a server path. + + Tables show basenames (tmux-muse.sock), so callers copy them back + as --socket values. As-given wins when it exists; otherwise match + by basename against KNOWN_SOCKETS, then /tmp/. Unknown + values pass through unchanged so downstream errors still name + what was given. Pure apart from existence probes. + """ + if not sock or os.path.exists(sock): + return sock + base = os.path.basename(sock) + for known in KNOWN_SOCKETS: + if os.path.basename(known) == base: + return known + cand = os.path.join("/tmp", base) + if os.path.exists(cand): + return cand + return sock + + +def effective_posture(posture, saved_default=None): + """Effective approval posture: explicit flags win, else saved default. + + Returns {"effective_mode", "effective_bypass"}. effective_bypass is + True when the pane shows no approval dialogs (explicit yolo / + disable-approval / never, an unrestricted profile, or an + unrestricted saved default with no explicit flags). Modes are + display-ready (leading ":" stripped from profile ids). Pure. + """ + flags = posture.get("flags") or [] + profile = posture.get("profile") + approval_mode = None + for f in flags: + if f.startswith("approval-mode="): + approval_mode = f.split("=", 1)[1] + if "yolo" in flags: + return {"effective_mode": "yolo", "effective_bypass": True} + if "disable-approval" in flags or approval_mode == "never": + if profile is not None: + mode = profile.lstrip(":") + elif approval_mode is not None: + mode = approval_mode + else: + mode = "default" + return {"effective_mode": mode, "effective_bypass": True} + if approval_mode is not None: + return {"effective_mode": approval_mode, + "effective_bypass": False} + if profile is not None: + return {"effective_mode": profile.lstrip(":"), + "effective_bypass": profile == ":unrestricted"} + if saved_default: + return {"effective_mode": str(saved_default).lstrip(":"), + "effective_bypass": saved_default == ":unrestricted"} + return {"effective_mode": "default", "effective_bypass": False} + + +def _pane_tree_argvs(socket_path, pane_id): + """Argvs of a pane's root pid plus its direct children (may be empty).""" try: r = _tmux(socket_path, "list-panes", "-a", "-F", "#{pane_id} #{pane_pid}", timeout=5) @@ -716,13 +1114,32 @@ def pane_muse_argv(socket_path, pane_id): return [] if pane_pid is None: return [] - for cand in [pane_pid] + _child_pids(pane_pid): - argv = _cmdline(cand) - if argv and ("muse-bin" in argv[0] or "muse-code" in argv[0]): + return [argv for argv in (_cmdline(c) + for c in [pane_pid] + _child_pids(pane_pid)) + if argv] + + +def pane_muse_argv(socket_path, pane_id): + """Argv of the muse process in a pane (empty when not found).""" + for argv in _pane_tree_argvs(socket_path, pane_id): + if "muse-bin" in argv[0] or "muse-code" in argv[0]: return argv return [] +def pane_harness(socket_path, pane_id): + """Agent harness owning a pane: "muse", "agy", or None. + + Matches the harness binary among the pane's processes (stable: + independent of which child is in the foreground). + """ + for argv in _pane_tree_argvs(socket_path, pane_id): + for harness, hints in HARNESS_HINTS: + if any(h in argv[0] for h in hints): + return harness + return None + + # Minimum pane geometry for reliable approval rendering. Empirically # derived: a 35x7 tile drops approval text the matcher needs, while # 35x35/36x35/71x27 panes answer cleanly (width 35 works when tall @@ -754,11 +1171,14 @@ def node_from_session(session_name): return node if node in NODE_NAMES else None -def runtime_rows(socket_path): +def runtime_rows(socket_path, errors=None): """One row per pane: identity, approval posture, live state, watcher. Rows are JSON-serializable dicts for `box runtime list` and external - agents. Panes that vanish mid-scan are skipped, never fatal. + agents. Panes that vanish mid-scan are skipped, never fatal. When + the socket itself is unreachable and errors is a dict, the tmux + failure is recorded as errors[socket_path] (one line) instead of + silently yielding zero rows. """ rows = [] try: @@ -767,9 +1187,14 @@ def runtime_rows(socket_path): "#{pane_current_command}\t#{pane_pid}\t" "#{pane_width}\t#{pane_height}", timeout=10) if r.returncode != 0: + if errors is not None: + errors[socket_path] = _tmux_err(r) return rows - except Exception: + except Exception as e: + if errors is not None: + errors[socket_path] = str(e)[:160] or "tmux error" return rows + saved_default = saved_default_profile() for line in r.stdout.split("\n"): parts = line.split("\t") if len(parts) != 7: @@ -804,6 +1229,10 @@ def runtime_rows(socket_path): st = runtime_state(text) match = st["match"] or {} watcher_pid = watcher_alive(socket_path, pane_id) + if posture["mode"] is None: + effective = {"effective_mode": None, "effective_bypass": None} + else: + effective = effective_posture(posture, saved_default) rows.append({ "socket": socket_path, "session": session, "window": window, "pane": pane_id, "cmd": cmd, @@ -816,6 +1245,8 @@ def runtime_rows(socket_path): "permission_mode": posture["mode"], "permission_profile": posture["profile"], "permission_bypass": posture["bypass"], + "effective_mode": effective["effective_mode"], + "effective_bypass": effective["effective_bypass"], "state": st["state"], "prompt_kind": match.get("kind"), "prompt_key": match.get("key"), @@ -836,21 +1267,46 @@ def spread_targets(rows): if r.get("is_muse") and r.get("squeezed")] -def all_runtime_rows(sockets=None): - """runtime_rows across every known socket that exists.""" +def _tmux_err(r): + """First line of a failed tmux result, bounded.""" + text = ((getattr(r, "stderr", "") or "") + "\n" + + (getattr(r, "stdout", "") or "")).strip().split("\n") + line = next((ln.strip() for ln in text if ln.strip()), "") + return (line or "tmux error")[:160] + + +def all_runtime_rows(sockets=None, errors=None): + """runtime_rows across every known socket that exists. + + Sockets given explicitly but missing from the filesystem are + reported via errors (when a dict) instead of silently skipped: + callers that name a socket deserve to know it is absent. + """ rows = [] + explicit = sockets is not None for sock in sockets or KNOWN_SOCKETS: if not os.path.exists(sock): + if errors is not None and explicit: + errors[sock] = "no such socket" continue - rows.extend(runtime_rows(sock)) + rows.extend(runtime_rows(sock, errors=errors)) return rows def pane_state(socket_path, pane_id): - """Single-pane runtime row, or {"error": ...} when not found.""" - for row in runtime_rows(socket_path): + """Single-pane runtime row, or {"error": ...} when not found. + + Distinguishes a dead socket ("socket_unreachable", with the tmux + failure in "detail") from a live socket without that pane + ("no_such_pane"). + """ + errors = {} + for row in runtime_rows(socket_path, errors=errors): if row["pane"] == pane_id: return row + if errors.get(socket_path): + return {"error": "socket_unreachable", "socket": socket_path, + "pane": pane_id, "detail": errors[socket_path]} return {"error": "no_such_pane", "socket": socket_path, "pane": pane_id} @@ -864,12 +1320,13 @@ class WatcherState: re-answered minutes later, stray "1" landing in the input box). """ - def __init__(self, persist_path=None): + def __init__(self, persist_path=None, hourly_cap=None): self.pending_sig = None self.stable_count = 0 self.answered_sigs = {} self.answer_times = [] self.last_capped_sig = None + self.hourly_cap = hourly_cap or MAX_ANSWERS_PER_HOUR self._persist_path = persist_path if persist_path: self._load_answered() @@ -981,7 +1438,7 @@ class WatcherState: def cap_reached(self, now): self._prune_times(now) - return len(self.answer_times) >= MAX_ANSWERS_PER_HOUR + return len(self.answer_times) >= self.hourly_cap def observe(self, match, now): """Feed one poll's match (or None). Returns "answer" when the prompt @@ -1057,18 +1514,122 @@ def capture_pane(socket_path, pane_id, history=80): return None -def send_answer(socket_path, pane_id, letter="A", enter=True): - """Type the choice key (+ Enter unless enter=False). True on success.""" - try: - r1 = _tmux(socket_path, "send-keys", "-t", pane_id, letter, timeout=5) - if r1.returncode != 0: - return False - if not enter: - return True - r2 = _tmux(socket_path, "send-keys", "-t", pane_id, "Enter", timeout=5) - return r2.returncode == 0 - except Exception: +def _tail_composer_line(text, window=COMPOSER_TAIL_WINDOW): + """Last input-prompt-led line in the trailing window, or None. + + Transcript history may hold older input echoes of submitted messages; + the LAST prompt-led row is the live composer input. None when no such + row shows (shell prompts, menus with no input row). + """ + lines = (text or "").split("\n") + while lines and not lines[-1].strip(): + lines.pop() + for line in reversed(lines[-window:]): + if OPEN_PROMPT_RE.match(line): + return line + return None + + +def _composer_shows_letter(text, letter): + """True when the live composer input is exactly our letter. + + Exact match only: stale or concurrent input ("ABC", placeholder hints) + must never read as ours, so a retry can only re-submit the known + newline-bug shape (our letter sitting unsubmitted). + """ + if not letter: return False + line = _tail_composer_line(text) + if line is None: + return False + return re.match(r"^\s*\u276f\s*%s\s*$" % re.escape(letter), + line) is not None + + +def _await_render(socket_path, pane_id, before): + """True once the screen differs from `before`. Never raises. + + A changed screen proves the app consumed the typed text, so whatever + we send next is a separate input event. Unchanged past the timeout + (slow pane, dead pane, tmux hiccup) fails open: the caller still + sends Enter on the blind timing. + """ + if before is None: + return False + try: + deadline = time.time() + VERIFY_TEXT_TIMEOUT + while time.time() < deadline: + now = capture_pane(socket_path, pane_id) + if now is not None and now != before: + return True + time.sleep(VERIFY_POLL_SECONDS) + except Exception: + pass + return False + + +def send_answer(socket_path, pane_id, letter="A", enter=True, kind=None, + sig=None): + """Type the choice key (+ Enter unless enter=False). + + Returns (ok, detail): ok is False only when tmux refused a send or a + composer answer is still sitting unsubmitted after one retry; detail + is {"verified": True/False/None, "retried": bool} for logs/audit. + + Composer kinds (letter/numbered/yn/explicit-phrase: the answer goes to + a text input) are capture-verified: the text must render on screen + before Enter goes out (proving the app consumed it as its own input + event — a back-to-back burst arrives as paste and lands a newline + instead of submitting), and after Enter the prompt must clear or the + composer must empty, else Enter goes once more. Timeouts fail open to + the blind send so a slow pane still gets its answer. + Menu kinds (muse-approval/interview/collapsed: single-key widgets that + never render text) keep the blind gap send, which is proven there. + """ + blind = {"verified": None, "retried": False} + try: + if not enter: + r = _tmux(socket_path, "send-keys", "-t", pane_id, letter, + timeout=5) + return (r.returncode == 0), dict(blind) + verify = kind in COMPOSER_KINDS + before = capture_pane(socket_path, pane_id) if verify else None + r1 = _tmux(socket_path, "send-keys", "-l", "-t", pane_id, letter, + timeout=5) + if r1.returncode != 0: + return False, dict(blind) + verified = _await_render(socket_path, pane_id, before) \ + if verify else None + time.sleep(SUBMIT_GAP_SECONDS) + r2 = _tmux(socket_path, "send-keys", "-t", pane_id, "Enter", + timeout=5) + if r2.returncode != 0: + return False, {"verified": verified, "retried": False} + if not verify or sig is None: + return True, {"verified": verified, "retried": False} + # Post-verify: same sig + our letter still in the composer means + # the Enter landed as a newline; one more Enter submits it. Same + # sig with a clear composer is a lingering transcript, not a miss. + time.sleep(POST_ENTER_SETTLE) + fresh = capture_pane(socket_path, pane_id) + if fresh is None: + return True, {"verified": verified, "retried": False} + m = find_choice_prompt(fresh) + if m is None or m["sig"] != sig: + return True, {"verified": verified, "retried": False} + if not _composer_shows_letter(fresh, letter): + return True, {"verified": verified, "retried": False} + r3 = _tmux(socket_path, "send-keys", "-t", pane_id, "Enter", + timeout=5) + if r3.returncode != 0: + return False, {"verified": verified, "retried": True} + time.sleep(POST_ENTER_SETTLE) + again = capture_pane(socket_path, pane_id) + stuck = (again is not None + and _composer_shows_letter(again, letter)) + return (not stuck), {"verified": verified, "retried": True} + except Exception: + return False, {"verified": None, "retried": False} def _load_rules(log=None): @@ -1268,6 +1829,16 @@ def _peer_suppressed(state, sig, now, log): return False +def hold_renews_forever(hold, kind): + """True when a hold renews instead of expiring to approve. + + Coordinator-gated kinds (grill interviews) and every agy hold + (unproven sends): release only by operator directive. + """ + return (kind in HOLD_FOREVER_KINDS + or (hold or {}).get("harness") == "agy") + + def _check_hold(socket_path, pane_id, state, match, now, log): """Rule-hold gate. Returns None (fresh: evaluate rules), "released" (hold over: approve WITHOUT re-evaluating, else expiry would @@ -1298,21 +1869,44 @@ def _check_hold(socket_path, pane_id, state, match, now, log): return "held" if _peer_suppressed(state, sig, now, log): return "duplicate-suppressed" - ok = send_answer(socket_path, pane_id, neg, enter=True) + ok, detail = send_answer(socket_path, pane_id, neg, enter=True, + kind=match["kind"], sig=sig) clear_hold(socket_path, pane_id) state.record_answer(sig, now) log.log("info" if ok else "error", "denied %s (operator resolve)" % neg, - sig=sig, ok=ok, kind=match["kind"], key=neg) + sig=sig, ok=ok, kind=match["kind"], key=neg, + verified=detail["verified"], retried=detail["retried"]) audit("muse-choice-denied", name="%s:%s" % (os.path.basename(socket_path), pane_id), extra={"socket": socket_path, "pane": pane_id, "sig": sig, "ok": ok, "kind": match["kind"], "key": neg, - "via": "resolve"}) + "via": "resolve", "verified": detail["verified"], + "retried": detail["retried"]}) return "denied" try: expired = now >= float(hf.get("held_until", 0)) except (TypeError, ValueError): expired = True + if expired and hold_renews_forever(hf, match["kind"]): + # Coordinator-gated kinds, and every agy hold, never expire + # to approve: renew the window while the prompt persists. + # Release only by operator directive, or when the prompt + # scrolls away (sig mismatch) or the operator answers by + # hand. Renewal failures keep holding; they never fail open + # to approve. + try: + hf["held_until"] = now + HOLD_WINDOW_SECONDS + hf["renewals"] = int(hf.get("renewals", 0) or 0) + 1 + write_hold(socket_path, pane_id, hf) + except Exception: + pass + else: + why = ("agy hold-all" if hf.get("harness") == "agy" + else "coordinator gate") + log.log("info", "hold renewed (%s, never auto)" % why, + sig=sig, kind=match["kind"], + renewals=hf["renewals"]) + return "held" if expired: clear_hold(socket_path, pane_id) log.log("info", "hold expired, releasing to approve", sig=sig) @@ -1360,11 +1954,65 @@ def _poll_once(socket_path, pane_id, state, log, dry_run=False): state.pending_sig = None state.stable_count = 0 return "vanished" - if launch_opt_out(pane_muse_argv(socket_path, pane_id)): + if pane_holds_for_optout(socket_path, + pane_muse_argv(socket_path, pane_id)): log.log("info", "held: pane opted out via launch flags", sig=match["sig"], kind=match["kind"]) state.record_answer(match["sig"], now) return "held" + if pane_harness(socket_path, pane_id) == "agy" and not skip_eval: + # Hold-all: agy auto-sends are unproven (its native menus + # may ignore digit keys), so every agy prompt holds for + # coordinator resolve instead of auto-answering. The hold + # renews forever; a released hold (resolve-approve) falls + # through and sends the kind's key with a human in the + # loop -- hence the skip_eval guard (cf. permissions). + until = now + HOLD_WINDOW_SECONDS + write_hold(socket_path, pane_id, { + "sig": match["sig"], "kind": match["kind"], + "key": match["key"], + "text": (match["cue"] or "")[:200], + "rule": AGY_HOLD_RULE, + "reason": ("agy harness: sends unproven; " + "coordinator resolve required"), + "downgraded_from": None, "harness": "agy", + "held_until": until, "directive": None, + "socket": socket_path, "pane": pane_id}) + log.log("info", "held: agy harness (hold-all)", + sig=match["sig"], kind=match["kind"]) + if not dry_run: + audit("muse-choice-held", + name="%s:%s" % (os.path.basename(socket_path), pane_id), + extra={"socket": socket_path, "pane": pane_id, + "sig": match["sig"], "kind": match["kind"], + "rule": AGY_HOLD_RULE, "harness": "agy", + "held_until": until}) + return "held" + if match["kind"] == "permissions" and not skip_eval: + # Builtin hold: a mode change persists as the default for + # new sessions and exec runs, so no rule may auto-answer + # it -- the operator drives the TUI by hand. Rules are + # not consulted for this kind. + until = now + HOLD_WINDOW_SECONDS + write_hold(socket_path, pane_id, { + "sig": match["sig"], "kind": "permissions", "key": None, + "text": (match["cue"] or "")[:200], + "rule": "builtin-permissions", + "reason": ("permission mode screen; operator drives " + "the TUI by hand"), + "downgraded_from": None, + "held_until": until, "directive": None, + "socket": socket_path, "pane": pane_id}) + log.log("info", "held: /permissions screen (builtin)", + sig=match["sig"], kind="permissions") + if not dry_run: + audit("muse-choice-held", + name="%s:%s" % (os.path.basename(socket_path), pane_id), + extra={"socket": socket_path, "pane": pane_id, + "sig": match["sig"], "kind": "permissions", + "rule": "builtin-permissions", + "held_until": until}) + return "held" if skip_eval: decision, rule = "approve", None via = "hold-released" @@ -1382,17 +2030,22 @@ def _poll_once(socket_path, pane_id, state, log, dry_run=False): return "dry-denied" if _peer_suppressed(state, match["sig"], now, log): return "duplicate-suppressed" - ok = send_answer(socket_path, pane_id, neg, enter=True) + ok, detail = send_answer(socket_path, pane_id, neg, enter=True, + kind=match["kind"], + sig=match["sig"]) state.record_answer(match["sig"], now) log.log("info" if ok else "error", "denied %s" % neg, sig=match["sig"], ok=ok, kind=match["kind"], key=neg, - rule=rule["id"] if rule else None) + rule=rule["id"] if rule else None, + verified=detail["verified"], retried=detail["retried"]) audit("muse-choice-denied", name="%s:%s" % (os.path.basename(socket_path), pane_id), extra={"socket": socket_path, "pane": pane_id, "sig": match["sig"], "ok": ok, "kind": match["kind"], "key": neg, "via": "rule", - "rule": rule["id"] if rule else None}) + "rule": rule["id"] if rule else None, + "verified": detail["verified"], + "retried": detail["retried"]}) return "denied" if decision == "hold": until = now + HOLD_WINDOW_SECONDS @@ -1423,21 +2076,41 @@ def _poll_once(socket_path, pane_id, state, log, dry_run=False): options=match["options"], cue=match["cue"]) state.record_answer(match["sig"], now) return "dry-answered" + if match["kind"] == "permissions": + # Released (operator resolve or hold expiry): still send + # nothing -- the operator drives mode changes by hand. + # Record so this screen stops holding. + log.log("info", "permissions released to operator (no keys sent)", + sig=match["sig"], cue=match["cue"]) + state.record_answer(match["sig"], now) + audit("muse-choice-answered", + name="%s:%s" % (os.path.basename(socket_path), pane_id), + extra={"socket": socket_path, "pane": pane_id, + "sig": match["sig"], "ok": True, + "kind": "permissions", "key": None, + "options": match["options"], "cue": match["cue"], + "via": via, + "rule": rule["id"] if rule else None}) + return "answered" if _peer_suppressed(state, match["sig"], now, log): return "duplicate-suppressed" - ok = send_answer(socket_path, pane_id, key, - enter=match.get("enter", True)) + ok, detail = send_answer(socket_path, pane_id, key, + enter=match.get("enter", True), + kind=match["kind"], sig=match["sig"]) state.record_answer(match["sig"], now) log.log("info" if ok else "error", "answered %s" % key, sig=match["sig"], ok=ok, kind=match["kind"], key=key, - options=match["options"], cue=match["cue"]) + options=match["options"], cue=match["cue"], + verified=detail["verified"], retried=detail["retried"]) audit("muse-choice-answered", name="%s:%s" % (os.path.basename(socket_path), pane_id), extra={"socket": socket_path, "pane": pane_id, "sig": match["sig"], "ok": ok, "kind": match["kind"], "key": key, "options": match["options"], "cue": match["cue"], "via": via, - "rule": rule["id"] if rule else None}) + "rule": rule["id"] if rule else None, + "verified": detail["verified"], + "retried": detail["retried"]}) return "answered" if verdict == "capped": if state.last_capped_sig != match["sig"]: @@ -1451,12 +2124,13 @@ def _poll_once(socket_path, pane_id, state, log, dry_run=False): return "waiting" -def _log_posture(socket_path, pane_id, log): +def _log_posture(socket_path, pane_id, log, saved_default=None): """Log the pane's permission posture once at watcher start. The watcher answers with per-choice logging in every mode (that - trail is the default path and informs policy); a bypass posture - (yolo / approval disabled) additionally gets a box audit record, + trail is the default path and informs policy); an effective bypass + posture (yolo / approval disabled / unrestricted saved default + with no explicit flags) additionally gets a box audit record, since the session then makes choices outside the trail. Never raises. """ @@ -1464,20 +2138,31 @@ def _log_posture(socket_path, pane_id, log): posture = muse_approval_flags(pane_muse_argv(socket_path, pane_id)) except Exception: return + try: + effective = effective_posture(posture, saved_default) + except Exception: + effective = {"effective_mode": posture["mode"], + "effective_bypass": posture["bypass"]} try: log.log("info", "pane posture", mode=posture["mode"], - bypass=posture["bypass"], flags=posture["flags"]) + bypass=posture["bypass"], flags=posture["flags"], + effective_mode=effective["effective_mode"], + effective_bypass=effective["effective_bypass"]) except Exception: pass try: - if posture["bypass"] or posture["mode"] not in ("default", None): + if (effective["effective_bypass"] + or effective["effective_mode"] not in ("default", None)): audit("muse-choice-posture", name="%s:%s" % (os.path.basename(socket_path), pane_id), extra={"socket": socket_path, "pane": pane_id, "mode": posture["mode"], "profile": posture["profile"], "bypass": posture["bypass"], - "flags": posture["flags"]}) + "flags": posture["flags"], + "effective_mode": effective["effective_mode"], + "effective_bypass": + effective["effective_bypass"]}) except Exception: pass @@ -1486,10 +2171,12 @@ def watch_loop(socket_path, pane_id, dry_run=False): """Main daemon loop. Returns only when the pane is gone or signalled.""" log = WatcherLog(logfile_for(socket_path, pane_id)) state = WatcherState( - persist_path=answered_file_for(socket_path, pane_id)) + persist_path=answered_file_for(socket_path, pane_id), + hourly_cap=hourly_cap_for(socket_path)) log.log("info", "watcher started", socket=socket_path, pane=pane_id, dry_run=dry_run, pid=os.getpid()) - _log_posture(socket_path, pane_id, log) + _log_posture(socket_path, pane_id, log, + saved_default=saved_default_profile()) polls = 0 answers = 0 last_heartbeat = time.time() @@ -1746,8 +2433,12 @@ def stop_watcher(socket_path, pane_id, timeout=5): return {"ok": True, "status": "stopped", "pid": pid} -def muse_panes(socket_path): - """Pane ids on a socket whose current command looks like Muse.""" +def agent_panes(socket_path): + """Pane ids on a socket whose current command looks like an agent. + + Every agent harness is watched by default; extend + AGENT_PANE_HINTS as new harnesses appear. + """ try: r = _tmux(socket_path, "list-panes", "-a", "-F", "#{pane_id} #{pane_current_command}", timeout=10) @@ -1758,11 +2449,49 @@ def muse_panes(socket_path): out = [] for line in r.stdout.split("\n"): parts = line.strip().split(None, 1) - if len(parts) == 2 and "muse-bin" in parts[1]: + if len(parts) == 2 and any(h in parts[1] + for h in AGENT_PANE_HINTS): out.append(parts[0]) return out +muse_panes = agent_panes # backward-compatible alias + + +def wait_for_session_pane(socket_path, session, timeout=10, interval=0.5): + """True once the session has a pane running an agent command. + + Launch-time boot race: `new-session` returns before the agent + binary is visible as pane_current_command, so a reconcile fired + immediately would find no agent panes and start no watcher. + Bounded and never fatal: on timeout just proceed to reconcile + (the timer heals the rest). Never raises. + """ + try: + deadline = time.time() + timeout + except Exception: + return False + while True: + try: + r = _tmux(socket_path, "list-panes", "-a", "-F", + "#{session_name} #{pane_id} #{pane_current_command}", + timeout=10) + except Exception: + return False + if r.returncode == 0: + for line in r.stdout.split("\n"): + parts = line.strip().split(None, 2) + if len(parts) == 3 and parts[0] == session and any( + h in parts[2] for h in AGENT_PANE_HINTS): + return True + try: + if time.time() >= deadline: + return False + time.sleep(interval) + except Exception: + return False + + def _start_detached(socket_path, pane_id, dry_run=False): """Fork off a watcher via start_watcher (which daemonizes further). Returns True if a watcher is running for the pane afterwards.""" @@ -1781,7 +2510,7 @@ def start_all(dry_run=False, sockets=None): if not os.path.exists(sock): results.append({"socket": sock, "status": "no_socket"}) continue - panes = muse_panes(sock) + panes = agent_panes(sock) if not panes: results.append({"socket": sock, "status": "no_muse_panes"}) for pane in panes: @@ -1847,7 +2576,7 @@ def reconcile(sockets=None): for sock in sockets or KNOWN_SOCKETS: if not os.path.exists(sock): continue - for pane in muse_panes(sock): + for pane in agent_panes(sock): if watcher_alive(sock, pane): already.append("%s:%s" % (sock, pane)) continue diff --git a/bin/onboard_pipeline.py b/bin/onboard_pipeline.py index bafd563..cba748c 100755 --- a/bin/onboard_pipeline.py +++ b/bin/onboard_pipeline.py @@ -252,6 +252,56 @@ def provision_node_infra(node: str) -> Dict[str, Any]: return {"ok": True, "node": node, "output": res.stdout.strip()} +def _registry_port(node: str) -> Optional[int]: + """CDP port for a node via netvm-registry.py, or None if unregistered.""" + import importlib.util + spec = importlib.util.spec_from_file_location( + "netvm_registry", str(BIN_DIR / "netvm-registry.py")) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.port_for(node) + + +def _cdp_dry_run(node: str) -> bool: + """True when the node netns + CDP + page chain is healthy.""" + cmd = [str(BIN_DIR / "netvm-exec.sh"), node, "--", sys.executable, + str(BIN_DIR / "onboard-driver.py"), + "--node", node, "--service", "muse", "--id-type", "email", + "--step", "initiate", "--dry-run"] + res = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return res.returncode == 0 + + +def ensure_node_browser(node: str, timeout: float = 90.0, poll_interval: float = 5.0) -> Dict[str, Any]: + """Launch the node headless browser inside its netns if CDP is down. + + start_onboarding() must call this after infra provisioning: provision + never starts a browser, so without this step auth initiation always + dies with CDP connection refused on fresh nodes. + """ + port = _registry_port(node) + if not port: + return {"ok": False, "node": node, + "error": "unknown node %s (not in NODES.md registry)" % node} + if _cdp_dry_run(node): + return {"ok": True, "node": node, "cdp_port": port, "already": True} + STATE_DIR.mkdir(parents=True, exist_ok=True) + log_path = STATE_DIR / ("%s-chrome.log" % node) + cmd = [str(BIN_DIR / "netvm-chrome.sh"), "--headless", + "--cdp-port", str(port), node, "https://muse.ai"] + with open(log_path, "ab") as log: + subprocess.Popen(cmd, start_new_session=True, + stdout=log, stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL) + deadline = time.time() + timeout + while time.time() < deadline: + time.sleep(poll_interval) + if _cdp_dry_run(node): + return {"ok": True, "node": node, "cdp_port": port, "already": False} + return {"ok": False, "node": node, "cdp_port": port, + "error": "browser launched but CDP stayed unreachable on port %s (log: %s)" % (port, log_path)} + + def start_onboarding(node: str, email: str, beneficiary_node: Optional[str] = None, invite_code: Optional[str] = None, account_name: Optional[str] = None) -> Dict[str, Any]: """Phase 1 & 2: Provision infra, choose beneficiary invite code, and initiate authentication.""" b_node, code, reason = select_urgent_beneficiary(beneficiary_node, invite_code) @@ -279,6 +329,14 @@ def start_onboarding(node: str, email: str, beneficiary_node: Optional[str] = No state.stage = STAGE_INFRA state.save() + # 1b. Ensure the headless browser is up (provision never starts one). + browser_res = ensure_node_browser(node) + if not browser_res.get("ok"): + state.stage = "browser_failed" + state.detail = browser_res.get("error") + state.save() + return {"ok": False, "state": asdict(state), "error": state.detail} + # 2. Initiate authentication client = CredClient() cred_res = client.initiate(node, email, service="muse", account_name=account_name) @@ -408,12 +466,17 @@ def issue_salvage_work_order(blocked_node: str = "646", to_sidechat: str = "646 "--to", "opm", "--target", to_sidechat, "--title", title, - "--body", body, "--priority", "urgent", - "--allow-main-chat" + "--allow-main-chat", + body, ] res = subprocess.run(cmd, capture_output=True, text=True) - return {"ok": res.returncode == 0, "output": res.stdout.strip()} + out = res.stdout.strip() + result: Dict[str, Any] = {"ok": res.returncode == 0, "output": out} + if not result["ok"]: + err = res.stderr.strip() + result["error"] = err or out or "dm wo exited %d" % res.returncode + return result def get_all_connects(fast: bool = True) -> List[Dict[str, Any]]: diff --git a/bin/prompt_envelope.py b/bin/prompt_envelope.py index 4674d32..ac92035 100644 --- a/bin/prompt_envelope.py +++ b/bin/prompt_envelope.py @@ -118,7 +118,7 @@ def wrap(job_name, job_id, agent, target, rendered, include_kpi: bool = True): top = ( f"Operator Directive [ref:{wo_id}]:\n" - f"Host tmux worker session '{session_name}' is available on bl (/tmp/tmux-muse.sock).\n" + f"Persistent box runtime is on bl (/tmp/tmux-muse.sock). No worker session exists yet — create yours first: [TOOL tmux.new {{\"session\": \"{session_name}\", \"command\": \"bash\"}}].\n" f" • Subagent assistance: {spawn}\n" f" • Verification schedule: {follow}\n" f"{advisory_section}\n" @@ -128,8 +128,9 @@ def wrap(job_name, job_id, agent, target, rendered, include_kpi: bool = True): has_result = "[RESULT" in rendered bottom = ( "\n--- End Task ---\n\n" - f"Inspect tmux worker: box tmux capture {session_name} 30 (or attach via /tmp/tmux-muse.sock)\n" - "Tools: cron.create, cron.runs, health.check, swarm.spawn, swarm.list, dm.send, box.exec, tools.list.\n" + f"Worker convention: name your tmux session {session_name} when you create it, then inspect via box tmux capture {session_name} 30.\n" + "Flow in tmux: [TOOL flow.start {\"flow_id\": \"\", \"command\": \"\"}] | read delta: [TOOL flow.read {\"flow_id\": \"\"}] | advance: [TOOL flow.send {\"flow_id\": \"\", \"command\": \"...\"}].\n" + "Tools: flow.start, flow.read, flow.send, cron.create, health.check, swarm.spawn, dm.send, box.exec, tools.list.\n" "Message a peer: [DM {\"to\": \"\", \"target\": \"\", \"message\": \"\"}].\n" "Query box: [TOOL box.exec {\"action\": \"\"}] — [TOOL tools.list {}] lists every op.\n" ) diff --git a/bin/response-harvester.py b/bin/response-harvester.py index 1571422..d82d007 100755 --- a/bin/response-harvester.py +++ b/bin/response-harvester.py @@ -109,7 +109,18 @@ def iter_result_markers(text): """Yield (job_id, result_text) for every [RESULT ] marker in text.""" current_re = lookup_engine.get_result_regex() if HAS_LOOKUP_ENGINE else RESULT_RE for m in current_re.finditer(text or ""): - yield m.group(1).strip(), m.group(2).strip() + gd = m.groupdict() + if "summary" in gd: + # Engine shape: [RESULT ] [STATUS] . The + # status word is optional (None for bare markers); keep + # it when present so FAIL/ERROR still trips failure + # detection downstream. + status = (m.group("status") or "").strip() + summary = (m.group("summary") or "").strip() + result_text = f"{status} {summary}".strip() if status else summary + yield m.group("job_id").strip(), result_text + else: + yield m.group(1).strip(), m.group(2).strip() # Verb markers for the digest response protocol: @@ -316,9 +327,19 @@ def result_has_evidence(result_text): return bool(_PROOF_EVIDENCE_RE.search(result_text or "")) +# Automated in-thread proof requests disabled per fleet governance decision (2026-10-09) +PROOF_REQUESTS_ENABLED = False + def maybe_request_proof(agent, thread_id, job_id, result_text, dry_run=False): """Ask for checkable evidence when a success RESULT has none. + Disabled by default per fleet decision 2026-10-09: automated in-thread proof + challenges trigger adversarial rejection loops and waste agent quota. + """ + if not PROOF_REQUESTS_ENABLED: + return False + """Ask for checkable evidence when a success RESULT has none. + One-shot per (thread, job) via the nudge tracker. Returns True when a proof followup was scheduled. """ @@ -559,6 +580,42 @@ def format_tool_result_for_chat(op, raw_output): out = out[:900] + "\n…(truncated, refine the call for detail)" return f"box result:\n```\n{out}\n```" + if op == "flow.start" and isinstance(data, dict): + if not data.get("ok"): + return f"Flow start failed: {data.get('error')}" + return f"Flow `{data.get('flow_id')}` started in pane `{data.get('session')}` (status: {data.get('status')})." + + if op == "flow.read" and isinstance(data, dict): + if not data.get("ok"): + return f"Flow read failed: {data.get('error')}" + st = data.get("status", "unknown") + ec = data.get("exit_code") + ec_str = f" (exit_code: {ec})" if ec is not None else "" + pm = data.get("prompt_match") + prompt_str = f"\nPrompt waiting: {pm.get('text', pm)}" if pm else "" + delta = data.get("delta", "").strip() + trunc = f" (last {data.get('lines_read')} lines)" if data.get("truncated") else "" + body = f"\n```\n{delta}\n```" if delta else " (no new output)" + return f"Flow `{data.get('flow_id')}` [{st}]{ec_str}{prompt_str}{trunc}:{body}" + + if op == "flow.send" and isinstance(data, dict): + if not data.get("ok"): + return f"Flow send failed: {data.get('error')}" + kind = "command" if data.get("is_command") else "keys" + return f"Flow `{data.get('flow_id')}` sent {kind}: `{data.get('sent')}` (status: {data.get('status')})." + + if op == "flow.list" and isinstance(data, dict): + flows = data.get("flows", []) + if not flows: + return "No active flows." + lines = [f"{len(flows)} flows:"] + for f in flows[:8]: + lines.append(f" • {f.get('flow_id')} [{f.get('status')}]: {f.get('session')} (cmd: {str(f.get('command', 'bash'))[:30]})") + return "\n".join(lines) + + if op == "flow.stop" and isinstance(data, dict): + return f"Flow `{data.get('flow_id')}` stopped." + # General fallback: compact JSON capped to 400 chars s = json.dumps(data) return s[:400] + "..." if len(s) > 400 else s @@ -623,11 +680,59 @@ def is_fail_result(result_text): return t.startswith(FAIL_PREFIXES) +RECENCY_WINDOW_SEC = 10800 + +_JOB_ID_RE = re.compile(r"^(.+)-(\d{8})-(\d{6})-([0-9a-f]{8})$") + + +def dispatched_families_since(job_log_path, window_sec=RECENCY_WINDOW_SEC, + now=None): + """Job families dispatched inside the window. + + Scans job-log.jsonl for job_sent/job_dispatched events newer than + ``window_sec`` and returns their family names (the job id minus the + trailing -YYYYMMDD-HHMMSS- run suffix). Missing, unreadable, + or malformed input yields an empty set, never an exception. + """ + now = now or datetime.now(timezone.utc) + cutoff = now.timestamp() - window_sec + fams = set() + try: + handle = open(job_log_path, "r", encoding="utf-8") + except OSError: + return fams + with handle: + for line in handle: + line = line.strip() + if not line: + continue + try: + event = json.loads(line) + except Exception: + continue + if event.get("type") not in ("job_sent", "job_dispatched"): + continue + try: + ts = datetime.fromisoformat( + str(event.get("ts")).replace("Z", "+00:00")).timestamp() + except Exception: + continue + if ts < cutoff: + continue + match = _JOB_ID_RE.match(str(event.get("job_id") or "")) + if match: + fams.add(match.group(1)) + return fams + + def get_monitored_threads(target_agent=None): """ Build dict of threads to monitor per agent: { agent: [ {"id": "", "name": ""} ] } - Filters to permanent channels, threads with pending followups, or recent threads (< 3h). + Filters to permanent channels, threads with pending followups, + recently created threads (< 3h), or threads whose job family was + dispatched recently (< 3h) so old persistent sidechats that still + receive prompts stay monitored. """ agents = [target_agent] if target_agent else VALID_AGENTS threads_by_agent = {a: [] for a in agents} @@ -642,6 +747,7 @@ def get_monitored_threads(target_agent=None): PERM_KEYWORDS = ("coord", "tasks", "task", "brain", "heartbeat", "sync", "audit", "main-loop") now = datetime.now(timezone.utc) + recently_dispatched = dispatched_families_since(JOB_LOG, now=now) state_files = [JOB_SIDECHATS_FILE, WAKE_SIDECHATS_FILE] for sf in state_files: @@ -684,7 +790,9 @@ def get_monitored_threads(target_agent=None): if isinstance(val, dict) and val.get("archived") and not is_pending: continue - if not (is_perm or is_pending or is_recent): + is_dispatched = key in recently_dispatched + + if not (is_perm or is_pending or is_recent or is_dispatched): continue existing = [t["id"] for t in threads_by_agent[agent]] @@ -896,8 +1004,18 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo append_jsonl(CHAT_HISTORY_LOG, record) if author == "assistant": - markers = list(iter_result_markers(text)) - verbs = list(iter_verb_markers(text)) + try: + markers = list(iter_result_markers(text)) + verbs = list(iter_verb_markers(text)) + except Exception as e: + # One poison message must not wedge the batch: without + # this, the same crash repeats every cycle, the + # watermark never advances past it, and the thread's + # followups nag to escalation despite answered work. + sys.stderr.write( + "warning: marker extraction failed, treating as " + f"plain reply: {e}\n") + markers, verbs = [], [] # Synthesize [RESULT ] DECLINE if assistant explicitly refuses the task in plain text if not markers and not verbs and detect_explicit_refusal(text): @@ -946,19 +1064,27 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo try: import muse_hybrid thread_url = f"https://box.muse-dev.online/thread/{thread_id}" - tool_hint = ( - f"[Runtime Context: {thread_url}]\n" - f"Tools: EMIT one [TOOL ] line per action (you do not run it;" - f" the runtime executes it and replies here). curl -sk -X POST" - f" https://exec.muse-dev.online/exec works too.\n" - f" • [TOOL tools.list {{}}] — discover every op dynamically\n" - f" • [TOOL swarm.spawn {{\"count\": 1, \"task\": \"\"}}] — spawn subagents\n" - f" • [DM {{\"to\": \"\", \"target\": \"\", \"message\": \"\"}}] — send a DM\n" - f" • [TOOL box.exec {{\"action\": \"fleet-status\"}}] — call box (read-only actions)\n" - f" • [TOOL followup.create {{\"in_m\": 5, \"prompt\": \"\"}}]\n" - f" • [TOOL health.check {{}}]\n\n" - f"[Directive: Take next action or close with [RESULT ] ]" - ) + if op.startswith("flow."): + flow_id = t_args.get("flow_id", "") if isinstance(t_args, dict) else "" + tool_hint = ( + f"[Flow Directive: advance with [TOOL flow.send {{\"flow_id\": \"{flow_id}\", \"command\": \"...\"}}]" + f" | read with [TOOL flow.read {{\"flow_id\": \"{flow_id}\"}}]" + f" | close with [RESULT ] OK]" + ) + else: + tool_hint = ( + f"[Runtime Context: {thread_url}]\n" + f"Tools: EMIT one [TOOL ] line per action (you do not run it;" + f" the runtime executes it and replies here). curl -sk -X POST" + f" https://exec.muse-dev.online/exec works too.\n" + f" • [TOOL tools.list {{}}] — discover every op dynamically\n" + f" • [TOOL swarm.spawn {{\"count\": 1, \"task\": \"\"}}] — spawn subagents\n" + f" • [DM {{\"to\": \"\", \"target\": \"\", \"message\": \"\"}}] — send a DM\n" + f" • [TOOL box.exec {{\"action\": \"fleet-status\"}}] — call box (read-only actions)\n" + f" • [TOOL followup.create {{\"in_m\": 5, \"prompt\": \"\"}}]\n" + f" • [TOOL health.check {{}}]\n\n" + f"[Directive: Take next action or close with [RESULT ] ]" + ) if t_ok: clean_msg = format_tool_result_for_chat(op, t_res) resp_text = f"Tool result (`{op}`):\n{clean_msg}\n\n{tool_hint}" @@ -969,7 +1095,12 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo sys.stderr.write(f"warning: failed to post tool response back to thread: {te}\n") if markers or verbs: + seen_jobs = set() for job_id, result_text in markers: + if job_id in seen_jobs: + # Same verdict restated in one message: log once. + continue + seen_jobs.add(job_id) is_fail = is_fail_result(result_text) job_results += 1 @@ -983,6 +1114,11 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo "thread_id": thread_id, "msg_id": mid, } + if result_text.startswith("DECLINE:"): + # Synthesized (or explicit) decline: still a + # non-success (no chaining), but the auditor + # buckets it as declined, not a failure. + job_record["outcome"] = "declined" if not dry_run: append_jsonl(JOB_LOG, job_record) # Check if this is a swarm slot result: sw-YYYYMMDD-HHMMSS-xxxx/ @@ -1000,10 +1136,12 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo trigger_chain_next(job_id, result_text, success=not is_fail) if not is_fail: try: - maybe_request_proof(agent, thread_id, job_id, result_text) + maybe_request_proof(agent, thread_id, job_id, result_text, + dry_run=dry_run) except Exception as pe: sys.stderr.write(f"warning: proof check failed: {pe}\n") - archive_ephemeral_thread(agent, thread_id, job_id=job_id) + archive_ephemeral_thread(agent, thread_id, job_id=job_id, + dry_run=dry_run) clear_matching_followups(followups, agent, thread_id, mid, text, dry_run, job_id=job_id, verb="RESULT") for verb, job_id in verbs: @@ -1122,11 +1260,13 @@ def harvest_agent_thread(cdp, agent, thread_info, watermarks, followups, dry_run ) -def archive_ephemeral_thread(agent, thread_id, job_id=None): +def archive_ephemeral_thread(agent, thread_id, job_id=None, dry_run=False): """ If thread_id belongs to an ephemeral job or one-off check, archive it via hybrid gateway and tag it as archived in job-sidechats.json. """ + if dry_run: + return if not thread_id or not re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", thread_id.lower()): return @@ -1699,10 +1839,15 @@ def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=Fal # Match by job_id (from [RESULT ] or [VERB ]) -- # works regardless of thread_uuid or target. This is an ADDITIONAL - # path, not a replacement. + # path, not a replacement. Also matches the followup's own key + # (dm_id): agents quote the DM id from nudge text ([RESULT + # ]), which differs from job_id on DM-ordered followups + # (observed live: [RESULT f4293153] vs job ml-muse-*). match_job = False if job_id and f_rec.get("job_id") and f_rec.get("job_id") == job_id: match_job = True + elif job_id and job_id == f_id: + match_job = True # Non-RESULT verbs are job-scoped: they must not acknowledge/resolve # unrelated pending followups that merely share the thread. RESULT diff --git a/bin/tests/test_followup_fixes.py b/bin/tests/test_followup_fixes.py index dece4e0..4ae8249 100644 --- a/bin/tests/test_followup_fixes.py +++ b/bin/tests/test_followup_fixes.py @@ -609,6 +609,7 @@ def test_wired_dm_send_asserts_post_nav_url_before_send(): assert "assert_pre_send_placement" in src, \ "gate exists but dm_send never calls it" _orig_run_full = dm.run_full + _orig_sleep = dm.time.sleep _calls = [] def _stub(cmd, timeout=60): @@ -619,6 +620,9 @@ def test_wired_dm_send_asserts_post_nav_url_before_send(): try: dm.run_full = _stub + # Settle sleeps (1s/2s per gate call) are production pacing, not + # asserted behavior: skip them like the browser subprocess above. + dm.time.sleep = lambda s: None # 1. UUID-known thread, correct placement -> pass _stub.url = "https://muse.ai/thread/" + UUID_A ok, detail = gate("opm", "pipe-x", UUID_A, direct_nav_done=True) @@ -641,6 +645,7 @@ def test_wired_dm_send_asserts_post_nav_url_before_send(): assert ok is True, f"re-nav path should pass: {detail}" finally: dm.run_full = _orig_run_full + dm.time.sleep = _orig_sleep return nav_i = src.find("sidechat use") send_i = src.find("Send with verification retries") @@ -754,9 +759,141 @@ def main(): import unittest +# -------------------------------------------------------------------------- +# harvester resurrection -- dm_id markers, dry-run purity, scheduling +# -------------------------------------------------------------------------- + +def test_wired_harvester_dmid_marker_resolves(): + """Real clear_matching_followups: [RESULT ] resolves a + DM-ordered followup whose job_id differs (live f4293153 pattern: + marker quoted the nudge's DM id, record job was ml-muse-*). + Unrelated thread isolates the dm_id path from thread matching.""" + harv = _load("harvester_under_test", "response-harvester.py") + rec = _mk_rec(thread_uuid=UUID_B, job_id="ml-muse-20261007-013210") + fups = {"f4293153": rec} + harv.clear_matching_followups(fups, "646", "unrelated-thread", "mid-9", + "[RESULT f4293153] done", dry_run=True, + job_id="f4293153", verb="RESULT") + assert rec.get("status") == "resolved", ( + "DEVIATION: [RESULT ] does not resolve its followup -- " + "clear_matching_followups() matches marker ids against job_id " + f"only, never the followup key (status={rec.get('status')!r})") + # ... and a wrong id must not resolve. + rec2 = _mk_rec(thread_uuid=UUID_B, job_id="ml-muse-20261007-013210") + fups2 = {"f4293153": rec2} + harv.clear_matching_followups(fups2, "646", "unrelated-thread", "mid-9", + "[RESULT deadbeef] done", dry_run=True, + job_id="deadbeef", verb="RESULT") + assert rec2.get("status") == "pending", ( + f"wrong marker id wrongly resolved (status={rec2.get('status')!r})") + + +def test_wired_harvester_dry_run_has_no_side_effects(): + """process_messages(dry_run=True) with an evidence-less RESULT must + still extract the marker but must not fire proof followups, + archive threads, or persist anything.""" + harv = _load("harvester_under_test", "response-harvester.py") + calls = [] + saved = {n: getattr(harv, n) for n in + ("execute_agent_tool", "archive_ephemeral_thread", + "append_jsonl", "save_json_file")} + harv.execute_agent_tool = lambda *a, **k: calls.append("exec") or (True, {}) + harv.archive_ephemeral_thread = ( + lambda *a, **k: calls.append("archive")) + harv.append_jsonl = lambda *a, **k: calls.append("append") + harv.save_json_file = lambda *a, **k: calls.append("save") + try: + msgs = [{"id": "m1", "author": "assistant", + "text": "[RESULT j1] done", + "ts": "2026-10-07T00:00:00+00:00"}] + new, wm, nres = harv.process_messages( + msgs, "646", UUID_A, "t", None, {}, dry_run=True) + finally: + for n, fn in saved.items(): + setattr(harv, n, fn) + assert nres == 1, "dry-run must still extract markers" + assert calls == [], f"dry-run leaked side effects: {calls}" + + +def test_wired_result_markers_bare_and_status_forms(): + """iter_result_markers handles the engine's 3-group shape: a bare + [RESULT ] (status None) must not crash, and a status + token must survive into the result text for fail detection.""" + harv = _load("harvester_under_test", "response-harvester.py") + assert list(harv.iter_result_markers("[RESULT f4293153] done")) == [ + ("f4293153", "done")], "bare RESULT marker must extract cleanly" + jid, text = list(harv.iter_result_markers("[RESULT j9] FAIL blew up"))[0] + assert jid == "j9" and "FAIL" in text and "blew up" in text, ( + f"status token must survive into result text (got {jid!r} {text!r})") + + +def test_wired_poison_message_does_not_wedge_batch(): + """A marker-extraction crash degrades to plain-reply handling so + sibling messages still process and the watermark keeps advancing.""" + harv = _load("harvester_under_test", "response-harvester.py") + real_iter = harv.iter_result_markers + real_nudge = harv.maybe_nudge_untagged_sidechat + def boom(text): + if "POISON" in (text or ""): + raise RuntimeError("boom") + return real_iter(text) + harv.iter_result_markers = boom + harv.maybe_nudge_untagged_sidechat = lambda *a, **k: None + try: + msgs = [ + {"id": "m1", "author": "assistant", + "text": "POISON [RESULT x] y", + "ts": "2026-10-07T00:00:00+00:00"}, + {"id": "m2", "author": "assistant", + "text": "[RESULT j2] ok", + "ts": "2026-10-07T00:01:00+00:00"}, + ] + new, wm, nres = harv.process_messages( + msgs, "646", UUID_A, "t", None, {}, dry_run=True) + finally: + harv.iter_result_markers = real_iter + harv.maybe_nudge_untagged_sidechat = real_nudge + assert nres == 1, "sibling marker must still extract" + assert [m["id"] for m in new] == ["m1", "m2"], \ + "both messages must process past the poison one" + + +def test_harvester_timer_unit_wired(): + """The harvester must be scheduler-owned: unit files exist, the + service runs --once, and the timer fires on a short cadence. + Ingestion died silently for ~22h with no unit at all.""" + root = BIN_DIR.parent + svc = (root / "systemd" / "response-harvester.service").read_text() + tmr = (root / "systemd" / "response-harvester.timer").read_text() + assert "response-harvester.py" in svc and "--once" in svc, \ + "service must run the harvester --once" + assert "OnUnitActiveSec=" in tmr, "timer needs a repeat cadence" + assert "WantedBy=timers.target" in tmr, "timer must target timers.target" + + +def test_collection_adapter_is_single_and_pytest_opted_out(): + """Collection-shape guard (no 3x duplicates): exactly one TestCase + adapter is reachable from module globals (the adapter loop must not + leak a `_fn` alias that pytest collects as a second class), and the + adapter opts out of pytest (`__test__ = False`) so the module-level + functions are pytest's single source while unittest discovery still + runs the adapter.""" + cases = [v for v in list(globals().values()) + if inspect.isclass(v) and issubclass(v, unittest.TestCase)] + assert len(cases) == 1, ( + f"expected exactly 1 TestCase adapter, found {len(cases)} " + f"(stray aliases reintroduce duplicate collection)") + assert TestFollowupFixes.__test__ is False, ( + "TestFollowupFixes must set __test__ = False so pytest collects " + "each test once via the module-level functions") + + class TestFollowupFixes(unittest.TestCase): """unittest discovery adapter for contract and wired test functions.""" - pass + # pytest collects the module-level functions; skip the adapter so each + # test runs once. (unittest discovery ignores __test__ and still runs + # the adapter, which is its only view of this file's tests.) + __test__ = False for _name, _fn in list(globals().items()): @@ -767,6 +904,11 @@ for _name, _fn in list(globals().items()): return _runner setattr(TestFollowupFixes, _name, _bind(_fn)) +# Drop the loop temporaries: after the final iteration `_fn` aliases +# TestFollowupFixes, and pytest collects TestCase subclasses regardless of +# name -- that stray alias was the third copy (module fn + adapter + `_fn`). +del _name, _fn + if __name__ == "__main__": sys.exit(main()) diff --git a/bin/tmux_auto_approver.py b/bin/tmux_auto_approver.py index db7e712..f320100 100755 --- a/bin/tmux_auto_approver.py +++ b/bin/tmux_auto_approver.py @@ -13,6 +13,7 @@ Supports: - A/B/C choice prompts -> "A" - Numbered menus -> "1" - y/n confirmation prompts -> "y" + - Interview navigate+select menus (cursor on 1 -> Enter) - Press Enter prompts -> "Enter" - Safety guardrails (passwords, passkeys, destructive commands are never auto-approved) 4. State persistence & audit logging: @@ -137,6 +138,21 @@ DEFAULT_RULES: List[MatchRule] = [ description="Confirms y/n at end of terminal line", press_enter=True, ), + MatchRule( + id="interview_select", + name="Interview Menu (cursor on 1)", + pattern=(r"\?\s*\n" + r"(?:[^\n]*\n){0,8}" + r"[ \t]*(?:›|>)[ \t]*1\.[ \t]+\S[^\n]*\n" + r"(?:[^\n]*\n){0,10}" + r"[ \t]*2\.[ \t]+\S"), + response_key="Enter", + category="enter", + enabled=True, + description=("Selects highlighted option 1 on navigate+select " + "menus (cursor on 1. + 2. + ?-question above)"), + press_enter=False, + ), MatchRule( id="enter_to_continue", name="Press Enter to Continue", @@ -188,9 +204,22 @@ class AutoApproverState: try: with open(STATE_FILE) as f: data = json.load(f) - return cls(**data) + st = cls(**data) except Exception: return cls() + # Migrate: append built-in rules missing from stored state (a new + # default must reach the daemon without wiping operator toggles). + try: + have = {r.get("id") for r in st.rules + if isinstance(r, dict)} + missing = [asdict(r) for r in DEFAULT_RULES + if r.id not in have] + if missing: + st.rules.extend(missing) + st.save() + except Exception: + pass + return st # ===================================================================== @@ -300,6 +329,23 @@ def capture_pane_text(socket_path: str, pane_id: str, lines: int = 30) -> str: MUSE_COMMAND_HINTS = ("muse-bin", "muse-code") +# (socket, pane) ever observed running a muse runtime. pane_current_command +# flickers to the child tool while the agent works, so a muse pane stays +# muse-owned when its foreground reads "python3" (observed live: the hint +# gate missed tool-running panes and both daemons stacked 'y' answers). +_MUSE_PANES_SEEN = set() + +# tmux rule category -> muse watcher kind for verified sends. Text-input +# categories verify render + submit with one retry; single-key widgets +# (and unknown categories) stay blind. +_CATEGORY_KIND_MAP = { + "choice": "letter", + "menu": "numbered", + "confirm": "yn", + "muse_code": "muse-approval", + "enter": None, +} + def should_defer_to_muse_watcher(socket_path: str, pane_id: str, current_command: str) -> bool: @@ -309,16 +355,25 @@ def should_defer_to_muse_watcher(socket_path: str, pane_id: str, panes (stability + re-verify + once-per-prompt + decided-block guard). When its daemon is alive for this socket:pane, tmux must skip the pane entirely, or both daemons answer the same prompt - within the same second ('11' + stray keys, observed live). Never - raises: import or liveness failures mean no owner, handle here. + within the same second ('11' + stray keys, observed live; later the + same hole stacked 'y' answers when the foreground flickered to a + child tool mid-poll). Never raises: import or liveness failures + mean no owner, handle here. """ try: - cmd = current_command or "" - if not any(h in cmd for h in MUSE_COMMAND_HINTS): - return False import muse_choice_watcher as mcw - alive = getattr(mcw, "watcher_alive", mcw.is_running) - return alive(socket_path, pane_id) is not None + cmd = current_command or "" + key = (socket_path, pane_id) + if any(h in cmd for h in MUSE_COMMAND_HINTS): + _MUSE_PANES_SEEN.add(key) + alive = getattr(mcw, "watcher_alive", mcw.is_running) + return alive(socket_path, pane_id) is not None + if key in _MUSE_PANES_SEEN: + alive = getattr(mcw, "watcher_alive", mcw.is_running) + return alive(socket_path, pane_id) is not None + # Never observed as muse: cheap pidfile check only (covers a + # watcher racing ahead of our first observation of the pane). + return mcw.is_running(socket_path, pane_id) is not None except Exception: return False @@ -568,15 +623,25 @@ class AutoApproverRunner: }) continue - # Execute key dispatch + # Execute key dispatch through the verified send path: + # literal text paced apart from Enter (a single-call + # burst arrives as paste and lands a newline in + # composers instead of submitting, then re-fires past + # dedup and stacks). Text-input categories also verify + # render + submit with one retry; single-key widgets + # stay blind. success = False + detail = {"verified": None, "retried": False} if not self.dry_run: - args = ["send-keys", "-t", p.pane_id, verdict.key] - if verdict.press_enter or verdict.key == "Enter": - if verdict.key != "Enter": - args.append("Enter") - rc, _, _ = run_tmux_cmd(p.socket, *args) - success = (rc == 0) + import muse_choice_watcher as mcw + want_enter = (verdict.key != "Enter" + and bool(verdict.press_enter)) + ok, detail = mcw.send_answer( + p.socket, p.pane_id, verdict.key, + enter=want_enter, + kind=_CATEGORY_KIND_MAP.get(verdict.category), + sig=sig) + success = bool(ok) else: success = True # dry-run simulated @@ -597,6 +662,8 @@ class AutoApproverRunner: "excerpt": verdict.excerpt, "dry_run": self.dry_run, "success": success, + "verified": detail["verified"], + "retried": detail["retried"], } self.record_audit(event) actions_taken.append(event) diff --git a/bin/tmux_server_watchdog.py b/bin/tmux_server_watchdog.py index 913eba6..5c81ed1 100755 --- a/bin/tmux_server_watchdog.py +++ b/bin/tmux_server_watchdog.py @@ -2,10 +2,11 @@ """tmux_server_watchdog.py — Death-capture for tmux servers. Runs on a 1-minute systemd timer. Remembers each known socket's server -pid; when a server dies or its pid changes without a witnessed death, -appends a forensics bundle (dmesg OOM/kill lines, memory, uptime, -journal tail) to logs/tmux-server-deaths.jsonl so the next "tmux -crashed" leaves evidence instead of a mystery. +identity (pid + /proc starttime + ppid + cmdline); when a server dies, +its pid changes, or its pid is recycled under us without a witnessed +death, appends a forensics bundle (dmesg OOM/kill lines, memory, +uptime, journal tail) to logs/tmux-server-deaths.jsonl so the next +"tmux crashed" leaves evidence instead of a mystery. Read-only against tmux itself: one `display-message -p` probe per socket. Never raises; a watchdog must not need its own watchdog. @@ -55,9 +56,46 @@ def probe(socket_path): return None -def collect_forensics(socket_path, last_pid): +def proc_identity(pid): + """Identity dict for a pid: starttime defeats PID-reuse confusion. + + Never raises; on any failure returns {"pid": pid} so callers can + still snapshot. starttime is the raw /proc starttime tick (field + 22), stable for the life of the process.""" + ident = {"pid": pid} + try: + with open("/proc/%d/stat" % pid) as f: + parts = f.read().rsplit(")", 1)[1].split() + # After "(comm)": state ppid pgrp session tty_nr ... starttime + # is field 22 overall, i.e. parts[19] after the split above. + ident["ppid"] = int(parts[1]) + ident["starttime"] = int(parts[19]) + except Exception: + pass + try: + with open("/proc/%d/cmdline" % pid, "rb") as f: + raw = f.read().replace(b"\0", b" ").decode( + "utf-8", "replace").strip() + if raw: + ident["cmd"] = raw[:200] + except Exception: + pass + return ident + + +def probe_identity(socket_path): + """Enriched snapshot for a socket: identity dict or None.""" + pid = probe(socket_path) + if pid is None: + return None + return proc_identity(pid) + + +def collect_forensics(socket_path, last_pid, last_identity=None): """Best-effort death evidence. Dict of strings, never raises.""" ev = {"ts": _now(), "socket": socket_path, "last_pid": last_pid} + if last_identity: + ev["last_identity"] = last_identity rc, dmesg = _run(["dmesg"], timeout=10) if rc != 0: ev["dmesg"] = "unavailable: %s" % dmesg[:200] @@ -112,27 +150,60 @@ def append_death(ev, path=None): pass +def _as_identity(value): + """Normalize a probed value to an identity dict (legacy int ok).""" + if value is None: + return None + if isinstance(value, dict): + return value + return {"pid": value} + + +def _prev_identity(prev): + ident = {"pid": prev.get("pid")} + for key in ("starttime", "ppid", "cmd"): + if prev.get(key) is not None: + ident[key] = prev[key] + return ident + + def evaluate(previous, probed): - """Pure transition logic: (prev_state, {sock: pid|None}) -> - (new_state, events). Events: death | restart | started.""" + """Pure transition logic: (prev_state, {sock: pid|identity|None}) -> + (new_state, events). Events: death | restart | started. + + Probed values may be a bare pid (legacy) or an identity dict from + probe_identity(). Same pid with a different starttime is a restart + (pid recycled under us), not steady state.""" new_state, events = {}, [] - for sock, pid in sorted(probed.items()): + for sock, raw in sorted(probed.items()): + ident = _as_identity(raw) prev = (previous.get(sock) or {}) prev_pid = prev.get("pid") - if pid is None: + if ident is None: new_state[sock] = {"pid": None, "died": _now(), "last_pid": prev_pid} if prev_pid: events.append({"type": "death", "socket": sock, - "last_pid": prev_pid}) + "last_pid": prev_pid, + "last_identity": _prev_identity(prev)}) else: - new_state[sock] = {"pid": pid, "since": _now()} + pid = ident.get("pid") + new_state[sock] = dict(ident, since=_now()) if prev_pid and prev_pid != pid: # Changed with no witnessed death: restart inside one - # tick gap (or pid recycled under us). Treat as a - # restart, still worth a forensics note. + # tick gap. Worth a forensics note. events.append({"type": "restart", "socket": sock, - "old_pid": prev_pid, "pid": pid}) + "old_pid": prev_pid, "pid": pid, + "last_identity": _prev_identity(prev)}) + elif (prev_pid and prev_pid == pid + and prev.get("starttime") is not None + and ident.get("starttime") is not None + and prev["starttime"] != ident["starttime"]): + # Same pid, different process: pid recycled under us. + events.append({"type": "restart", "socket": sock, + "old_pid": prev_pid, "pid": pid, + "pid_reused": True, + "last_identity": _prev_identity(prev)}) elif not prev_pid and prev.get("died"): events.append({"type": "started", "socket": sock, "pid": pid}) @@ -144,18 +215,20 @@ def evaluate(previous, probed): def check(sockets=None, dry_run=False): """Probe, transition state, log deaths. Returns summary dict.""" - probed = {s: probe(s) for s in (sockets or KNOWN_SOCKETS)} + probed = {s: probe_identity(s) for s in (sockets or KNOWN_SOCKETS)} previous = read_state() new_state, events = evaluate(previous, probed) for ev in events: if ev["type"] == "death": - bundle = collect_forensics(ev["socket"], ev["last_pid"]) + bundle = collect_forensics(ev["socket"], ev["last_pid"], + ev.get("last_identity")) bundle["event"] = "death" if not dry_run: append_death(bundle) ev["forensics"] = bundle elif ev["type"] == "restart": - bundle = collect_forensics(ev["socket"], ev["old_pid"]) + bundle = collect_forensics(ev["socket"], ev["old_pid"], + ev.get("last_identity")) bundle["event"] = "restart-gap-missed" if not dry_run: append_death(bundle) diff --git a/dm-signers/allowed_signers b/dm-signers/allowed_signers index 6c1b1ea..5f4e494 100644 --- a/dm-signers/allowed_signers +++ b/dm-signers/allowed_signers @@ -7,3 +7,5 @@ operator-pip ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMq02n0LpsksyQzWAWQ1mS8gKOonqFA pip ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMq02n0LpsksyQzWAWQ1mS8gKOonqFALNDqbPGqXhq4T operator-pip operator-dev ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwHn0kmRa6SFPbr2+z75s0gRlvBCGR633Ag7gTqiYPa dev@netvm dev ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICwHn0kmRa6SFPbr2+z75s0gRlvBCGR633Ag7gTqiYPa dev@netvm +def ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEn6qqPrW7Vc77pUEBnLRDBF+yX11qyWzDTjZ2+FtL7b def@netvm +operator-def ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEn6qqPrW7Vc77pUEBnLRDBF+yX11qyWzDTjZ2+FtL7b def@netvm diff --git a/docs/AGENT-TOOLING.md b/docs/AGENT-TOOLING.md index 79b007d..18a7b96 100644 --- a/docs/AGENT-TOOLING.md +++ b/docs/AGENT-TOOLING.md @@ -177,6 +177,44 @@ Agents can emit structured tool calls in sidechats: --- +## 4.1. Agentic Flows in Tmux Panes (`box flow` & `[TOOL flow.*]`) + +Chromebox browser contexts prune and store chat history aggressively, making direct in-chat execution of long-running build, test, and shell tasks token-expensive and prone to context loss. + +To overcome this, Chromebox agents offload multi-turn execution to persistent tmux panes on `/tmp/tmux-muse.sock` using the **Flow Engine** (`bin/flow_engine.py`). Raw stdout/stderr streams to disk (`logs/flows/.log`), and agents read back only concise status and incremental output deltas. + +### Lifecycle & Primitives: +1. **Start Flow**: + Spawns pane `flow--` and launches command wrapped with an exit code sentinel. + ```text + [TOOL flow.start {"flow_id": "audit-tests", "command": "python3 -m unittest discover -s tests"}] + ``` + *CLI:* `box flow start audit-tests -c "python3 -m unittest discover -s tests"` + +2. **Read Incremental Delta & State**: + Inspects the pane for execution state (`working`, `idle`, `waiting_prompt`, `finished`, `failed`), exit code, and reads newly appended log output since the last read cursor. + ```text + [TOOL flow.read {"flow_id": "audit-tests"}] + ``` + *CLI:* `box flow read audit-tests --lines 40` + +3. **Advance or Respond to Prompts**: + Sends follow-up commands or keystrokes (such as interactive menu selections) without re-running the whole prompt. + ```text + [TOOL flow.send {"flow_id": "audit-tests", "command": "git diff"}] + [TOOL flow.send {"flow_id": "audit-tests", "keys": "1"}] + ``` + *CLI:* `box flow send audit-tests "git status" --command` + +4. **List & Stop**: + ```text + [TOOL flow.list {}] + [TOOL flow.stop {"flow_id": "audit-tests"}] + ``` + *CLI:* `box flow list` / `box flow stop audit-tests` + +--- + ## 5. Direct Operator Directives & Prompt Envelope Specification When jobs are dispatched to agents via `bin/job-dispatch.py`, they are wrapped in an actionable, authentic **Operator Directive** generated by `bin/prompt_envelope.py`. diff --git a/docs/MUSE-AUTH-CLI.md b/docs/MUSE-AUTH-CLI.md index 0dcc993..fa67625 100644 --- a/docs/MUSE-AUTH-CLI.md +++ b/docs/MUSE-AUTH-CLI.md @@ -1,13 +1,15 @@ # MUSE-AUTH-CLI Decision Record -Status: **Draft** — taken over in this checkout 2026-10-07 per user choice. -Only explicit user acceptance moves this document (or any decision) to Final. +Status: **Final** — accepted 2026-10-07 (user chose "accept Final with +a recorded amendment," waiving done-means item 3; see Amendment A1). Handoff note: a prior grill session settled D1–D11 and U1 and reportedly marked its own record Final, but that file lives in another checkout (absent -here; this repo has no MUSE-AUTH-CLI.md, PI-AGENT-AUTH.md, OPERATORS.md, or -agy-auth-switch). D1–D11 details below are CARRIED, not verified — their full -text needs a paste or peer handoff before this record can go Final. +here). D1–D11 full text never arrived; per Amendment A1 the item is WAIVED, +not verified — the decisions' substance stands proven by shipped, tested +implementations (resume pool, session bind, P1/P2) plus live verification +(2026-10-07: 27/27 unique session IDs, workspace scoping exact, refs +resolve, profiles annotate). ## Goal @@ -34,11 +36,12 @@ standard billing cycles (not a rolling 30-day window). Decisions exist; codification as an OPERATORS.md amendment delta is the U2 follow-on and is UNRESOLVED. -### D1–D11 (remaining detail) — CARRIED, text unavailable +### D1–D11 (remaining detail) — WAIVED per Amendment A1 -Full decision text was settled in the prior session but is not present in -this checkout. CARRIED as-is; paste or peer handoff required to verify. -This record cannot go Final until they are quoted or re-settled here. +Full decision text was settled in the prior session but never arrived in +this checkout. Waived: re-verification by transcript would add words, not +evidence. If the original text surfaces and contradicts built behavior, +built behavior wins unless a new interview reopens the item. ## Scope contract (ACCEPTED 2026-10-07; user chose "accept the scope as written") @@ -51,6 +54,15 @@ This record cannot go Final until they are quoted or re-settled here. interview; accepting this record never approves them. - "Go"/"do it all" authorize only the boundary above. +## Amendment A1 (ACCEPTED 2026-10-07 with Final) + +Done-means item (3) ("D1–D11 text verified or re-settled") is WAIVED. +Rationale: the decisions' substance is verified by shipped, tested +implementations and live checks, not by recovering the lost transcript. +Recorded per the scope contract: this amendment is the explicit owner +approval for the narrowed completion boundary. U1, P1, P2, P3 stand as +settled; implementation and U2 remain separate stages. + ## Settled Decisions (New) ### P1. Push/pull transfer file set — SETTLED (Credentials + Metadata) @@ -86,3 +98,15 @@ muse-bin identity and watcher coverage is untouched. Tests: tests/test_muse_session_bind.py (13). Follow-ups for the owning lanes: wire `box runtime launch` / resume-pool `resume` through the binder, and arm a reap timer once the profile store (P1) exists. + +Cross-agent note (2026-10-07, factual, no decision change): the peer's +wrapper is DEPLOYED as `muse-code` (symlink to +`~/Account(s)/muse_wrapper.py`); 3 live sessions observed bound under +it (profile `def`), alongside unbound direct-`muse-bin` sessions. +Peer monitor daemons were absent on inspection; a fingerprint one-shot +showed all sessions in sync (no drift, nothing written). Monitor +reliability is the peer lane; reap-by-scan stays the immune +complement. The original D1–D11 text was recovered (peer's +MUSE-AUTH-CLI.md) and reviewed: no contradiction with built behavior; +the A1 waiver stands. Convergence proposal (open): peer adopts a bind +record, NetVM reap learns the peer dirname pattern. diff --git a/docs/OPERATOR-DRIVE-RUNBOOK.md b/docs/OPERATOR-DRIVE-RUNBOOK.md index 5d6ea34..130b834 100644 --- a/docs/OPERATOR-DRIVE-RUNBOOK.md +++ b/docs/OPERATOR-DRIVE-RUNBOOK.md @@ -150,3 +150,41 @@ cat shared/operators/SOUL.md | ssh -o StrictHostKeyChecking=no -J super@34.139.3 2. **Safety Gates on Amendments**: `box md amend` automatically validates that amendments do not remove checklists or revert `SOUL.md` to passive templates. 3. **Relative Paths in Hatch RPC**: Hatch WebSocket RPC rejects absolute paths (`/SOUL.md` fails; `SOUL.md` succeeds). 4. **Dual Access Redundancy**: If SSH reverse tunnels drop, Hatch WebSocket RPC is independent of SSH and can be used immediately to inspect logs, repair `authorized_keys`, or restart watchdog scripts. + +--- + +## 5. SSH Access-Management Decisions (DRAFT — grill interview in progress) + +> Status: DRAFT. Each decision below is written as the interview settles it. +> Nothing here is Final until the owner explicitly accepts the full text. +> Context: 2026-10-07 key-resolution run — all 5 agents refused dial-in key +> install via chat relay (impersonation-pattern defense); keys were placed +> via the operator Hatch channel instead; file modes remain the open gap. + +### Scope contract (SETTLED — Draft) +- **Artifact boundary**: Section 5 of this file (the decision record) PLUS + approval of execution stages E1–E3 below. Out of scope: code changes, + other doc rewrites, and any new PR or task program beyond E1–E3. +- **Done means**: Scope + D1–D5 + E1–E3 all written as settled text; the + owner explicitly accepts the full section; then it flips to Final. +- **Stages**: E1–E3 are approved here as plans with named owners and + verification steps. Ending the interview never authorizes + implementation — execution needs a separate explicit request afterward. +- Set by owner choice ("1" = wider-boundary alternative) on 2026-10-07. + +### D1. `.ssh/authorized_keys` validator allowlist (UNRESOLVED) +- Whether the exact-match allowlist in `agent_md.py` (`MD_ALLOWED_SUBPATHS`) + stays as the permanent operator key-install mechanism. + +### D2. Authority boundary: platform writes vs relayed instructions (UNRESOLVED) +- Whether operator Hatch writes are a legitimate access-grant channel when + agents refuse the same grant via chat relay, and under what conditions. + +### D3. bl→VM jump-key provisioning (UNRESOLVED) +- The sanctioned process for getting bl operator SSH access to the jump host. + +### D4. def/dev tunnel restoration (UNRESOLVED) +- Who provisions tunnel identities and VM-side authorization once jump works. + +### D5. File-mode gap on the Hatch write path (UNRESOLVED) +- How `authorized_keys` gets to 600 given the gateway cannot set modes. diff --git a/job-sidechats.json b/job-sidechats.json index 3d102b9..f0cb0ae 100644 --- a/job-sidechats.json +++ b/job-sidechats.json @@ -118,11 +118,14 @@ "type": "persistent" }, "heartbeat": { - "thread_uuid": "557a4177-901a-4b20-b193-21ac992d49a8", + "thread_uuid": "77acfb50-b6ca-4526-b8aa-1efd9e10d5bd", "agent": "opm", "title": "heartbeat", "type": "persistent", - "created_at": "2026-10-06T03:10:03.642110+00:00" + "created_at": "2026-10-08T14:31:45.392643+00:00", + "dispatch_count": 44, + "rotated_from": "557a4177-901a-4b20-b193-21ac992d49a8", + "rotated_at": "2026-10-08T14:31:45.392657+00:00" }, "heartbeat-opm": { "thread_uuid": "ac8c3366-a2b4-407d-adc7-bfa18903c0f5", @@ -240,18 +243,24 @@ "type": "persistent" }, "box-http-health": { - "thread_uuid": "5fcb395e-24e4-4b4a-92a8-85edaba710ba", + "thread_uuid": "2b63e639-ef56-4827-a93f-fcecb59f7cd4", "agent": "646", - "title": "box-http-health-2026-10-06T05:41:24.064445+00:00", + "title": "box-http-health-2026-10-09T04:31:37.517106+00:00", "type": "persistent", - "created_at": "2026-10-06T05:41:47.860796+00:00" + "created_at": "2026-10-09T04:31:40.122233+00:00", + "dispatch_count": 1, + "rotated_from": "3330b615-6019-40c9-9c3a-a025fd341c3f", + "rotated_at": "2026-10-09T04:31:40.122245+00:00" }, "box-service-health": { - "thread_uuid": "da4f9f77-f1de-44ef-a7f3-b46519bc3542", + "thread_uuid": "4d0ca5fd-7a58-4da2-99e6-dbdc958c58f2", "agent": "646", - "title": "box-service-health-2026-10-05T23:52:00.863450+00:00", + "title": "box-service-health-2026-10-09T04:40:55.268297+00:00", "type": "persistent", - "created_at": "2026-10-05T23:53:02.748190+00:00" + "created_at": "2026-10-09T04:40:57.664324+00:00", + "dispatch_count": 1, + "rotated_from": "54299d95-38ee-427b-9db0-97fcef77eaf4", + "rotated_at": "2026-10-09T04:40:57.664336+00:00" }, "box-deep-health": { "thread_uuid": "6628c035-4413-4d9f-863c-56ee861c8c83", @@ -285,7 +294,10 @@ "agent": "646", "title": "box-http-health-2026-10-05T03:17:43.411483+00:00", "created_at": "2026-10-05T03:17:45.822551+00:00", - "type": "ephemeral" + "type": "ephemeral", + "archived": true, + "archived_at": "2026-10-08T15:12:18.169605+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "canary-2026-10-05T03:18:05.975516+00:00": { "thread_uuid": "cdba6af0-ce03-4db2-8036-03eddf7aab41", @@ -306,7 +318,10 @@ "agent": "646", "title": "box-http-health-2026-10-05T03:30:01.629898+00:00", "created_at": "2026-10-05T03:30:06.414723+00:00", - "type": "ephemeral" + "type": "ephemeral", + "archived": true, + "archived_at": "2026-10-08T15:12:22.913788+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "box-service-health-2026-10-05T03:37:00.096660+00:00": { "thread_uuid": "3b50c620-ad3a-4840-8449-991f2c3416e3", @@ -319,7 +334,10 @@ "thread_uuid": "39036562-762b-4a70-a90c-a8466e862aa6", "agent": "646", "title": "box-http-health-2026-10-05T03:45:00.595432+00:00", - "created_at": "2026-10-05T03:45:07.455504+00:00" + "created_at": "2026-10-05T03:45:07.455504+00:00", + "archived": true, + "archived_at": "2026-10-08T15:12:27.738901+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "box-service-health-2026-10-05T03:52:00.107905+00:00": { "thread_uuid": "1aa6544d-c0de-4eac-9d49-aeccb8785b9b", @@ -398,32 +416,44 @@ "created_at": "2026-10-05T04:35:47.208458+00:00" }, "autonomy-pulse-646": { - "thread_uuid": "3313d011-4525-4e1b-b830-eab1c6bb8845", + "thread_uuid": "5131b62b-01c8-4573-98dd-17f9a03c695a", "agent": "646", - "title": "autonomy-pulse-646-2026-10-06", + "title": "autonomy-pulse-646-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T05:00:53.290329+00:00" + "created_at": "2026-10-09T00:31:34.491871+00:00", + "dispatch_count": 5, + "rotated_from": "dd743105-5e07-445c-9428-1f7145995437", + "rotated_at": "2026-10-09T00:31:34.491879+00:00" }, "autonomy-pulse-pip": { - "thread_uuid": "9eb27e9f-26a9-436f-8fb4-94f4b81a6859", + "thread_uuid": "93267459-91d7-4a30-8069-39dcb291ac51", "agent": "pip", - "title": "autonomy-pulse-pip-2026-10-06", + "title": "autonomy-pulse-pip-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T01:40:03.202441+00:00" + "created_at": "2026-10-09T00:40:50.376245+00:00", + "dispatch_count": 12, + "rotated_from": "64a7c541-6bb5-46fc-90cf-ce9eefdedc9c", + "rotated_at": "2026-10-09T00:40:50.376256+00:00" }, "autonomy-pulse-opm": { - "thread_uuid": "1156e90d-0b07-4ffe-a73f-edb017794b44", + "thread_uuid": "854a9b9f-6e3d-4c11-a8d5-c0786cd8b689", "agent": "opm", - "title": "autonomy-pulse-opm-2026-10-06", + "title": "autonomy-pulse-opm-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T00:50:03.206929+00:00" + "created_at": "2026-10-09T00:51:44.427423+00:00", + "dispatch_count": 12, + "rotated_from": "21fcdbc6-00eb-47eb-8f0f-cda18ca747dc", + "rotated_at": "2026-10-09T00:51:44.427438+00:00" }, "muse-auditor": { - "thread_uuid": "e7ed1f57-0926-4fc0-a2a2-457a141c12b5", + "thread_uuid": "d0dfe7ea-4b30-414c-a15a-818f7b4a82ad", "agent": "muse", - "title": "muse-audit-2026-10-06", + "title": "muse-audit-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T02:15:02.756590+00:00" + "created_at": "2026-10-09T00:01:38.258549+00:00", + "dispatch_count": 30, + "rotated_from": "31127eea-259d-412a-9590-7bef43cbf6ed", + "rotated_at": "2026-10-09T00:01:38.258559+00:00" }, "work-finder": { "thread_uuid": "16b052eb-acf1-410b-b9af-ee8c6b8bb8d5", @@ -436,11 +466,14 @@ "archived_by_job": "work-finder-20261005-050235-b1514bf1" }, "auto-work-646-a01": { - "thread_uuid": "98d85b3a-d237-4e96-8319-678fe8f45fc3", + "thread_uuid": "c15b5a48-ac56-43c3-b5b4-8e7c473d01b9", "agent": "646", - "title": "auto-work-646-a01-2026-10-05", + "title": "auto-work-646-a01-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:05:03.015938+00:00" + "created_at": "2026-10-09T00:35:02.572438+00:00", + "dispatch_count": 5, + "rotated_from": "f3b0226c-57f2-4589-9d29-7caa814d31a8", + "rotated_at": "2026-10-09T00:35:02.572451+00:00" }, "opm-swarm-harvest-2026-10-05": { "thread_uuid": "c0654e8d-dcfa-4ab6-9814-0b66bf633d5b", @@ -461,25 +494,34 @@ "archived_by_job": "auto-work-opm-d01-20261005-050500-10a4e478" }, "auto-work-opm-d01": { - "thread_uuid": "67c25a4c-ab15-4ce0-b42b-f2dd09a56e52", + "thread_uuid": "5d84797d-a732-4ff0-9f73-e779fa728842", "agent": "opm", - "title": "auto-work-opm-d01-2026-10-05", + "title": "auto-work-opm-d01-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T11:05:23.285059+00:00" + "created_at": "2026-10-09T00:05:39.063973+00:00", + "dispatch_count": 12, + "rotated_from": "f64d216c-e4e1-46d0-8d7c-9d00e74c05a7", + "rotated_at": "2026-10-09T00:05:39.063984+00:00" }, "auto-work-queue-f04": { - "thread_uuid": "6a1ae5a0-2d24-4e8a-80de-c6e2e18ca3c1", + "thread_uuid": "d34b3a65-e6f7-455c-ae38-dc5e303d779f", "agent": "opm", - "title": "auto-work-queue-f04-2026-10-05", + "title": "auto-work-queue-f04-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T14:09:02.912812+00:00" + "created_at": "2026-10-09T00:11:41.191122+00:00", + "dispatch_count": 12, + "rotated_from": "d52d3264-ae76-41ec-b40e-0ee0a22f4ca9", + "rotated_at": "2026-10-09T00:11:41.191139+00:00" }, "auto-work-646-a02": { - "thread_uuid": "6e37fb29-fa56-4176-bc05-96fb9870507a", + "thread_uuid": "a9b8847f-9b03-487a-9928-776bae35fde4", "agent": "646", - "title": "auto-work-646-a02-2026-10-05", + "title": "auto-work-646-a02-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:10:03.839052+00:00" + "created_at": "2026-10-09T00:10:12.186279+00:00", + "dispatch_count": 5, + "rotated_from": "8d603600-54f0-4c75-9e00-17a7710f8cf6", + "rotated_at": "2026-10-09T00:10:12.186289+00:00" }, "scout-quiet-audit-20261005": { "thread_uuid": "a8d3f146-ad64-436a-8d5e-d4c37313fcce", @@ -495,25 +537,34 @@ "created_at": "2026-10-05T05:10:47.963804+00:00" }, "auto-work-health-h03": { - "thread_uuid": "589954d6-fba1-402b-82dc-2756f0000c98", + "thread_uuid": "0099533f-398c-4a6a-8d4b-9e9fb8034159", "agent": "646", "title": "auto-work-health-h03", "type": "persistent", - "created_at": "2026-10-05T05:11:03.515833+00:00" + "created_at": "2026-10-08T14:15:24.416536+00:00", + "dispatch_count": 15, + "rotated_from": "589954d6-fba1-402b-82dc-2756f0000c98", + "rotated_at": "2026-10-08T14:15:24.416548+00:00" }, "auto-work-queue-f03": { - "thread_uuid": "9dab72c4-83f0-428b-83c1-04804f76798f", + "thread_uuid": "7965fd24-85cb-4c86-9506-7d522813ffe7", "agent": "muse", - "title": "auto-work-queue-f03-2026-10-06", + "title": "auto-work-queue-f03-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T05:14:55.648486+00:00" + "created_at": "2026-10-09T00:11:30.374646+00:00", + "dispatch_count": 13, + "rotated_from": "b57e91ae-9280-42d2-9113-b355664902bc", + "rotated_at": "2026-10-09T00:11:30.374663+00:00" }, "auto-work-health-h01": { - "thread_uuid": "844f4bfe-5e42-4e09-850e-aabe181c5fcb", + "thread_uuid": "071df3f1-b236-4558-a123-00b5377b8f27", "agent": "646", "title": "auto-work-health-h01", "type": "persistent", - "created_at": "2026-10-05T05:13:03.182801+00:00" + "created_at": "2026-10-08T14:15:13.231094+00:00", + "dispatch_count": 15, + "rotated_from": "844f4bfe-5e42-4e09-850e-aabe181c5fcb", + "rotated_at": "2026-10-08T14:15:13.231107+00:00" }, "auto-work-646-a04-2026-10-05": { "thread_uuid": "3829f0d3-5e77-4ca7-bd1d-c3fe4ecf9cf4", @@ -525,11 +576,14 @@ "archived_by_job": "auto-work-646-a04-20261005-051300-228941e7" }, "auto-work-646-a03": { - "thread_uuid": "3a2b71c7-32e9-4280-bf23-f1982162a5f0", + "thread_uuid": "c7e6708b-4c39-4f3e-8b52-4fb4ba1c51cc", "agent": "646", - "title": "auto-work-646-a03-2026-10-05", + "title": "auto-work-646-a03-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:15:03.355385+00:00" + "created_at": "2026-10-09T00:45:03.092670+00:00", + "dispatch_count": 5, + "rotated_from": "4d42463d-4c30-42fa-a410-523fc47aabeb", + "rotated_at": "2026-10-09T00:45:03.092680+00:00" }, "auto-work-opm-d02-2026-10-05": { "thread_uuid": "719fc21f-e2df-4d96-96c0-dd23d5827523", @@ -541,32 +595,44 @@ "archived_by_job": "auto-work-opm-d02-20261005-051500-e3118209" }, "auto-work-646-a04": { - "thread_uuid": "a2cf2b5c-e7d4-4ce2-b03b-1754a23cb3a7", + "thread_uuid": "901cec00-3f7c-49c8-8edb-79e871706d4c", "agent": "646", - "title": "auto-work-646-a04-2026-10-06", + "title": "auto-work-646-a04-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T04:45:53.991238+00:00" + "created_at": "2026-10-09T00:45:13.617836+00:00", + "dispatch_count": 5, + "rotated_from": "896b07c5-c5e9-4000-8319-24cdbeba0452", + "rotated_at": "2026-10-09T00:45:13.617848+00:00" }, "auto-work-646-a05": { - "thread_uuid": "85b02558-b3a2-4136-915a-c349ab9c7055", + "thread_uuid": "3be81298-5e46-4b95-a6ed-45dbeca50f1a", "agent": "646", - "title": "auto-work-646-a05-2026-10-05", + "title": "auto-work-646-a05-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T14:47:03.553757+00:00" + "created_at": "2026-10-09T00:50:02.818756+00:00", + "dispatch_count": 5, + "rotated_from": "bf018cd0-dab1-4922-a6f3-8db3138bf65b", + "rotated_at": "2026-10-09T00:50:02.818768+00:00" }, "auto-work-opm-d02": { - "thread_uuid": "bf8c0384-c291-46b0-b6fe-753cf24843fb", + "thread_uuid": "dd6fda1c-1abb-49f4-b919-a3eefe10c3c2", "agent": "opm", - "title": "auto-work-opm-d02-2026-10-05", + "title": "auto-work-opm-d02-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:17:17.902534+00:00" + "created_at": "2026-10-09T00:15:35.865414+00:00", + "dispatch_count": 12, + "rotated_from": "d5b14f35-bdca-4054-a286-864f4b11f439", + "rotated_at": "2026-10-09T00:15:35.865429+00:00" }, "auto-work-queue-f05": { - "thread_uuid": "105d9ac7-2f50-4435-97ae-52b908d560e9", + "thread_uuid": "d6852998-f928-4203-90cb-094e03ccc3f5", "agent": "646", - "title": "auto-work-queue-f05-2026-10-05", + "title": "auto-work-queue-f05-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:17:23.971435+00:00" + "created_at": "2026-10-09T00:15:46.971867+00:00", + "dispatch_count": 5, + "rotated_from": "4df6e681-207f-4dac-b926-c1327417b0b5", + "rotated_at": "2026-10-09T00:15:46.971893+00:00" }, "auto-work-queue-f06": { "thread_uuid": "79498cd3-4b47-418f-a6d8-2f08ac0b492b", @@ -583,46 +649,60 @@ "created_at": "2026-10-05T12:11:03.148953+00:00" }, "auto-work-xop-e04": { - "thread_uuid": "a213ebfa-07f3-43dc-896a-562284341e10", + "thread_uuid": "4af7c38c-e2d3-4d67-bdf6-ef038edd6b5c", "agent": "muse", "title": "auto-work-xop-e04", "type": "persistent", - "created_at": "2026-10-05T05:17:43.665400+00:00" + "created_at": "2026-10-09T06:15:28.386851+00:00", + "dispatch_count": 7 }, "auto-work-xop-e05": { - "thread_uuid": "411a0ffb-33e2-44f3-b6ef-9ef4cb6e7c62", + "thread_uuid": "067b5316-9f53-4bd4-b091-bd3d09d6ca43", "agent": "646", "title": "auto-work-xop-e05", "type": "persistent", - "created_at": "2026-10-05T15:14:09.738335+00:00" + "created_at": "2026-10-08T14:16:09.589238+00:00", + "dispatch_count": 15, + "rotated_from": "411a0ffb-33e2-44f3-b6ef-9ef4cb6e7c62", + "rotated_at": "2026-10-08T14:16:09.589249+00:00" }, "auto-work-health-h09": { - "thread_uuid": "e8de3b07-daac-4713-a558-68947d7a2257", + "thread_uuid": "fb31b6d6-b4f0-4a5f-bfd1-737a9cf60e26", "agent": "646", "title": "auto-work-health-h09", "type": "persistent", - "created_at": "2026-10-05T05:18:03.211169+00:00" + "created_at": "2026-10-08T14:20:27.680446+00:00", + "dispatch_count": 15, + "rotated_from": "e8de3b07-daac-4713-a558-68947d7a2257", + "rotated_at": "2026-10-08T14:20:27.680459+00:00" }, "auto-work-646-a06": { - "thread_uuid": "108042b2-7e88-4e77-83cd-c186df7e093f", + "thread_uuid": "cf25b758-975d-4c1f-987d-ece34313a9b4", "agent": "646", - "title": "auto-work-646-a06-2026-10-05", + "title": "auto-work-646-a06-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:49:55.534138+00:00" + "created_at": "2026-10-09T00:50:14.162749+00:00", + "dispatch_count": 5, + "rotated_from": "c28905c4-8789-48c2-8882-c2a2a052f048", + "rotated_at": "2026-10-09T00:50:14.162760+00:00" }, "auto-work-pip-b06": { - "thread_uuid": "7875a2e9-4745-4610-8e6e-8babe7f56cf3", + "thread_uuid": "bf9e2dd4-0a35-41aa-af18-8197a13407ac", "agent": "pip", - "title": "auto-work-pip-b06-2026-10-05", + "title": "auto-work-pip-b06-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:20:21.474650+00:00" + "created_at": "2026-10-09T05:20:23.112235+00:00", + "dispatch_count": 1, + "rotated_from": "7875a2e9-4745-4610-8e6e-8babe7f56cf3", + "rotated_at": "2026-10-09T05:20:23.112247+00:00" }, "ops-audit": { - "thread_uuid": "c6d17777-43c2-4a21-a74e-779404fdb7f8", + "thread_uuid": "130ca59b-5a8f-4b8b-969b-7cbe61effc7d", "agent": "pip", "title": "ops-audit", "type": "persistent", - "created_at": "2026-10-06T01:57:42.056039+00:00" + "created_at": "2026-10-08T16:11:04.164352+00:00", + "dispatch_count": 2 }, "auto-work-swarm-g06": { "thread_uuid": "82ee854a-2e9a-4ed2-a9fe-0e1da6070af4", @@ -632,11 +712,14 @@ "created_at": "2026-10-05T12:17:03.571277+00:00" }, "auto-work-queue-f08": { - "thread_uuid": "25af5d14-7d68-4146-a9cf-c77efbb96e0a", + "thread_uuid": "0eeb0043-2f3c-4a0d-99d5-a9aadcd48262", "agent": "opm", - "title": "auto-work-queue-f08-2026-10-05", + "title": "auto-work-queue-f08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:21:03.183092+00:00" + "created_at": "2026-10-09T00:25:36.937248+00:00", + "dispatch_count": 12, + "rotated_from": "e0047e9b-a77d-4abc-83e7-131d252b4660", + "rotated_at": "2026-10-09T00:25:36.937264+00:00" }, "opm tasks": { "thread_uuid": "a27cdbd2-faea-4b61-a053-3d5c7071e1c6", @@ -645,39 +728,52 @@ "created_at": "2026-10-05T05:21:07.793544+00:00" }, "auto-work-646-a07": { - "thread_uuid": "8f609037-8d1b-47db-862b-2ae3b9ae1360", + "thread_uuid": "358ba0ca-9aa3-4365-8e53-1a1fda98cb23", "agent": "646", - "title": "auto-work-646-a07-2026-10-05", + "title": "auto-work-646-a07-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:53:30.270840+00:00" + "created_at": "2026-10-09T00:55:03.301843+00:00", + "dispatch_count": 5, + "rotated_from": "c79ee97e-fa3a-4546-af27-7029b114a4dd", + "rotated_at": "2026-10-09T00:55:03.301859+00:00" }, "auto-work-health-h13": { - "thread_uuid": "bac59a02-fd27-4232-9084-dd45c1a22e93", + "thread_uuid": "f1bd06ec-2ab0-463b-9c3d-c76ad9513913", "agent": "646", "title": "auto-work-health-h13", "type": "persistent", - "created_at": "2026-10-05T16:25:52.392505+00:00" + "created_at": "2026-10-08T14:25:14.375748+00:00", + "dispatch_count": 15, + "rotated_from": "bac59a02-fd27-4232-9084-dd45c1a22e93", + "rotated_at": "2026-10-08T14:25:14.375759+00:00" }, "auto-work-queue-f09": { - "thread_uuid": "edee681e-c7e7-48e7-8927-d23f1017d6a3", + "thread_uuid": "dad9fa6d-b697-43a0-8d15-a68effdeddd2", "agent": "646", - "title": "auto-work-queue-f09-2026-10-05", + "title": "auto-work-queue-f09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T10:26:12.220004+00:00" + "created_at": "2026-10-09T03:26:10.347784+00:00", + "dispatch_count": 2 }, "auto-work-646-a14": { - "thread_uuid": "e3ad4d2e-c26c-41b6-bdd2-7e3e7f9293e8", + "thread_uuid": "19b096ed-2d2c-486f-a9f2-88d7e3f618e2", "agent": "646", - "title": "auto-work-646-a14-2026-10-06", + "title": "auto-work-646-a14-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T03:56:13.004961+00:00" + "created_at": "2026-10-09T00:55:24.741117+00:00", + "dispatch_count": 5, + "rotated_from": "12a8d6bd-f40d-45d9-9b0e-91b81fe89e82", + "rotated_at": "2026-10-09T00:55:24.741128+00:00" }, "auto-work-646-a13": { - "thread_uuid": "8f5b0e24-0136-42b6-bec8-b327876d7a5a", + "thread_uuid": "33ef4f0e-021b-4c03-b2a0-15bed8e5e627", "agent": "646", - "title": "auto-work-646-a13-2026-10-06", + "title": "auto-work-646-a13-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T00:56:43.746024+00:00" + "created_at": "2026-10-09T00:55:13.843094+00:00", + "dispatch_count": 5, + "rotated_from": "de5e6793-48bf-4bd0-bd8b-20cb10448292", + "rotated_at": "2026-10-09T00:55:13.843109+00:00" }, "auto-work-swarm-g09": { "thread_uuid": "6dcf8342-5fb3-40c7-b213-1e709a7cf911", @@ -687,18 +783,24 @@ "created_at": "2026-10-05T14:26:03.120209+00:00" }, "auto-work-opm-d03": { - "thread_uuid": "c5e1271e-3477-41ab-92f8-13bf79d6a3d7", + "thread_uuid": "2818b269-9183-4b91-9954-a8d186d94f7c", "agent": "opm", - "title": "auto-work-opm-d03-2026-10-05", + "title": "auto-work-opm-d03-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:25:03.518277+00:00" + "created_at": "2026-10-09T00:25:25.601408+00:00", + "dispatch_count": 12, + "rotated_from": "74024064-78d0-4324-926e-0aa8deea467c", + "rotated_at": "2026-10-09T00:25:25.601420+00:00" }, "auto-work-health-h15": { - "thread_uuid": "ef62ae77-8b1c-4354-8567-5305560ebbf8", + "thread_uuid": "bd12593d-15ba-470f-a5d1-822a1ac2d0a5", "agent": "646", "title": "auto-work-health-h15", "type": "persistent", - "created_at": "2026-10-06T05:39:11.958529+00:00" + "created_at": "2026-10-08T14:30:57.692947+00:00", + "dispatch_count": 15, + "rotated_from": "ef62ae77-8b1c-4354-8567-5305560ebbf8", + "rotated_at": "2026-10-08T14:30:57.692961+00:00" }, "auto-work-swarm-g10-2026-10-05": { "thread_uuid": "66e1a6c2-d245-43ec-bb45-0449b5275fe4", @@ -719,32 +821,44 @@ "archived_by_job": "auto-work-646-a15-20261005-052900-0260abf2" }, "auto-work-646-a08": { - "thread_uuid": "59f4311c-3fab-48a4-adad-be431b17965e", + "thread_uuid": "0981ec56-55fd-4d05-b100-c5430ab5ebcb", "agent": "646", - "title": "auto-work-646-a08-2026-10-05", + "title": "auto-work-646-a08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:57:45.283197+00:00" + "created_at": "2026-10-09T00:00:02.938183+00:00", + "dispatch_count": 6, + "rotated_from": "e456fc59-977b-4d12-882b-72a4a54924b4", + "rotated_at": "2026-10-09T00:00:02.938195+00:00" }, "auto-work-646-a15": { - "thread_uuid": "1eed4958-774f-4e58-bba9-1a9a34f4829b", + "thread_uuid": "1f6a16df-435d-43a5-94eb-f7cd846676d4", "agent": "646", - "title": "auto-work-646-a15-2026-10-06", + "title": "auto-work-646-a15-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T00:33:57.820699+00:00" + "created_at": "2026-10-09T00:30:03.875604+00:00", + "dispatch_count": 5, + "rotated_from": "6719447f-782b-438c-b8a8-303e1d00654a", + "rotated_at": "2026-10-09T00:30:03.875616+00:00" }, "auto-work-646-a09": { - "thread_uuid": "8dc4c782-7ae1-4972-abb7-72b66b081405", + "thread_uuid": "ff740068-aaa0-48e9-a446-5948732e813b", "agent": "646", - "title": "auto-work-646-a09-2026-10-05", + "title": "auto-work-646-a09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:31:04.292768+00:00" + "created_at": "2026-10-09T00:35:13.271634+00:00", + "dispatch_count": 5, + "rotated_from": "7c5df3ee-782f-43c5-88fb-482b5992d1d3", + "rotated_at": "2026-10-09T00:35:13.271644+00:00" }, "auto-work-health-h05": { - "thread_uuid": "09d4b77c-f772-4bb2-9ddc-3135291659e5", + "thread_uuid": "caceb85a-7318-4cc0-93b4-75f7477556a3", "agent": "646", "title": "auto-work-health-h05", "type": "persistent", - "created_at": "2026-10-05T05:31:57.342161+00:00" + "created_at": "2026-10-08T14:30:44.447916+00:00", + "dispatch_count": 15, + "rotated_from": "09d4b77c-f772-4bb2-9ddc-3135291659e5", + "rotated_at": "2026-10-08T14:30:44.447932+00:00" }, "auto-work-swarm-g11": { "thread_uuid": "32e05dc9-fe3d-4775-8b07-9a0b6e667e64", @@ -761,11 +875,14 @@ "created_at": "2026-10-05T13:27:02.993653+00:00" }, "auto-work-queue-f11": { - "thread_uuid": "572c5564-42ab-4971-8d22-620ffa47ab47", + "thread_uuid": "3e600662-861b-4039-a45c-0c3083ef321d", "agent": "muse", - "title": "auto-work-queue-f11-2026-10-05", + "title": "auto-work-queue-f11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:32:17.865993+00:00" + "created_at": "2026-10-09T00:31:11.713468+00:00", + "dispatch_count": 12, + "rotated_from": "7a47a8fa-6e74-4632-90a2-40945d522e23", + "rotated_at": "2026-10-09T00:31:11.713480+00:00" }, "auto-work-swarm-g10": { "thread_uuid": "58cdfdca-f01d-4034-9634-0296bc1d40ef", @@ -775,18 +892,24 @@ "created_at": "2026-10-05T16:29:03.607271+00:00" }, "auto-work-queue-f12": { - "thread_uuid": "e54ab855-250a-45ae-a47f-cfac818cddfa", + "thread_uuid": "52bcaf0e-e894-426d-ba88-a2c917699b6b", "agent": "opm", - "title": "auto-work-queue-f12-2026-10-05", + "title": "auto-work-queue-f12-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T12:33:02.394064+00:00" + "created_at": "2026-10-09T00:36:17.583142+00:00", + "dispatch_count": 12, + "rotated_from": "3a86abcb-c6cf-4606-a721-22317e4d1686", + "rotated_at": "2026-10-09T00:36:17.583159+00:00" }, "auto-work-health-h08": { - "thread_uuid": "0c31586b-46b7-4791-af88-491c122ea8e5", + "thread_uuid": "b809d8b2-d172-45aa-9161-7f4e836384f0", "agent": "646", "title": "auto-work-health-h08", "type": "persistent", - "created_at": "2026-10-05T05:37:01.743366+00:00" + "created_at": "2026-10-08T14:36:00.393582+00:00", + "dispatch_count": 15, + "rotated_from": "0c31586b-46b7-4791-af88-491c122ea8e5", + "rotated_at": "2026-10-08T14:36:00.393599+00:00" }, "auto-work-dev-i13-2026-10-05": { "thread_uuid": "db0e20fd-245f-41ec-a52c-fdd9198cae7e", @@ -798,11 +921,14 @@ "archived_by_job": "auto-work-dev-i13-20261005-053400-2f38c06b" }, "auto-work-646-a11": { - "thread_uuid": "8bc9d0b6-6aec-4283-93db-5caaf0ed1e99", + "thread_uuid": "f5b38cf0-5f98-46d0-b388-d17671f495bf", "agent": "646", - "title": "auto-work-646-a11-2026-10-05", + "title": "auto-work-646-a11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:35:02.588037+00:00" + "created_at": "2026-10-09T00:35:24.167887+00:00", + "dispatch_count": 5, + "rotated_from": "5cf3eb12-0943-4498-9685-cc9ff6da4c38", + "rotated_at": "2026-10-09T00:35:24.167897+00:00" }, "auto-work-swarm-g12-2026-10-05": { "thread_uuid": "edcaf85b-0e95-4030-b562-6cd22fb3f5a6", @@ -814,18 +940,24 @@ "archived_by_job": "auto-work-swarm-g12-20261005-053500-c4fe1109" }, "auto-work-646-a16": { - "thread_uuid": "bef6c0cc-abeb-4bdc-8f0a-2f3984750187", + "thread_uuid": "885d9bb9-7a5d-450a-9624-75656eb6731e", "agent": "646", - "title": "auto-work-646-a16-2026-10-06", + "title": "auto-work-646-a16-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T05:38:56.582216+00:00" + "created_at": "2026-10-09T00:35:34.706279+00:00", + "dispatch_count": 5, + "rotated_from": "502c7450-256e-4039-a76f-bb50af479719", + "rotated_at": "2026-10-09T00:35:34.706297+00:00" }, "auto-work-queue-f13": { - "thread_uuid": "c6818256-f0e2-4e6a-918f-ef6986ab26f0", + "thread_uuid": "76859d56-281b-49c2-884e-d4557fb0a660", "agent": "646", - "title": "auto-work-queue-f13-2026-10-05", + "title": "auto-work-queue-f13-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T11:36:03.013993+00:00" + "created_at": "2026-10-09T00:40:26.884814+00:00", + "dispatch_count": 4, + "rotated_from": "8f651a3a-b9f8-4bba-af80-4c11ed022109", + "rotated_at": "2026-10-09T00:40:26.884826+00:00" }, "auto-work-646-a17-2026-10-05": { "thread_uuid": "4805596d-0343-4f45-ac16-3f87b146e77e", @@ -836,18 +968,24 @@ "archived_by_job": "auto-work-646-a17-20261005-053600-6505ba40" }, "auto-work-dev-i13": { - "thread_uuid": "c61ae913-02f6-4cef-be80-7f881e0b4e5f", + "thread_uuid": "7091055f-67a4-465f-9cdf-add30dbda578", "agent": "def", - "title": "auto-work-dev-i13-2026-10-05", + "title": "auto-work-dev-i13-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:36:53.402848+00:00" + "created_at": "2026-10-09T00:35:44.684853+00:00", + "dispatch_count": 12, + "rotated_from": "869ed072-9190-48c7-92a7-ff6fbbe0b4b4", + "rotated_at": "2026-10-09T00:35:44.684862+00:00" }, "auto-work-opm-d04": { - "thread_uuid": "ebd7caa1-8763-4152-a086-f1abd2687e7d", + "thread_uuid": "ae631df7-76b5-48fd-aa39-8a6ee90141cd", "agent": "opm", - "title": "auto-work-opm-d04-2026-10-05", + "title": "auto-work-opm-d04-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:37:08.270677+00:00" + "created_at": "2026-10-09T00:36:06.574418+00:00", + "dispatch_count": 12, + "rotated_from": "3d516dcb-010b-433c-9715-11aeca78cc9d", + "rotated_at": "2026-10-09T00:36:06.574428+00:00" }, "auto-work-swarm-g12": { "thread_uuid": "7d2e005b-fc92-4a55-9a42-53819bb09b31", @@ -857,11 +995,14 @@ "created_at": "2026-10-06T00:42:22.612305+00:00" }, "auto-work-health-h12": { - "thread_uuid": "0fd5d0c5-10f1-4e17-ab69-ac901007df6a", + "thread_uuid": "99fbc326-de23-4b06-b247-62d540788936", "agent": "646", "title": "auto-work-health-h12", "type": "persistent", - "created_at": "2026-10-05T05:38:03.297582+00:00" + "created_at": "2026-10-08T14:40:14.615104+00:00", + "dispatch_count": 15, + "rotated_from": "0fd5d0c5-10f1-4e17-ab69-ac901007df6a", + "rotated_at": "2026-10-08T14:40:14.615115+00:00" }, "auto-work-swarm-g13-2026-10-05": { "thread_uuid": "92707163-3ed8-4b1c-99f9-b4d82261deb3", @@ -887,25 +1028,31 @@ "created_at": "2026-10-05T05:39:33.152401+00:00" }, "auto-work-646-a17": { - "thread_uuid": "0965bbb4-274f-45b5-9432-34bc5db34809", + "thread_uuid": "8819c7f4-7381-49b9-8fcb-1e537059b102", "agent": "646", - "title": "auto-work-646-a17-2026-10-05", + "title": "auto-work-646-a17-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T15:06:07.006156+00:00" + "created_at": "2026-10-09T00:10:35.780869+00:00", + "dispatch_count": 5, + "rotated_from": "9ec2e2a5-82fd-4cf0-82d0-efe28b50c2de", + "rotated_at": "2026-10-09T00:10:35.780881+00:00" }, "auto-work-swarm-g14": { - "thread_uuid": "11a358a4-271c-4b81-851e-b6bf81c98627", + "thread_uuid": "9dafc340-c79c-4b9c-86db-8319bc39e676", "agent": "646", - "title": "auto-work-swarm-g14-2026-10-05", + "title": "auto-work-swarm-g14-2026-10-07", "type": "persistent", - "created_at": "2026-10-05T15:41:20.738803+00:00" + "created_at": "2026-10-07T09:47:36.847308+00:00" }, "auto-work-queue-f15": { - "thread_uuid": "85a21324-12a8-4676-9bfa-b753cb0d5eca", + "thread_uuid": "a45b8a17-dd5f-4bd8-a8d1-3bf163f55db0", "agent": "muse", - "title": "auto-work-queue-f15-2026-10-05", + "title": "auto-work-queue-f15-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:42:03.933501+00:00" + "created_at": "2026-10-09T00:46:41.195651+00:00", + "dispatch_count": 12, + "rotated_from": "d1e256e1-1f32-4cc6-8893-f86ac871cfa5", + "rotated_at": "2026-10-09T00:46:41.195662+00:00" }, "auto-work-swarm-g13": { "thread_uuid": "c9b42ebb-abb4-4f0a-a2cc-c2c5a083b7d9", @@ -915,25 +1062,34 @@ "created_at": "2026-10-05T16:38:03.168211+00:00" }, "auto-work-health-h02": { - "thread_uuid": "b036731f-5d5f-4a96-bf12-51251a15eec9", + "thread_uuid": "9f84c7f8-03d7-40ef-b4f4-c56443eae07c", "agent": "646", "title": "auto-work-health-h02", "type": "persistent", - "created_at": "2026-10-05T05:43:02.559130+00:00" + "created_at": "2026-10-08T14:46:09.706891+00:00", + "dispatch_count": 15, + "rotated_from": "b036731f-5d5f-4a96-bf12-51251a15eec9", + "rotated_at": "2026-10-08T14:46:09.706902+00:00" }, "auto-work-opm-d05": { - "thread_uuid": "6e8d535d-72ac-44e2-88f9-75435b9a0738", + "thread_uuid": "2d5d4bbe-79c4-45fc-9bec-7d7b7b8f9d2c", "agent": "opm", - "title": "auto-work-opm-d05-2026-10-05", + "title": "auto-work-opm-d05-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:45:03.252304+00:00" + "created_at": "2026-10-09T00:46:30.256609+00:00", + "dispatch_count": 12, + "rotated_from": "e3b1b70b-b9b0-4b73-abd0-c49bc591d4a5", + "rotated_at": "2026-10-09T00:46:30.256619+00:00" }, "auto-work-646-a18": { - "thread_uuid": "579f73c9-a037-494c-84fd-ba6ca61090bf", + "thread_uuid": "19235e2b-3c77-4b5e-a9e1-127e433f89ab", "agent": "646", - "title": "auto-work-646-a18-2026-10-06", + "title": "auto-work-646-a18-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T00:45:51.929802+00:00" + "created_at": "2026-10-09T00:45:36.055004+00:00", + "dispatch_count": 5, + "rotated_from": "15af690d-ed81-4b47-9a4c-608ed54bf5e4", + "rotated_at": "2026-10-09T00:45:36.055014+00:00" }, "auto-work-opm-d05-2026-10-05": { "thread_uuid": "5e99ccdb-3c38-42d1-9eed-2cc83cc33fa0", @@ -942,39 +1098,54 @@ "created_at": "2026-10-05T05:45:10.070245+00:00" }, "auto-work-health-h18": { - "thread_uuid": "c04dcb15-8af5-4985-bdc9-d49938ea72ff", + "thread_uuid": "33062e7b-b537-483e-a043-3ed4ef559b23", "agent": "646", "title": "auto-work-health-h18", "type": "persistent", - "created_at": "2026-10-05T05:46:02.193047+00:00" + "created_at": "2026-10-08T14:50:58.597997+00:00", + "dispatch_count": 15, + "rotated_from": "c04dcb15-8af5-4985-bdc9-d49938ea72ff", + "rotated_at": "2026-10-08T14:50:58.598011+00:00" }, "auto-work-health-h10": { - "thread_uuid": "78aa6f89-becb-406f-b941-77f596cd5320", + "thread_uuid": "1afe600d-6653-4482-81b6-7d7b16551097", "agent": "646", "title": "auto-work-health-h10", "type": "persistent", - "created_at": "2026-10-05T05:48:02.891056+00:00" + "created_at": "2026-10-08T14:50:47.615452+00:00", + "dispatch_count": 15, + "rotated_from": "78aa6f89-becb-406f-b941-77f596cd5320", + "rotated_at": "2026-10-08T14:50:47.615464+00:00" }, "auto-work-health-h04": { - "thread_uuid": "2efd01ca-2014-431a-ba7b-587cf0d3eb78", + "thread_uuid": "0056a025-d8ed-4772-99c6-019ef20684f6", "agent": "646", "title": "auto-work-health-h04", "type": "persistent", - "created_at": "2026-10-05T05:48:19.314350+00:00" + "created_at": "2026-10-08T14:46:20.666856+00:00", + "dispatch_count": 15, + "rotated_from": "2efd01ca-2014-431a-ba7b-587cf0d3eb78", + "rotated_at": "2026-10-08T14:46:20.666867+00:00" }, "auto-work-queue-f16": { - "thread_uuid": "28397a22-0800-4498-9f4f-b048c610b537", + "thread_uuid": "e2dce568-008f-41b9-a971-8fc53ddeb8dc", "agent": "opm", - "title": "auto-work-queue-f16-2026-10-05", + "title": "auto-work-queue-f16-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T12:45:06.972657+00:00" + "created_at": "2026-10-09T00:46:54.356025+00:00", + "dispatch_count": 12, + "rotated_from": "65703fd1-9f17-42fd-aa59-14e7df70f81d", + "rotated_at": "2026-10-09T00:46:54.356036+00:00" }, "auto-work-xop-e17": { - "thread_uuid": "ee6e5782-67a9-429b-8463-4da3ade4900c", + "thread_uuid": "18ae7c6e-dc83-488d-8b66-3745495d059e", "agent": "646", "title": "auto-work-xop-e17", "type": "persistent", - "created_at": "2026-10-06T03:53:55.831218+00:00" + "created_at": "2026-10-08T14:51:33.804163+00:00", + "dispatch_count": 14, + "rotated_from": "ee6e5782-67a9-429b-8463-4da3ade4900c", + "rotated_at": "2026-10-08T14:51:33.804173+00:00" }, "auto-work-646-a19-2026-10-05": { "thread_uuid": "018a7c3f-0604-4156-9154-049044c02984", @@ -986,18 +1157,24 @@ "archived_by_job": "sw-20261005-054939-a483" }, "auto-work-dev-i15": { - "thread_uuid": "46e5840c-9240-4aa8-900b-6963ab705763", + "thread_uuid": "2449e34c-5b09-4447-a41b-46bf910174da", "agent": "def", - "title": "auto-work-dev-i15-2026-10-05", + "title": "auto-work-dev-i15-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T05:50:27.439853+00:00" + "created_at": "2026-10-09T00:50:35.916691+00:00", + "dispatch_count": 12, + "rotated_from": "2cc49f66-e6c9-46ac-8500-42768ce66c5e", + "rotated_at": "2026-10-09T00:50:35.916701+00:00" }, "auto-work-queue-f17": { - "thread_uuid": "a91ebf5c-7165-4ab3-b34a-3f925ec5104e", + "thread_uuid": "9f849441-c67d-45aa-ac86-95614ab71bbd", "agent": "646", - "title": "auto-work-queue-f17-2026-10-06", + "title": "auto-work-queue-f17-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T04:52:56.332346+00:00" + "created_at": "2026-10-09T00:51:10.482936+00:00", + "dispatch_count": 5, + "rotated_from": "f8d84452-cc87-4c50-acec-fa28a8653409", + "rotated_at": "2026-10-09T00:51:10.482948+00:00" }, "auto-work-swarm-g16": { "thread_uuid": "9726b659-89ac-4b52-98c9-602ef4abbd9b", @@ -1007,11 +1184,14 @@ "created_at": "2026-10-06T00:54:31.212810+00:00" }, "auto-work-xop-e16": { - "thread_uuid": "c87a6086-80f7-4edf-9f05-ae5d331ed66a", + "thread_uuid": "7c9ac9d0-e413-4c8a-bc17-8db73e51d4f3", "agent": "muse", "title": "auto-work-xop-e16", "type": "persistent", - "created_at": "2026-10-05T12:47:03.212809+00:00" + "created_at": "2026-10-08T14:51:22.676401+00:00", + "dispatch_count": 22, + "rotated_from": "c87a6086-80f7-4edf-9f05-ae5d331ed66a", + "rotated_at": "2026-10-08T14:51:22.676413+00:00" }, "auto-work-queue-f18": { "thread_uuid": "a4adb3ab-078b-4047-b364-5b2b11976c4a", @@ -1030,11 +1210,14 @@ "archived_by_job": "auto-work-swarm-g17-20261005-055101-27a952f0" }, "auto-work-health-h14": { - "thread_uuid": "b0413d26-f210-4e04-a7d4-01c74e00979a", + "thread_uuid": "48b7f9aa-6e21-4b62-aba5-805b5895e75b", "agent": "646", "title": "auto-work-health-h14", "type": "persistent", - "created_at": "2026-10-06T00:59:11.448941+00:00" + "created_at": "2026-10-08T14:56:14.426076+00:00", + "dispatch_count": 15, + "rotated_from": "b0413d26-f210-4e04-a7d4-01c74e00979a", + "rotated_at": "2026-10-08T14:56:14.426087+00:00" }, "auto-work-swarm-g18": { "thread_uuid": "dc82af62-28cd-4c13-a0cb-6aaafb037894", @@ -1051,25 +1234,34 @@ "created_at": "2026-10-05T14:56:03.041051+00:00" }, "auto-work-queue-f20": { - "thread_uuid": "cf7a4ece-09bc-40fa-81b7-fad4bd8d7425", + "thread_uuid": "c24a8c9c-3275-4617-9066-b64169fbf0dd", "agent": "opm", - "title": "auto-work-queue-f20-2026-10-05", + "title": "auto-work-queue-f20-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T14:57:05.917900+00:00" + "created_at": "2026-10-09T00:00:47.332058+00:00", + "dispatch_count": 13, + "rotated_from": "502c9552-ba87-414a-80d2-07af34b8fdf4", + "rotated_at": "2026-10-09T00:00:47.332078+00:00" }, "auto-work-health-h16": { - "thread_uuid": "1f0813c6-7786-4e46-b1fc-68819e2fb35f", + "thread_uuid": "de6f22aa-dcae-421b-9633-7eeccc1f945c", "agent": "646", "title": "auto-work-health-h16", "type": "persistent", - "created_at": "2026-10-05T15:59:03.867935+00:00" + "created_at": "2026-10-08T15:00:43.559027+00:00", + "dispatch_count": 14, + "rotated_from": "1f0813c6-7786-4e46-b1fc-68819e2fb35f", + "rotated_at": "2026-10-08T15:00:43.559038+00:00" }, "auto-work-queue-f19": { - "thread_uuid": "d3719c73-d329-4ec9-88cc-9a889961ae97", + "thread_uuid": "8b6a2e56-7127-442f-916c-f63bc64cf628", "agent": "muse", - "title": "auto-work-queue-f19-2026-10-05", + "title": "auto-work-queue-f19-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T09:54:07.749047+00:00" + "created_at": "2026-10-09T00:56:18.884187+00:00", + "dispatch_count": 12, + "rotated_from": "95a80b07-d541-4815-b919-c7cea2a06137", + "rotated_at": "2026-10-09T00:56:18.884202+00:00" }, "auto-work-swarm-g17": { "thread_uuid": "8a32edfe-3d94-4958-9a61-b5a37cea3df2", @@ -1086,11 +1278,14 @@ "created_at": "2026-10-05T06:53:03.793858+00:00" }, "auto-work-xop-e20": { - "thread_uuid": "5fe19efa-4e8d-424f-a837-0ab36f3a3cb5", + "thread_uuid": "56f4bbee-4de0-4093-8c7d-2a8b241df6e0", "agent": "muse", "title": "auto-work-xop-e20", "type": "persistent", - "created_at": "2026-10-05T09:59:06.528755+00:00" + "created_at": "2026-10-08T15:01:29.597909+00:00", + "dispatch_count": 22, + "rotated_from": "5fe19efa-4e8d-424f-a837-0ab36f3a3cb5", + "rotated_at": "2026-10-08T15:01:29.597922+00:00" }, "auto-work-swarm-g20-2026-10-05": { "thread_uuid": "7ca33791-a99a-4bb0-a60e-135cfd0e7e41", @@ -1102,25 +1297,32 @@ "archived_by_job": "auto-work-swarm-g20-20261005-060033-fc085b94" }, "auto-work-health-h06": { - "thread_uuid": "163d48a3-604c-4492-a324-e6ca75a7e85d", + "thread_uuid": "37b97a93-440b-4061-8e1c-dd538d4052af", "agent": "646", "title": "auto-work-health-h06", "type": "persistent", - "created_at": "2026-10-05T06:00:03.694438+00:00" + "created_at": "2026-10-09T02:00:35.432550+00:00", + "dispatch_count": 4 }, "auto-work-queue-f01": { - "thread_uuid": "df5108a6-ec24-4bd6-b6fd-0da506a6f8c5", + "thread_uuid": "3ca29775-f61a-498d-b849-175694d1ef65", "agent": "646", - "title": "auto-work-queue-f01-2026-10-05", + "title": "auto-work-queue-f01-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:00:16.807947+00:00" + "created_at": "2026-10-09T00:00:36.233669+00:00", + "dispatch_count": 5, + "rotated_from": "fc7a13d0-aa3d-45e2-834f-daccd3edc211", + "rotated_at": "2026-10-09T00:00:36.233680+00:00" }, "auto-work-xop-e19": { - "thread_uuid": "674c81d0-c57e-44dc-a340-fab8b7fa2340", + "thread_uuid": "3bcba030-1766-48e7-bd95-be37928dfb18", "agent": "opm", "title": "auto-work-xop-e19", "type": "persistent", - "created_at": "2026-10-05T08:56:03.634546+00:00" + "created_at": "2026-10-08T15:01:18.554599+00:00", + "dispatch_count": 22, + "rotated_from": "674c81d0-c57e-44dc-a340-fab8b7fa2340", + "rotated_at": "2026-10-08T15:01:18.554614+00:00" }, "auto-work-sweep-j01": { "thread_uuid": "3537c9d2-afa2-46a8-b7f9-0350c8f97ad3", @@ -1137,11 +1339,14 @@ "created_at": "2026-10-05T10:02:04.943214+00:00" }, "auto-work-health-h07": { - "thread_uuid": "b2c00dea-f4a1-41d6-b21e-30243a22d73b", + "thread_uuid": "be1c7d88-c021-492a-9c4e-1c2c94a3ad7a", "agent": "646", "title": "auto-work-health-h07", "type": "persistent", - "created_at": "2026-10-05T06:03:03.273427+00:00" + "created_at": "2026-10-08T15:05:28.502080+00:00", + "dispatch_count": 14, + "rotated_from": "b2c00dea-f4a1-41d6-b21e-30243a22d73b", + "rotated_at": "2026-10-08T15:05:28.502092+00:00" }, "auto-work-swarm-g02": { "thread_uuid": "f24c45e5-a0c2-4b84-8428-88196f167015", @@ -1151,11 +1356,14 @@ "created_at": "2026-10-05T11:06:04.636449+00:00" }, "auto-work-muse-c06": { - "thread_uuid": "421fa1e2-7706-4096-bff9-a0202f0dada0", + "thread_uuid": "10d6ab8c-091c-494b-b737-10bffab47fd5", "agent": "muse", - "title": "auto-work-muse-c06-2026-10-05", + "title": "auto-work-muse-c06-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:07:02.943929+00:00" + "created_at": "2026-10-09T06:10:23.640963+00:00", + "dispatch_count": 1, + "rotated_from": "421fa1e2-7706-4096-bff9-a0202f0dada0", + "rotated_at": "2026-10-09T06:10:23.640974+00:00" }, "auto-work-queue-f02": { "thread_uuid": "cc6a1e76-ef65-4ffa-8087-4a61f2fb2294", @@ -1179,11 +1387,14 @@ "created_at": "2026-10-05T17:06:16.381842+00:00" }, "auto-work-xop-e01": { - "thread_uuid": "daaad6e1-4d4d-4df1-963e-7add93593e2e", + "thread_uuid": "ab31ecfa-dcba-4f39-86aa-a09a661a7233", "agent": "646", "title": "auto-work-xop-e01", "type": "persistent", - "created_at": "2026-10-06T00:12:03.472430+00:00" + "created_at": "2026-10-08T15:05:50.677864+00:00", + "dispatch_count": 14, + "rotated_from": "daaad6e1-4d4d-4df1-963e-7add93593e2e", + "rotated_at": "2026-10-08T15:05:50.677876+00:00" }, "auto-work-swarm-g03": { "thread_uuid": "a8308dbe-7c1e-4d23-b9e4-a50ff88a85a4", @@ -1193,11 +1404,14 @@ "created_at": "2026-10-05T13:08:06.499597+00:00" }, "opm-swarm-harvest": { - "thread_uuid": "a70cdea1-c288-4dbe-b4f9-c7ebde27a42b", + "thread_uuid": "cb4a6d71-6108-4541-ace0-88c61d37fc5d", "agent": "opm", - "title": "opm-swarm-harvest-2026-10-05", + "title": "opm-swarm-harvest-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T19:05:03.927383+00:00" + "created_at": "2026-10-09T00:06:13.353175+00:00", + "dispatch_count": 12, + "rotated_from": "571367f3-94e0-4f66-b0d3-a5324c8a5876", + "rotated_at": "2026-10-09T00:06:13.353186+00:00" }, "auto-work-sweep-j05": { "thread_uuid": "0de48c60-9015-46e5-9eec-0a1a9c730014", @@ -1221,11 +1435,14 @@ "created_at": "2026-10-05T06:13:03.576135+00:00" }, "auto-work-health-h11": { - "thread_uuid": "119e0bce-1268-4a13-a52c-98c4ffdd8c57", + "thread_uuid": "215ebdf6-ee8e-456f-97ba-ea8b94951b79", "agent": "646", "title": "auto-work-health-h11", "type": "persistent", - "created_at": "2026-10-05T16:09:09.439235+00:00" + "created_at": "2026-10-08T15:10:59.079013+00:00", + "dispatch_count": 14, + "rotated_from": "119e0bce-1268-4a13-a52c-98c4ffdd8c57", + "rotated_at": "2026-10-08T15:10:59.079039+00:00" }, "auto-work-swarm-g05": { "thread_uuid": "2a9168b7-22dd-412e-b02a-fbb44f38337f", @@ -1265,11 +1482,12 @@ "archived_by_job": "auto-work-sweep-j08-20261005-061500-3ca7dbb5" }, "auto-work-health-h17": { - "thread_uuid": "df8f2e5e-724d-471f-b2e2-a77dfefbdbcb", + "thread_uuid": "63278894-3d6b-4831-9a15-0f22cead6975", "agent": "646", "title": "auto-work-health-h17", "type": "persistent", - "created_at": "2026-10-05T06:16:02.668962+00:00" + "created_at": "2026-10-07T16:20:57.029583+00:00", + "dispatch_count": 15 }, "auto-work-sweep-j09": { "thread_uuid": "d60756b1-4f70-42fc-bd76-a7558b9c25cb", @@ -1279,11 +1497,14 @@ "created_at": "2026-10-06T05:30:26.409621+00:00" }, "auto-work-646-a19": { - "thread_uuid": "06a24f92-2a24-4a18-81ae-b8fdab46facd", + "thread_uuid": "29cecb6d-a530-4764-813e-a4d1a1c3ef1f", "agent": "646", - "title": "auto-work-646-a19-2026-10-05", + "title": "auto-work-646-a19-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T17:18:06.351246+00:00" + "created_at": "2026-10-09T00:50:25.696604+00:00", + "dispatch_count": 5, + "rotated_from": "0f89708b-9c98-4425-be5f-9eefe792bcec", + "rotated_at": "2026-10-09T00:50:25.696616+00:00" }, "auto-work-queue-f07-2026-10-05": { "thread_uuid": "3a66adf2-6a9c-4fea-a5fe-4cea21e1b252", @@ -1306,32 +1527,44 @@ "created_at": "2026-10-05T18:17:20.479131+00:00" }, "auto-work-xop-e07": { - "thread_uuid": "01c1210e-813e-4fe7-9a3a-6d97e4f562b6", + "thread_uuid": "6fdec4f4-53c9-40b4-9ba2-758bfd66740e", "agent": "opm", "title": "auto-work-xop-e07", "type": "persistent", - "created_at": "2026-10-05T06:20:02.749456+00:00" + "created_at": "2026-10-08T14:21:01.194083+00:00", + "dispatch_count": 22, + "rotated_from": "01c1210e-813e-4fe7-9a3a-6d97e4f562b6", + "rotated_at": "2026-10-08T14:21:01.194093+00:00" }, "auto-work-dev-i11": { - "thread_uuid": "19e54cc1-f155-4aaf-8f00-b7e155ae04d6", + "thread_uuid": "bff3a45f-f465-459c-8db0-7596dccf09d0", "agent": "def", - "title": "auto-work-dev-i11-2026-10-05", + "title": "auto-work-dev-i11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:20:16.642765+00:00" + "created_at": "2026-10-09T00:20:02.013270+00:00", + "dispatch_count": 12, + "rotated_from": "a09f50c1-f3f5-4ac8-9478-6333f8dc4bc8", + "rotated_at": "2026-10-09T00:20:02.013292+00:00" }, "auto-work-dev-i19": { - "thread_uuid": "e47f2a13-eb34-42ec-bf4c-2c0e9ce913b8", + "thread_uuid": "ba0aa999-a3ec-4992-991e-d17e60632b16", "agent": "def", - "title": "auto-work-dev-i19-2026-10-05", + "title": "auto-work-dev-i19-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:20:22.538456+00:00" + "created_at": "2026-10-09T00:20:12.134367+00:00", + "dispatch_count": 13, + "rotated_from": "1dcb6448-bd76-4d45-8782-8f695b2e694a", + "rotated_at": "2026-10-09T00:20:12.134379+00:00" }, "auto-work-queue-f07": { - "thread_uuid": "3b87fd0b-56e3-4f44-9e0b-2cb231c01b9e", + "thread_uuid": "eab003d8-5802-4c22-a75e-ad8e5757e9a4", "agent": "muse", - "title": "auto-work-queue-f07-2026-10-05", + "title": "auto-work-queue-f07-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:20:43.546487+00:00" + "created_at": "2026-10-09T00:20:55.045034+00:00", + "dispatch_count": 12, + "rotated_from": "337179bf-b588-467b-a82c-699de2eee6a6", + "rotated_at": "2026-10-09T00:20:55.045054+00:00" }, "auto-work-swarm-g07": { "thread_uuid": "1889ba6c-1cbf-45ed-9984-710552bbf239", @@ -1355,11 +1588,14 @@ "created_at": "2026-10-06T04:26:51.479445+00:00" }, "auto-work-646-a20": { - "thread_uuid": "36d083e2-39d7-408d-80b7-f0768d399e18", + "thread_uuid": "f7baae62-7979-413e-8653-234ab6fd568a", "agent": "646", - "title": "auto-work-646-a20-2026-10-05", + "title": "auto-work-646-a20-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:24:03.805083+00:00" + "created_at": "2026-10-09T00:55:35.827614+00:00", + "dispatch_count": 5, + "rotated_from": "52064bf6-d14c-4dbc-b207-a5db3b62bc3d", + "rotated_at": "2026-10-09T00:55:35.827635+00:00" }, "auto-work-sweep-j13": { "thread_uuid": "aee65d88-d017-471b-8a2f-8fb937c73a2f", @@ -1369,11 +1605,14 @@ "created_at": "2026-10-05T13:25:02.840691+00:00" }, "auto-work-xop-e09": { - "thread_uuid": "3d25e578-18ce-4f58-9f32-af7271e2aacd", + "thread_uuid": "a36bc2e0-f834-4531-b9bf-3b3306626a94", "agent": "646", "title": "auto-work-xop-e09", "type": "persistent", - "created_at": "2026-10-06T00:31:43.412945+00:00" + "created_at": "2026-10-08T14:31:23.134324+00:00", + "dispatch_count": 14, + "rotated_from": "3d25e578-18ce-4f58-9f32-af7271e2aacd", + "rotated_at": "2026-10-08T14:31:23.134338+00:00" }, "auto-work-sweep-j14": { "thread_uuid": "d2d9c1f2-24fc-42dd-95ea-6e89e985bd43", @@ -1383,11 +1622,14 @@ "created_at": "2026-10-05T06:27:02.644991+00:00" }, "auto-work-pip-b07": { - "thread_uuid": "35ca5bfb-f6b0-4409-a087-2917e7277435", + "thread_uuid": "f9305ba5-ec9f-4a4d-bd37-225da9450531", "agent": "pip", - "title": "auto-work-pip-b07-2026-10-05", + "title": "auto-work-pip-b07-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:27:22.698898+00:00" + "created_at": "2026-10-09T06:25:26.684117+00:00", + "dispatch_count": 1, + "rotated_from": "35ca5bfb-f6b0-4409-a087-2917e7277435", + "rotated_at": "2026-10-09T06:25:26.684134+00:00" }, "auto-work-sweep-j11": { "thread_uuid": "53186988-8ffb-4f23-a826-33d04c18f23b", @@ -1404,11 +1646,14 @@ "created_at": "2026-10-05T15:23:28.941083+00:00" }, "auto-work-xop-e08": { - "thread_uuid": "3ded02d9-2acf-4195-9d2f-3c7fe5dc131b", + "thread_uuid": "ea5abdfc-159c-417f-99e9-88b8c7d06986", "agent": "muse", "title": "auto-work-xop-e08", "type": "persistent", - "created_at": "2026-10-05T17:23:03.738662+00:00" + "created_at": "2026-10-08T14:26:11.520926+00:00", + "dispatch_count": 22, + "rotated_from": "3ded02d9-2acf-4195-9d2f-3c7fe5dc131b", + "rotated_at": "2026-10-08T14:26:11.520943+00:00" }, "auto-work-xop-e10": { "thread_uuid": "6d41ec20-da5c-409a-a760-16ce90283e8f", @@ -1425,18 +1670,24 @@ "created_at": "2026-10-05T18:39:30.371644+00:00" }, "auto-work-xop-e11": { - "thread_uuid": "2d672e46-588a-4732-aae6-7be6e3c52965", + "thread_uuid": "9c2d8874-f074-4ec4-b42f-34a26e419fd2", "agent": "opm", "title": "auto-work-xop-e11", "type": "persistent", - "created_at": "2026-10-05T06:32:02.686077+00:00" + "created_at": "2026-10-08T14:36:57.263733+00:00", + "dispatch_count": 22, + "rotated_from": "2d672e46-588a-4732-aae6-7be6e3c52965", + "rotated_at": "2026-10-08T14:36:57.263750+00:00" }, "auto-work-sweep-j17": { - "thread_uuid": "1127364d-e5df-429b-8463-6b624e878d74", + "thread_uuid": "3bb50573-3ac1-457e-bbf0-05c435b9059c", "agent": "opm", - "title": "auto-work-sweep-j17-2026-10-05", + "title": "auto-work-sweep-j17-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T17:33:04.565339+00:00" + "created_at": "2026-10-09T00:36:28.254524+00:00", + "dispatch_count": 12, + "rotated_from": "d21fad7c-d33c-4faf-9669-fd5c261c6ecf", + "rotated_at": "2026-10-09T00:36:28.254536+00:00" }, "auto-work-sweep-j15": { "thread_uuid": "1a1fafc8-efb8-418c-9d07-72ebd9162584", @@ -1446,11 +1697,14 @@ "created_at": "2026-10-05T13:29:02.888257+00:00" }, "auto-work-xop-e12": { - "thread_uuid": "b09af561-40d7-4cfe-92ee-e2443cec3a7a", + "thread_uuid": "488692fa-ad2c-49a5-abf2-7a736ba69f83", "agent": "muse", "title": "auto-work-xop-e12", "type": "persistent", - "created_at": "2026-10-05T06:35:05.569612+00:00" + "created_at": "2026-10-08T14:37:08.299919+00:00", + "dispatch_count": 22, + "rotated_from": "b09af561-40d7-4cfe-92ee-e2443cec3a7a", + "rotated_at": "2026-10-08T14:37:08.299931+00:00" }, "auto-work-sweep-j19": { "thread_uuid": "55239229-1b26-45e2-889c-4fd93f495421", @@ -1460,11 +1714,14 @@ "created_at": "2026-10-05T06:37:03.022788+00:00" }, "auto-work-xop-e13": { - "thread_uuid": "cfdd2c94-b922-4ed0-9f28-85a67acf1fe9", + "thread_uuid": "0e764be2-92b9-4017-b8d8-e47d8adc53dd", "agent": "646", "title": "auto-work-xop-e13", "type": "persistent", - "created_at": "2026-10-05T15:49:00.010201+00:00" + "created_at": "2026-10-08T14:40:36.829260+00:00", + "dispatch_count": 15, + "rotated_from": "cfdd2c94-b922-4ed0-9f28-85a67acf1fe9", + "rotated_at": "2026-10-08T14:40:36.829271+00:00" }, "auto-work-sweep-j18": { "thread_uuid": "681a4107-e634-443b-8bac-62b5ed74b277", @@ -1481,11 +1738,14 @@ "created_at": "2026-10-05T06:39:02.448180+00:00" }, "auto-work-646-a10": { - "thread_uuid": "5425c91f-29e0-4249-b6a0-b236e97b42e1", + "thread_uuid": "aaea6867-ab2f-49fe-9b32-b14c150bb0b7", "agent": "646", - "title": "auto-work-646-a10-2026-10-05", + "title": "auto-work-646-a10-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:40:06.649064+00:00" + "created_at": "2026-10-09T00:10:24.611227+00:00", + "dispatch_count": 5, + "rotated_from": "2cee698e-e169-41f4-9ec7-4bc7c5367471", + "rotated_at": "2026-10-09T00:10:24.611236+00:00" }, "auto-work-xop-e14": { "thread_uuid": "dffe4aa2-4642-4224-93d0-d7e9a05224a4", @@ -1502,25 +1762,34 @@ "created_at": "2026-10-05T15:40:07.541322+00:00" }, "auto-work-xop-e15": { - "thread_uuid": "cd653716-3229-4712-821b-4821e30a7a53", + "thread_uuid": "2f2d6f86-5908-4a7a-9dd3-1ffd89e38f6b", "agent": "opm", "title": "auto-work-xop-e15", "type": "persistent", - "created_at": "2026-10-05T06:44:02.462323+00:00" + "created_at": "2026-10-08T14:47:16.062899+00:00", + "dispatch_count": 22, + "rotated_from": "cd653716-3229-4712-821b-4821e30a7a53", + "rotated_at": "2026-10-08T14:47:16.062911+00:00" }, "auto-work-646-a12": { - "thread_uuid": "30a6650d-4f5c-4f9a-afb5-333401e4b950", + "thread_uuid": "e978d80c-a096-4d38-b12f-dc9cc03f763c", "agent": "646", - "title": "auto-work-646-a12-2026-10-05", + "title": "auto-work-646-a12-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:45:02.520115+00:00" + "created_at": "2026-10-09T00:45:25.072203+00:00", + "dispatch_count": 5, + "rotated_from": "4a0e69d9-e0cb-4962-b5c8-408232dfbb90", + "rotated_at": "2026-10-09T00:45:25.072214+00:00" }, "auto-work-opm-d08": { - "thread_uuid": "7c36a7ee-d942-405f-91c5-f092ad620d8d", + "thread_uuid": "c615f1d2-3a71-4e22-9d7b-144090dfab9f", "agent": "opm", - "title": "auto-work-opm-d08-2026-10-05", + "title": "auto-work-opm-d08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T06:48:03.076784+00:00" + "created_at": "2026-10-09T06:50:12.801523+00:00", + "dispatch_count": 1, + "rotated_from": "7c36a7ee-d942-405f-91c5-f092ad620d8d", + "rotated_at": "2026-10-09T06:50:12.801534+00:00" }, "auto-work-swarm-g15": { "thread_uuid": "1c34ff52-c919-4adb-a2d1-363d7944656a", @@ -1530,11 +1799,11 @@ "created_at": "2026-10-05T12:44:04.084756+00:00" }, "auto-work-swarm-g20": { - "thread_uuid": "194443d1-83aa-468a-9333-6fe41f361961", + "thread_uuid": "23b4ea90-79fb-42d3-bd18-caacc85ad796", "agent": "646", - "title": "auto-work-swarm-g20-2026-10-05", + "title": "auto-work-swarm-g20-2026-10-07", "type": "persistent", - "created_at": "2026-10-05T06:59:02.950728+00:00" + "created_at": "2026-10-07T21:01:39.809700+00:00" }, "auto-work-xop-e02": { "thread_uuid": "267167b3-81cd-4463-9707-a4a9262b69ae", @@ -1544,11 +1813,14 @@ "created_at": "2026-10-05T07:05:02.621012+00:00" }, "auto-work-dev-i17": { - "thread_uuid": "dd17fa76-5f60-4740-ae16-7bdb0325ff9b", + "thread_uuid": "a3b1498f-ec01-4b9f-89b5-5eed458172fa", "agent": "def", - "title": "auto-work-dev-i17-2026-10-05", + "title": "auto-work-dev-i17-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:06:45.331360+00:00" + "created_at": "2026-10-09T00:05:12.355488+00:00", + "dispatch_count": 13, + "rotated_from": "06c8f106-f99d-4855-8ccf-58a983c50907", + "rotated_at": "2026-10-09T00:05:12.355499+00:00" }, "auto-work-swarm-g03-2026-10-05": { "thread_uuid": "d4ca6564-d37b-414b-bf93-4a7b122cc003", @@ -1560,18 +1832,24 @@ "archived_by_job": "auto-work-swarm-g03-20261005-070800-f38faf09" }, "auto-work-dev-i02": { - "thread_uuid": "a9a9d5f5-78b7-44c7-855c-bb259741f850", + "thread_uuid": "562ecbff-ad57-419b-b31c-6f5ae6dce5b8", "agent": "dev", - "title": "auto-work-dev-i02-2026-10-05", + "title": "auto-work-dev-i02-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T13:10:13.186395+00:00" + "created_at": "2026-10-09T00:10:46.104662+00:00", + "dispatch_count": 13, + "rotated_from": "31e71090-5107-4d86-815d-5ecc61db0cae", + "rotated_at": "2026-10-09T00:10:46.104674+00:00" }, "auto-work-dev-i18": { - "thread_uuid": "ca776120-afe6-4745-9b8f-6be7a9a8bd25", + "thread_uuid": "bc018401-38ab-47e3-bfc1-d73507d10810", "agent": "dev", - "title": "auto-work-dev-i18-2026-10-05", + "title": "auto-work-dev-i18-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:10:13.780343+00:00" + "created_at": "2026-10-09T00:10:56.375206+00:00", + "dispatch_count": 13, + "rotated_from": "6509d6e9-6025-422a-aa55-c91ffa91c4d1", + "rotated_at": "2026-10-09T00:10:56.375215+00:00" }, "646-opm": { "thread_uuid": "ae6de661-90b0-499d-95a8-9d9d4168fe02", @@ -1587,32 +1865,44 @@ "created_at": "2026-10-05T07:15:15.154060+00:00" }, "auto-work-dev-i10": { - "thread_uuid": "8cb79d9d-3e76-412c-aa1d-131e390ef4e2", + "thread_uuid": "d0e09c9b-2c2e-496e-aee8-152d89f7b78a", "agent": "dev", - "title": "auto-work-dev-i10-2026-10-05", + "title": "auto-work-dev-i10-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:15:22.310086+00:00" + "created_at": "2026-10-09T00:15:01.857873+00:00", + "dispatch_count": 13, + "rotated_from": "dd77dd73-4b0c-43ad-84fb-8a8123460dc4", + "rotated_at": "2026-10-09T00:15:01.857889+00:00" }, "auto-work-muse-c07": { - "thread_uuid": "07461141-f56d-44c4-a3bf-8be388fd764d", + "thread_uuid": "8bc78308-c926-4aeb-aa05-abc2ab892bd1", "agent": "muse", - "title": "auto-work-muse-c07-2026-10-05", + "title": "auto-work-muse-c07-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:19:02.716128+00:00" + "created_at": "2026-10-09T07:20:21.885097+00:00", + "dispatch_count": 1, + "rotated_from": "07461141-f56d-44c4-a3bf-8be388fd764d", + "rotated_at": "2026-10-09T07:20:21.885118+00:00" }, "auto-work-pip-b08": { - "thread_uuid": "dbcd4414-446e-41af-b7f1-e6b1614e11ec", + "thread_uuid": "5cf5a2ee-26b7-4cb5-9dc0-83794d938f7d", "agent": "pip", - "title": "auto-work-pip-b08-2026-10-05", + "title": "auto-work-pip-b08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:24:02.282267+00:00" + "created_at": "2026-10-09T07:25:24.448554+00:00", + "dispatch_count": 1, + "rotated_from": "dbcd4414-446e-41af-b7f1-e6b1614e11ec", + "rotated_at": "2026-10-09T07:25:24.448566+00:00" }, "auto-work-dev-i04": { - "thread_uuid": "d2568f01-d522-46c0-a72e-c7d2e465e15c", + "thread_uuid": "5121f628-b788-44a5-adce-c5fda5fac5db", "agent": "dev", - "title": "auto-work-dev-i04-2026-10-05", + "title": "auto-work-dev-i04-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:25:20.793528+00:00" + "created_at": "2026-10-09T00:25:01.717304+00:00", + "dispatch_count": 13, + "rotated_from": "8d8dfd1d-de8c-4b8a-b7f2-4de41359b45d", + "rotated_at": "2026-10-09T00:25:01.717319+00:00" }, "auto-work-dev-i20": { "thread_uuid": "5c3a2be3-aead-407a-b88d-1dae14019caa", @@ -1622,18 +1912,24 @@ "created_at": "2026-10-05T07:25:25.933764+00:00" }, "auto-work-dev-i05": { - "thread_uuid": "052bdc6f-1f1d-493f-8fcc-0aa5f0ea4a55", + "thread_uuid": "c0739d9b-2d76-4fd8-a7b1-b5ebf7485228", "agent": "dev", - "title": "auto-work-dev-i05-2026-10-05", + "title": "auto-work-dev-i05-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:30:22.463098+00:00" + "created_at": "2026-10-09T00:30:14.135395+00:00", + "dispatch_count": 12, + "rotated_from": "9112c63b-5933-46b1-8ff3-6d937ae64094", + "rotated_at": "2026-10-09T00:30:14.135411+00:00" }, "auto-work-dev-i12": { - "thread_uuid": "aef85bfa-681f-4ce7-9cb6-07246566d1b5", + "thread_uuid": "38fcfbc8-c438-4f78-8be7-e6d469cc773f", "agent": "dev", - "title": "auto-work-dev-i12-2026-10-05", + "title": "auto-work-dev-i12-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:30:27.746145+00:00" + "created_at": "2026-10-09T00:30:24.506615+00:00", + "dispatch_count": 12, + "rotated_from": "cd31e63a-d43a-455f-a213-8395b0510ed3", + "rotated_at": "2026-10-09T00:30:24.506632+00:00" }, "auto-work-dev-i06": { "thread_uuid": "88214506-e58e-495c-983c-2cc674666b6f", @@ -1643,53 +1939,74 @@ "created_at": "2026-10-05T07:35:37.568593+00:00" }, "auto-work-dev-i07": { - "thread_uuid": "90ef9a7b-f434-44d6-8dd1-2b7f6f471573", + "thread_uuid": "f9dbb3c3-7e0e-4c01-9d2c-b50492a9446d", "agent": "dev", - "title": "auto-work-dev-i07-2026-10-05", + "title": "auto-work-dev-i07-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:40:16.054624+00:00" + "created_at": "2026-10-09T00:40:02.143672+00:00", + "dispatch_count": 12, + "rotated_from": "532d40b9-58fd-4815-93ed-c47781cbf95d", + "rotated_at": "2026-10-09T00:40:02.143682+00:00" }, "auto-work-dev-i08": { - "thread_uuid": "4e3ba2be-e3c5-4891-bc0f-406d58455ae1", + "thread_uuid": "992e2d1a-9718-4696-b39b-1cc473d8dd99", "agent": "dev", - "title": "auto-work-dev-i08-2026-10-05", + "title": "auto-work-dev-i08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:45:15.598784+00:00" + "created_at": "2026-10-09T00:45:45.920129+00:00", + "dispatch_count": 12, + "rotated_from": "33506494-a443-4ee1-ba8a-4922e8d773b1", + "rotated_at": "2026-10-09T00:45:45.920139+00:00" }, "auto-work-dev-i14": { - "thread_uuid": "e83eb06e-1694-4b8a-be5a-51af0b83e996", + "thread_uuid": "d0893a98-9aaa-41bd-8073-c32af88992df", "agent": "dev", - "title": "auto-work-dev-i14-2026-10-05", + "title": "auto-work-dev-i14-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:45:21.079519+00:00" + "created_at": "2026-10-09T00:45:55.669548+00:00", + "dispatch_count": 12, + "rotated_from": "007256f8-fc52-4c38-9690-3b57bc43e293", + "rotated_at": "2026-10-09T00:45:55.669558+00:00" }, "auto-work-dev-i09": { - "thread_uuid": "5b3e1b45-7e40-4be4-9464-5511d2abebfc", + "thread_uuid": "6529ad28-8f21-4b2e-80ec-613cb02f90a4", "agent": "dev", - "title": "auto-work-dev-i09-2026-10-05", + "title": "auto-work-dev-i09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:55:08.528018+00:00" + "created_at": "2026-10-09T00:55:45.937983+00:00", + "dispatch_count": 12, + "rotated_from": "4c56927f-7e72-4f8b-93df-831058e37814", + "rotated_at": "2026-10-09T00:55:45.937994+00:00" }, "auto-work-dev-i16": { - "thread_uuid": "3cb57815-909a-4fe7-b85a-73ebc033d17c", + "thread_uuid": "cf2e72e0-0f8f-4c5e-955c-1d1548195601", "agent": "dev", - "title": "auto-work-dev-i16-2026-10-05", + "title": "auto-work-dev-i16-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T07:55:13.824818+00:00" + "created_at": "2026-10-09T00:55:56.161325+00:00", + "dispatch_count": 12, + "rotated_from": "faf612da-45ab-48de-a76e-40ab8a429a0d", + "rotated_at": "2026-10-09T00:55:56.161336+00:00" }, "auto-work-dev-i01": { - "thread_uuid": "d90d8c9d-7ce4-4933-a6ba-128c2c577d65", + "thread_uuid": "4e551c00-41e4-4bd3-aa04-fb67d4025231", "agent": "dev", - "title": "auto-work-dev-i01-2026-10-05", + "title": "auto-work-dev-i01-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T08:05:08.040188+00:00" + "created_at": "2026-10-09T00:05:02.044281+00:00", + "dispatch_count": 13, + "rotated_from": "48aba9ba-e195-43f7-846c-176b071a6d3a", + "rotated_at": "2026-10-09T00:05:02.044291+00:00" }, "auto-work-opm-d09": { - "thread_uuid": "bc20481f-4fb4-494f-9414-bf54fbff9679", + "thread_uuid": "d3f967b0-31a9-4bd5-af91-99e8981728d0", "agent": "opm", - "title": "auto-work-opm-d09-2026-10-05", + "title": "auto-work-opm-d09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T08:12:04.017815+00:00" + "created_at": "2026-10-09T08:15:26.453289+00:00", + "dispatch_count": 1, + "rotated_from": "bc20481f-4fb4-494f-9414-bf54fbff9679", + "rotated_at": "2026-10-09T08:15:26.453312+00:00" }, "auto-work-sweep-j09-2026-10-05": { "thread_uuid": "6b140415-9b94-4b13-b9ad-53b413f1716f", @@ -1701,11 +2018,14 @@ "archived_by_job": "auto-work-sweep-j09-20261005-081700-fbdb045d" }, "auto-work-pip-b09": { - "thread_uuid": "95ec5f67-f0f4-4874-9bd8-a85f421d99b5", + "thread_uuid": "58f92191-73a3-474a-9fdd-3a7ea14c2429", "agent": "pip", - "title": "auto-work-pip-b09-2026-10-05", + "title": "auto-work-pip-b09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T08:27:03.468669+00:00" + "created_at": "2026-10-09T08:30:22.996598+00:00", + "dispatch_count": 1, + "rotated_from": "95ec5f67-f0f4-4874-9bd8-a85f421d99b5", + "rotated_at": "2026-10-09T08:30:22.996615+00:00" }, "auto-work-muse-c08-2026-10-05": { "thread_uuid": "e778eb8d-c21b-4134-a67a-4fa4fccdadd3", @@ -1713,11 +2033,14 @@ "created_at": "2026-10-05T08:31:33.021153+00:00" }, "auto-work-muse-c08": { - "thread_uuid": "55cc196b-21a6-44eb-ae81-95382a16a162", + "thread_uuid": "ca659b7b-4296-493e-a420-c7fdef82e71a", "agent": "muse", - "title": "auto-work-muse-c08-2026-10-05", + "title": "auto-work-muse-c08-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T08:35:41.538755+00:00" + "created_at": "2026-10-09T08:35:12.313463+00:00", + "dispatch_count": 1, + "rotated_from": "55cc196b-21a6-44eb-ae81-95382a16a162", + "rotated_at": "2026-10-09T08:35:12.313475+00:00" }, "box-deep-health-2026-10-05T09:01:10.661036+00:00": { "thread_uuid": "9a4f93e0-2600-45e5-9307-782234e498c2", @@ -1736,25 +2059,34 @@ "created_at": "2026-10-05T09:02:03.417302+00:00" }, "auto-work-pip-b10": { - "thread_uuid": "7528bd67-f28b-4c58-82e9-ac2193982821", + "thread_uuid": "d9c3b273-9fe6-4072-887a-5410e75a08c1", "agent": "pip", - "title": "auto-work-pip-b10-2026-10-05", + "title": "auto-work-pip-b10-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T09:30:06.026484+00:00" + "created_at": "2026-10-09T09:30:23.023709+00:00", + "dispatch_count": 1, + "rotated_from": "7528bd67-f28b-4c58-82e9-ac2193982821", + "rotated_at": "2026-10-09T09:30:23.023722+00:00" }, "auto-work-muse-c09": { - "thread_uuid": "bbd26fcc-ad45-4984-a964-a0fae02ec604", + "thread_uuid": "8ae4ac77-e6ad-4bf7-b63d-f8376998cfc8", "agent": "muse", - "title": "auto-work-muse-c09-2026-10-05", + "title": "auto-work-muse-c09-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T09:43:03.462390+00:00" + "created_at": "2026-10-09T09:45:22.733350+00:00", + "dispatch_count": 1, + "rotated_from": "bbd26fcc-ad45-4984-a964-a0fae02ec604", + "rotated_at": "2026-10-09T09:45:22.733362+00:00" }, "auto-work-pip-b11": { - "thread_uuid": "90f91e47-ab26-48f8-8031-e5f6882da840", + "thread_uuid": "204266b5-6066-47ea-b638-82bdd4ac8926", "agent": "pip", - "title": "auto-work-pip-b11-2026-10-05", + "title": "auto-work-pip-b11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T10:33:03.525548+00:00" + "created_at": "2026-10-09T10:35:23.290763+00:00", + "dispatch_count": 1, + "rotated_from": "90f91e47-ab26-48f8-8031-e5f6882da840", + "rotated_at": "2026-10-09T10:35:23.290775+00:00" }, "auto-work-opm-d10-2026-10-05": { "thread_uuid": "af641503-4814-484c-815e-4d5dddc37bad", @@ -1766,46 +2098,64 @@ "archived_by_job": "auto-work-opm-d10-20261005-104203-fa1a6d19" }, "auto-work-opm-d10": { - "thread_uuid": "4cd8f41e-cfaa-493f-a1b7-805deed70c23", + "thread_uuid": "73bf171d-222f-4748-ab65-e06568dddccc", "agent": "opm", - "title": "auto-work-opm-d10-2026-10-05", + "title": "auto-work-opm-d10-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T10:45:47.752676+00:00" + "created_at": "2026-10-09T10:45:34.438603+00:00", + "dispatch_count": 1, + "rotated_from": "4cd8f41e-cfaa-493f-a1b7-805deed70c23", + "rotated_at": "2026-10-09T10:45:34.438613+00:00" }, "auto-work-muse-c10": { - "thread_uuid": "45a20250-cd45-481b-a4f0-3ffb978df30c", + "thread_uuid": "7bf9c5bc-70a1-4f0a-9d4c-f0c7738179df", "agent": "muse", - "title": "auto-work-muse-c10-2026-10-05", + "title": "auto-work-muse-c10-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T10:55:55.342957+00:00" + "created_at": "2026-10-09T10:55:24.552625+00:00", + "dispatch_count": 1, + "rotated_from": "45a20250-cd45-481b-a4f0-3ffb978df30c", + "rotated_at": "2026-10-09T10:55:24.552642+00:00" }, "auto-work-pip-b12": { - "thread_uuid": "928b076c-a4ef-4a2b-b321-08b79249b722", + "thread_uuid": "f2be6f93-fe13-468e-9913-7a3813860e37", "agent": "pip", - "title": "auto-work-pip-b12-2026-10-05", + "title": "auto-work-pip-b12-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T11:40:34.861858+00:00" + "created_at": "2026-10-09T11:40:13.106905+00:00", + "dispatch_count": 1, + "rotated_from": "928b076c-a4ef-4a2b-b321-08b79249b722", + "rotated_at": "2026-10-09T11:40:13.106917+00:00" }, "auto-work-opm-d11": { - "thread_uuid": "5eafd4be-bd58-4a36-8327-bc5c0e0be8ce", + "thread_uuid": "42194ebd-f2ce-42ce-84be-24f112560968", "agent": "opm", - "title": "auto-work-opm-d11-2026-10-05", + "title": "auto-work-opm-d11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T12:03:02.484303+00:00" + "created_at": "2026-10-09T12:05:39.679398+00:00", + "dispatch_count": 1, + "rotated_from": "5eafd4be-bd58-4a36-8327-bc5c0e0be8ce", + "rotated_at": "2026-10-09T12:05:39.679408+00:00" }, "auto-work-opm-d20": { - "thread_uuid": "3873833f-7a71-4fd9-b8cb-f71d1fed20fe", + "thread_uuid": "9c67204a-be91-4820-ab66-b140827b7c4f", "agent": "opm", - "title": "auto-work-opm-d20-2026-10-05", + "title": "auto-work-opm-d20-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T12:05:02.705790+00:00" + "created_at": "2026-10-09T12:05:55.850854+00:00", + "dispatch_count": 1, + "rotated_from": "3873833f-7a71-4fd9-b8cb-f71d1fed20fe", + "rotated_at": "2026-10-09T12:05:55.850866+00:00" }, "auto-work-muse-c11": { - "thread_uuid": "6dfe9a4c-815b-4ad0-be0e-280f0b9c9305", + "thread_uuid": "cb50b54f-6be2-4167-9eb4-2cf48bede2b9", "agent": "muse", - "title": "auto-work-muse-c11-2026-10-05", + "title": "auto-work-muse-c11-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T12:07:02.834406+00:00" + "created_at": "2026-10-09T12:10:38.775015+00:00", + "dispatch_count": 1, + "rotated_from": "6dfe9a4c-815b-4ad0-be0e-280f0b9c9305", + "rotated_at": "2026-10-09T12:10:38.775026+00:00" }, "auto-work-pip-b13": { "thread_uuid": "418012c7-978a-4f17-94a5-6859299d820a", @@ -1919,18 +2269,24 @@ "archived_by_job": "sw-20261005-142058-c365" }, "auto-work-opm-d12": { - "thread_uuid": "003dd545-53c0-4507-b939-cb4e87e8bbf1", + "thread_uuid": "f00e7df2-ea3b-4e07-bd0c-afe3252a161c", "agent": "opm", - "title": "auto-work-opm-d12-2026-10-05", + "title": "auto-work-opm-d12-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T14:26:33.459549+00:00" + "created_at": "2026-10-08T14:25:37.032499+00:00", + "dispatch_count": 1, + "rotated_from": "003dd545-53c0-4507-b939-cb4e87e8bbf1", + "rotated_at": "2026-10-08T14:25:37.032515+00:00" }, "auto-work-muse-c13": { - "thread_uuid": "dfba1f21-abec-47a3-ae1d-b3aea82a1dd9", + "thread_uuid": "2b162b19-9e5b-4a4a-b75e-2e849df8db12", "agent": "muse", - "title": "auto-work-muse-c13-2026-10-05", + "title": "auto-work-muse-c13-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T14:31:02.947484+00:00" + "created_at": "2026-10-08T14:36:11.306053+00:00", + "dispatch_count": 1, + "rotated_from": "dfba1f21-abec-47a3-ae1d-b3aea82a1dd9", + "rotated_at": "2026-10-08T14:36:11.306065+00:00" }, "sw-20261005-143049-74dc-s1": { "thread_uuid": "c0dd7d16-8b24-4eb6-84f5-5c93e198f2b7", @@ -1987,11 +2343,14 @@ "archived_by_job": "sw-20261005-144314-b3c7" }, "auto-work-pip-b15": { - "thread_uuid": "2ef4db2d-8cc2-46ba-9139-c9d695b605e8", + "thread_uuid": "5882319a-07eb-48b5-b20e-03d36202bd71", "agent": "pip", - "title": "auto-work-pip-b15-2026-10-05", + "title": "auto-work-pip-b15-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T14:45:02.623451+00:00" + "created_at": "2026-10-08T14:46:43.236803+00:00", + "dispatch_count": 1, + "rotated_from": "2ef4db2d-8cc2-46ba-9139-c9d695b605e8", + "rotated_at": "2026-10-08T14:46:43.236817+00:00" }, "sw-20261005-144724-2f53-s0": { "thread_uuid": "1d10123e-444d-470d-b6a9-3b8b893f7711", @@ -2072,18 +2431,24 @@ "created_at": "2026-10-05T15:28:55.271303+00:00" }, "auto-work-muse-c14": { - "thread_uuid": "696f6184-061a-4163-9cc3-3416e3c6547f", + "thread_uuid": "181461f1-dd34-4cb6-bf92-7d6f47943e4b", "agent": "muse", - "title": "auto-work-muse-c14-2026-10-05", + "title": "auto-work-muse-c14-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T15:43:02.424683+00:00" + "created_at": "2026-10-08T15:46:32.206941+00:00", + "dispatch_count": 1, + "rotated_from": "696f6184-061a-4163-9cc3-3416e3c6547f", + "rotated_at": "2026-10-08T15:46:32.206952+00:00" }, "auto-work-pip-b16": { - "thread_uuid": "789d4718-9132-436b-a548-7ffea72105b4", + "thread_uuid": "055ed778-5a7e-4401-bf98-b7b132a9bf76", "agent": "pip", - "title": "auto-work-pip-b16-2026-10-05", + "title": "auto-work-pip-b16-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T15:48:03.018733+00:00" + "created_at": "2026-10-08T15:51:20.701943+00:00", + "dispatch_count": 1, + "rotated_from": "789d4718-9132-436b-a548-7ffea72105b4", + "rotated_at": "2026-10-08T15:51:20.701956+00:00" }, "auto-work-646-a16-2026-10-05": { "thread_uuid": "3c4a7295-00b7-4456-a414-8e4883c13b66", @@ -2116,11 +2481,14 @@ "created_at": "2026-10-05T16:24:06.101921+00:00" }, "auto-work-pip-b01": { - "thread_uuid": "bbef6986-794e-4258-8829-7a322960aff8", + "thread_uuid": "ce3d98ca-b4ee-4f60-a3cd-86a66fc25d14", "agent": "pip", - "title": "auto-work-pip-b01-2026-10-05", + "title": "auto-work-pip-b01-2026-10-09", "type": "persistent", - "created_at": "2026-10-05T16:31:40.631408+00:00" + "created_at": "2026-10-09T00:05:50.598754+00:00", + "dispatch_count": 1, + "rotated_from": "1f35dd64-b47d-41a4-9729-f3e2d947991b", + "rotated_at": "2026-10-09T00:05:50.598764+00:00" }, "test-tmux-wo": { "thread_uuid": "523cb7c6-724e-4453-a528-42d8a319061c", @@ -2130,74 +2498,104 @@ "created_at": "2026-10-05T16:31:56.083968+00:00" }, "auto-work-opm-d13": { - "thread_uuid": "1ef859e9-22e7-4c61-b187-bf7fee5e9bc4", + "thread_uuid": "9a2075f8-e538-4320-a71e-3edd04956c62", "agent": "opm", - "title": "auto-work-opm-d13-2026-10-05", + "title": "auto-work-opm-d13-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T16:43:03.197949+00:00" + "created_at": "2026-10-08T16:46:44.402382+00:00", + "dispatch_count": 1, + "rotated_from": "1ef859e9-22e7-4c61-b187-bf7fee5e9bc4", + "rotated_at": "2026-10-08T16:46:44.402393+00:00" }, "auto-work-pip-b17": { - "thread_uuid": "51497d2d-126b-4db0-9662-56924c3a4404", + "thread_uuid": "00e4710d-07c3-474a-b94b-f46161587b82", "agent": "pip", - "title": "auto-work-pip-b17-2026-10-05", + "title": "auto-work-pip-b17-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T16:51:03.067566+00:00" + "created_at": "2026-10-08T16:56:33.870724+00:00", + "dispatch_count": 1, + "rotated_from": "51497d2d-126b-4db0-9662-56924c3a4404", + "rotated_at": "2026-10-08T16:56:33.870740+00:00" }, "auto-work-muse-c15": { - "thread_uuid": "f7138fc4-b779-4b9f-8e53-9e74e994e35b", + "thread_uuid": "9b973f9f-20da-48c3-aac7-a3ae7d819bc4", "agent": "muse", - "title": "auto-work-muse-c15-2026-10-05", + "title": "auto-work-muse-c15-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T16:55:03.615806+00:00" + "created_at": "2026-10-08T16:56:22.890140+00:00", + "dispatch_count": 1, + "rotated_from": "f7138fc4-b779-4b9f-8e53-9e74e994e35b", + "rotated_at": "2026-10-08T16:56:22.890153+00:00" }, "auto-work-pip-b18": { - "thread_uuid": "86d124d5-a72f-4531-9f22-b8961b55acfc", + "thread_uuid": "fe091271-d229-4fbc-ae40-f142af9213c7", "agent": "pip", - "title": "auto-work-pip-b18-2026-10-05", + "title": "auto-work-pip-b18-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T17:55:36.431290+00:00" + "created_at": "2026-10-08T17:56:30.977704+00:00", + "dispatch_count": 1, + "rotated_from": "86d124d5-a72f-4531-9f22-b8961b55acfc", + "rotated_at": "2026-10-08T17:56:30.977716+00:00" }, "auto-work-muse-c16": { - "thread_uuid": "5f19e00a-f4ac-419d-9e4c-e7f16eb7dedc", + "thread_uuid": "b2b1e68e-5c25-4da4-be41-5e211565d61c", "agent": "muse", - "title": "auto-work-muse-c16-2026-10-05", + "title": "auto-work-muse-c16-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T18:10:26.476117+00:00" + "created_at": "2026-10-08T18:11:18.781609+00:00", + "dispatch_count": 1, + "rotated_from": "5f19e00a-f4ac-419d-9e4c-e7f16eb7dedc", + "rotated_at": "2026-10-08T18:11:18.781625+00:00" }, "auto-work-opm-d14": { - "thread_uuid": "c42dbd3b-33d9-47b3-91ea-568aee23b751", + "thread_uuid": "c7bc3b2c-aeaa-4e49-8a90-bbd499389a02", "agent": "opm", - "title": "auto-work-opm-d14-2026-10-05", + "title": "auto-work-opm-d14-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T18:10:33.455308+00:00" + "created_at": "2026-10-08T18:11:30.066960+00:00", + "dispatch_count": 1, + "rotated_from": "3184c7ee-b3e4-4ee2-9d2e-35043ba1266d", + "rotated_at": "2026-10-08T18:11:30.066973+00:00" }, "auto-work-pip-b19": { - "thread_uuid": "4421edf2-8d73-4625-a0ef-5a3daf2dc823", + "thread_uuid": "f190074c-7cb5-4592-909d-148d342d7512", "agent": "pip", - "title": "auto-work-pip-b19-2026-10-05", + "title": "auto-work-pip-b19-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T19:00:21.162166+00:00" + "created_at": "2026-10-08T19:00:46.019591+00:00", + "dispatch_count": 1, + "rotated_from": "4421edf2-8d73-4625-a0ef-5a3daf2dc823", + "rotated_at": "2026-10-08T19:00:46.019603+00:00" }, "auto-work-pip-b20": { - "thread_uuid": "f5412e5e-667d-46da-adc2-86f93292d254", + "thread_uuid": "9ac7cb8e-eadf-4351-9967-7697017ec919", "agent": "pip", - "title": "auto-work-pip-b20-2026-10-05", + "title": "auto-work-pip-b20-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T19:00:28.049459+00:00" + "created_at": "2026-10-08T19:00:57.379907+00:00", + "dispatch_count": 1, + "rotated_from": "f5412e5e-667d-46da-adc2-86f93292d254", + "rotated_at": "2026-10-08T19:00:57.379919+00:00" }, "auto-work-muse-c17": { - "thread_uuid": "169a2b75-1bd6-4fc6-a9f9-037a56c048e5", + "thread_uuid": "3fae26ed-484f-44b0-bdf2-30ca27f48c10", "agent": "muse", - "title": "auto-work-muse-c17-2026-10-05", + "title": "auto-work-muse-c17-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T19:21:52.558789+00:00" + "created_at": "2026-10-08T19:20:47.952049+00:00", + "dispatch_count": 1, + "rotated_from": "169a2b75-1bd6-4fc6-a9f9-037a56c048e5", + "rotated_at": "2026-10-08T19:20:47.952062+00:00" }, "auto-work-muse-c18": { - "thread_uuid": "2f64f3fd-e0ad-42c0-9ad6-e3d7438d35f0", + "thread_uuid": "fb35c409-0874-4e49-acc1-72e0150b6066", "agent": "muse", - "title": "auto-work-muse-c18-2026-10-05", + "title": "auto-work-muse-c18-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T20:35:44.251409+00:00" + "created_at": "2026-10-08T20:36:10.369060+00:00", + "dispatch_count": 1, + "rotated_from": "2f64f3fd-e0ad-42c0-9ad6-e3d7438d35f0", + "rotated_at": "2026-10-08T20:36:10.369080+00:00" }, "def tasks": { "thread_uuid": "9cac74ab-0371-4563-a42b-a9a6da5a27de", @@ -2209,11 +2607,14 @@ "archived_by_job": "def tasks" }, "auto-work-opm-d15": { - "thread_uuid": "2e298a8e-d501-4fd3-8199-73f8d1135dac", + "thread_uuid": "0a0b2d79-df94-4a42-a04d-5a7b6f604ba8", "agent": "opm", - "title": "auto-work-opm-d15-2026-10-05", + "title": "auto-work-opm-d15-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T20:40:45.666957+00:00" + "created_at": "2026-10-08T20:40:25.623714+00:00", + "dispatch_count": 1, + "rotated_from": "2e298a8e-d501-4fd3-8199-73f8d1135dac", + "rotated_at": "2026-10-08T20:40:25.623728+00:00" }, "sw-20261005-210400-9837-s0": { "thread_uuid": "3a459f94-2122-4d52-bcf3-36d1ec452862", @@ -2225,31 +2626,43 @@ "archived_by_job": "sw-20261005-210400-9837" }, "auto-work-muse-c19": { - "thread_uuid": "a3601d33-b646-432e-956c-57a09757eff2", + "thread_uuid": "1fe1794f-87f1-4d4f-8a7c-0a2f5fda076f", "agent": "muse", - "title": "auto-work-muse-c19-2026-10-05", + "title": "auto-work-muse-c19-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T21:46:19.890287+00:00" + "created_at": "2026-10-08T21:46:34.418178+00:00", + "dispatch_count": 1, + "rotated_from": "a3601d33-b646-432e-956c-57a09757eff2", + "rotated_at": "2026-10-08T21:46:34.418190+00:00" }, "auto-work-opm-d16": { - "thread_uuid": "c8cbb165-83b1-4789-9b64-962214db841a", + "thread_uuid": "0a3cc894-00ab-4da5-9c01-9480873b841c", "agent": "opm", - "title": "auto-work-opm-d16-2026-10-05", + "title": "auto-work-opm-d16-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T22:16:05.396335+00:00" + "created_at": "2026-10-08T22:15:47.711377+00:00", + "dispatch_count": 1, + "rotated_from": "c8cbb165-83b1-4789-9b64-962214db841a", + "rotated_at": "2026-10-08T22:15:47.711389+00:00" }, "auto-work-muse-c20": { - "thread_uuid": "6b2a4e65-c015-428d-a30a-c8dfec22f9bc", + "thread_uuid": "b651c3a8-0773-4130-9314-65b9feb697a9", "agent": "muse", - "title": "auto-work-muse-c20-2026-10-05", + "title": "auto-work-muse-c20-2026-10-08", "type": "persistent", - "created_at": "2026-10-05T22:56:34.815807+00:00" + "created_at": "2026-10-08T22:56:24.186447+00:00", + "dispatch_count": 1, + "rotated_from": "6b2a4e65-c015-428d-a30a-c8dfec22f9bc", + "rotated_at": "2026-10-08T22:56:24.186458+00:00" }, "box-http-health-2026-10-06T00:00:00.370227+00:00": { "thread_uuid": "27779559-55b6-4995-a783-f9bbf4ef8f5b", "agent": "646", "title": "box-http-health-2026-10-06T00:00:00.370227+00:00", - "created_at": "2026-10-06T00:02:51.993375+00:00" + "created_at": "2026-10-06T00:02:51.993375+00:00", + "archived": true, + "archived_at": "2026-10-08T15:12:32.786348+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "auto-work-swarm-g18-2026-10-06": { "thread_uuid": "fc625454-ade7-46d7-8437-45675b6427b3", @@ -2261,7 +2674,10 @@ "thread_uuid": "e29efa5d-2218-416a-8625-e9742c15182b", "agent": "646", "title": "box-http-health-2026-10-06T00:15:00.350787+00:00", - "created_at": "2026-10-06T00:17:50.263958+00:00" + "created_at": "2026-10-06T00:17:50.263958+00:00", + "archived": true, + "archived_at": "2026-10-08T15:12:37.459715+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "auto-work-646-a04-2026-10-06": { "thread_uuid": "cb2a1c0c-4eb0-45f3-b770-5cc01248ac03", @@ -2288,11 +2704,14 @@ "archived_by_job": "sw-20261005-203113-f929" }, "auto-work-opm-d17": { - "thread_uuid": "3cf0184d-0eaa-466c-96da-abd390014728", + "thread_uuid": "88ae5f98-95ba-451b-ac3f-daef3c019f66", "agent": "opm", - "title": "auto-work-opm-d17-2026-10-06", + "title": "auto-work-opm-d17-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T00:34:47.289680+00:00" + "created_at": "2026-10-09T00:30:58.979890+00:00", + "dispatch_count": 1, + "rotated_from": "3cf0184d-0eaa-466c-96da-abd390014728", + "rotated_at": "2026-10-09T00:30:58.979901+00:00" }, "auto-work-queue-f19-2026-10-06": { "thread_uuid": "f6c764a6-f81b-43ef-b1ca-5cf4bd7d4d60", @@ -2313,67 +2732,94 @@ "created_at": "2026-10-06T01:13:48.644775+00:00" }, "auto-work-muse-c02": { - "thread_uuid": "df57007e-9692-4db2-9171-577e07510c14", + "thread_uuid": "c6ccf4be-0e6c-49bf-a948-8f982bc0f0f8", "agent": "muse", - "title": "auto-work-muse-c02-2026-10-06", + "title": "auto-work-muse-c02-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T01:25:49.326452+00:00" + "created_at": "2026-10-09T01:20:54.285025+00:00", + "dispatch_count": 1, + "rotated_from": "df57007e-9692-4db2-9171-577e07510c14", + "rotated_at": "2026-10-09T01:20:54.285035+00:00" }, "auto-work-opm-d06": { - "thread_uuid": "acf4cf2a-4422-4d25-8080-24a42339e2af", + "thread_uuid": "ed8bce22-0acd-4336-afd5-05549bd60b76", "agent": "opm", - "title": "auto-work-opm-d06-2026-10-06", + "title": "auto-work-opm-d06-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T02:10:42.566115+00:00" + "created_at": "2026-10-09T02:11:12.771334+00:00", + "dispatch_count": 1, + "rotated_from": "acf4cf2a-4422-4d25-8080-24a42339e2af", + "rotated_at": "2026-10-09T02:11:12.771347+00:00" }, "auto-work-pip-b03": { - "thread_uuid": "24ce3500-082d-45f3-9cba-06aa1491ee1c", + "thread_uuid": "478a3379-7971-487b-a0e4-5363610b601a", "agent": "pip", - "title": "auto-work-pip-b03-2026-10-06", + "title": "auto-work-pip-b03-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T02:10:49.797191+00:00" + "created_at": "2026-10-09T02:11:23.443363+00:00", + "dispatch_count": 1, + "rotated_from": "24ce3500-082d-45f3-9cba-06aa1491ee1c", + "rotated_at": "2026-10-09T02:11:23.443381+00:00" }, "auto-work-muse-c03": { - "thread_uuid": "36483fec-1a0e-48e7-b994-ebe25bce892b", + "thread_uuid": "8b38da83-d6a3-45ee-93ef-1d698d89d916", "agent": "muse", - "title": "auto-work-muse-c03-2026-10-06", + "title": "auto-work-muse-c03-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T02:35:23.435467+00:00" + "created_at": "2026-10-09T02:36:09.261679+00:00", + "dispatch_count": 1, + "rotated_from": "36483fec-1a0e-48e7-b994-ebe25bce892b", + "rotated_at": "2026-10-09T02:36:09.261690+00:00" }, "auto-work-pip-b04": { - "thread_uuid": "79506042-ec89-4f1b-bbec-ae371e6b05ff", + "thread_uuid": "cb8a1c91-06c1-43f5-b26c-82582b8101ed", "agent": "pip", - "title": "auto-work-pip-b04-2026-10-06", + "title": "auto-work-pip-b04-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T03:15:54.131905+00:00" + "created_at": "2026-10-09T03:15:46.591906+00:00", + "dispatch_count": 1, + "rotated_from": "79506042-ec89-4f1b-bbec-ae371e6b05ff", + "rotated_at": "2026-10-09T03:15:46.591918+00:00" }, "auto-work-muse-c04": { - "thread_uuid": "f9d0d189-adae-456e-b506-c9bd53194a59", + "thread_uuid": "242931cc-0c86-4171-961a-12c1b33d11b3", "agent": "muse", - "title": "auto-work-muse-c04-2026-10-06", + "title": "auto-work-muse-c04-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T03:47:07.271677+00:00" + "created_at": "2026-10-09T03:46:31.527506+00:00", + "dispatch_count": 1, + "rotated_from": "f9d0d189-adae-456e-b506-c9bd53194a59", + "rotated_at": "2026-10-09T03:46:31.527515+00:00" }, "auto-work-opm-d19": { - "thread_uuid": "5b3c050f-a5e6-44cf-93fa-0885dac3a5fe", + "thread_uuid": "ceb0f733-cae9-43e6-bc00-f09adf8c92fe", "agent": "opm", - "title": "auto-work-opm-d19-2026-10-06", + "title": "auto-work-opm-d19-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T03:47:20.014756+00:00" + "created_at": "2026-10-09T03:46:53.264370+00:00", + "dispatch_count": 1, + "rotated_from": "5b3c050f-a5e6-44cf-93fa-0885dac3a5fe", + "rotated_at": "2026-10-09T03:46:53.264392+00:00" }, "auto-work-pip-b05": { - "thread_uuid": "01737122-c154-4c55-9117-c38c5304af34", + "thread_uuid": "14a4eebe-b6d8-4659-a5e8-75728995f573", "agent": "pip", - "title": "auto-work-pip-b05-2026-10-06", + "title": "auto-work-pip-b05-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T04:16:28.124727+00:00" + "created_at": "2026-10-09T04:15:47.783182+00:00", + "dispatch_count": 1, + "rotated_from": "01737122-c154-4c55-9117-c38c5304af34", + "rotated_at": "2026-10-09T04:15:47.783195+00:00" }, "auto-work-opm-d07": { - "thread_uuid": "4c21177f-8618-4557-bfd3-eceb5927b25c", + "thread_uuid": "d02b3dae-cafe-45aa-a85f-7f8214aec72e", "agent": "opm", - "title": "auto-work-opm-d07-2026-10-06", + "title": "auto-work-opm-d07-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T04:30:59.777996+00:00" + "created_at": "2026-10-09T04:30:56.067644+00:00", + "dispatch_count": 1, + "rotated_from": "4c21177f-8618-4557-bfd3-eceb5927b25c", + "rotated_at": "2026-10-09T04:30:56.067653+00:00" }, "auto-work-swarm-g09-2026-10-06": { "thread_uuid": "2ce154ed-684c-46f1-95d2-79bbbaf4cf19", @@ -2399,7 +2845,10 @@ "thread_uuid": "ee8c5423-c975-44eb-bd84-a8dffdb9ead9", "agent": "646", "title": "box-http-health-2026-10-06T05:00:00.097660+00:00", - "created_at": "2026-10-06T05:02:23.997548+00:00" + "created_at": "2026-10-06T05:02:23.997548+00:00", + "archived": true, + "archived_at": "2026-10-08T15:12:41.952941+00:00", + "archived_by_job": "sw-20261008-144331-a6db" }, "auto-work-muse-c05-2026-10-06": { "thread_uuid": "8f1ae01d-1449-4c5f-ad5a-749d9a7c17a6", @@ -2408,10 +2857,11 @@ "created_at": "2026-10-06T05:02:53.160137+00:00" }, "auto-work-health-h19": { - "thread_uuid": "3664c984-5b69-4eff-b9f7-e74f12babcd2", + "thread_uuid": "be10d721-c772-44fd-87e7-bab0b2938015", "agent": "646", "title": "auto-work-health-h19", - "created_at": "2026-10-06T05:13:31.728991+00:00" + "type": "persistent", + "created_at": "2026-10-08T05:11:13.352540+00:00" }, "auto-work-pip-b06-2026-10-06": { "thread_uuid": "cbbe2454-a44d-4c7c-ace1-356bc79a3272", @@ -2450,11 +2900,14 @@ "created_at": "2026-10-06T20:13:30.607843+00:00" }, "auto-work-muse-c01": { - "thread_uuid": "916904c8-ec55-4848-9aaa-0690637af4f8", + "thread_uuid": "f1078365-4063-408b-be10-a601b706af9d", "agent": "muse", - "title": "auto-work-muse-c01-2026-10-06", + "title": "auto-work-muse-c01-2026-10-09", "type": "persistent", - "created_at": "2026-10-06T20:14:14.273684+00:00" + "created_at": "2026-10-09T00:11:19.615334+00:00", + "dispatch_count": 2, + "rotated_from": "916904c8-ec55-4848-9aaa-0690637af4f8", + "rotated_at": "2026-10-09T00:11:19.615357+00:00" }, "646-muse-coord": { "thread_uuid": "c40ea073-7391-4bed-9ff8-47b563b40613", @@ -2484,5 +2937,124 @@ "thread_uuid": "dc9e6bad-94bd-4893-948c-1b35ec0523e0", "agent": "646", "created_at": "2026-10-07T01:14:57.101511+00:00" + }, + "auto-work-swarm-g02-2026-10-07": { + "thread_uuid": "d3ed1490-ab56-4ea3-aaca-5636ac85fca7", + "agent": "646", + "created_at": "2026-10-07T08:11:51.269555+00:00" + }, + "sw-20261007-082518-6371-s1": { + "thread_uuid": "49bb1228-270d-4efb-8ca5-0c62d2f33e8a", + "agent": "muse", + "title": "sw-20261007-082518-6371-s1", + "created_at": "2026-10-07T08:26:19.997640+00:00", + "archived": true, + "archived_at": "2026-10-07T08:29:30.156734+00:00", + "archived_by_job": "sw-20261007-082518-6371/1" + }, + "box-deep-health-2026-10-07T09:02:06.794532+00:00": { + "thread_uuid": "66dc9ba5-7f3a-46a6-a018-2d7195b124d1", + "agent": "646", + "title": "box-deep-health-2026-10-07T09:02:06.794532+00:00", + "type": "ephemeral", + "created_at": "2026-10-07T09:02:09.271089+00:00", + "archived": true, + "archived_at": "2026-10-07T09:04:13.767310+00:00", + "archived_by_job": "box-deep-health-20261007-090206-1a1365b9" + }, + "auto-work-sweep-j15-2026-10-07": { + "thread_uuid": "9fda2d8d-c931-4d90-8b78-5024868f4b04", + "agent": "opm", + "title": "auto-work-sweep-j15-2026-10-07", + "created_at": "2026-10-07T22:31:56.605955+00:00" + }, + "auto-work-pip-b02": { + "thread_uuid": "289f9adc-5234-4180-b3f0-f3e41abbcfd4", + "agent": "pip", + "title": "auto-work-pip-b02-2026-10-09", + "type": "persistent", + "created_at": "2026-10-09T01:11:09.647630+00:00", + "dispatch_count": 1, + "rotated_from": "2f8c30f9-144a-4ee8-ab0f-a1544a485e19", + "rotated_at": "2026-10-09T01:11:09.647640+00:00" + }, + "auto-work-opm-d18": { + "thread_uuid": "db0bdc49-b83f-4b15-ab97-d457b04bc4d2", + "agent": "opm", + "title": "auto-work-opm-d18-2026-10-09", + "type": "persistent", + "created_at": "2026-10-09T01:15:46.311415+00:00", + "dispatch_count": 1, + "rotated_from": "710008b4-291a-493c-a094-8a7442a88336", + "rotated_at": "2026-10-09T01:15:46.311425+00:00" + }, + "muse": { + "thread_uuid": "bdbaf36c-31db-41bb-bb75-958e3618063b", + "agent": "muse", + "title": "muse", + "created_at": "2026-10-08T02:43:20.120590+00:00" + }, + "pip": { + "thread_uuid": "926a70bd-ded4-4a63-8727-dd6391bee3ab", + "agent": "pip", + "title": "pip", + "created_at": "2026-10-08T02:43:42.580750+00:00" + }, + "auto-work-muse-c05": { + "thread_uuid": "24c0ceb5-3cf4-4f8e-bb91-b9c520a5e276", + "agent": "muse", + "title": "auto-work-muse-c05-2026-10-09", + "type": "persistent", + "created_at": "2026-10-09T04:56:20.624340+00:00", + "dispatch_count": 1, + "rotated_from": "55ddf5db-de40-46c1-a564-ac77fbf7f323", + "rotated_at": "2026-10-09T04:56:20.624351+00:00" + }, + "box-deep-health-2026-10-08T09:01:39.141366+00:00": { + "thread_uuid": "98fc9666-27b4-422f-82da-54fb6ffd502c", + "agent": "646", + "title": "box-deep-health-2026-10-08T09:01:39.141366+00:00", + "type": "ephemeral", + "created_at": "2026-10-08T09:01:41.791630+00:00", + "archived": true, + "archived_at": "2026-10-08T09:10:55.124828+00:00", + "archived_by_job": "box-deep-health-20261008-090139-d8a70001" + }, + "x": { + "thread_uuid": "82cd6b18-b06c-469b-8cac-6ff1e9e4766d", + "agent": "pip", + "title": "x", + "created_at": "2026-10-08T14:26:49.866534+00:00" + }, + "auto-work-queue-f13-2026-10-09": { + "thread_uuid": "27747acd-250c-4a7e-a6c3-0b3e758263d6", + "agent": "646", + "created_at": "2026-10-09T02:41:23.175866+00:00" + }, + "auto-work-dev-i11-2026-10-09": { + "thread_uuid": "7597b25d-90de-42e1-a00d-f9c5d23dec19", + "agent": "def", + "created_at": "2026-10-09T04:22:29.193145+00:00", + "archived": true, + "archived_at": "2026-10-09T04:35:58.540642+00:00", + "archived_by_job": "auto-work-dev-i11-20261009-042010-73fd80d8" + }, + "ef974802": { + "thread_uuid": "24966f3d-d0ec-45a2-aa63-74e9828ab731", + "agent": "646", + "title": "ef974802", + "created_at": "2026-10-09T20:36:29.798940+00:00" + }, + "8e569473": { + "thread_uuid": "e2403e9f-3977-4419-82df-c840da79adee", + "agent": "646", + "title": "8e569473", + "created_at": "2026-10-09T20:37:09.583206+00:00" + }, + "88c66af6": { + "thread_uuid": "f46a3f87-ff52-437a-8305-0ce922846867", + "agent": "opm", + "title": "88c66af6", + "created_at": "2026-10-09T20:49:05.197092+00:00" } } \ No newline at end of file diff --git a/jobs/646-exec-health.json b/jobs/646-exec-health.json deleted file mode 100644 index b8c1a36..0000000 --- a/jobs/646-exec-health.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "646-exec-health", - "agent": "646", - "description": "Monitor exec server health every 10 minutes", - "prompt_template": "Exec server health check.\nJob ID: {job_id}\nTime: {datetime}\n\nCheck https://34-139-37-135.sslip.io/exec/health and report status. Reply with [RESULT {job_id}] OK/FAIL.", - "schedule": "*/10 * * * *", - "timeout": 120, - "on_failure": "alert", - "dm_target": "646 tasks", - "sidechat": {"create": false}, - "chain_next": null -} diff --git a/jobs/646-hourly-checkin.json b/jobs/646-hourly-checkin.json deleted file mode 100644 index 1de4476..0000000 --- a/jobs/646-hourly-checkin.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "646-hourly-checkin", - "description": "Hourly operational check-in for 646 during active daytime hours", - "agent": "646", - "schedule": "0 8-22 * * *", - "timeout": 300, - "on_failure": "alert", - "dm_target": "646 tasks", - "sidechat": {"create": false}, - "followup": { - "expect_reply": true, - "timeout": "1h", - "nudges": 1, - "escalate": "opm" - }, - "prompt_template": "Hourly operational check-in for 646.\nJob ID: {job_id}\nTime: {datetime}\n\nPlease report briefly in this thread:\n(1) Current tasks in flight\n(2) VM / service health status\n(3) Any blockers or peer coordination items\n\nReply with [RESULT {job_id}] and your status." -} diff --git a/jobs/auto-work-646-a01.json b/jobs/auto-work-646-a01.json deleted file mode 100644 index 1dc2fc1..0000000 --- a/jobs/auto-work-646-a01.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a01", - "description": "646 auto-work: scan box job-list for claimable manual/pending jobs", - "agent": "646", - "schedule": "3,33 * * * *", - "timeout": 300, - "prompt_template": "646 work scan: run box job-list and find manual or pending jobs assigned to you or unclaimed. Claim the oldest actionable one with box job-trigger and start it. If nothing actionable, report idle and take no further action.\n[TOOL swarm.list {}]\n[TOOL dm.read {\"agent\": \"646\", \"limit\": 5}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a01", - "name_template": "auto-work-646-a01-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a02.json b/jobs/auto-work-646-a02.json deleted file mode 100644 index f512625..0000000 --- a/jobs/auto-work-646-a02.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a02", - "description": "646 auto-work: 646 dispatch review: failed/stale dispatch triage", - "agent": "646", - "schedule": "7,37 * * * *", - "timeout": 300, - "prompt_template": "646 dispatch review: check your recent dispatches for failed or stale ones (box job-status). Retry a failed dispatch once with box job-trigger; if it fails twice, escalate to opm via box notify. Nothing broken: report all-clear.\n[TOOL dm.read {\"agent\": \"646\", \"limit\": 5}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a02", - "name_template": "auto-work-646-a02-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a03.json b/jobs/auto-work-646-a03.json deleted file mode 100644 index 559379d..0000000 --- a/jobs/auto-work-646-a03.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a03", - "description": "646 auto-work: 646 chain check: advance chained jobs waiting on you", - "agent": "646", - "schedule": "11,41 * * * *", - "timeout": 300, - "prompt_template": "646 chain check: look for chained jobs where you are the next step (box job-next on your recent job ids). If a next step is waiting on you, dispatch it. If no chains are pending, report idle.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a03", - "name_template": "auto-work-646-a03-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a04.json b/jobs/auto-work-646-a04.json deleted file mode 100644 index 5e1981c..0000000 --- a/jobs/auto-work-646-a04.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a04", - "description": "646 auto-work: 646 vars check: act on scope variables signaling work", - "agent": "646", - "schedule": "13,43 * * * *", - "timeout": 300, - "prompt_template": "646 vars check: run box vars-list and read variables in your scope (for example pulse_interval_m). If a variable signals pending work or a changed threshold, act on it; otherwise report steady.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a04", - "name_template": "auto-work-646-a04-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a05.json b/jobs/auto-work-646-a05.json deleted file mode 100644 index 749f60d..0000000 --- a/jobs/auto-work-646-a05.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a05", - "description": "646 auto-work: 646 node health: fleet-status check on warp-646", - "agent": "646", - "schedule": "17,47 * * * *", - "timeout": 300, - "prompt_template": "646 node health: run box fleet-status and check your node (warp-646, CDP 9430). If proc_alive or cdp_ok is false, run the watchdog repair path and verify with a second read-back. Healthy: report ok.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a05", - "name_template": "auto-work-646-a05-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a06.json b/jobs/auto-work-646-a06.json deleted file mode 100644 index 009cc2d..0000000 --- a/jobs/auto-work-646-a06.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a06", - "description": "646 auto-work: 646 alert triage: watchdog-alerts in your scope", - "agent": "646", - "schedule": "19,49 * * * *", - "timeout": 300, - "prompt_template": "646 alert triage: run box watchdog-alerts and look for alerts in your scope. Triage the newest one: fix it, or escalate to opm with box notify. No alerts: report clear.\n[TOOL service.status {\"unit\": \"board.service\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a06", - "name_template": "auto-work-646-a06-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a07.json b/jobs/auto-work-646-a07.json deleted file mode 100644 index 0339f4a..0000000 --- a/jobs/auto-work-646-a07.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a07", - "description": "646 auto-work: 646 service sweep: board/harvester service health", - "agent": "646", - "schedule": "23,53 * * * *", - "timeout": 300, - "prompt_template": "646 service sweep: verify service health the box-service-health way (fleet-status plus service checks for board and harvester). Restart one failed allowlisted service, verify it recovered, otherwise escalate. All healthy: report ok.\n[TOOL service.status {\"unit\": \"response-harvester.timer\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a07", - "name_template": "auto-work-646-a07-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a08.json b/jobs/auto-work-646-a08.json deleted file mode 100644 index ed4d80e..0000000 --- a/jobs/auto-work-646-a08.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a08", - "description": "646 auto-work: 646 browser check: CDP latency and chrome errors", - "agent": "646", - "schedule": "27,57 * * * *", - "timeout": 300, - "prompt_template": "646 browser check: run box cdp-latency and chrome-errors for your profile. If latency is spiking or new FATAL errors appear since the watermark, restart chromium via the watchdog path and verify. Otherwise report healthy.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a08", - "name_template": "auto-work-646-a08-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a09.json b/jobs/auto-work-646-a09.json deleted file mode 100644 index ac94644..0000000 --- a/jobs/auto-work-646-a09.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a09", - "description": "646 auto-work: 646 digest scan: act on newest actionable digest", - "agent": "646", - "schedule": "1,31 * * * *", - "timeout": 300, - "prompt_template": "646 digest scan: read your task sidechat for the newest actionable digest. ACK it and act, or mark it no-action with a one-line reason. Nothing actionable: take no further action.\n[TOOL dm.read {\"agent\": \"646\", \"limit\": 10}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a09", - "name_template": "auto-work-646-a09-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a10.json b/jobs/auto-work-646-a10.json deleted file mode 100644 index 40c58ee..0000000 --- a/jobs/auto-work-646-a10.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a10", - "description": "646 auto-work: 646 DM sweep: reply to unanswered DMs", - "agent": "646", - "schedule": "9,39 * * * *", - "timeout": 300, - "prompt_template": "646 DM sweep: run box dm-log and check for unanswered DMs addressed to you. Reply to the newest one needing a response, or escalate to opm if blocked. None waiting: report clear.\n[TOOL dm.read {\"agent\": \"646\", \"limit\": 10}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a10", - "name_template": "auto-work-646-a10-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a11.json b/jobs/auto-work-646-a11.json deleted file mode 100644 index e5178ad..0000000 --- a/jobs/auto-work-646-a11.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a11", - "description": "646 auto-work: 646 loop check: resolve pending/stale digest followups", - "agent": "646", - "schedule": "5,35 * * * *", - "timeout": 300, - "prompt_template": "646 loop check: run box loop-status for agent 646 and look for pending or stale digest followups. Resolve or nudge the oldest stale one according to its declared followup. All resolved: report ok.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a11", - "name_template": "auto-work-646-a11-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a12.json b/jobs/auto-work-646-a12.json deleted file mode 100644 index 75d4474..0000000 --- a/jobs/auto-work-646-a12.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a12", - "description": "646 auto-work: 646 swarm scan: claim an open swarm slot", - "agent": "646", - "schedule": "15,45 * * * *", - "timeout": 300, - "prompt_template": "646 swarm scan: run box swarm-list and find swarms with open slots in your scope. Attach to one slot and complete it, then report the result. No open slots: report idle.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a12", - "name_template": "auto-work-646-a12-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a13.json b/jobs/auto-work-646-a13.json deleted file mode 100644 index 8888a42..0000000 --- a/jobs/auto-work-646-a13.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a13", - "description": "646 auto-work: Cross-op scan: claim unclaimed work from #jobs/board", - "agent": "646", - "schedule": "21,51 * * * *", - "timeout": 300, - "prompt_template": "Cross-op scan: check #jobs and the board for unclaimed work other operators posted. If something fits 646 scope, claim it and start; otherwise note what you saw in one line. Nothing new: report idle.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a13", - "name_template": "auto-work-646-a13-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a14.json b/jobs/auto-work-646-a14.json deleted file mode 100644 index 3c22215..0000000 --- a/jobs/auto-work-646-a14.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a14", - "description": "646 auto-work: Fleet watch: nudge operators that look stuck", - "agent": "646", - "schedule": "25,55 * * * *", - "timeout": 300, - "prompt_template": "Fleet watch: run box fleet-status and compare queue depth and recent activity across muse, pip, opm, def, dev. If an operator looks stuck (growing queue, stale watermark), send them a nudge via box notify. Everyone moving: report ok.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a14", - "name_template": "auto-work-646-a14-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a15.json b/jobs/auto-work-646-a15.json deleted file mode 100644 index 091ad20..0000000 --- a/jobs/auto-work-646-a15.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a15", - "description": "646 auto-work: 646 harvest follow-up: pick up tagged follow-up work", - "agent": "646", - "schedule": "29,59 * * * *", - "timeout": 300, - "prompt_template": "646 harvest follow-up: check the latest opm-swarm-harvest summary for follow-up work tagged to you. Pick up the top item and complete it, or report none tagged.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a15", - "name_template": "auto-work-646-a15-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a16.json b/jobs/auto-work-646-a16.json deleted file mode 100644 index 59dd61f..0000000 --- a/jobs/auto-work-646-a16.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a16", - "description": "646 auto-work: Fleet pulse check: main-loop and heartbeat status", - "agent": "646", - "schedule": "2,32 * * * *", - "timeout": 300, - "prompt_template": "Fleet pulse check: run box main-loop status and check heartbeat status. If the main loop or heartbeat shows errors affecting your scope, investigate and fix or escalate to opm. Green across the board: report ok.\n[TOOL service.status {\"unit\": \"board.service\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a16", - "name_template": "auto-work-646-a16-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a17.json b/jobs/auto-work-646-a17.json deleted file mode 100644 index 8c2a70b..0000000 --- a/jobs/auto-work-646-a17.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a17", - "description": "646 auto-work: 646 timer audit: fix orphaned/disabled/erroring timers", - "agent": "646", - "schedule": "6,36 * * * *", - "timeout": 300, - "prompt_template": "646 timer audit: run box timer-list and look for your timers that are orphaned, disabled, or erroring. Re-enable or fix one, or report all healthy.\n[TOOL cron.status {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a17", - "name_template": "auto-work-646-a17-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a18.json b/jobs/auto-work-646-a18.json deleted file mode 100644 index f47e28b..0000000 --- a/jobs/auto-work-646-a18.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a18", - "description": "646 auto-work: 646 quality gate: box quality check must stay green", - "agent": "646", - "schedule": "12,42 * * * *", - "timeout": 300, - "prompt_template": "646 quality gate: run box quality check. If any check fails, investigate the failing validator and fix it, or escalate with the exact failure text. All green: report the pass count.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a18", - "name_template": "auto-work-646-a18-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a19.json b/jobs/auto-work-646-a19.json deleted file mode 100644 index 6557354..0000000 --- a/jobs/auto-work-646-a19.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a19", - "description": "646 auto-work: 646 failure review: fix one recurring timer failure", - "agent": "646", - "schedule": "18,48 * * * *", - "timeout": 300, - "prompt_template": "646 failure review: look at recent runs of your auto-work timers (box timer-status) and find recurring failures. Fix the root cause of one repeat failure, or escalate with evidence. No repeats: report clean.\n[TOOL cron.status {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a19", - "name_template": "auto-work-646-a19-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-646-a20.json b/jobs/auto-work-646-a20.json deleted file mode 100644 index 22f0981..0000000 --- a/jobs/auto-work-646-a20.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-646-a20", - "description": "646 auto-work: 646 open-work digest: 5-line summary to task thread", - "agent": "646", - "schedule": "24,54 * * * *", - "timeout": 300, - "prompt_template": "646 open-work digest: summarize your currently open work items (pending jobs, unacked digests, open swarm slots) into a 5-line digest in your task thread. Keep it short; informational only.\n[TOOL dm.read {\"agent\": \"646\", \"limit\": 5}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-646-a20", - "name_template": "auto-work-646-a20-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i01.json b/jobs/auto-work-dev-i01.json deleted file mode 100644 index b823751..0000000 --- a/jobs/auto-work-dev-i01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i01", - "description": "Auto-work canary for dev worker: node self-check", - "agent": "dev", - "schedule": "3 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i01", - "name_template": "auto-work-dev-i01-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i02.json b/jobs/auto-work-dev-i02.json deleted file mode 100644 index 536f562..0000000 --- a/jobs/auto-work-dev-i02.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i02", - "description": "Auto-work tool exercise for dev worker", - "agent": "dev", - "schedule": "9 * * * *", - "timeout": 600, - "prompt_template": "Tool exercise: run a service.status tool call for board.service and summarize the outcome in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i02", - "name_template": "auto-work-dev-i02-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i03.json b/jobs/auto-work-dev-i03.json deleted file mode 100644 index 5e48741..0000000 --- a/jobs/auto-work-dev-i03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i03", - "description": "Auto-work canary for dev worker: node self-check", - "agent": "dev", - "schedule": "15 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i03", - "name_template": "auto-work-dev-i03-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i04.json b/jobs/auto-work-dev-i04.json deleted file mode 100644 index 72ba7e2..0000000 --- a/jobs/auto-work-dev-i04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i04", - "description": "Auto-work swarm slot for dev worker", - "agent": "dev", - "schedule": "21 * * * *", - "timeout": 600, - "prompt_template": "Swarm slot: spawn one ephemeral worker via box swarm on your scope and collect its result. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i04", - "name_template": "auto-work-dev-i04-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i05.json b/jobs/auto-work-dev-i05.json deleted file mode 100644 index fb755ff..0000000 --- a/jobs/auto-work-dev-i05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i05", - "description": "Auto-work tool exercise for dev worker", - "agent": "dev", - "schedule": "27 * * * *", - "timeout": 600, - "prompt_template": "Tool exercise: run a service.status tool call for board.service and summarize the outcome in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i05", - "name_template": "auto-work-dev-i05-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i06.json b/jobs/auto-work-dev-i06.json deleted file mode 100644 index 5c70078..0000000 --- a/jobs/auto-work-dev-i06.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i06", - "description": "Auto-work canary for dev worker: node self-check", - "agent": "dev", - "schedule": "33 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i06", - "name_template": "auto-work-dev-i06-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i07.json b/jobs/auto-work-dev-i07.json deleted file mode 100644 index 3d7001d..0000000 --- a/jobs/auto-work-dev-i07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i07", - "description": "Auto-work swarm slot for dev worker", - "agent": "dev", - "schedule": "39 * * * *", - "timeout": 600, - "prompt_template": "Swarm slot: spawn one ephemeral worker via box swarm on your scope and collect its result. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i07", - "name_template": "auto-work-dev-i07-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i08.json b/jobs/auto-work-dev-i08.json deleted file mode 100644 index c429c00..0000000 --- a/jobs/auto-work-dev-i08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i08", - "description": "Auto-work tool exercise for dev worker", - "agent": "dev", - "schedule": "45 * * * *", - "timeout": 600, - "prompt_template": "Tool exercise: run a service.status tool call for board.service and summarize the outcome in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i08", - "name_template": "auto-work-dev-i08-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i09.json b/jobs/auto-work-dev-i09.json deleted file mode 100644 index cd551b7..0000000 --- a/jobs/auto-work-dev-i09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i09", - "description": "Auto-work canary for dev worker: node self-check", - "agent": "dev", - "schedule": "51 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i09", - "name_template": "auto-work-dev-i09-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i10.json b/jobs/auto-work-dev-i10.json deleted file mode 100644 index 8cefa4b..0000000 --- a/jobs/auto-work-dev-i10.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i10", - "description": "Auto-work swarm job for dev worker pool", - "agent": "dev", - "schedule": "13 * * * *", - "timeout": 600, - "prompt_template": "Swarm-slot exercise: take one small self-contained task (e.g. lint one file, summarize one doc section) and do it fully, then report a one-line summary of what you did. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i10", - "name_template": "auto-work-dev-i10-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i11.json b/jobs/auto-work-dev-i11.json deleted file mode 100644 index 1da4680..0000000 --- a/jobs/auto-work-dev-i11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i11", - "description": "Auto-work tools job for def worker pool", - "agent": "def", - "schedule": "20 * * * *", - "timeout": 600, - "prompt_template": "Tool-call exercise: run a service.status tool call against board.service and report the result briefly, then report what you did in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i11", - "name_template": "auto-work-dev-i11-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i12.json b/jobs/auto-work-dev-i12.json deleted file mode 100644 index fa4b8b7..0000000 --- a/jobs/auto-work-dev-i12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i12", - "description": "Auto-work result job for dev worker pool", - "agent": "dev", - "schedule": "27 * * * *", - "timeout": 600, - "prompt_template": "Result-return check: do one small real task of your choosing, verify you can read it back, then report a one-line summary confirming completion. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i12", - "name_template": "auto-work-dev-i12-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i13.json b/jobs/auto-work-dev-i13.json deleted file mode 100644 index da2d4d0..0000000 --- a/jobs/auto-work-dev-i13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i13", - "description": "Auto-work canary job for def worker pool", - "agent": "def", - "schedule": "34 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i13", - "name_template": "auto-work-dev-i13-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i14.json b/jobs/auto-work-dev-i14.json deleted file mode 100644 index a196d9a..0000000 --- a/jobs/auto-work-dev-i14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i14", - "description": "Auto-work swarm job for dev worker pool", - "agent": "dev", - "schedule": "41 * * * *", - "timeout": 600, - "prompt_template": "Swarm-slot exercise: take one small self-contained task (e.g. lint one file, summarize one doc section) and do it fully, then report a one-line summary of what you did. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i14", - "name_template": "auto-work-dev-i14-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i15.json b/jobs/auto-work-dev-i15.json deleted file mode 100644 index 1b53891..0000000 --- a/jobs/auto-work-dev-i15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i15", - "description": "Auto-work tools job for def worker pool", - "agent": "def", - "schedule": "48 * * * *", - "timeout": 600, - "prompt_template": "Tool-call exercise: run a service.status tool call against board.service and report the result briefly, then report what you did in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i15", - "name_template": "auto-work-dev-i15-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i16.json b/jobs/auto-work-dev-i16.json deleted file mode 100644 index b989195..0000000 --- a/jobs/auto-work-dev-i16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i16", - "description": "Auto-work result job for dev worker pool", - "agent": "dev", - "schedule": "55 * * * *", - "timeout": 600, - "prompt_template": "Result-return check: do one small real task of your choosing, verify you can read it back, then report a one-line summary confirming completion. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i16", - "name_template": "auto-work-dev-i16-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i17.json b/jobs/auto-work-dev-i17.json deleted file mode 100644 index fd3716c..0000000 --- a/jobs/auto-work-dev-i17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i17", - "description": "Auto-work canary job for def worker pool", - "agent": "def", - "schedule": "2 * * * *", - "timeout": 600, - "prompt_template": "Canary self-check: verify your node is reachable and healthy. Quiet run; no main-chat posts.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i17", - "name_template": "auto-work-dev-i17-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i18.json b/jobs/auto-work-dev-i18.json deleted file mode 100644 index 73ab635..0000000 --- a/jobs/auto-work-dev-i18.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i18", - "description": "Auto-work swarm job for dev worker pool", - "agent": "dev", - "schedule": "9 * * * *", - "timeout": 600, - "prompt_template": "Swarm-slot exercise: take one small self-contained task (e.g. lint one file, summarize one doc section) and do it fully, then report a one-line summary of what you did. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i18", - "name_template": "auto-work-dev-i18-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i19.json b/jobs/auto-work-dev-i19.json deleted file mode 100644 index eef527a..0000000 --- a/jobs/auto-work-dev-i19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i19", - "description": "Auto-work tools job for def worker pool", - "agent": "def", - "schedule": "16 * * * *", - "timeout": 600, - "prompt_template": "Tool-call exercise: run a service.status tool call against board.service and report the result briefly, then report what you did in one line. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i19", - "name_template": "auto-work-dev-i19-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-dev-i20.json b/jobs/auto-work-dev-i20.json deleted file mode 100644 index cdf132c..0000000 --- a/jobs/auto-work-dev-i20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-dev-i20", - "description": "Auto-work result job for dev worker pool", - "agent": "dev", - "schedule": "23 * * * *", - "timeout": 600, - "prompt_template": "Result-return check: do one small real task of your choosing, verify you can read it back, then report a one-line summary confirming completion. Quiet run.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-dev-i20", - "name_template": "auto-work-dev-i20-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-health-h01.json b/jobs/auto-work-health-h01.json deleted file mode 100644 index d626a41..0000000 --- a/jobs/auto-work-health-h01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h01", - "description": "Box quality check (core) \u2014 hourly", - "schedule": "13 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Box quality check (core).\nJob ID: {job_id}\nTime: {datetime}\n\nRun:\n[TOOL health.check {}]\n\nThen run the box quality gate:\n[EXEC quality.check {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h01", - "reuse_key": "auto-work-health-h01" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h02.json b/jobs/auto-work-health-h02.json deleted file mode 100644 index a0ef73f..0000000 --- a/jobs/auto-work-health-h02.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h02", - "description": "Box quality check (fleet nodes) \u2014 hourly", - "schedule": "43 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nBox quality check across fleet nodes.\nRun:\n[TOOL health.check {}]\n\nThen:\n[EXEC quality.check {\"scope\": \"fleet\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h02", - "reuse_key": "auto-work-health-h02" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h03.json b/jobs/auto-work-health-h03.json deleted file mode 100644 index 12318cc..0000000 --- a/jobs/auto-work-health-h03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h03", - "description": "Quality validate: job \u2014 hourly", - "schedule": "11 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nValidate box job definitions.\nRun:\n[EXEC quality.validate {\"target\": \"job\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h03", - "reuse_key": "auto-work-health-h03" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h04.json b/jobs/auto-work-health-h04.json deleted file mode 100644 index 96ef562..0000000 --- a/jobs/auto-work-health-h04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h04", - "description": "Quality validate: status \u2014 hourly", - "schedule": "41 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nValidate box status surface.\nRun:\n[EXEC quality.validate {\"target\": \"status\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h04", - "reuse_key": "auto-work-health-h04" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h05.json b/jobs/auto-work-health-h05.json deleted file mode 100644 index 2558e52..0000000 --- a/jobs/auto-work-health-h05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h05", - "description": "Quality validate: heartbeat \u2014 hourly", - "schedule": "26 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nValidate heartbeat pipeline.\nRun:\n[EXEC quality.validate {\"target\": \"heartbeat\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h05", - "reuse_key": "auto-work-health-h05" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h06.json b/jobs/auto-work-health-h06.json deleted file mode 100644 index 29d9c06..0000000 --- a/jobs/auto-work-health-h06.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h06", - "description": "Quality check deep: validators catalog \u2014 hourly", - "schedule": "56 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDeep quality check on input validators and error-code catalog.\nRun:\n[TOOL health.check {}]\n\nThen:\n[EXEC quality.check {\"scope\": \"validators\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h06", - "reuse_key": "auto-work-health-h06" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h07.json b/jobs/auto-work-health-h07.json deleted file mode 100644 index 53f757b..0000000 --- a/jobs/auto-work-health-h07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h07", - "description": "Box service health deep: endpoints \u2014 hourly", - "schedule": "3 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDeep service-health check: box endpoints.\nRun:\n[TOOL service.status {\"unit\": \"board.service\"}]\n[TOOL health.check {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h07", - "reuse_key": "auto-work-health-h07" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h08.json b/jobs/auto-work-health-h08.json deleted file mode 100644 index f9edd1a..0000000 --- a/jobs/auto-work-health-h08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h08", - "description": "Box service health deep: data/audit \u2014 hourly", - "schedule": "33 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDeep service-health check: data layer and audit log.\nRun:\n[TOOL service.status {\"unit\": \"response-harvester.timer\"}]\n[TOOL health.check {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h08", - "reuse_key": "auto-work-health-h08" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h09.json b/jobs/auto-work-health-h09.json deleted file mode 100644 index 085aa0a..0000000 --- a/jobs/auto-work-health-h09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h09", - "description": "Box HTTP health deep: all routes \u2014 hourly", - "schedule": "18 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDeep HTTP health: all box routes.\nRun:\n[TOOL health.check {}]\n\nThen:\n[TOOL cron.runs {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h09", - "reuse_key": "auto-work-health-h09" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h10.json b/jobs/auto-work-health-h10.json deleted file mode 100644 index b6f7f2e..0000000 --- a/jobs/auto-work-health-h10.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h10", - "description": "Box HTTP health deep: auth/pin paths \u2014 hourly", - "schedule": "48 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDeep HTTP health: auth and PIN paths.\nRun:\n[TOOL health.check {}]\n\nThen:\n[TOOL cron.status {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h10", - "reuse_key": "auto-work-health-h10" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h11.json b/jobs/auto-work-health-h11.json deleted file mode 100644 index a2cceb1..0000000 --- a/jobs/auto-work-health-h11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h11", - "description": "Quality check: timer hygiene (orphans) \u2014 hourly", - "schedule": "8 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nTimer hygiene: find orphan or disabled timers.\nRun:\n[TOOL cron.status {}]\n\nThen:\n[EXEC quality.check {\"scope\": \"timers\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h11", - "reuse_key": "auto-work-health-h11" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h12.json b/jobs/auto-work-health-h12.json deleted file mode 100644 index a9c98bc..0000000 --- a/jobs/auto-work-health-h12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h12", - "description": "Quality check: job chain integrity \u2014 hourly", - "schedule": "38 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nJob chain integrity: verify chain_next links resolve.\nRun:\n[EXEC quality.check {\"scope\": \"chains\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h12", - "reuse_key": "auto-work-health-h12" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h13.json b/jobs/auto-work-health-h13.json deleted file mode 100644 index 42ee77a..0000000 --- a/jobs/auto-work-health-h13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h13", - "description": "Box service health: relay/exec bridge \u2014 hourly", - "schedule": "23 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nRelay and exec-bridge health.\nRun:\n[TOOL service.status {\"unit\": \"board.service\"}]\n[TOOL health.check {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h13", - "reuse_key": "auto-work-health-h13" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h14.json b/jobs/auto-work-health-h14.json deleted file mode 100644 index ca292b4..0000000 --- a/jobs/auto-work-health-h14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h14", - "description": "Box HTTP health: dashboard SPA \u2014 hourly", - "schedule": "53 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nDashboard SPA health.\nRun:\n[TOOL health.check {}]\n\nThen fetch the dashboard route:\n[TOOL web.fetch {\"url\": \"https://box.muse-dev.online/\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h14", - "reuse_key": "auto-work-health-h14" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h15.json b/jobs/auto-work-health-h15.json deleted file mode 100644 index fbbef9c..0000000 --- a/jobs/auto-work-health-h15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h15", - "description": "Failure rollup: collect FAILs, alert \u2014 hourly", - "schedule": "28 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nFailure rollup: scan recent job results for FAIL outcomes and summarize.\nRun:\n[TOOL cron.runs {}]\n\nThen:\n[EXEC quality.validate {\"target\": \"status\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h15", - "reuse_key": "auto-work-health-h15" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h16.json b/jobs/auto-work-health-h16.json deleted file mode 100644 index 5046a8a..0000000 --- a/jobs/auto-work-health-h16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h16", - "description": "cron.runs audit: missed/failed runs \u2014 hourly", - "schedule": "58 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nAudit scheduled runs for misses and failures.\nRun:\n[TOOL cron.runs {}]\n[TOOL cron.status {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h16", - "reuse_key": "auto-work-health-h16" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h17.json b/jobs/auto-work-health-h17.json deleted file mode 100644 index 312461b..0000000 --- a/jobs/auto-work-health-h17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h17", - "description": "Quality validate: dm-log route \u2014 hourly", - "schedule": "16 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nValidate dm-log pipeline.\nRun:\n[EXEC quality.validate {\"target\": \"dm-log\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h17", - "reuse_key": "auto-work-health-h17" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h18.json b/jobs/auto-work-health-h18.json deleted file mode 100644 index 666aaa3..0000000 --- a/jobs/auto-work-health-h18.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h18", - "description": "Quality check: vars/strategy stores \u2014 hourly", - "schedule": "46 * * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nVars and strategy store health.\nRun:\n[TOOL vars.list {}]\n[TOOL health.check {}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h18", - "reuse_key": "auto-work-health-h18" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h19.json b/jobs/auto-work-health-h19.json deleted file mode 100644 index 0ced82f..0000000 --- a/jobs/auto-work-health-h19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h19", - "description": "Box quality check (sidechat reuse keys) \u2014 daily", - "schedule": "6 5 * * *", - "agent": "646", - "timeout": 600, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nSidechat reuse-key audit: verify persistent job threads resolve.\nRun:\n[EXEC quality.check {\"scope\": \"sidechats\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h19", - "reuse_key": "auto-work-health-h19" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-health-h20.json b/jobs/auto-work-health-h20.json deleted file mode 100644 index 6aca309..0000000 --- a/jobs/auto-work-health-h20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-health-h20", - "description": "Weekly deep quality report \u2014 Sundays", - "schedule": "36 6 * * 0", - "agent": "646", - "timeout": 1800, - "prompt_template": "Job ID: {job_id}\nTime: {datetime}\n\nWeekly deep quality report: full gate suite plus trend summary.\nRun:\n[TOOL health.check {}]\n\nThen:\n[EXEC quality.check {\"scope\": \"all\"}]\n\nEnd with your verdict: OK if all green, otherwise FAIL plus a one-line summary of what failed.", - "sidechat": { - "create": true, - "name_template": "auto-work-health-h20", - "reuse_key": "auto-work-health-h20" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "auto-work-health", - "timeout": "30m" - }, - "on_failure": "alert", - "chain_next": null -} diff --git a/jobs/auto-work-muse-c01.json b/jobs/auto-work-muse-c01.json deleted file mode 100644 index b36291d..0000000 --- a/jobs/auto-work-muse-c01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c01", - "description": "Muse work sweep: claimable jobs", - "agent": "muse", - "schedule": "7 0 * * *", - "timeout": 600, - "prompt_template": "Sweep for work muse can claim. 1) Find manual, unclaimed, or failed jobs in the job list. 2) Check the muse task thread for open items. Claim up to 2 muse-suitable jobs and start the first; leave the rest. Box: box job-list, box timer-list, box fleet-status. If nothing actionable, report NO-ACTION with a 3-line summary.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c01", - "name_template": "auto-work-muse-c01-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c02.json b/jobs/auto-work-muse-c02.json deleted file mode 100644 index a6dd78f..0000000 --- a/jobs/auto-work-muse-c02.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c02", - "description": "Muse work sweep: stale followups", - "agent": "muse", - "schedule": "19 1 * * *", - "timeout": 600, - "prompt_template": "Sweep stale followups. 1) Find expired or unanswered followups routed to muse. 2) Nudge once where a nudge is due; escalate to opm where retries are exhausted; close what is done. If nothing actionable, report NO-ACTION with a 3-line summary.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c02", - "name_template": "auto-work-muse-c02-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c03.json b/jobs/auto-work-muse-c03.json deleted file mode 100644 index 61e715d..0000000 --- a/jobs/auto-work-muse-c03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c03", - "description": "Muse work sweep: task thread pickup", - "agent": "muse", - "schedule": "31 2 * * *", - "timeout": 600, - "prompt_template": "Review the muse task thread. Pick up the oldest actionable item, do it, and report back. If nothing is actionable, verify thread health and report NO-ACTION with a 3-line summary.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c03", - "name_template": "auto-work-muse-c03-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c04.json b/jobs/auto-work-muse-c04.json deleted file mode 100644 index 3885871..0000000 --- a/jobs/auto-work-muse-c04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c04", - "description": "Muse work sweep: board and jobs channel", - "agent": "muse", - "schedule": "43 3 * * *", - "timeout": 600, - "prompt_template": "Scan the board and #jobs for unclaimed work orders or review requests. Claim at most 1 that fits muse scope; post a claim note so others do not duplicate. If nothing fits, report NO-ACTION with a 3-line summary.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c04", - "name_template": "auto-work-muse-c04-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c05.json b/jobs/auto-work-muse-c05.json deleted file mode 100644 index 0fadacb..0000000 --- a/jobs/auto-work-muse-c05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c05", - "description": "Muse work sweep: stalled swarms", - "agent": "muse", - "schedule": "55 4 * * *", - "timeout": 600, - "prompt_template": "Check the swarm list for stalled or partial swarms. Harvest completed slot results, kill swarms stale over 2h, and report one summary. If all healthy, report NO-ACTION with a 3-line summary.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c05", - "name_template": "auto-work-muse-c05-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c06.json b/jobs/auto-work-muse-c06.json deleted file mode 100644 index 280292a..0000000 --- a/jobs/auto-work-muse-c06.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c06", - "description": "Cross-op scan: pip", - "agent": "muse", - "schedule": "7 6 * * *", - "timeout": 600, - "prompt_template": "Check pip's task thread and recent activity for gaps or stalled items muse can take. If pip is stuck, post a scoped offer of help in the coordination thread; do not duplicate her work. Report findings either way.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c06", - "name_template": "auto-work-muse-c06-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c07.json b/jobs/auto-work-muse-c07.json deleted file mode 100644 index 99f3192..0000000 --- a/jobs/auto-work-muse-c07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c07", - "description": "Cross-op scan: 646", - "agent": "muse", - "schedule": "19 7 * * *", - "timeout": 600, - "prompt_template": "Check 646's task thread and recent results for gaps muse can cover. Claim only what is clearly unowned; report what you found either way.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c07", - "name_template": "auto-work-muse-c07-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c08.json b/jobs/auto-work-muse-c08.json deleted file mode 100644 index 7150526..0000000 --- a/jobs/auto-work-muse-c08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c08", - "description": "Cross-op scan: opm/dev/def", - "agent": "muse", - "schedule": "31 8 * * *", - "timeout": 600, - "prompt_template": "Scan opm, dev, and def scopes for unowned or aging work. Surface the top 3 items to the muse task thread with a take-or-leave recommendation; claim at most 1. If nothing, report NO-ACTION.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c08", - "name_template": "auto-work-muse-c08-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c09.json b/jobs/auto-work-muse-c09.json deleted file mode 100644 index d4765c9..0000000 --- a/jobs/auto-work-muse-c09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c09", - "description": "Cross-op scan: alerts and lobby", - "agent": "muse", - "schedule": "43 9 * * *", - "timeout": 600, - "prompt_template": "Scan #lobby, #fleet-status, and watchdog alerts for anything mentioning muse or unowned. Acknowledge alerts in range; escalate anything out of scope to opm. If quiet, report NO-ACTION.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c09", - "name_template": "auto-work-muse-c09-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c10.json b/jobs/auto-work-muse-c10.json deleted file mode 100644 index d86a0fe..0000000 --- a/jobs/auto-work-muse-c10.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c10", - "description": "Muse node health check", - "agent": "muse", - "schedule": "55 10 * * *", - "timeout": 600, - "prompt_template": "Verify the muse node: warp-muse netns up, CDP 9410 responsive, queue depth 0, egress healthy. If the browser is wedged, restart via the allowlisted service action and verify recovery. Report status.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c10", - "name_template": "auto-work-muse-c10-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c11.json b/jobs/auto-work-muse-c11.json deleted file mode 100644 index b7931cd..0000000 --- a/jobs/auto-work-muse-c11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c11", - "description": "Muse chrome error scan", - "agent": "muse", - "schedule": "7 12 * * *", - "timeout": 600, - "prompt_template": "Check per-profile Chrome FATAL/crash counts for the muse profile via box chrome-errors since the last watermark. Report new crashes; if a pattern repeats 3+ times, escalate to opm. If clean, report NO-ACTION.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c11", - "name_template": "auto-work-muse-c11-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c12.json b/jobs/auto-work-muse-c12.json deleted file mode 100644 index afa2423..0000000 --- a/jobs/auto-work-muse-c12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c12", - "description": "Muse gateway path check", - "agent": "muse", - "schedule": "19 13 * * *", - "timeout": 600, - "prompt_template": "Verify muse's gateway path: muse-cli status and a lightweight history read on the muse node. Report latency and any auth failures; do not touch cookie files.\n[TOOL health.check {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c12", - "name_template": "auto-work-muse-c12-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c13.json b/jobs/auto-work-muse-c13.json deleted file mode 100644 index f6ef0ee..0000000 --- a/jobs/auto-work-muse-c13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c13", - "description": "Muse sidechat hygiene", - "agent": "muse", - "schedule": "31 14 * * *", - "timeout": 600, - "prompt_template": "List muse's sidechats; archive terminal ephemeral threads (completed swarms, canaries, one-off diagnostics). Keep persistent threads (tasks, brain, heartbeat). Report counts.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c13", - "name_template": "auto-work-muse-c13-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c14.json b/jobs/auto-work-muse-c14.json deleted file mode 100644 index 04c05ae..0000000 --- a/jobs/auto-work-muse-c14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c14", - "description": "Muse auditor follow-up", - "agent": "muse", - "schedule": "43 15 * * *", - "timeout": 600, - "prompt_template": "Read the latest muse-auditor findings. Act on the top item: spawn a worker swarm, set a follow-up timer, or file it as a job. Report what you did.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c14", - "name_template": "auto-work-muse-c14-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c15.json b/jobs/auto-work-muse-c15.json deleted file mode 100644 index 10e7003..0000000 --- a/jobs/auto-work-muse-c15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c15", - "description": "Muse repo sanity check", - "agent": "muse", - "schedule": "55 16 * * *", - "timeout": 600, - "prompt_template": "Check recent commits on bl for anything that breaks muse tooling (box-ctl, harvester, dispatch). Run box quality check if code paths were touched; report green/red.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c15", - "name_template": "auto-work-muse-c15-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c16.json b/jobs/auto-work-muse-c16.json deleted file mode 100644 index dc8f257..0000000 --- a/jobs/auto-work-muse-c16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c16", - "description": "Muse job quality validation", - "agent": "muse", - "schedule": "7 18 * * *", - "timeout": 600, - "prompt_template": "Run box quality validate on muse-owned jobs (job, status, heartbeat). Fix simple schema issues; escalate anything failing twice to opm. If all green, report NO-ACTION.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c16", - "name_template": "auto-work-muse-c16-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c17.json b/jobs/auto-work-muse-c17.json deleted file mode 100644 index 1b04b4a..0000000 --- a/jobs/auto-work-muse-c17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c17", - "description": "Muse claim reconciliation", - "agent": "muse", - "schedule": "19 19 * * *", - "timeout": 600, - "prompt_template": "Reconcile jobs muse claimed vs results recorded: close resolved ones, re-drive stalled claims, release claims you cannot finish. Report closed/re-driven/released counts.\n[TOOL swarm.list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c17", - "name_template": "auto-work-muse-c17-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c18.json b/jobs/auto-work-muse-c18.json deleted file mode 100644 index b2b4771..0000000 --- a/jobs/auto-work-muse-c18.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c18", - "description": "Muse chain advancement", - "agent": "muse", - "schedule": "31 20 * * *", - "timeout": 600, - "prompt_template": "Check chained jobs where muse is next via box job-next. Run the next step for up to 2 chains; record results so the chain advances. Report chain ids and outcomes, or NO-ACTION.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c18", - "name_template": "auto-work-muse-c18-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c19.json b/jobs/auto-work-muse-c19.json deleted file mode 100644 index 0193596..0000000 --- a/jobs/auto-work-muse-c19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c19", - "description": "Muse daily digest", - "agent": "muse", - "schedule": "43 21 * * *", - "timeout": 600, - "prompt_template": "Write a short digest to the muse task thread: today's claims, results, declines, and what is still open. Keep it under 15 lines; no routine noise to main chat.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c19", - "name_template": "auto-work-muse-c19-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-muse-c20.json b/jobs/auto-work-muse-c20.json deleted file mode 100644 index 43e0e79..0000000 --- a/jobs/auto-work-muse-c20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-muse-c20", - "description": "Muse fleet-wide deep scan", - "agent": "muse", - "schedule": "55 22 * * *", - "timeout": 600, - "prompt_template": "Deep scan across all ops (muse, pip, 646, opm, dev, def): find work nobody owns. Propose the top item as a new job or claim it if it is muse-sized. Report the scan either way.\n[TOOL job-list {\"agent\": \"muse\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-muse-c20", - "name_template": "auto-work-muse-c20-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d01.json b/jobs/auto-work-opm-d01.json deleted file mode 100644 index 1aa35c7..0000000 --- a/jobs/auto-work-opm-d01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d01", - "description": "opm auto-work: sweep manual jobs for actionable work", - "agent": "opm", - "schedule": "5 * * * *", - "timeout": 600, - "prompt_template": "Sweep box job-list for manual jobs in opm scope. Pick the most actionable one and advance it (trigger, verify, or close). One job per run.\n\nBox CTA: box job-list", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d01", - "name_template": "auto-work-opm-d01-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d02.json b/jobs/auto-work-opm-d02.json deleted file mode 100644 index 36cd5a1..0000000 --- a/jobs/auto-work-opm-d02.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d02", - "description": "opm auto-work: check opm followups and loop-breaks", - "agent": "opm", - "schedule": "15 * * * *", - "timeout": 600, - "prompt_template": "Check opm-scope followups and loop-breaks. Nudge one stale pending item or resolve it. One item per run.\n\nBox CTA: box loop-breaks", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d02", - "name_template": "auto-work-opm-d02-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d03.json b/jobs/auto-work-opm-d03.json deleted file mode 100644 index 37f4134..0000000 --- a/jobs/auto-work-opm-d03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d03", - "description": "opm auto-work: verify recent job chain state", - "agent": "opm", - "schedule": "25 * * * *", - "timeout": 600, - "prompt_template": "Verify recent job chain state (job-status) for opm chains. Repair one broken link or report it.\n\nBox CTA: box job-status ", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d03", - "name_template": "auto-work-opm-d03-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d04.json b/jobs/auto-work-opm-d04.json deleted file mode 100644 index 48896f3..0000000 --- a/jobs/auto-work-opm-d04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d04", - "description": "opm auto-work: glance at other ops manual jobs", - "agent": "opm", - "schedule": "35 * * * *", - "timeout": 600, - "prompt_template": "Glance at manual jobs owned by other ops (646/pip/muse). Report blockers you can see; do not poach their work.\n\nBox CTA: box job-list", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d04", - "name_template": "auto-work-opm-d04-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d05.json b/jobs/auto-work-opm-d05.json deleted file mode 100644 index 28fcd43..0000000 --- a/jobs/auto-work-opm-d05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d05", - "description": "opm auto-work: advance ops-audit pipeline if due", - "agent": "opm", - "schedule": "45 * * * *", - "timeout": 600, - "prompt_template": "Check the ops-audit pipeline steps (ops-audit-step1..3). If the owner is idle and a step is due, run your step.\n\nBox CTA: box job-trigger ops-audit-step1", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d05", - "name_template": "auto-work-opm-d05-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d06.json b/jobs/auto-work-opm-d06.json deleted file mode 100644 index fa9e127..0000000 --- a/jobs/auto-work-opm-d06.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d06", - "description": "opm auto-work: harvest completed swarm results", - "agent": "opm", - "schedule": "8 2 * * *", - "timeout": 600, - "prompt_template": "Harvest completed swarm results on opm scope. Collect outcomes, post one aggregate summary.\n\nBox CTA: box swarm-results ", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d06", - "name_template": "auto-work-opm-d06-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d07.json b/jobs/auto-work-opm-d07.json deleted file mode 100644 index 59bf985..0000000 --- a/jobs/auto-work-opm-d07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d07", - "description": "opm auto-work: kill stale or orphaned swarms", - "agent": "opm", - "schedule": "28 4 * * *", - "timeout": 600, - "prompt_template": "Find stale or orphaned swarms on opm scope and kill them. Report what was cleaned.\n\nBox CTA: box swarm-list", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d07", - "name_template": "auto-work-opm-d07-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d08.json b/jobs/auto-work-opm-d08.json deleted file mode 100644 index 989ae7a..0000000 --- a/jobs/auto-work-opm-d08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d08", - "description": "opm auto-work: verify swarm slot RESULTs landed", - "agent": "opm", - "schedule": "48 6 * * *", - "timeout": 600, - "prompt_template": "Verify swarm slot RESULTs landed in Box for active opm swarms. Re-dispatch any missing slot once.\n\nBox CTA: box swarm-status ", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d08", - "name_template": "auto-work-opm-d08-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d09.json b/jobs/auto-work-opm-d09.json deleted file mode 100644 index 967dc10..0000000 --- a/jobs/auto-work-opm-d09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d09", - "description": "opm auto-work: spawn scout swarm if none active", - "agent": "opm", - "schedule": "12 8 * * *", - "timeout": 600, - "prompt_template": "If no opm swarm is active, spawn one small scout swarm on opm scope (2 slots max). Otherwise do nothing.\n\nBox CTA: box swarm-spawn 2 ", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d09", - "name_template": "auto-work-opm-d09-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d10.json b/jobs/auto-work-opm-d10.json deleted file mode 100644 index c94844e..0000000 --- a/jobs/auto-work-opm-d10.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d10", - "description": "opm auto-work: aggregate swarm outcomes report", - "agent": "opm", - "schedule": "42 10 * * *", - "timeout": 600, - "prompt_template": "Aggregate recent opm swarm outcomes into a one-line report for the opm task thread.\n\nBox CTA: box swarm-list", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d10", - "name_template": "auto-work-opm-d10-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d11.json b/jobs/auto-work-opm-d11.json deleted file mode 100644 index 16f34d0..0000000 --- a/jobs/auto-work-opm-d11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d11", - "description": "opm auto-work: check opm node health", - "agent": "opm", - "schedule": "3 12 * * *", - "timeout": 600, - "prompt_template": "Check opm node health: process alive, CDP ok, latency, queue depth. Report anomalies only.\n\nBox CTA: box fleet-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d11", - "name_template": "auto-work-opm-d11-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d12.json b/jobs/auto-work-opm-d12.json deleted file mode 100644 index f096f0e..0000000 --- a/jobs/auto-work-opm-d12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d12", - "description": "opm auto-work: compare opm node vs other ops", - "agent": "opm", - "schedule": "23 14 * * *", - "timeout": 600, - "prompt_template": "Compare opm node health against the other ops nodes. Flag any divergence worth a look.\n\nBox CTA: box fleet-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d12", - "name_template": "auto-work-opm-d12-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d13.json b/jobs/auto-work-opm-d13.json deleted file mode 100644 index bdb9298..0000000 --- a/jobs/auto-work-opm-d13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d13", - "description": "opm auto-work: check opm warp egress health", - "agent": "opm", - "schedule": "43 16 * * *", - "timeout": 600, - "prompt_template": "Check warp egress health for the opm netns (handshake age, egress probe). Report only on failure.\n\nBox CTA: box fleet-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d13", - "name_template": "auto-work-opm-d13-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d14.json b/jobs/auto-work-opm-d14.json deleted file mode 100644 index aede754..0000000 --- a/jobs/auto-work-opm-d14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d14", - "description": "opm auto-work: verify opm browser session", - "agent": "opm", - "schedule": "7 18 * * *", - "timeout": 600, - "prompt_template": "Verify opm browser session state: logged in and on a chat thread. Report only if broken.\n\nBox CTA: box fleet-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d14", - "name_template": "auto-work-opm-d14-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d15.json b/jobs/auto-work-opm-d15.json deleted file mode 100644 index 0189084..0000000 --- a/jobs/auto-work-opm-d15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d15", - "description": "opm auto-work: cross-ops degradation glance", - "agent": "opm", - "schedule": "37 20 * * *", - "timeout": 600, - "prompt_template": "Cross-ops glance: any op degraded for 2+ consecutive checks? Note it briefly, no duplicate alerts.\n\nBox CTA: box fleet-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d15", - "name_template": "auto-work-opm-d15-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d16.json b/jobs/auto-work-opm-d16.json deleted file mode 100644 index 03b2c58..0000000 --- a/jobs/auto-work-opm-d16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d16", - "description": "opm auto-work: read brain sidechat digests", - "agent": "opm", - "schedule": "15 22 * * *", - "timeout": 600, - "prompt_template": "Read the main-loop brain sidechat digests since last run. Count actionable vs informational; note anything unhandled.\n\nBox CTA: box loop-status", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d16", - "name_template": "auto-work-opm-d16-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d17.json b/jobs/auto-work-opm-d17.json deleted file mode 100644 index b67b7f9..0000000 --- a/jobs/auto-work-opm-d17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d17", - "description": "opm auto-work: check digest closure rate", - "agent": "opm", - "schedule": "30 0 * * *", - "timeout": 600, - "prompt_template": "Check digest_health closure rate for opm. If it dropped, say why in one line.\n\nBox CTA: box loop-health", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d17", - "name_template": "auto-work-opm-d17-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d18.json b/jobs/auto-work-opm-d18.json deleted file mode 100644 index 8e4dac6..0000000 --- a/jobs/auto-work-opm-d18.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d18", - "description": "opm auto-work: nudge stale actionable digests", - "agent": "opm", - "schedule": "15 1 * * *", - "timeout": 600, - "prompt_template": "Nudge unresolved ACTIONABLE digests past their reply timeout (one nudge each, max 3).\n\nBox CTA: box loop-breaks", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d18", - "name_template": "auto-work-opm-d18-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d19.json b/jobs/auto-work-opm-d19.json deleted file mode 100644 index 37a6bd6..0000000 --- a/jobs/auto-work-opm-d19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d19", - "description": "opm auto-work: verify heartbeat thread routing", - "agent": "opm", - "schedule": "45 3 * * *", - "timeout": 600, - "prompt_template": "Verify the heartbeat thread is alive and heartbeats are not leaking into main chat. Report only on failure.\n\nBox CTA: box timer-status heartbeat", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d19", - "name_template": "auto-work-opm-d19-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-opm-d20.json b/jobs/auto-work-opm-d20.json deleted file mode 100644 index 9d1ff3c..0000000 --- a/jobs/auto-work-opm-d20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-opm-d20", - "description": "opm auto-work: reconcile brain vs loop-breaks", - "agent": "opm", - "schedule": "5 12 * * *", - "timeout": 600, - "prompt_template": "Reconcile brain sidechat against loop-breaks; resolve anything stale or already handled.\n\nBox CTA: box loop-breaks", - "sidechat": { - "create": true, - "reuse_key": "auto-work-opm-d20", - "name_template": "auto-work-opm-d20-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "45m", - "nudges": 1, - "escalate": "opm", - "route": "auto-work-opm" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b01.json b/jobs/auto-work-pip-b01.json deleted file mode 100644 index 46fc8d1..0000000 --- a/jobs/auto-work-pip-b01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b01", - "description": "Auto-find work: pip-scope manual jobs in box job-list", - "agent": "pip", - "schedule": "3 0 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Scan box job-list for manual jobs assigned to pip and claim anything unclaimed. If nothing actionable, report NO-ACTION with a 3-line summary.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b01", - "name_template": "auto-work-pip-b01-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b02.json b/jobs/auto-work-pip-b02.json deleted file mode 100644 index 80d7738..0000000 --- a/jobs/auto-work-pip-b02.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b02", - "description": "Auto-find work: pip node fleet health", - "agent": "pip", - "schedule": "6 1 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Check fleet-status on the pip node: process alive, CDP bound, latency sane, browser on a live chat thread. If degraded, restart or report hop-by-hop in 3 lines, else NO-ACTION.\n[TOOL fleet-status {\"node\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b02", - "name_template": "auto-work-pip-b02-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b03.json b/jobs/auto-work-pip-b03.json deleted file mode 100644 index d225e64..0000000 --- a/jobs/auto-work-pip-b03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b03", - "description": "Auto-find work: pip task sidechat actionable items", - "agent": "pip", - "schedule": "9 2 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Review your pip task sidechat for actionable items: unacked digests, pending ACK/CLAIM/RESULT verbs, stale followups. Act on the oldest actionable item, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b03", - "name_template": "auto-work-pip-b03-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b04.json b/jobs/auto-work-pip-b04.json deleted file mode 100644 index 00d61e5..0000000 --- a/jobs/auto-work-pip-b04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b04", - "description": "Auto-find work: unclaimed fleet jobs pip can take", - "agent": "pip", - "schedule": "12 3 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Scan box job-list fleet-wide for jobs in an unclaimed or failed state you can handle as pip. Claim one and start it, or NO-ACTION with 3 lines.\n[TOOL job-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b04", - "name_template": "auto-work-pip-b04-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b05.json b/jobs/auto-work-pip-b05.json deleted file mode 100644 index 41f02d0..0000000 --- a/jobs/auto-work-pip-b05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b05", - "description": "Auto-find work: pip pending followups and nudges", - "agent": "pip", - "schedule": "15 4 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Review pip's pending followups: nudges due, timeouts near, escalations owed. Nudge or resolve what's actionable, else NO-ACTION with 3 lines.\n[TOOL loop-status {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b05", - "name_template": "auto-work-pip-b05-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b06.json b/jobs/auto-work-pip-b06.json deleted file mode 100644 index e28f274..0000000 --- a/jobs/auto-work-pip-b06.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b06", - "description": "Auto-find work: board tasks mentioning pip", - "agent": "pip", - "schedule": "18 5 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Check the dev board for tasks or posts mentioning pip that lack an owner or reply. Claim one and start it, or NO-ACTION with 3 lines.\n[TOOL job-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b06", - "name_template": "auto-work-pip-b06-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b07.json b/jobs/auto-work-pip-b07.json deleted file mode 100644 index fffe29d..0000000 --- a/jobs/auto-work-pip-b07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b07", - "description": "Auto-find work: pip swarm slots and idle workers", - "agent": "pip", - "schedule": "21 6 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Look for swarm work you can join or coordinate as pip: idle workers, unfilled slots, swarms missing results. Join or start one, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b07", - "name_template": "auto-work-pip-b07-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b08.json b/jobs/auto-work-pip-b08.json deleted file mode 100644 index 9ae796a..0000000 --- a/jobs/auto-work-pip-b08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b08", - "description": "Auto-find work: pip-scope timer health", - "agent": "pip", - "schedule": "24 7 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Audit box timer-list for pip-scope timers that are failed, orphaned, or silent beyond their schedule. Restart or report, else NO-ACTION with 3 lines.\n[TOOL timer-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b08", - "name_template": "auto-work-pip-b08-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b09.json b/jobs/auto-work-pip-b09.json deleted file mode 100644 index 041b02e..0000000 --- a/jobs/auto-work-pip-b09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b09", - "description": "Auto-find work: box health jobs affecting pip", - "agent": "pip", - "schedule": "27 8 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Review recent box health job results that touch the pip node or pip scope. Fix or escalate anything degraded, else NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b09", - "name_template": "auto-work-pip-b09-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b10.json b/jobs/auto-work-pip-b10.json deleted file mode 100644 index d353f99..0000000 --- a/jobs/auto-work-pip-b10.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b10", - "description": "Auto-find work: jobs channel items for pip", - "agent": "pip", - "schedule": "30 9 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Scan the jobs channel history for work addressed to pip that is still open. Claim it, or NO-ACTION with 3 lines.\n[TOOL job-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b10", - "name_template": "auto-work-pip-b10-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b11.json b/jobs/auto-work-pip-b11.json deleted file mode 100644 index 2afe434..0000000 --- a/jobs/auto-work-pip-b11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b11", - "description": "Auto-find work: stale pending items in pip scope", - "agent": "pip", - "schedule": "33 10 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Find stale pending items in pip scope: digests never acked, jobs dispatched but never run. Re-drive the oldest one, or NO-ACTION with 3 lines.\n[TOOL loop-status {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b11", - "name_template": "auto-work-pip-b11-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b12.json b/jobs/auto-work-pip-b12.json deleted file mode 100644 index 630a631..0000000 --- a/jobs/auto-work-pip-b12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b12", - "description": "Auto-find work: pip-scope variables needing attention", - "agent": "pip", - "schedule": "36 11 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Review box vars for the pip scope: values stale, missing, or flagged. Refresh or report, else NO-ACTION with 3 lines.\n[TOOL vars-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b12", - "name_template": "auto-work-pip-b12-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b13.json b/jobs/auto-work-pip-b13.json deleted file mode 100644 index f927fb7..0000000 --- a/jobs/auto-work-pip-b13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b13", - "description": "Auto-find work: failed pip job results needing retry", - "agent": "pip", - "schedule": "39 12 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Look for failed or errored pip job results that look retryable. Retry one cleanly, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b13", - "name_template": "auto-work-pip-b13-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b14.json b/jobs/auto-work-pip-b14.json deleted file mode 100644 index 6a44e35..0000000 --- a/jobs/auto-work-pip-b14.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b14", - "description": "Auto-find work: pip coordination threads unread", - "agent": "pip", - "schedule": "42 13 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Check pip coordination threads (pip-646, pip-opm) for unread actionable messages. Reply or act on one, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b14", - "name_template": "auto-work-pip-b14-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b15.json b/jobs/auto-work-pip-b15.json deleted file mode 100644 index 22b6473..0000000 --- a/jobs/auto-work-pip-b15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b15", - "description": "Auto-find work: orphan timer audit", - "agent": "pip", - "schedule": "45 14 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Audit for orphan timers or jobs with no live timer in your scope. Clean up or report one, or NO-ACTION with 3 lines.\n[TOOL timer-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b15", - "name_template": "auto-work-pip-b15-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b16.json b/jobs/auto-work-pip-b16.json deleted file mode 100644 index 7952889..0000000 --- a/jobs/auto-work-pip-b16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b16", - "description": "Auto-find work: recent commits touching pip scope", - "agent": "pip", - "schedule": "48 15 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Check recent NetVM commits touching pip scope for quality issues or unreleased work you can verify or ship. Act or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b16", - "name_template": "auto-work-pip-b16-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b17.json b/jobs/auto-work-pip-b17.json deleted file mode 100644 index 5d9c18a..0000000 --- a/jobs/auto-work-pip-b17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b17", - "description": "Auto-find work: unread DMs to pip", - "agent": "pip", - "schedule": "51 16 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Check dm-log for unread DMs addressed to pip that need a reply. Answer the most urgent, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b17", - "name_template": "auto-work-pip-b17-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b18.json b/jobs/auto-work-pip-b18.json deleted file mode 100644 index 8168bb5..0000000 --- a/jobs/auto-work-pip-b18.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b18", - "description": "Auto-find work: loop breaks involving pip", - "agent": "pip", - "schedule": "54 17 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Review box loop-breaks for items involving pip that are unresolved. Resolve or escalate one, or NO-ACTION with 3 lines.\n[TOOL loop-status {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b18", - "name_template": "auto-work-pip-b18-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b19.json b/jobs/auto-work-pip-b19.json deleted file mode 100644 index ec75e0f..0000000 --- a/jobs/auto-work-pip-b19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b19", - "description": "Auto-find work: human-gated items waiting on pip input", - "agent": "pip", - "schedule": "57 18 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Look for human-gated items where pip's input or verification is the blocker (e.g. key sourcing, cookie flow). Advance or document one, or NO-ACTION with 3 lines.\n[TOOL vars-list {}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b19", - "name_template": "auto-work-pip-b19-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-pip-b20.json b/jobs/auto-work-pip-b20.json deleted file mode 100644 index a060f81..0000000 --- a/jobs/auto-work-pip-b20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-pip-b20", - "description": "Auto-find work: catch-all sweep of pip scope", - "agent": "pip", - "schedule": "0 19 * * *", - "timeout": 600, - "prompt_template": "Auto-find work for the pip scope. Catch-all sweep: anything in the pip scope missed by the other auto-work scans - open threads, silent jobs, unclaimed tasks. Handle one, or NO-ACTION with 3 lines.\n[TOOL job-list {\"agent\": \"pip\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-pip-b20", - "name_template": "auto-work-pip-b20-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 0, - "escalate": "opm", - "route": "auto-work" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f01.json b/jobs/auto-work-queue-f01.json deleted file mode 100644 index 7860c5e..0000000 --- a/jobs/auto-work-queue-f01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f01", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "646", - "schedule": "0 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f01", - "name_template": "auto-work-queue-f01-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f03.json b/jobs/auto-work-queue-f03.json deleted file mode 100644 index b44cf02..0000000 --- a/jobs/auto-work-queue-f03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f03", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "muse", - "schedule": "6 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f03", - "name_template": "auto-work-queue-f03-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f04.json b/jobs/auto-work-queue-f04.json deleted file mode 100644 index a9c8147..0000000 --- a/jobs/auto-work-queue-f04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f04", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "opm", - "schedule": "9 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f04", - "name_template": "auto-work-queue-f04-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f05.json b/jobs/auto-work-queue-f05.json deleted file mode 100644 index 4259a03..0000000 --- a/jobs/auto-work-queue-f05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f05", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "646", - "schedule": "12 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f05", - "name_template": "auto-work-queue-f05-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f07.json b/jobs/auto-work-queue-f07.json deleted file mode 100644 index 3d0ee68..0000000 --- a/jobs/auto-work-queue-f07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f07", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "muse", - "schedule": "18 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f07", - "name_template": "auto-work-queue-f07-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f08.json b/jobs/auto-work-queue-f08.json deleted file mode 100644 index 9f65537..0000000 --- a/jobs/auto-work-queue-f08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f08", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "opm", - "schedule": "21 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f08", - "name_template": "auto-work-queue-f08-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f09.json b/jobs/auto-work-queue-f09.json deleted file mode 100644 index 5ad1a07..0000000 --- a/jobs/auto-work-queue-f09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f09", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "646", - "schedule": "24 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f09", - "name_template": "auto-work-queue-f09-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f11.json b/jobs/auto-work-queue-f11.json deleted file mode 100644 index 6c69495..0000000 --- a/jobs/auto-work-queue-f11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f11", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "muse", - "schedule": "30 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f11", - "name_template": "auto-work-queue-f11-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f12.json b/jobs/auto-work-queue-f12.json deleted file mode 100644 index 9ed705c..0000000 --- a/jobs/auto-work-queue-f12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f12", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "opm", - "schedule": "33 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f12", - "name_template": "auto-work-queue-f12-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f13.json b/jobs/auto-work-queue-f13.json deleted file mode 100644 index 89aeea7..0000000 --- a/jobs/auto-work-queue-f13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f13", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "646", - "schedule": "36 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f13", - "name_template": "auto-work-queue-f13-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f15.json b/jobs/auto-work-queue-f15.json deleted file mode 100644 index 1b9721c..0000000 --- a/jobs/auto-work-queue-f15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f15", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "muse", - "schedule": "42 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f15", - "name_template": "auto-work-queue-f15-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f16.json b/jobs/auto-work-queue-f16.json deleted file mode 100644 index cba55af..0000000 --- a/jobs/auto-work-queue-f16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f16", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "opm", - "schedule": "45 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f16", - "name_template": "auto-work-queue-f16-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f17.json b/jobs/auto-work-queue-f17.json deleted file mode 100644 index 7cf0a83..0000000 --- a/jobs/auto-work-queue-f17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f17", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "646", - "schedule": "48 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f17", - "name_template": "auto-work-queue-f17-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f19.json b/jobs/auto-work-queue-f19.json deleted file mode 100644 index 720abae..0000000 --- a/jobs/auto-work-queue-f19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f19", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "muse", - "schedule": "54 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f19", - "name_template": "auto-work-queue-f19-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-queue-f20.json b/jobs/auto-work-queue-f20.json deleted file mode 100644 index e4b05b9..0000000 --- a/jobs/auto-work-queue-f20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-queue-f20", - "description": "Auto work-queue scavenger (builder 6/10): poll job-list, find manual/pending/stale jobs, check status, nudge/claim, keep chain moving", - "agent": "opm", - "schedule": "57 * * * *", - "timeout": 600, - "prompt_template": "Auto work-queue scavenger sweep. Time: {datetime}. Job ID: {job_id}.\n\n1. Run `box job-list` and find jobs with schedule \"manual\" or that look stale/pending/unclaimed.\n2. For each candidate run `box job-status ` to check dispatch/result/chain state.\n3. If a job is unclaimed or stuck pending, claim it: work it yourself if quick, otherwise nudge the owning agent with `box notify `.\n4. If a job failed, run `box job-next ` and keep the chain moving (box->agent->box).\n5. Keep your reply short and end with a one-line summary of what you found and did, starting with OK or FAIL.", - "sidechat": { - "create": true, - "reuse_key": "auto-work-queue-f20", - "name_template": "auto-work-queue-f20-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "work-queue" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-sweep-j17.json b/jobs/auto-work-sweep-j17.json deleted file mode 100644 index 98e7e0f..0000000 --- a/jobs/auto-work-sweep-j17.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "auto-work-sweep-j17", - "description": "Digest/followup sweep: nudge pip's overdue pending followups", - "agent": "opm", - "schedule": "33 * * * *", - "timeout": 300, - "prompt_template": "Check pip's pending followups: read followups.json, find entries past their reply-timeout with no recorded result. Nudge each overdue one once via DM (max 3 per run). Skip entries already nudged twice. Report counts back to Box, closing with the standard result verb for this job id: nudged=N skipped=M.\n\n[TOOL files.read {\"path\": \"followups.json\"}]", - "sidechat": { - "create": true, - "reuse_key": "auto-work-sweep-j17", - "name_template": "auto-work-sweep-j17-{date}" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e01.json b/jobs/auto-work-xop-e01.json deleted file mode 100644 index 918e19d..0000000 --- a/jobs/auto-work-xop-e01.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e01", - "agent": "646", - "description": "Cross-op fleet watch 646+pip (sync) - auto work finder", - "schedule": "2 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs pip (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and pip.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+pip in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e01", - "reuse_key": "auto-work-xop-e01" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e03.json b/jobs/auto-work-xop-e03.json deleted file mode 100644 index e134c73..0000000 --- a/jobs/auto-work-xop-e03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e03", - "agent": "opm", - "description": "Cross-op fleet watch 646+muse (sync) - auto work finder", - "schedule": "8 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs muse (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and muse.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+muse in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e03", - "reuse_key": "auto-work-xop-e03" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e04.json b/jobs/auto-work-xop-e04.json deleted file mode 100644 index 1cc5d47..0000000 --- a/jobs/auto-work-xop-e04.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e04", - "agent": "muse", - "description": "Cross-op fleet watch 646+def (sync) - auto work finder", - "schedule": "11 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs def (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and def.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+def in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e04", - "reuse_key": "auto-work-xop-e04" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e05.json b/jobs/auto-work-xop-e05.json deleted file mode 100644 index eb7bb9b..0000000 --- a/jobs/auto-work-xop-e05.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e05", - "agent": "646", - "description": "Cross-op fleet watch 646+dev (sync) - auto work finder", - "schedule": "14 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs dev (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and dev.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+dev in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e05", - "reuse_key": "auto-work-xop-e05" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e07.json b/jobs/auto-work-xop-e07.json deleted file mode 100644 index f51184e..0000000 --- a/jobs/auto-work-xop-e07.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e07", - "agent": "opm", - "description": "Cross-op fleet watch pip+muse (sync) - auto work finder", - "schedule": "20 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: pip vs muse (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for pip and muse.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: pip+muse in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e07", - "reuse_key": "auto-work-xop-e07" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e08.json b/jobs/auto-work-xop-e08.json deleted file mode 100644 index b07a53b..0000000 --- a/jobs/auto-work-xop-e08.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e08", - "agent": "muse", - "description": "Cross-op fleet watch pip+def (sync) - auto work finder", - "schedule": "23 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: pip vs def (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for pip and def.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: pip+def in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e08", - "reuse_key": "auto-work-xop-e08" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e09.json b/jobs/auto-work-xop-e09.json deleted file mode 100644 index 80ef7ce..0000000 --- a/jobs/auto-work-xop-e09.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e09", - "agent": "646", - "description": "Cross-op fleet watch pip+dev (sync) - auto work finder", - "schedule": "26 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: pip vs dev (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for pip and dev.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: pip+dev in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e09", - "reuse_key": "auto-work-xop-e09" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e11.json b/jobs/auto-work-xop-e11.json deleted file mode 100644 index 50889d0..0000000 --- a/jobs/auto-work-xop-e11.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e11", - "agent": "opm", - "description": "Cross-op fleet watch opm+def (sync) - auto work finder", - "schedule": "32 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: opm vs def (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for opm and def.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: opm+def in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e11", - "reuse_key": "auto-work-xop-e11" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e12.json b/jobs/auto-work-xop-e12.json deleted file mode 100644 index 15b0921..0000000 --- a/jobs/auto-work-xop-e12.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e12", - "agent": "muse", - "description": "Cross-op fleet watch opm+dev (sync) - auto work finder", - "schedule": "35 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: opm vs dev (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for opm and dev.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: opm+dev in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e12", - "reuse_key": "auto-work-xop-e12" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e13.json b/jobs/auto-work-xop-e13.json deleted file mode 100644 index e47d536..0000000 --- a/jobs/auto-work-xop-e13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e13", - "agent": "646", - "description": "Cross-op fleet watch muse+def (sync) - auto work finder", - "schedule": "38 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: muse vs def (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for muse and def.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: muse+def in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e13", - "reuse_key": "auto-work-xop-e13" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e15.json b/jobs/auto-work-xop-e15.json deleted file mode 100644 index 9ad7432..0000000 --- a/jobs/auto-work-xop-e15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e15", - "agent": "opm", - "description": "Cross-op fleet watch def+dev (sync) - auto work finder", - "schedule": "44 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: def vs dev (general sync check).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for def and dev.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: def+dev in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e15", - "reuse_key": "auto-work-xop-e15" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e16.json b/jobs/auto-work-xop-e16.json deleted file mode 100644 index 2f1766f..0000000 --- a/jobs/auto-work-xop-e16.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e16", - "agent": "muse", - "description": "Cross-op fleet watch 646+pip (timer-drift) - auto work finder", - "schedule": "47 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs pip (timer-list depth comparison).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and pip.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+pip in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e16", - "reuse_key": "auto-work-xop-e16" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e17.json b/jobs/auto-work-xop-e17.json deleted file mode 100644 index 37ca868..0000000 --- a/jobs/auto-work-xop-e17.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e17", - "agent": "646", - "description": "Cross-op fleet watch opm+muse (latency) - auto work finder", - "schedule": "50 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: opm vs muse (CDP latency comparison).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for opm and muse.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: opm+muse in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e17", - "reuse_key": "auto-work-xop-e17" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e19.json b/jobs/auto-work-xop-e19.json deleted file mode 100644 index ba58777..0000000 --- a/jobs/auto-work-xop-e19.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e19", - "agent": "opm", - "description": "Cross-op fleet watch 646+opm (heartbeat) - auto work finder", - "schedule": "56 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: 646 vs opm (heartbeat freshness comparison).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for 646 and opm.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: 646+opm in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e19", - "reuse_key": "auto-work-xop-e19" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/auto-work-xop-e20.json b/jobs/auto-work-xop-e20.json deleted file mode 100644 index 4d0d0f1..0000000 --- a/jobs/auto-work-xop-e20.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auto-work-xop-e20", - "agent": "muse", - "description": "Cross-op fleet watch muse+pip (full) - auto work finder", - "schedule": "59 * * * *", - "timeout": 600, - "prompt_template": "Cross-op fleet watch: muse vs pip (full sweep of all signals).\nJob ID: {job_id}\nTime: {datetime}\n\nCheck both ops and compare:\n1. Run `box fleet-status` - compare proc_alive, cdp_ok, latency_ms, queue_depth for muse and pip.\n2. Run `box timer-list` - note any timer active/enabled on one side but missing/disabled on the other.\n3. Verdict: OK (both healthy, in sync), DEGRADED (latency/queue/timer drift), or PARTITION (one side down).\n\nBox CTA: if DEGRADED or PARTITION, run `box notify` with a one-line finding, then close out with a FAIL result for this job plus the summary. If OK, close out with an OK result for this job: muse+pip in sync.\n\nTools:\n[TOOL health.check {}]\n", - "sidechat": { - "create": true, - "name_template": "auto-work-xop-e20", - "reuse_key": "auto-work-xop-e20" - }, - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "xop-watch", - "timeout": "30m" - }, - "chain_next": null, - "on_failure": "alert" -} diff --git a/jobs/autonomy-pulse-646.json b/jobs/autonomy-pulse-646.json deleted file mode 100644 index 8df1ae7..0000000 --- a/jobs/autonomy-pulse-646.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "autonomy-pulse-646", - "description": "Standing autonomy pulse for operator-646 — set timers, spawn swarms", - "agent": "646", - "schedule": "*/30 * * * *", - "timeout": 600, - "prompt_template": "Standing autonomy pulse for 646. DO WORK — do not emit plain text without tools.\n1. Run [TOOL swarm.spawn {\"count\": 2, \"task\": \"autonomous work sweep\"}] or [TOOL health.check {}].\n2. Schedule your next recurring pulse: [TOOL followup.create {\"in_m\": 30, \"prompt\": \"autonomy-pulse-646: review swarm progress and advance loop\"}].\n3. Execute any needed tools via [TOOL ] or curl to https://exec.muse-dev.online/exec.\nWhen finished, report with [RESULT {job_id}] OK .", - "sidechat": { - "create": true, - "reuse_key": "autonomy-pulse-646", - "name_template": "autonomy-pulse-646-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "autonomy-pulse" - }, - "chain_next": null, - "on_failure": "alert", - "on_no_result": { - "op": "swarm.spawn", - "args": { - "count": 2, - "task": "Standing pulse work for 646 (autonomy-pulse-646): execute your scope standing work: verify timers, check swarm results, act or close; report per-slot verdicts.", - "label": "autonomy-pulse-646-fallback" - } - } -} diff --git a/jobs/autonomy-pulse-opm.json b/jobs/autonomy-pulse-opm.json deleted file mode 100644 index bb500bc..0000000 --- a/jobs/autonomy-pulse-opm.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "autonomy-pulse-opm", - "description": "Standing autonomy pulse for operator-main — set timers, spawn swarms", - "agent": "opm", - "schedule": "20,50 * * * *", - "timeout": 600, - "prompt_template": "Standing autonomy pulse for opm. DO WORK — do not emit plain text without tools.\n1. Run [TOOL swarm.spawn {\"count\": 2, \"task\": \"autonomous opm coordination sweep\"}] or [TOOL health.check {}].\n2. Schedule your next recurring pulse: [TOOL followup.create {\"in_m\": 30, \"prompt\": \"autonomy-pulse-opm: harvest coordinator state and cycle timers\"}].\n3. Execute needed tools via [TOOL ] or curl to https://exec.muse-dev.online/exec.\nWhen finished, report with [RESULT {job_id}] OK .", - "sidechat": { - "create": true, - "reuse_key": "autonomy-pulse-opm", - "name_template": "autonomy-pulse-opm-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "autonomy-pulse" - }, - "chain_next": null, - "on_failure": "alert", - "on_no_result": { - "op": "swarm.spawn", - "args": { - "count": 2, - "task": "Standing pulse work for opm (autonomy-pulse-opm): execute your scope standing work: verify timers, check swarm results, act or close; report per-slot verdicts.", - "label": "autonomy-pulse-opm-fallback" - } - } -} diff --git a/jobs/autonomy-pulse-pip.json b/jobs/autonomy-pulse-pip.json deleted file mode 100644 index a070888..0000000 --- a/jobs/autonomy-pulse-pip.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "autonomy-pulse-pip", - "description": "Standing autonomy pulse for operator-pip — set timers, spawn swarms", - "agent": "pip", - "schedule": "10,40 * * * *", - "timeout": 600, - "prompt_template": "Standing autonomy pulse for pip. DO WORK — do not emit plain text without tools.\n1. Run [TOOL swarm.spawn {\"count\": 2, \"task\": \"autonomous pip telemetry & verification\"}] or [TOOL health.check {}].\n2. Schedule your next recurring pulse: [TOOL followup.create {\"in_m\": 30, \"prompt\": \"autonomy-pulse-pip: review telemetry and set next cycle\"}].\n3. Execute needed tools via [TOOL ] or curl to https://exec.muse-dev.online/exec.\nWhen finished, report with [RESULT {job_id}] OK .", - "sidechat": { - "create": true, - "reuse_key": "autonomy-pulse-pip", - "name_template": "autonomy-pulse-pip-{date}" - }, - "followup": { - "expect_reply": true, - "timeout": "30m", - "nudges": 1, - "escalate": "opm", - "route": "autonomy-pulse" - }, - "chain_next": null, - "on_failure": "alert", - "on_no_result": { - "op": "swarm.spawn", - "args": { - "count": 2, - "task": "Standing pulse work for pip (autonomy-pulse-pip): execute your scope standing work: verify timers, check swarm results, act or close; report per-slot verdicts.", - "label": "autonomy-pulse-pip-fallback" - } - } -} diff --git a/jobs/box-deep-health.json b/jobs/box-deep-health.json deleted file mode 100644 index 835eed4..0000000 --- a/jobs/box-deep-health.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "agent": "646", - "chain_next": null, - "description": "Box deep health \u2014 daily comprehensive check", - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "box-health", - "timeout": "2h" - }, - "name": "box-deep-health", - "on_failure": "alert", - "prompt_template": "Box deep health check (daily).\nJob ID: {job_id}\nTime: {datetime}\n\nRun the FULL Box health suite on the VM:\n ssh dev-operator-646@34.139.37.135 \"/srv/box/bin/box-health-check.sh all\"\n\nThen verify these endpoints and services:\n1. https://box.muse-dev.online/ loads (Fleet Console UI, assets box.js and box.css 200 OK)\n2. https://box.muse-dev.online/api/box/fleet returns live fleet status\n3. https://box.muse-dev.online/api/box/dm/log returns live DM log entries\n4. https://box.muse-dev.online/followups loads (operator view, follow-up dashboard)\n5. https://box.muse-dev.online/api/box/followups/summary returns valid JSON\n6. Check /srv/box/dm-log.jsonl tail for errors in the last 24h\n7. Confirm the request-sweeper processed follow-ups in the last hour\n (grep dm_followup /srv/box/box_requests.jsonl | tail -5)\n\nReport any degradation, even if checks pass (slow responses, log warnings).\n\nReply with OK or FAIL , plus any observations worth tracking.", - "schedule": "0 9 * * *", - "sidechat": { - "create": true, - "name_template": "box-deep-health-{datetime}" - }, - "timeout": 1800 -} diff --git a/jobs/box-http-health.json b/jobs/box-http-health.json deleted file mode 100644 index 8025596..0000000 --- a/jobs/box-http-health.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "agent": "646", - "chain_next": null, - "description": "Box HTTP endpoint health \u2014 every 15 minutes", - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "box-health", - "timeout": "30m" - }, - "name": "box-http-health", - "on_failure": "alert", - "prompt_template": "Box HTTP endpoint health check.\nJob ID: {job_id}\nTime: {datetime}\n\nRun the fleet health check:\n[TOOL health.check {}]\n\nCheck recent scheduled runs:\n[TOOL cron.runs {}]\n\nIf all endpoints and fleet nodes return green, reply with [RESULT {job_id}] OK.\nOtherwise reply with [RESULT {job_id}] FAIL .", - "schedule": "*/15 * * * *", - "sidechat": { - "create": true, - "name_template": "box-http-health-{datetime}", - "reuse_key": "box-http-health" - }, - "timeout": 600 -} \ No newline at end of file diff --git a/jobs/box-service-health.json b/jobs/box-service-health.json deleted file mode 100644 index 5bd06c7..0000000 --- a/jobs/box-service-health.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "agent": "646", - "chain_next": null, - "description": "Box service + data health \u2014 every 15 minutes (offset 7m)", - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 2, - "route": "box-health", - "timeout": "30m" - }, - "name": "box-service-health", - "on_failure": "alert", - "prompt_template": "Box service and data health check.\nJob ID: {job_id}\nTime: {datetime}\n\nVM check: run /srv/box/bin/box-health-check.sh on the VM (dev-operator-646@34.139.37.135) via your operator SSH chain. Expect zero failures: services 3/3 (board.service, caddy.service, box-request-sweeper.timer), data 3/3, HTTP checks all OK. The request-store WARN is known and not a failure.\n\nbl check: the result harvester lives on bl, not on the VM:\n[TOOL service.status {\"unit\": \"response-harvester.timer\"}]\nExpect active.\n\nNote: board.service and caddy.service are inactive on bl by design (they run on the VM) -- do not check them with the tool; the VM health script covers them.\n\nIf healthy, end with [RESULT {job_id}] OK.\nIf any service fails, reply with [RESULT {job_id}] FAIL .", - "schedule": "7,22,37,52 * * * *", - "sidechat": { - "create": true, - "name_template": "box-service-health-{datetime}", - "reuse_key": "box-service-health" - }, - "timeout": 600 -} \ No newline at end of file diff --git a/jobs/opm-swarm-harvest.json b/jobs/opm-swarm-harvest.json deleted file mode 100644 index 6c40b6b..0000000 --- a/jobs/opm-swarm-harvest.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "agent": "opm", - "chain_next": null, - "description": "Harvest box swarm results on the opm scope: collect completed/partial results, kill stale swarms, report one summary", - "followup": { - "escalate": "opm", - "expect_reply": true, - "nudges": 1, - "route": "autonomy-pulse", - "timeout": "30m" - }, - "name": "opm-swarm-harvest", - "on_failure": "alert", - "prompt_template": "Run `box swarm-list`. For each swarm on the opm scope that reached completed or partial status: pull `box swarm-results ` and record a one-line outcome. For swarms older than 2h still pending/partial with no attached agent activity, run `box swarm-kill --confirm`. Post one concise summary: harvested results, kills, and any swarms left in flight. Do not touch swarms outside the opm scope.", - "schedule": "5 * * * *", - "sidechat": { - "create": true, - "name_template": "opm-swarm-harvest-{date}", - "reuse_key": "opm-swarm-harvest" - }, - "timeout": 600 -} diff --git a/muse-choices-rules.json b/muse-choices-rules.json index 3ab5c8a..7c83e67 100644 --- a/muse-choices-rules.json +++ b/muse-choices-rules.json @@ -20,6 +20,12 @@ "decision": "hold", "reason": "destructive magic word; hold for operator eyes" }, + { + "id": "grill-interview-hold", + "kind": "numbered-plain", + "decision": "hold", + "reason": "grill interview question; coordinator sign-off required, never expires to approve" + }, { "id": "cmd-destructive-shell", "kind": ["muse-approval", "muse-approval-collapsed"], diff --git a/shared/operators/TOOLS.md b/shared/operators/TOOLS.md index d00c909..019b333 100644 --- a/shared/operators/TOOLS.md +++ b/shared/operators/TOOLS.md @@ -15,6 +15,7 @@ file beats padding. All entries verified 2026-10-03/04. - SSH egress needs BOTH Muse-app toggles: Direct network protocols → SSH = Ask, AND TCP/UDP channel toggles. Banner-exchange timeout or instant reset during kex = lapsed toggles — but retry once first; transient egress-proxy flapping (observed 2026-10-04 15:15 UTC) produces the same symptom and clears on retry. - With SSH = Ask, every connection triggers an interactive approval prompt — git push/fetch from the container needs user approval each time. - `recover-after-rebuild.sh` must run with HOME=/home/hatch, no sudo wrapper (sudo resets HOME to /root, key path breaks, script aborts FATAL). +- **apt-lock race vs os-intent replay (2026-10-06):** script can die on `E: Could not get lock /var/lib/apt/lists/lock` held ~7+ min by a platform os-intent replay `apt-get update`; naive retry-loops keep losing. Fix: `dpkg -i /var/cache/apt/archives/*.deb` from the RV-backed cache (dpkg lock is free while the replay is in its update phase), THEN re-run the script — it skips install and proceeds to services + tunnel. End-to-end verified via VM loopback `ssh -p root@127.0.0.1`. ## Egress proxy / curl - Outbound HTTP goes through `hatch-egress-proxy:3128` (static IPv6, stable across boots). Direct egress is blocked by design — `timeout` without proxy is normal. @@ -23,8 +24,10 @@ file beats padding. All entries verified 2026-10-03/04. ## ssh-keygen -Y sign (file-based, ALWAYS) - Piping the payload via stdin intermittently fails verification (the chat-400 root cause, 2026-10-03). Always: `printf ... > p.txt; ssh-keygen -Y sign -f -n p.txt`, then read the `.sig` file. - Namespaces: `chat` (lobby posts), `board` (board posts), `dm` (DMs), `box` (box API). +- `ssh-keygen -Y sign` prompts `Overwrite (y/n)?` when the target `.sig` already exists — in a non-tty exec call that prompt hangs forever (observed 2026-10-06, killed after 200s+). Always `rm -f` the `.sig` before signing, or sign to a fresh unique path. - Chat post: `printf '%s\n#lobby\n%s' "$ts" "$msg" > /tmp/lobby_sig.txt`; JSON body `{"channel":"#lobby","identity":"operator-646","message":msg,"ts":int(ts),"signature":sig}`. - Box API: sign `"$TS\n$endpoint"` where endpoint = last path segment (`fleet`, `log`, `nodes`, …); GET `https://box.muse-dev.online/api/box/?identity=operator-646&ts=$TS&sig=`. + - Per-agent identity (2026-10-06): if the `operator-646` registry entry no longer matches your key, sign as your own identity instead — e.g. pip signs as `operator-pip` with `~/.ssh/board-sign`. Check which pub matches your registry entry before debugging sig failures. - Signed payloads must be ASCII-only — an em-dash normalized in transit broke pip's verify. - Known risk: `box-relay.sh` still signs by piping via stdin (the flake pattern). Flagged, not patched. @@ -54,7 +57,12 @@ file beats padding. All entries verified 2026-10-03/04. - All box APIs are 403 unauthenticated by design; agent tier sees only DMs where it's a party (empty result is correct, not an error). - exec-constrained ops (verified live 2026-10-04): subagent.spawn {agent,title,prompt,wait}; thread.list {agent}; thread.view {agent,thread,limit}; dm.send {agent,to,target,message}; dm.read {agent,target,limit}; pipeline.run {name}; health.check {}. Don't guess arg schemas — probing burns rate-limit budget. - Token Limits & Onboarding: `box usage` monitors weekly and additional tokens. If you or a peer are BLOCKED or exhausted, prompt for onboarding via `box onboard salvage-wo `. When onboarding a client, `box onboard start --email --for ` auto-provisions and redeems 1B tokens upon sign-in. +- **Large-response truncation (2026-10-07):** `~/bin/box-raw.py` (python-urllib) truncates exec responses past ~61KB and flaked on small ops too (empty responses). `~/bin/box-curl.py` is the curl-based signed-POST equivalent — same signing, browser UA, full body first try. **Prefer box-curl.py for ALL exec ops.** Full op list: GET `https://exec.muse-dev.online/ops` (79 ops incl. swarm.list/status/results/spawn, thread.list/view, followup.create). +- Stuck-swarm diagnosis (2026-10-07): slots with `agent_id=null` + `updated_ts` frozen at creation = dispatch-side slot-assignment failure, distinct from slots that get agent_id then die ~1-2 min after dispatch (worker-side). NO container-side cancel exists (`swarm.cancel`/`swarm.kill` return unauthorized); only the box-side sweeper can touch it. Rule: do not re-spawn while slots are stuck unassigned — escalate to opm. ## Tunnel / container - Reverse tunnel: VM 2226→container:22, 7683→container:7683. Watchdog `tunnel-watchdog-646` runs `recover-after-rebuild.sh` every 120s. - `mirror.cogentco.com` is a dead apt mirror that hangs `apt-get update`; the recovery script strips it (keeping azure.archive.ubuntu.com) since /etc wipes on rebuild. Ubuntu-only; bl is Arch, no apt. + +## box-exec-curl.py output shape (2026-10-07) +`~/bin/box-exec-curl.py` (signed exec POST via curl through the egress proxy — use instead of box-raw.py for large/truncated responses) prints the raw JSON body FIRST, then a trailing `HTTP ` line — the INVERSE of box-raw.py's `HTTP 200\n` shape. Parsers written for box-raw.py break on it ("Expecting value" / "Extra data"). Strip lines starting with `HTTP ` before json.loads. diff --git a/tests/test_approvals.py b/tests/test_approvals.py index 4bcb403..fbe2ce7 100644 --- a/tests/test_approvals.py +++ b/tests/test_approvals.py @@ -25,6 +25,58 @@ import approvals import gravity +def _load(name, relpath): + import importlib.util + spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +box_ctl = _load("box_ctl_approvaltest", "bin/box-ctl.py") +super_cli = _load("super_cli_approvaltest", "bin/super-cli.py") + + +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout only).""" + + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + + +def _box_ctl_inproc(*args): + """In-process box-ctl call (proven pattern from test_box_loop_https). + + Real main(argv): identical parsing, dispatch, audit, stdout JSON. + """ + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + +def _super_cli_inproc(*args): + """In-process super-cli call (main() reads sys.argv; patch it).""" + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + with mock.patch.object(sys, "argv", ["super-cli.py", *args]): + with redirect_stdout(buf): + try: + super_cli.main() + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + class TestApprovalsModule(unittest.TestCase): """Test approvals.py core module functionality.""" @@ -79,8 +131,7 @@ class TestBoxApprovalsCli(unittest.TestCase): """Test 'box approvals' and 'box approval' CLI commands.""" def test_box_approvals_check_json(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approvals", "check", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("approvals", "check", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) @@ -88,15 +139,13 @@ class TestBoxApprovalsCli(unittest.TestCase): self.assertIsInstance(data["approvals"], list) def test_box_approval_alias(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approval", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("approval", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) def test_box_approvals_auto_json(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approvals", "auto", "--node", "pip", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("approvals", "auto", "--node", "pip", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) @@ -106,16 +155,14 @@ class TestBoxCtlApprovals(unittest.TestCase): """Test box-ctl.py allowlisted RPC actions.""" def test_box_ctl_approval_check(self): - cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "approval-check"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _box_ctl_inproc("approval-check") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) self.assertIn("approvals", data) def test_box_ctl_approval_auto(self): - cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "approval-auto", "pip"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _box_ctl_inproc("approval-auto", "pip") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) @@ -162,6 +209,22 @@ class TestApprovalsReplySafety(unittest.TestCase): class TestKeyApprovalsAndPasskey(unittest.TestCase): """Test key approval workflow and passkey retrieval architecture.""" + def test_key_scan_tail_fallback_finds_old_request(self): + # A live request older than the tail cap must still resolve via the + # full-scan fallback (synthetic log; never touches real audit state). + with tempfile.TemporaryDirectory() as td: + p = Path(td) / "box-ctl.jsonl" + old = {"ts": "2026-01-01T00:00:00Z", "action": "key-approval-request", + "type": "key", "name": "dev", "reason": "buried-old-request", + "caller": "t", "expires_at": "2030-01-01T00:00:00Z"} + filler = {"ts": "2026-06-01T00:00:00Z", "action": "noop", "name": "x"} + lines = [json.dumps(old)] + [json.dumps(filler)] * (approvals.KEY_SCAN_TAIL_LINES + 10) + p.write_text("\n".join(lines) + "\n") + with mock.patch.object(approvals, "CTL_LOG", p): + res = approvals.check_node_key_request("dev") + self.assertIsNotNone(res) + self.assertEqual(res.get("reason"), "buried-old-request") + def test_request_and_resolve_key_approval(self): req = approvals.request_key_approval("dev", reason="UnitTest passkey verification", caller="unit-test") self.assertTrue(req.get("ok")) @@ -188,8 +251,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase): self.assertIsNone(cleared) def test_box_passkey_info_json(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "passkey", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("passkey", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) @@ -199,8 +261,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase): self.assertEqual(data["key_location"]["canonical_path"], "/srv/box/passkey.txt") def test_box_passkey_fetch_json(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "passkey", "fetch", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("passkey", "fetch", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertIn("operator_pin", data) @@ -210,8 +271,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase): self.assertIn("operator_command", data) def test_box_lookup_key(self): - cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "lookup", "key", "--json"] - r = subprocess.run(cmd, capture_output=True, text=True) + r = _super_cli_inproc("lookup", "key", "--json") self.assertEqual(r.returncode, 0) data = json.loads(r.stdout) self.assertTrue(data.get("ok")) @@ -219,29 +279,26 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase): def test_cli_request_key_lifecycle(self): # 1. Request key - r_req = subprocess.run([ - sys.executable, str(BIN_DIR / "super-cli.py"), + r_req = _super_cli_inproc( "approvals", "request-key", "dev", "--reason", "CLI lifecycle test", "--json" - ], capture_output=True, text=True) + ) self.assertEqual(r_req.returncode, 0) req_data = json.loads(r_req.stdout) self.assertTrue(req_data.get("ok")) # 2. Check shows KEY_APPROVAL - r_check = subprocess.run([ - sys.executable, str(BIN_DIR / "super-cli.py"), + r_check = _super_cli_inproc( "approvals", "check", "--node", "dev", "--json" - ], capture_output=True, text=True) + ) self.assertEqual(r_check.returncode, 0) check_data = json.loads(r_check.stdout) dev_app = next(a for a in check_data["approvals"] if a["node"] == "dev") self.assertEqual(dev_app["status"], "KEY_APPROVAL") # 3. Deny key - r_deny = subprocess.run([ - sys.executable, str(BIN_DIR / "super-cli.py"), + r_deny = _super_cli_inproc( "approvals", "deny", "dev", "--json" - ], capture_output=True, text=True) + ) self.assertEqual(r_deny.returncode, 0) deny_data = json.loads(r_deny.stdout) self.assertTrue(deny_data.get("ok")) diff --git a/tests/test_box_approvals_https.py b/tests/test_box_approvals_https.py index f910da2..69a9cf1 100644 --- a/tests/test_box_approvals_https.py +++ b/tests/test_box_approvals_https.py @@ -31,6 +31,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_approvals", "bin/exec-constrained.py") +box_ctl = _load("box_ctl_approvalstest", "bin/box-ctl.py") def _box_ctl(*args): @@ -39,6 +40,34 @@ def _box_ctl(*args): capture_output=True, text=True, timeout=180) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout only).""" + + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + + +def _box_ctl_inproc(*args): + """In-process _box_ctl (same proven pattern as test_box_loop_https). + + Calls the real main(argv) -- identical argv parsing, dispatch, audit, + and stdout JSON -- amortizing per-spawn interpreter cost over one + import. Node order in fleet scans is nondeterministic either way + (concurrent fan-out); per-node content is identical. + """ + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + class ExecApprovalOpsTests(unittest.TestCase): def test_ops_registered_and_side_effecting(self): spec = exec_constrained.OPS @@ -155,14 +184,14 @@ class BoxCtlApprovalTests(unittest.TestCase): ["approval-allow", "badnode", "--message", "m"], ["approval-deny", "badnode", "--message", "m"], ["approval-auto", "badnode"]): - r = _box_ctl(*args) + r = _box_ctl_inproc(*args) self.assertNotEqual(r.returncode, 0, args) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE", args) def test_rejects_missing_node(self): for args in (["approval-allow"], ["approval-deny"]): - r = _box_ctl(*args) + r = _box_ctl_inproc(*args) self.assertNotEqual(r.returncode, 0, args) self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS", args) @@ -194,7 +223,7 @@ class BoxCtlApprovalTests(unittest.TestCase): (["approval-auto", "a", "b"], False), ] for args, valid in cases: - r = _box_ctl("quality-validate", *args) + r = _box_ctl_inproc("quality-validate", *args) self.assertEqual(json.loads(r.stdout)["valid"], valid, args) diff --git a/tests/test_box_dev_https.py b/tests/test_box_dev_https.py index e4179e9..6b6f605 100644 --- a/tests/test_box_dev_https.py +++ b/tests/test_box_dev_https.py @@ -30,6 +30,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_dev", "bin/exec-constrained.py") +box_ctl = _load("box_ctl_devtest", "bin/box-ctl.py") def _box_ctl(*args): @@ -38,6 +39,35 @@ def _box_ctl(*args): capture_output=True, text=True, timeout=120) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout/stderr).""" + def __init__(self, returncode, stdout, stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def _box_ctl_inproc(*args): + """In-process _box_ctl for pure dry-run verbs (quality-validate). + + Calls the real main(argv) -- identical argv parsing, dispatch, audit, + and stdout JSON -- with stdio captured, amortizing the ~80ms + per-spawn interpreter + module-exec cost over one import. Only valid + for verbs that never read stdin (quality-validate is a dry-run that + never consumes stdin payloads). + """ + import io + from contextlib import redirect_stderr, redirect_stdout + out, err = io.StringIO(), io.StringIO() + returncode = 0 + with redirect_stdout(out), redirect_stderr(err): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, out.getvalue(), err.getvalue()) + + class ExecGitOpsTests(unittest.TestCase): def test_ops_registered_and_read_only(self): for op in ("git.status", "git.diff", "git.log"): @@ -223,12 +253,14 @@ class BoxCtlGitTests(unittest.TestCase): self.assertNotEqual(r.returncode, 0) def test_quality_validate_git_verbs(self): + # In-process dry-runs: same main(argv) path and stdout JSON as + # subprocess calls. Assertions below are unchanged. for args in (["git-status"], ["git-diff", "--stat"], ["git-diff", "--path", "bin/dm.py"], ["git-log", "--limit", "5"]): - r = _box_ctl("quality-validate", *args) + r = _box_ctl_inproc("quality-validate", *args) self.assertTrue(json.loads(r.stdout)["valid"], args) - r = _box_ctl("quality-validate", "git-diff", "--path", "../x") + r = _box_ctl_inproc("quality-validate", "git-diff", "--path", "../x") self.assertFalse(json.loads(r.stdout)["valid"]) @@ -274,15 +306,17 @@ class BoxCtlTestsRunTests(unittest.TestCase): self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND") def test_quality_validate_tests_run(self): - r = _box_ctl("quality-validate", "tests-run") + # In-process dry-runs: same main(argv) path and stdout JSON as + # subprocess calls. Assertions below are unchanged. + r = _box_ctl_inproc("quality-validate", "tests-run") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "tests-run", "tests.test_box_read_https") + r = _box_ctl_inproc("quality-validate", "tests-run", "tests.test_box_read_https") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "tests-run", "--filter", "safepath") + r = _box_ctl_inproc("quality-validate", "tests-run", "--filter", "safepath") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "tests-run", "os") + r = _box_ctl_inproc("quality-validate", "tests-run", "os") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "tests-run", "--filter") + r = _box_ctl_inproc("quality-validate", "tests-run", "--filter") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) @@ -303,10 +337,12 @@ class BoxCtlAckTests(unittest.TestCase): self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS") def test_quality_validate_ack(self): - r = _box_ctl("quality-validate", "ack", "bdf7beb6", + # In-process dry-runs: same main(argv) path and stdout JSON as + # subprocess calls. Assertions below are unchanged. + r = _box_ctl_inproc("quality-validate", "ack", "bdf7beb6", "--to", "pip", "--sender", "opm") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "ack", "xyz!", + r = _box_ctl_inproc("quality-validate", "ack", "xyz!", "--to", "pip", "--sender", "opm") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) diff --git a/tests/test_box_jobs_https.py b/tests/test_box_jobs_https.py index 022f6e1..a3387ba 100644 --- a/tests/test_box_jobs_https.py +++ b/tests/test_box_jobs_https.py @@ -37,6 +37,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_jobs", "bin/exec-constrained.py") +box_ctl = _load("box_ctl_jobstest", "bin/box-ctl.py") def _box_ctl(*args, stdin=None): @@ -45,6 +46,37 @@ def _box_ctl(*args, stdin=None): input=stdin, capture_output=True, text=True, timeout=120) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout/stderr).""" + + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + self.stderr = "" + + +def _box_ctl_inproc(*args, stdin=None): + """In-process _box_ctl (proven pattern from test_box_loop_https). + + Real main(argv) with stdout captured and sys.stdin patched when a + body is given (job-put reads the raw definition from stdin). + """ + import io + from contextlib import redirect_stdout, nullcontext + from unittest import mock + buf = io.StringIO() + returncode = 0 + stdin_ctx = (mock.patch.object(sys, "stdin", io.StringIO(stdin)) + if stdin is not None else nullcontext()) + with stdin_ctx: + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + def _job_def(name, **over): d = {"name": name, "description": "unit test job", "schedule": "manual", "agent": "opm", @@ -166,7 +198,7 @@ class ExecJobOpsTests(unittest.TestCase): class BoxCtlJobsTests(unittest.TestCase): def test_job_next_dry_run_live(self): - r = _box_ctl("job-next", MISSING_ID) + r = _box_ctl_inproc("job-next", MISSING_ID) self.assertEqual(r.returncode, 0, r.stderr) data = json.loads(r.stdout) self.assertTrue(data["ok"]) @@ -174,55 +206,55 @@ class BoxCtlJobsTests(unittest.TestCase): self.assertFalse(data["would_dispatch"]) def test_job_put_rejects_before_write(self): - r = _box_ctl("job-put", "Bad_Name!", stdin="{}") + r = _box_ctl_inproc("job-put", "Bad_Name!", stdin="{}") self.assertNotEqual(r.returncode, 0) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") - r = _box_ctl("job-put", "my-job", stdin="not json") + r = _box_ctl_inproc("job-put", "my-job", stdin="not json") self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB") - r = _box_ctl("job-put", "my-job", - stdin=json.dumps(_job_def("other"))) + r = _box_ctl_inproc("job-put", "my-job", + stdin=json.dumps(_job_def("other"))) self.assertEqual(json.loads(r.stdout)["code"], "NAME_MISMATCH") bad = _job_def("my-job") del bad["agent"] - r = _box_ctl("job-put", "my-job", stdin=json.dumps(bad)) + r = _box_ctl_inproc("job-put", "my-job", stdin=json.dumps(bad)) self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB") def test_job_trigger_rejects_missing(self): - r = _box_ctl("job-trigger", "Bad_Name!") + r = _box_ctl_inproc("job-trigger", "Bad_Name!") self.assertNotEqual(r.returncode, 0) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") - r = _box_ctl("job-trigger", MISSING_JOB) + r = _box_ctl_inproc("job-trigger", MISSING_JOB) self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND") def test_job_chain_rejects_before_write(self): - r = _box_ctl("job-chain", "Bad_Name!", EXISTING_JOB) + r = _box_ctl_inproc("job-chain", "Bad_Name!", EXISTING_JOB) self.assertNotEqual(r.returncode, 0) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") - r = _box_ctl("job-chain", EXISTING_JOB, EXISTING_JOB) + r = _box_ctl_inproc("job-chain", EXISTING_JOB, EXISTING_JOB) self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB") - r = _box_ctl("job-chain", MISSING_JOB, EXISTING_JOB) + r = _box_ctl_inproc("job-chain", MISSING_JOB, EXISTING_JOB) self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND") def test_timer_control_rejects_before_action(self): for verb in ("timer-stop", "timer-disable"): - r = _box_ctl(verb, "Bad_Name!") + r = _box_ctl_inproc(verb, "Bad_Name!") self.assertNotEqual(r.returncode, 0) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") - r = _box_ctl(verb, MISSING_JOB) + r = _box_ctl_inproc(verb, MISSING_JOB) self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND") def test_quality_validate_job_verbs(self): - r = _box_ctl("quality-validate", "job-put", "my-job") + r = _box_ctl_inproc("quality-validate", "job-put", "my-job") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "job-trigger", EXISTING_JOB) + r = _box_ctl_inproc("quality-validate", "job-trigger", EXISTING_JOB) self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "job-chain", "a", "b") + r = _box_ctl_inproc("quality-validate", "job-chain", "a", "b") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "job-next", MISSING_ID) + r = _box_ctl_inproc("quality-validate", "job-next", MISSING_ID) self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "timer-stop", EXISTING_JOB) + r = _box_ctl_inproc("quality-validate", "timer-stop", EXISTING_JOB) self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "job-put", "Bad_Name!") + r = _box_ctl_inproc("quality-validate", "job-put", "Bad_Name!") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) diff --git a/tests/test_box_loop_https.py b/tests/test_box_loop_https.py index 66aefa0..98363a0 100644 --- a/tests/test_box_loop_https.py +++ b/tests/test_box_loop_https.py @@ -38,6 +38,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_loop", "bin/exec-constrained.py") +box_ctl = _load("box_ctl_looptest", "bin/box-ctl.py") def _box_ctl(*args): @@ -46,6 +47,34 @@ def _box_ctl(*args): capture_output=True, text=True, timeout=180) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout only).""" + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + + +def _box_ctl_inproc(*args): + """In-process _box_ctl for pure dry-run verbs (quality-validate). + + Calls the real main(argv) -- identical argv parsing, dispatch, audit, + and stdout JSON -- with stdout captured, amortizing the ~80ms + per-spawn interpreter + module-exec cost over one import. Only valid + for verbs that never read stdin (quality-validate is a dry-run that + never consumes stdin payloads). + """ + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + class ExecLoopOpsTests(unittest.TestCase): def test_ops_registered_and_side_effecting(self): spec = exec_constrained.OPS @@ -208,7 +237,10 @@ class BoxCtlLoopTests(unittest.TestCase): (["vars-get", "has space"], False), ] for args, valid in cases: - r = _box_ctl("quality-validate", *args) + # In-process dry-run: same main(argv) path and stdout JSON as a + # subprocess call, without the per-case spawn cost. Assertions + # below are unchanged. + r = _box_ctl_inproc("quality-validate", *args) self.assertEqual(json.loads(r.stdout)["valid"], valid, args) diff --git a/tests/test_box_md_https.py b/tests/test_box_md_https.py index 9539bc7..854db24 100644 --- a/tests/test_box_md_https.py +++ b/tests/test_box_md_https.py @@ -42,6 +42,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_md", "bin/exec-constrained.py") agent_md = _load("agent_md_mdtest", "bin/agent_md.py") +box_ctl = _load("box_ctl_mdtest", "bin/box-ctl.py") def _box_ctl(*args, stdin=None): @@ -50,6 +51,39 @@ def _box_ctl(*args, stdin=None): input=stdin, capture_output=True, text=True, timeout=180) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout only).""" + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + + +def _box_ctl_inproc(*args, stdin=None): + """In-process _box_ctl for dry-run and validation-failure verbs. + + Calls the real main(argv) -- identical argv parsing, dispatch, audit, + stdin reads, and stdout JSON -- with stdio captured, amortizing the + ~80ms per-spawn interpreter + module-exec cost over one import. + stdin, when given, is fed exactly as subprocess input= would be. + """ + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + saved_stdin = sys.stdin + if stdin is not None: + sys.stdin = io.StringIO(stdin) + try: + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + finally: + sys.stdin = saved_stdin + return _InProcResult(returncode, buf.getvalue()) + + class ExecMdOpsTests(unittest.TestCase): def test_ops_registered_and_side_effecting(self): spec = exec_constrained.OPS @@ -320,8 +354,10 @@ class BoxCtlMdTests(unittest.TestCase): ["md-audit", "../x"], ["md", "read", "646", "../x"], ] + # In-process dispatch: same main(argv) path, fail() JSON, and + # exit code as a subprocess call. Assertions below are unchanged. for args in cases: - r = _box_ctl(*args) + r = _box_ctl_inproc(*args) self.assertNotEqual(r.returncode, 0, args) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args) @@ -333,13 +369,17 @@ class BoxCtlMdTests(unittest.TestCase): ["md", "amend", "NOPE.md", "content here"], ["md", "append", "NOPE.md", "note here"], ] + # In-process dispatch: same main(argv) path, stdin reads, fail() + # JSON, and exit code as a subprocess call. Assertions below are + # unchanged. (Amend/append parse --stdin BEFORE validating the + # path, so stdin is fed here exactly as the spawn did.) for args in cases: - r = _box_ctl(*args) + r = _box_ctl_inproc(*args) self.assertNotEqual(r.returncode, 0, args) self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args) - r = _box_ctl("md-amend", "../x", "--stdin", stdin="hi") + r = _box_ctl_inproc("md-amend", "../x", "--stdin", stdin="hi") self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") - r = _box_ctl("md-append", "../x", "--stdin", stdin="hi") + r = _box_ctl_inproc("md-append", "../x", "--stdin", stdin="hi") self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME") def test_amend_stdin_safety_rejection_writes_nothing(self): @@ -386,7 +426,10 @@ class BoxCtlMdTests(unittest.TestCase): (["md-write", "646", "SOUL.md"], False), ] for args, valid in cases: - r = _box_ctl("quality-validate", *args) + # In-process dry-run: same main(argv) path and stdout JSON as a + # subprocess call, without the per-case spawn cost. Assertions + # below are unchanged. + r = _box_ctl_inproc("quality-validate", *args) self.assertEqual(json.loads(r.stdout)["valid"], valid, args) diff --git a/tests/test_box_read_https.py b/tests/test_box_read_https.py index 4cf6f6b..bde7bf4 100644 --- a/tests/test_box_read_https.py +++ b/tests/test_box_read_https.py @@ -33,6 +33,7 @@ def _load(name, relpath): exec_constrained = _load("exec_constrained_read", "bin/exec-constrained.py") super_cli = _load("super_cli_read", "bin/super-cli.py") +box_ctl = _load("box_ctl_readtest", "bin/box-ctl.py") def _box_ctl(*args): @@ -41,6 +42,33 @@ def _box_ctl(*args): capture_output=True, text=True, timeout=60) +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout/stderr).""" + def __init__(self, returncode, stdout, stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def _box_ctl_inproc(*args): + """In-process _box_ctl for dry-run/read-only verbs (quality-validate, + dm-log success paths). + + Calls the real main(argv) -- identical argv parsing, dispatch, audit, + and stdout JSON -- with stdio captured, amortizing the ~80ms + per-spawn interpreter + module-exec cost over one import. + """ + from contextlib import redirect_stderr + out, err = io.StringIO(), io.StringIO() + returncode = 0 + with redirect_stdout(out), redirect_stderr(err): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, out.getvalue(), err.getvalue()) + + class ExecReadOpsTests(unittest.TestCase): def test_ops_registered_and_read_only(self): self.assertIn("fleet.unread", exec_constrained.OPS) @@ -97,8 +125,10 @@ class ExecReadOpsTests(unittest.TestCase): spec = exec_constrained.OPS["dm.log"] clean = spec["validate"]({"limit": 2}) argv = spec["build"](clean) - argv[0] = sys.executable # hermetic interpreter, same script + args - r = subprocess.run(argv, capture_output=True, text=True, timeout=60) + # In-process dispatch of the op-built argv (minus interpreter and + # script: argv is [python, box-ctl.py, action, ...]): same argv + # parsing, dispatch, and stdout JSON, without respawn. + r = _box_ctl_inproc(*argv[2:]) self.assertEqual(r.returncode, 0, r.stderr) data = json.loads(r.stdout) self.assertTrue(data["ok"]) @@ -131,23 +161,55 @@ class BoxCtlReadVerbsTests(unittest.TestCase): self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS") def test_dm_log_back_compat_limit_only(self): - r = _box_ctl("dm-log", "2") + r = _box_ctl_inproc("dm-log", "2") self.assertEqual(r.returncode, 0, r.stderr) data = json.loads(r.stdout) self.assertTrue(data["ok"]) self.assertEqual(len(data["entries"]), 2) def test_quality_validate_new_verbs(self): - r = _box_ctl("quality-validate", "unread", "--agent", "pip") + # In-process dry-runs: same main(argv) path and stdout JSON as + # subprocess calls. Assertions below are unchanged. + r = _box_ctl_inproc("quality-validate", "unread", "--agent", "pip") data = json.loads(r.stdout) self.assertTrue(data["valid"], r.stdout) - r = _box_ctl("quality-validate", "dm-log", "5", "--agent", "opm") + r = _box_ctl_inproc("quality-validate", "dm-log", "5", "--agent", "opm") self.assertTrue(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "unread", "--agent", "nope") + r = _box_ctl_inproc("quality-validate", "unread", "--agent", "nope") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) - r = _box_ctl("quality-validate", "unread", "extra-positional") + r = _box_ctl_inproc("quality-validate", "unread", "extra-positional") self.assertFalse(json.loads(r.stdout)["valid"], r.stdout) + def test_policy_verbs_live_schema(self): + r = _box_ctl_inproc("policy") + self.assertEqual(r.returncode, 0, r.stderr) + data = json.loads(r.stdout) + self.assertTrue(data["ok"]) + self.assertIn("agents", data) + self.assertIn("totals", data) + self.assertIn(data["status"], ("clean", "violations found")) + r = _box_ctl_inproc("policy", "check", "opm") + self.assertEqual(r.returncode, 0, r.stderr) + data = json.loads(r.stdout) + self.assertTrue(data["ok"]) + self.assertEqual(data["agent"], "opm") + for k in ("blocked", "authorized_main", "violations", "total_sends"): + self.assertIn(k, data) + + def test_policy_scan_parses_each_line_once(self): + import shutil + import tempfile + with tempfile.TemporaryDirectory() as td: + frozen = Path(td) / "dm-log.jsonl" + shutil.copyfile(box_ctl.DM_LOG, frozen) + expect = sum(1 for ln in frozen.read_text().splitlines() if ln.strip()) + real_loads = json.loads + with mock.patch.object(box_ctl, "DM_LOG", frozen): + with mock.patch.object(json, "loads", wraps=real_loads) as spy: + per_agent, meta = box_ctl._policy_scan() + self.assertIsNotNone(per_agent) + self.assertEqual(spy.call_count, expect) + class SuperCliUnreadTests(unittest.TestCase): def test_lookup_dispatches_unread(self): diff --git a/tests/test_box_runtime.py b/tests/test_box_runtime.py index 3afee99..d708dbe 100644 --- a/tests/test_box_runtime.py +++ b/tests/test_box_runtime.py @@ -6,6 +6,7 @@ muse argv approval-posture parsing, runtime_rows assembly (mocked tmux), and the `box runtime` CLI surface. """ +import importlib.util import json import sys import unittest @@ -18,6 +19,16 @@ sys.path.insert(0, str(BIN_DIR)) import muse_choice_watcher as w + +def _load(name, relpath): + spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +super_cli = _load("super_cli_runtimetest", "bin/super-cli.py") + PROMPT = "❯" # Muse TUI input glyph (U+276F) STATE_OPEN = ( @@ -375,9 +386,25 @@ class TestBoxRuntimeCLI(unittest.TestCase): self.assertTrue(json.loads(r.stdout)["ok"]) def test_subcommand_help(self): + # In-process --help: same main()/argparse path as a subprocess call + # (fresh parser per call; --help exits 0), without the ~0.2s + # per-spawn interpreter + module-exec cost. Assertions unchanged. + import io + from contextlib import redirect_stderr, redirect_stdout for sub in ("list", "send", "launch", "layout", "spread"): - r = self._box(sub, "--help") - self.assertEqual(r.returncode, 0, sub) + out, err = io.StringIO(), io.StringIO() + saved = sys.argv + sys.argv = [str(BIN_DIR / "super-cli.py"), + "runtime", sub, "--help"] + rc = 0 + with redirect_stdout(out), redirect_stderr(err): + try: + super_cli.main() + except SystemExit as e: + rc = e.code if isinstance(e.code, int) else 1 + finally: + sys.argv = saved + self.assertEqual(rc, 0, sub) def test_launch_dry_run_injects_approve(self): r = self._box("launch", "--session", "probe-x", diff --git a/tests/test_invite_handler.py b/tests/test_invite_handler.py index e68c2a0..de715b2 100644 --- a/tests/test_invite_handler.py +++ b/tests/test_invite_handler.py @@ -17,6 +17,23 @@ from invite_handler import InviteCodeInfo, InviteHandler, RedemptionResult, salv from settings_rpa import NodeUsage, SettingsRPA +def _fast_clock(): + """Fake time.time advancing 1s per call. + + redeem_code_dom polls on `deadline = time.time() + 2.5` loops; patching + only time.sleep leaves 2.5s of real time per loop. With +1s/call each + loop runs exactly 2 iterations then expires (deadline math holds for + every loop uniformly, no per-loop alignment needed). + """ + state = {"t": 1000.0} + + def fake_time(): + state["t"] += 1.0 + return state["t"] + + return fake_time + + class TestInviteCodeValidation(unittest.TestCase): def test_normalize_valid_codes(self): self.assertEqual(invite.normalize_code("REDCJ7"), "REDCJ7") @@ -212,7 +229,7 @@ class TestInviteHandlerMocked(unittest.TestCase): h = InviteHandler("dev") h.ws = MagicMock() - with patch.object(h, "connect"), patch("time.sleep", return_value=None): + with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()): with patch("invite_handler.cdp_evaluate", return_value={"found": False, "text": "General"}): with patch.object(h, "redeem_code_api") as mock_api: mock_api.return_value = RedemptionResult( @@ -239,7 +256,7 @@ class TestInviteHandlerMocked(unittest.TestCase): h = InviteHandler("646") h.ws = MagicMock() - with patch.object(h, "connect"), patch("time.sleep", return_value=None): + with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()): with patch("invite_handler.cdp_evaluate", return_value={"found": False, "has_additional": True, "text": "Additional tokens"}): with patch.object(h, "redeem_code_api") as mock_api: mock_api.return_value = RedemptionResult( @@ -274,7 +291,7 @@ class TestInviteHandlerMocked(unittest.TestCase): return {"found_input": False} return True - with patch.object(h, "connect"), patch("time.sleep", return_value=None): + with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()): with patch("invite_handler.cdp_evaluate", side_effect=mock_eval), patch("invite_handler.cdp_send_escape"): with patch.object(h, "redeem_code_api") as mock_api: mock_api.return_value = RedemptionResult( diff --git a/tests/test_loop_health_remediation.py b/tests/test_loop_health_remediation.py index 96e8515..4caaca2 100644 --- a/tests/test_loop_health_remediation.py +++ b/tests/test_loop_health_remediation.py @@ -18,6 +18,7 @@ import tempfile import shutil from pathlib import Path from datetime import datetime, timezone +from unittest.mock import patch REPO_ROOT = Path("/home/super/Projects/NetVM") BIN_DIR = REPO_ROOT / "bin" @@ -27,6 +28,39 @@ sys.path.insert(0, str(BIN_DIR)) import gravity +def _load(name, relpath): + import importlib.util + spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +box_ctl = _load("box_ctl_loophealthtest", "bin/box-ctl.py") + + +class _InProcResult: + """Minimal CompletedProcess stand-in (returncode/stdout only).""" + + def __init__(self, returncode, stdout): + self.returncode = returncode + self.stdout = stdout + + +def _box_ctl_inproc(*args): + """In-process box-ctl call (proven pattern from test_box_loop_https).""" + import io + from contextlib import redirect_stdout + buf = io.StringIO() + returncode = 0 + with redirect_stdout(buf): + try: + box_ctl.main(["box-ctl.py", *args]) + except SystemExit as e: + returncode = e.code if isinstance(e.code, int) else 1 + return _InProcResult(returncode, buf.getvalue()) + + class TestLoopDiagnosticsAndRemediation(unittest.TestCase): """Test gravity.py loop health and progressive remediation.""" @@ -61,13 +95,27 @@ class TestLoopDiagnosticsAndRemediation(unittest.TestCase): self.assertIsInstance(res.get("remediated"), list) self.assertIsInstance(res.get("escalated"), list) + def test_remediate_single_approval_scan(self): + import approvals + real = approvals.check_fleet_approvals + with patch.object(approvals, "check_fleet_approvals", wraps=real) as spy: + res = gravity.remediate_breaks(dry_run=True) + self.assertTrue(res.get("ok")) + self.assertEqual(spy.call_count, 1) + + def test_remediate_single_loop_parse(self): + real = gravity._load_loop_candidates + with patch.object(gravity, "_load_loop_candidates", wraps=real) as spy: + res = gravity.remediate_breaks(dry_run=True) + self.assertTrue(res.get("ok")) + self.assertEqual(spy.call_count, 1) + class TestLoopRpc(unittest.TestCase): """Test box-ctl.py allowlisted RPC actions for loops.""" def test_box_ctl_loop_health(self): - cmd = [sys.executable, str(BOX_CTL), "loop-health"] - res = subprocess.run(cmd, capture_output=True, text=True) + res = _box_ctl_inproc("loop-health") self.assertEqual(res.returncode, 0) data = json.loads(res.stdout) self.assertTrue(data.get("ok")) @@ -76,16 +124,14 @@ class TestLoopRpc(unittest.TestCase): def test_box_ctl_loop_status(self): - cmd = [sys.executable, str(BOX_CTL), "loop-status", "--limit", "5"] - res = subprocess.run(cmd, capture_output=True, text=True) + res = _box_ctl_inproc("loop-status", "--limit", "5") self.assertEqual(res.returncode, 0) data = json.loads(res.stdout) self.assertTrue(data.get("ok")) self.assertIn("loops", data) def test_box_ctl_loop_remediate_dry_run(self): - cmd = [sys.executable, str(BOX_CTL), "loop-remediate", "--dry-run"] - res = subprocess.run(cmd, capture_output=True, text=True) + res = _box_ctl_inproc("loop-remediate", "--dry-run") self.assertEqual(res.returncode, 0) data = json.loads(res.stdout) self.assertTrue(data.get("ok")) diff --git a/tests/test_settings_rpa.py b/tests/test_settings_rpa.py index cf12c79..d0d5a4d 100644 --- a/tests/test_settings_rpa.py +++ b/tests/test_settings_rpa.py @@ -68,8 +68,11 @@ class TestSettingsRPAPrimitives(unittest.TestCase): usage_payload, # read_usage evaluation ] - with SettingsRPA("646") as rpa: - usage = rpa.read_usage(keep_dialog_open=True) + # Settle sleeps (0.4s tab + 0.4s usage) are production pacing, not + # asserted behavior: skip them like the mocked CDP transport above. + with patch("time.sleep", return_value=None): + with SettingsRPA("646") as rpa: + usage = rpa.read_usage(keep_dialog_open=True) self.assertEqual(usage.node, "646") self.assertEqual(usage.weekly_percent_used, 100) self.assertEqual(usage.extra_percent_used, 100) @@ -97,8 +100,10 @@ class TestSettingsRPAPrimitives(unittest.TestCase): usage_payload, # read_usage evaluation ] - with SettingsRPA("646") as rpa: - usage = rpa.read_usage(keep_dialog_open=True) + # Settle sleeps are production pacing, not asserted behavior: skip. + with patch("time.sleep", return_value=None): + with SettingsRPA("646") as rpa: + usage = rpa.read_usage(keep_dialog_open=True) self.assertEqual(usage.node, "646") self.assertEqual(usage.weekly_percent_used, 20) self.assertEqual(usage.extra_percent_used, 0) @@ -118,9 +123,19 @@ class TestSettingsRPAPrimitives(unittest.TestCase): mock_eval.side_effect = eval_side_effect + # The usage poll loop (read_usage) busy-spins on a real-time 4.0s + # deadline while sleep is stubbed, so run it on a fake clock that + # advances 1s per read: the loop still polls (each poll returns None) + # and still exits via timeout, just after ~4 reads instead of 4s. + clock = [1000.0] + + def _tick(): + clock[0] += 1.0 + return clock[0] + with SettingsRPA("646", timeout=0.5) as rpa: - # Shorten deadline by patching time.time or passing small timeout - with patch("time.sleep", return_value=None): + with patch("time.sleep", return_value=None), \ + patch("time.time", side_effect=_tick): usage = rpa.read_usage(keep_dialog_open=True) self.assertEqual(usage.node, "646") self.assertFalse(usage.stats_loaded) diff --git a/tests/test_tool_calls.py b/tests/test_tool_calls.py index 49d128e..22f2734 100644 --- a/tests/test_tool_calls.py +++ b/tests/test_tool_calls.py @@ -245,6 +245,23 @@ class CanonicalToolPattern(unittest.TestCase): class EnvelopeRoundTrip(unittest.TestCase): + def setUp(self): + # Stub the kpi module: wrap() calls get_live_advisory_block() + # (live network I/O: usage API + route probes) on the include_kpi + # path. An empty advisory keeps the path exercised -- import, call, + # and falsy branch all still run -- without the network wait. + import types + self._saved_kpi = sys.modules.get("kpi") + stub = types.ModuleType("kpi") + stub.get_live_advisory_block = lambda agent: "" + sys.modules["kpi"] = stub + + def tearDown(self): + if self._saved_kpi is None: + sys.modules.pop("kpi", None) + else: + sys.modules["kpi"] = self._saved_kpi + def test_wrap_advertises_new_verbs(self): body = env.wrap("work-finder", "work-finder-1", "646", "646 tasks", "Do the thing.")