tickmarkr 1.58.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -415,9 +415,15 @@ These are optional — the CLI works standalone. Skills are repo-scoped and ship
415
415
 
416
416
  ## Contributing
417
417
 
418
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the green bar (build/test/lint), and
418
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the green bar (build/test/lint), and
419
419
  design invariants. Pull requests welcome; non-trivial changes should include test coverage.
420
420
 
421
+ **Boundaries:** this repo is a squashed export of private development — each release is a verified
422
+ snapshot, not a live mirror of every commit. Before tickmarkr 2.0, minor versions may break with every
423
+ break noted in [CHANGELOG.md](CHANGELOG.md). Support is best effort for the latest version only;
424
+ accepted contributions are credited via `Co-authored-by:` on the release commit. Details in
425
+ [CONTRIBUTING.md](CONTRIBUTING.md).
426
+
421
427
  ## Documentation
422
428
 
423
429
  - **[LICENSE](LICENSE)** — MIT license
@@ -1,5 +1,6 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { parseArgs } from "node:util";
5
6
  import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
@@ -61,7 +62,7 @@ function nextSteps(cwd, scaffoldedSpec) {
61
62
  return `next: edit ${scaffoldedSpec}, then tickmarkr compile ${scaffoldedSpec} && tickmarkr plan && tickmarkr run`;
62
63
  }
63
64
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
64
- const AGENT_SKILLS = ["tickmarkr-loop", "tickmarkr-auto"];
65
+ const AGENT_SKILLS = ["tickmarkr-loop", "tickmarkr-auto", "tickmarkr-overseer"];
65
66
  const DOCS_BEGIN = "<!-- tickmarkr:agent-docs begin -->";
66
67
  const DOCS_END = "<!-- tickmarkr:agent-docs end -->";
67
68
  const AGENT_DOCS = `${DOCS_BEGIN}
@@ -110,9 +111,16 @@ A run is green only when the run-end event exists in the journal AND tip verify
110
111
  When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the target pane afterward; never report "relayed" without read-back.
111
112
  ${DOCS_END}
112
113
  `;
113
- const skillsRoot = (cwd) => existsSync(join(cwd, ".claude", "skills")) ? join(cwd, ".claude", "skills") : join(cwd, ".agents", "skills");
114
- const skillPath = (cwd, skill) => join(skillsRoot(cwd), skill, "SKILL.md");
115
- const skillsInstalled = (cwd) => AGENT_SKILLS.every((s) => existsSync(skillPath(cwd, s)));
114
+ // Every applicable host location gets the skills, each paired with its own repository guidance
115
+ // file: codex discovers .agents/skills + AGENTS.md (always applicable); claude discovers
116
+ // .claude/skills + CLAUDE.md (applicable when the repo already shows claude usage).
117
+ const hostTargets = (cwd) => {
118
+ const targets = [{ skillsDir: join(cwd, ".agents", "skills"), docPath: join(cwd, "AGENTS.md") }];
119
+ if (existsSync(join(cwd, ".claude")) || existsSync(join(cwd, "CLAUDE.md")))
120
+ targets.push({ skillsDir: join(cwd, ".claude", "skills"), docPath: join(cwd, "CLAUDE.md") });
121
+ return targets;
122
+ };
123
+ const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
116
124
  const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : "auto";
