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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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
|
|
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
|
|
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")"
|