117
125
  async function installAgentFiles(cwd, force, docs, notes) {
118
126
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -124,31 +132,30 @@ async function installAgentFiles(cwd, force, docs, notes) {
124
132
  return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
125
133
  };
126
134
  try {
127
- for (const skill of AGENT_SKILLS) {
128
- const dest = skillPath(cwd, skill);
129
- const exists = existsSync(dest);
130
- if (exists && !force && !(await confirm(`Overwrite ${dest}?`))) {
131
- notes.push(`skipped existing ${dest}; pass --force to overwrite it`);
132
- continue;
135
+ for (const { skillsDir, docPath } of hostTargets(cwd)) {
136
+ for (const skill of AGENT_SKILLS) {
137
+ const dest = join(skillsDir, skill, "SKILL.md");
138
+ const exists = existsSync(dest);
139
+ if (exists && !force && !(await confirm(`Overwrite ${dest}?`))) {
140
+ notes.push(`skipped existing ${dest}; pass --force to overwrite it`);
141
+ continue;
142
+ }
143
+ // whole skill dir, not just SKILL.md — the overseer ships its pane-watcher script
144
+ cpSync(fileURLToPath(new URL(`../../../skills/${skill}`, import.meta.url)), join(skillsDir, skill), { recursive: true });
145
+ notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
146
+ }
147
+ const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
148
+ if (current.includes(DOCS_BEGIN) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs begin -->`)
149
+ || current.includes(DOCS_END) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs end -->`)) {
150
+ notes.push(`kept existing tickmarkr agent docs in ${docPath}`);
151
+ }
152
+ else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
153
+ appendFileSync(docPath, `${current ? current.endsWith("\n") ? "\n" : "\n\n" : ""}${AGENT_DOCS}`);
154
+ notes.push(`appended tickmarkr agent docs to ${docPath}`);
155
+ }
156
+ else {
157
+ notes.push(`skipped agent docs for ${docPath}; pass --docs to append them`);
133
158
  }
134
- mkdirSync(join(skillsRoot(cwd), skill), { recursive: true });
135
- writeFileSync(dest, readFileSync(new URL(`../../../skills/${skill}/SKILL.md`, import.meta.url)), exists ? undefined : { flag: "wx" });
136
- notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
137
- }
138
- const claude = join(cwd, "CLAUDE.md");
139
- const agents = join(cwd, "AGENTS.md");
140
- const docPath = existsSync(claude) || !existsSync(agents) ? claude : agents;
141
- const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
142
- if (current.includes(DOCS_BEGIN) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs begin -->`)
143
- || current.includes(DOCS_END) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs end -->`)) {
144
- notes.push(`kept existing tickmarkr agent docs in ${docPath}`);
145
- }
146
- else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
147
- appendFileSync(docPath, `${current ? current.endsWith("\n") ? "\n" : "\n\n" : ""}${AGENT_DOCS}`);
148
- notes.push(`appended tickmarkr agent docs to ${docPath}`);
149
- }
150
- else {
151
- notes.push(`skipped agent docs for ${docPath}; pass --docs to append them`);
152
159
  }
153
160
  }
154
161
  finally {
@@ -183,7 +190,7 @@ async function runInitWizard(cwd) {
183
190
  let installSkills = false;
184
191
  if (!skillsInstalled(cwd)) {
185
192
  const skillsDef = existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd);
186
- installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto)?", skillsDef);
193
+ installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto/tickmarkr-overseer)?", skillsDef);
187
194
  }
188
195
  return { overlay: { driver, concurrency, visibility: { llm } }, installSkills };
189
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.58.0",
3
+ "version": "1.59.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.json",
33
33
  "lint": "oxlint src tests scripts",
34
+ "pretest": "npm run build",
34
35
  "test": "vitest run",
35
36
  "test:coverage": "vitest run --coverage",
36
37
  "schema": "tsx scripts/emit-schema.ts",
@@ -14,7 +14,9 @@ When working in a multi-agent terminal environment, decide your role before star
14
14
 
15
15
  - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
16
16
  - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
17
- - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with `--permission-mode bypassPermissions`, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
17
+ - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
18
+ - **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
19
+ - **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --ask-for-approval never --sandbox workspace-write`
18
20
 
19
21
  Outside a multi-agent terminal environment, run the loop directly.
20
22
 
@@ -13,7 +13,9 @@ When working in a multi-agent terminal environment, decide your role before star
13
13
 
14
14
  - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
15
15
  - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
16
- - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with `--permission-mode bypassPermissions`, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
16
+ - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
17
+ - **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
18
+ - **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --ask-for-approval never --sandbox workspace-write`
17
19
 
18
20
  Outside a multi-agent terminal environment, run the loop directly.
19
21
 
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: tickmarkr-overseer
3
+ description: "Use when the user asks to oversee/supervise/babysit an autonomous tickmarkr run in a Herdr workspace (e.g. '/tickmarkr-overseer run the milestone', 'supervise this tickmarkr run', 'babysit this pipeline'). Requires HERDR_ENV=1. The skill argument is the mission (what to run end-to-end)."
4
+ ---
5
+
6
+ # Overseer (tickmarkr)
7
+
8
+ Become the OVERSEER for this workspace. Do no heavy work directly — build and supervise a two-tier
9
+ hierarchy of VISIBLE agents (you → orchestrator → tickmarkr's own worker fleet), and route human decisions
10
+ to the user with evidence.
11
+
12
+ The mission is the skill argument. If empty, ask the user what to run end-to-end before doing anything else.
13
+ Requires `HERDR_ENV=1`; if unset, say so and stop.
14
+
15
+ ## Setup
16
+
17
+ 0. **Adopt before you build.** If this workspace already has a supervision hierarchy — an
18
+ OVERSEER/ORCHESTRATOR tab, a live agent named `*orch*`, or a `<repo>/<state-dir>/overseer/` dir
19
+ (state dir = `.tickmarkr/`; legacy standalone `<repo>/.overseer/` counts
20
+ too) — do NOT spawn a duplicate (two orchestrators risk two concurrent tickmarkr runs in one repo,
21
+ which tickmarkr forbids). Read that dir's `DECISIONS.md` + `ORCH-BRIEF.md`, check the existing agents'
22
+ status, and either ADOPT the
23
+ existing orchestrator (updated brief, re-armed watchers) or, if the old hierarchy is dead, archive the
24
+ stale brief and build fresh.
25
+ 1. Load the `herdr` skill. `herdr pane list` to map the workspace — the focused pane is yours. Rename your
26
+ tab OVERSEER; create ONE tab ORCHESTRATOR.
27
+ **Live tab labels (standing operator rule, 2026-07-12):** on every decision or state change (role
28
+ handoff, task done/merged, run end) rename the affected tabs — and keep labels SHORT: the role as the
29
+ main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
30
+ fraction (`ORCH · v1.19 4/5`, updated on every task-done); WORKERS carries the task token (tickmarkr
31
+ updates it). Never long context strings or ✓-chains.
32
+ 2. **Orchestrator**: Launch the orchestrator with your agent host. For Claude Code, use `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions` (pin a strong model with `--model <m>` if the operator has a policy). For Codex, use `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --ask-for-approval never --sandbox workspace-write` (add `--model <m>` to specify the model). Workers you never spawn — tickmarkr spawns its own visible worker panes.
33
+ 3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
34
+ truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
35
+ (inside the tickmarkr state dir — already self-gitignored, no exclude step needed), then send one line:
36
+ `herdr pane run <orch> "Read .tickmarkr/overseer/ORCH-BRIEF.md and follow it exactly."` The brief MUST contain: the
37
+ mission, the pane mechanics below, rules 1–2, and require a verbatim one-sentence acknowledgment of the
38
+ human-checkpoint rule before anything is dispatched. Delete the dir at mission end.
39
+ 4. Arm the watcher (Supervision). Report the hierarchy map (pane ids + names) to the user.
40
+
41
+ ## Supervising tickmarkr as the executor
42
+
43
+ When the mission runs `/tickmarkr-auto` (tickmarkr dispatches the workers), supervision changes shape:
44
+
45
+ - **Give the run a live surface.** `tickmarkr run` is stdout-silent until run-end by design — split a pane in the
46
+ ORCHESTRATOR tab running `tickmarkr status --watch`. Narration also arrives as herdr notifications.
47
+ - **Watch the journal, not the panes.** The append-only journal
48
+ (`.tickmarkr/runs/<runId>/journal.jsonl`) is the
49
+ source of truth. Arm a background watcher on `run-end` / `task-human` / `task-failed` / `consult-verdict`
50
+ events; never sleep-poll inside an agent turn.
51
+ - **Daemon liveness ≠ journal activity.** A dead daemon emits no events, so journal watchers sleep through
52
+ its death. `tickmarkr status` prints last-event age + daemon pid liveness; check it before diagnosing a stall.
53
+ Recovery is `tickmarkr resume <runId>` — crash-safe by design (journal replay restores attempt counts and
54
+ consult channel bans).
55
+ - **Gate quiet ≠ idle.** Between `worker-result` and the batched `gate-result`s tickmarkr runs shell gates plus a
56
+ headless LLM judge/review with little visible signal — check the journal timestamps before intervening.
57
+ - **Classify gate failures before reacting.** The same fingerprint failing across DIFFERENT workers, or a
58
+ scope/test catch-22 (attempt N edits a file → scope gate fails; attempt N+1 leaves it → test gate fails),
59
+ is a PLAN defect: widen `files_modified` in the phase PLAN, recompile the phase dir after the run ends or
60
+ the task parks, release (`human → pending`), resume. A cross-vendor review rejection with concrete findings
61
+ is a REAL defect — let the escalation ladder work.
62
+ - **Dialog watchers go stale per attempt.** Every retry/escalation may spawn a new pane; re-arm dialog
63
+ watchers on each `task-dispatch` journal event.
64
+
65
+ ## Pane mechanics that bite
66
+
67
+ - **Verified send protocol**: `herdr agent send` writes WITHOUT Enter, and `pane run`'s Enter can be swallowed
68
+ by bracketed-paste on long payloads. Robust sequence: read the pane (bare prompt required) → send-text →
69
+ sleep 2–3s → send-keys Enter → read back (input empty / agent `working`). Never report "briefed" without
70
+ the read-back. Long content goes in a brief file, never pane text.
71
+ - **Guard-before-Enter** (race-safe prompt answering): chain with `&&` — pane get shows `blocked` && pane
72
+ read shows the expected option under the cursor && only then send-keys. If no longer `blocked`, someone
73
+ already answered; do nothing.
74
+ - `herdr wait agent-status` exits 1 on timeout, 0 on match — but ALSO 0 (with an error JSON) when the pane is
75
+ GONE. Never chain `wait && act` without confirming the pane exists.
76
+ - Stale typed input is unclearable via CLI — supersede it:
77
+ `pane run "<-- disregard everything before this arrow (stale draft). ACTUAL: <message>"`.
78
+
79
+ ## Supervision watcher
80
+
81
+ Arm the bundled watcher as its OWN Bash call with `run_in_background` — chaining it after other commands
82
+ with `&` orphans it from the wake chain. It prints one wake reason and exits; re-arm after every wake.
83
+
84
+ ```bash
85
+ .claude/skills/tickmarkr-overseer/scripts/watch-panes.sh WORKER_PANE ORCH_PANE [--fast-blocked]
86
+ ```
87
+
88
+ Default mode wakes only when both panes are quiet (dropped handoff) or the orchestrator blocks; the
89
+ orchestrator gets a 90s grace window to handle worker blocks first. For long parked stretches a targeted
90
+ `herdr wait agent-status <pane> --status <s> --timeout <ms>` beats the watcher. When parking a human
91
+ checkpoint, also fire `herdr notification show "HUMAN CHECKPOINT: <gate>" --sound request`.
92
+
93
+ ## Specialist pipeline rules
94
+
95
+ - **Dedicated consultant tab**: Consultants (agents spawned to gather synthesis input for decisions like SCOPER analysis or architectural reviews) must run in a DEDICATED tab separate from the ORCHESTRATOR tab. When the orchestrator stands down, the consultant panes should persist so their assessments remain available for review and reference.
96
+ - **Scoper worktree rule**: The SCOPER (or any worktree-based specialist synthesizing into the spec pipeline) must do ALL git operations in a dedicated worktree (e.g., `git worktree add /private/tmp/tkr-scoper-v155 -b spec/...`), never switching the main checkout's branch. This prevents race conditions between the specialist's branch operations and the orchestrator's shipping logic.
97
+
98
+ ## Non-negotiable rules
99
+
100
+ 1. **Takeover rule**: only act on a worker if it needs input AND the orchestrator is not `working`.
101
+ 2. **Human checkpoints (absolute)**: any gate marked `autonomous: false` or asking for product/visual
102
+ sign-off is NEVER auto-answered — regardless of how obviously correct the highlighted option looks. Leave
103
+ it blocked and bring the user the decision WITH evidence. If the mission explicitly delegates authority,
104
+ routine-class gates may be overseer-decided after polling the operator first — but spend and ship gates
105
+ NEVER self-decide.
106
+ 3. **Trust disk over transcripts**: verify artifacts on disk before building on them; a subagent killed
107
+ mid-flight still renders "Done" without writing its artifact.
108
+ 4. **Report concisely on every state change**: what happened, who handled it, what's next. Lead with the
109
+ outcome. Surface product decisions; never make them.
110
+ 5. **Log every abnormality** to `.planning/OBSERVATIONS.md` (or the project's ledger), even mid-run.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # watch-panes.sh — Herdr overseer watcher.
3
+ # Polls worker + orchestrator agent_status via `herdr pane get`, prints ONE wake
4
+ # reason to stdout, then exits. Run it via the Bash tool with run_in_background
5
+ # so the overseer is re-invoked when it fires. Re-run (re-arm) after every wake.
6
+ #
7
+ # Usage:
8
+ # watch-panes.sh WORKER_PANE ORCH_PANE [--fast-blocked] [--cap-seconds N] [--settle-seconds N]
9
+ #
10
+ # Options:
11
+ # --fast-blocked Wake fast (20s debounce) whenever the WORKER blocks, even
12
+ # if the orchestrator is working. Use during stretches with
13
+ # human checkpoints. Without it, a blocked worker gets a 90s
14
+ # grace window for the orchestrator to handle it first
15
+ # (takeover rule), and only wakes if BOTH panes are quiet.
16
+ # --cap-seconds N Max watch duration (default 14400 = 4h).
17
+ # --settle-seconds N Initial sleep before polling (default 60) so a
18
+ # just-dispatched turn can start without an instant refire.
19
+ #
20
+ # Wake reasons printed:
21
+ # WORKER_PANE_GONE / ORCH_PANE_GONE — a pane disappeared
22
+ # ORCH:blocked WORKER:<s> — orchestrator itself needs input
23
+ # WORKER:blocked ORCH:<s> — worker blocked (fast-blocked mode)
24
+ # WORKER:<s> ORCH:<s> — both quiet: dropped handoff / done
25
+ # WATCH_CAP_REACHED ... — cap hit, everything still working
26
+ set -u
27
+ WORKER="${1:?worker pane id required}"
28
+ ORCH="${2:?orchestrator pane id required}"
29
+ shift 2
30
+ FAST_BLOCKED=false
31
+ CAP=14400
32
+ SETTLE=60
33
+ while [ $# -gt 0 ]; do
34
+ case "$1" in
35
+ --fast-blocked) FAST_BLOCKED=true ;;
36
+ --cap-seconds) CAP="$2"; shift ;;
37
+ --settle-seconds) SETTLE="$2"; shift ;;
38
+ esac
39
+ shift
40
+ done
41
+
42
+ get_status() {
43
+ herdr pane get "$1" 2>/dev/null | python3 -c '
44
+ import sys, json
45
+ try:
46
+ d = json.load(sys.stdin)
47
+ p = d.get("result", {}).get("pane", d.get("result", {}))
48
+ print(p.get("agent_status", "unknown"))
49
+ except Exception:
50
+ print("gone")
51
+ ' 2>/dev/null || echo gone
52
+ }
53
+
54
+ # Transient CLI/server failures also read as "gone" — recheck before declaring.
55
+ confirmed_gone() {
56
+ sleep 5
57
+ [ "$(get_status "$1")" = "gone" ]
58
+ }
59
+
60
+ END=$((SECONDS + CAP))
61
+ sleep "$SETTLE"
62
+ while [ $SECONDS -lt $END ]; do
63
+ WS=$(get_status "$WORKER")
64
+ OS=$(get_status "$ORCH")
65
+ if [ "$WS" = "gone" ]; then
66
+ confirmed_gone "$WORKER" && { echo "WORKER_PANE_GONE ORCH:$(get_status "$ORCH")"; exit 0; }
67
+ continue
68
+ fi
69
+ if [ "$OS" = "gone" ]; then
70
+ confirmed_gone "$ORCH" && { echo "ORCH_PANE_GONE WORKER:$(get_status "$WORKER")"; exit 0; }
71
+ continue
72
+ fi
73
+ # orchestrator itself stuck on a prompt (debounced)
74
+ if [ "$OS" = "blocked" ]; then
75
+ sleep 15
76
+ [ "$(get_status "$ORCH")" = "blocked" ] && { echo "ORCH:blocked WORKER:$WS"; exit 0; }
77
+ fi
78
+ # human-checkpoint mode: wake fast on any persisting worker block
79
+ if $FAST_BLOCKED && [ "$WS" = "blocked" ]; then
80
+ sleep 20
81
+ [ "$(get_status "$WORKER")" = "blocked" ] && { echo "WORKER:blocked ORCH:$(get_status "$ORCH")"; exit 0; }
82
+ fi
83
+ # standard mode: worker needs attention AND orchestrator is not driving
84
+ if [ "$WS" != "working" ]; then
85
+ sleep 90
86
+ WS2=$(get_status "$WORKER"); OS2=$(get_status "$ORCH")
87
+ if [ "$WS2" != "working" ] && [ "$OS2" != "working" ]; then
88
+ echo "WORKER:$WS2 ORCH:$OS2"; exit 0
89
+ fi
90
+ fi
91
+ sleep 20
92
+ done
93
+ echo "WATCH_CAP_REACHED WORKER:$(get_status "$WORKER") ORCH:$(get_status "$ORCH")"