tickmarkr 2.5.2 → 2.5.4

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.
Files changed (41) hide show
  1. package/dist/adapters/qwen.d.ts +1 -0
  2. package/dist/adapters/qwen.js +8 -0
  3. package/dist/adapters/types.d.ts +1 -0
  4. package/dist/cli/commands/doctor.d.ts +10 -0
  5. package/dist/cli/commands/doctor.js +50 -1
  6. package/dist/cli/commands/plan.js +144 -3
  7. package/dist/cli/commands/resume.js +2 -0
  8. package/dist/cli/commands/run.js +12 -0
  9. package/dist/cli/commands/status.js +8 -14
  10. package/dist/compile/collateral.d.ts +2 -0
  11. package/dist/compile/collateral.js +50 -9
  12. package/dist/compile/ownership.d.ts +16 -0
  13. package/dist/compile/ownership.js +113 -13
  14. package/dist/config/config.d.ts +9 -0
  15. package/dist/config/config.js +13 -2
  16. package/dist/drivers/herdr.d.ts +5 -0
  17. package/dist/drivers/herdr.js +14 -3
  18. package/dist/drivers/types.d.ts +1 -0
  19. package/dist/gates/baseline.d.ts +5 -1
  20. package/dist/gates/baseline.js +18 -5
  21. package/dist/gates/llm.d.ts +1 -1
  22. package/dist/gates/llm.js +34 -12
  23. package/dist/gates/review.d.ts +46 -2
  24. package/dist/gates/review.js +111 -21
  25. package/dist/gates/run-gates.d.ts +2 -0
  26. package/dist/gates/run-gates.js +19 -10
  27. package/dist/route/router.d.ts +13 -0
  28. package/dist/route/router.js +64 -11
  29. package/dist/run/daemon.d.ts +3 -1
  30. package/dist/run/daemon.js +332 -53
  31. package/dist/run/git.d.ts +25 -0
  32. package/dist/run/git.js +68 -1
  33. package/dist/run/journal.js +16 -7
  34. package/dist/run/operator-state.d.ts +1 -1
  35. package/dist/run/operator-state.js +5 -9
  36. package/dist/tui/cockpit/live-runtime.js +12 -10
  37. package/package.json +1 -1
  38. package/skills/tickmarkr-auto/SKILL.md +1 -1
  39. package/skills/tickmarkr-loop/SKILL.md +1 -1
  40. package/skills/tickmarkr-overseer/SKILL.md +82 -4
  41. package/skills/tickmarkr-overseer/scripts/watch-launch.sh +38 -0
package/dist/run/git.d.ts CHANGED
@@ -179,3 +179,28 @@ export type BaseContainment = {
179
179
  */
180
180
  export declare function declaredBaseContainment(cwd: string, declaredRef: string, targetRef?: string): Promise<BaseContainment>;
181
181
  export declare function removeWorktree(repo: string, dir: string): Promise<void>;
182
+ export declare const REFS_PREFLIGHT_PREFIX = "refs/tickmarkr/preflight";
183
+ export declare const REFS_PROBE_REMEDY = "run from the main repository or a full clone; a sandbox that denies writes under that path cannot host a run";
184
+ export interface RefsProbeOk {
185
+ ok: true;
186
+ path: string;
187
+ refsDir: string;
188
+ }
189
+ export interface RefsProbeRefusal {
190
+ ok: false;
191
+ path: string;
192
+ refsDir: string;
193
+ error: string;
194
+ }
195
+ export type RefsProbeResult = RefsProbeOk | RefsProbeRefusal;
196
+ export declare function refsRefusalMessage(action: "run" | "resume", path: string, error: string): string;
197
+ /**
198
+ * OBS-983/984: prove the repository's common git directory accepts a ref write before starting
199
+ * the daemon. In a linked worktree, git resolves refs to the main repository's git directory.
200
+ * Creates and deletes a real ref under refs/tickmarkr/preflight/ through git itself, leaving
201
+ * nothing behind on success.
202
+ */
203
+ export declare function probeRefsWritable(cwd?: string): Promise<RefsProbeResult>;
204
+ export declare const probeRefs: typeof probeRefsWritable;
205
+ export declare const refsProbe: typeof probeRefsWritable;
206
+ export declare function assertRefsWritable(cwd?: string, action?: "run" | "resume"): Promise<RefsProbeResult>;
package/dist/run/git.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { availableParallelism, tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { StringDecoder } from "node:string_decoder";
@@ -565,3 +565,70 @@ export async function removeWorktree(repo, dir) {
565
565
  await shGit(`rm -rf ${shq(dir)}`, repo);
566
566
  await shGit("git worktree prune", repo);
567
567
  }
568
+ export const REFS_PREFLIGHT_PREFIX = "refs/tickmarkr/preflight";
569
+ export const REFS_PROBE_REMEDY = "run from the main repository or a full clone; a sandbox that denies writes under that path cannot host a run";
570
+ export function refsRefusalMessage(action, path, error) {
571
+ return `refusing to ${action}: repository refs directory ${path} is not writable (${error}). `
572
+ + `Remedy: ${REFS_PROBE_REMEDY}.`;
573
+ }
574
+ /**
575
+ * OBS-983/984: prove the repository's common git directory accepts a ref write before starting
576
+ * the daemon. In a linked worktree, git resolves refs to the main repository's git directory.
577
+ * Creates and deletes a real ref under refs/tickmarkr/preflight/ through git itself, leaving
578
+ * nothing behind on success.
579
+ */
580
+ export async function probeRefsWritable(cwd = process.cwd()) {
581
+ const refsPathRes = await shGit("git rev-parse --git-path refs", cwd);
582
+ if (refsPathRes.code !== 0) {
583
+ const error = (refsPathRes.stderr || refsPathRes.stdout).trim() || "git rev-parse --git-path refs failed";
584
+ return { ok: false, path: "", refsDir: "", error };
585
+ }
586
+ const raw = refsPathRes.stdout.trim();
587
+ let refsDir = resolve(cwd, raw);
588
+ try {
589
+ refsDir = realpathSync(refsDir);
590
+ }
591
+ catch {
592
+ // keep resolved path if realpath fails
593
+ }
594
+ const headRes = await shGit("git rev-parse HEAD", cwd);
595
+ const target = headRes.code === 0 && headRes.stdout.trim()
596
+ ? headRes.stdout.trim()
597
+ : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
598
+ const probeId = `probe-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
599
+ const ref = `${REFS_PREFLIGHT_PREFIX}/${probeId}`;
600
+ const createRes = await shGit(`git update-ref ${shq(ref)} ${shq(target)}`, cwd);
601
+ if (createRes.code !== 0) {
602
+ const error = (createRes.stderr || createRes.stdout).trim() || `git update-ref failed (${createRes.code})`;
603
+ return { ok: false, path: refsDir, refsDir, error };
604
+ }
605
+ const delRes = await shGit(`git update-ref -d ${shq(ref)}`, cwd);
606
+ if (delRes.code !== 0) {
607
+ const error = (delRes.stderr || delRes.stdout).trim() || `git update-ref -d failed (${delRes.code})`;
608
+ return { ok: false, path: refsDir, refsDir, error };
609
+ }
610
+ // Clean up empty directories left behind by git
611
+ try {
612
+ const preflightDir = join(refsDir, "tickmarkr", "preflight");
613
+ if (existsSync(preflightDir) && readdirSync(preflightDir).length === 0) {
614
+ rmSync(preflightDir, { recursive: true, force: true });
615
+ }
616
+ const tmDir = join(refsDir, "tickmarkr");
617
+ if (existsSync(tmDir) && readdirSync(tmDir).length === 0) {
618
+ rmSync(tmDir, { recursive: true, force: true });
619
+ }
620
+ }
621
+ catch {
622
+ // best-effort cleanup of empty dirs
623
+ }
624
+ return { ok: true, path: refsDir, refsDir };
625
+ }
626
+ export const probeRefs = probeRefsWritable;
627
+ export const refsProbe = probeRefsWritable;
628
+ export async function assertRefsWritable(cwd = process.cwd(), action = "run") {
629
+ const probe = await probeRefsWritable(cwd);
630
+ if (!probe.ok) {
631
+ throw new Error(refsRefusalMessage(action, probe.path, probe.error));
632
+ }
633
+ return probe;
634
+ }
@@ -949,18 +949,27 @@ export function gateResultJournalData(gate, pass, details, meta = {}) {
949
949
  const signalBasis = deriveSignalBasis(gate, pass, details, meta);
950
950
  return { gate, pass, details, ...meta, signalBasis, signalQuality: signalQualityFromBasis(signalBasis) };
951
951
  }
952
- // T3 (Sol #2 / Fable F2): one canonical engagement identity, shared by status AND resume. The run-start
953
- // event records graphDefinitionHash (over compiled task definitions only — see graph.graphDefinitionHash);
954
- // this is the single field both consumers read, and the single comparator below is the single place the
955
- // journal↔graph join is decided. unbound (no recorded definition hash, e.g. a pre-v1.44 journal) and
952
+ // T3 (Sol #2 / Fable F2) + OBS-978: one canonical engagement identity, shared by status, plan, the operator
953
+ // fold AND resume. The run-start event records graphDefinitionHash (over compiled task definitions only — see
954
+ // graph.graphDefinitionHash); each audited graph-rehash row (resume --graph-changed) then moves the identity to
955
+ // its `to`, so the recorded hash is the last audited rehash, else run-start. A row is audited when its `from`
956
+ // names the identity it replaced, or the run-start one (all pre-OBS-978 daemons wrote); a row auditing neither
957
+ // binds nothing — the journal is unbound until a release from null. unbound (also a pre-v1.44 journal) and
956
958
  // mismatch are both not-comparable — status renders the notice either way; resume refuses either way and
957
959
  // distinguishes the reason only for its message and the --graph-changed release event.
958
960
  export function recordedGraphDefinitionHash(events) {
961
+ const start = events.find((e) => e.event === "run-start");
962
+ if (!start)
963
+ return undefined;
964
+ const origin = typeof start.data.graphDefinitionHash === "string" ? start.data.graphDefinitionHash : null;
965
+ let recorded = origin;
959
966
  for (const e of events) {
960
- if (e.event === "run-start" && typeof e.data.graphDefinitionHash === "string")
961
- return e.data.graphDefinitionHash;
967
+ if (e.event !== "graph-rehash")
968
+ continue;
969
+ const audited = e.data.from === recorded || e.data.from === origin;
970
+ recorded = audited && typeof e.data.to === "string" ? e.data.to : null;
962
971
  }
963
- return undefined;
972
+ return recorded ?? undefined;
964
973
  }
965
974
  // THE shared comparator (criterion: status and resume decide through one comparator). status reads
966
975
  // .comparable; resume reads .comparable plus .reason/.recorded for its refusal message and the release.
@@ -57,7 +57,7 @@ export declare class OperatorStateFold {
57
57
  private tasks;
58
58
  private start?;
59
59
  private startEvent?;
60
- private latestGraphRehash?;
60
+ private rehashes;
61
61
  private end?;
62
62
  private active;
63
63
  private approved;
@@ -10,7 +10,7 @@ export class OperatorStateFold {
10
10
  tasks = new Map();
11
11
  start;
12
12
  startEvent;
13
- latestGraphRehash;
13
+ rehashes = [];
14
14
  end;
15
15
  active = false;
16
16
  approved = false;
@@ -34,7 +34,7 @@ export class OperatorStateFold {
34
34
  this.tipFailed = false;
35
35
  }
36
36
  if (e.event === "graph-rehash")
37
- this.latestGraphRehash = { ...e, data: { from: e.data.from, to: e.data.to } };
37
+ this.rehashes = [...this.rehashes, { ...e, data: { from: e.data.from, to: e.data.to } }];
38
38
  if (e.event === "tip-verify-failed" || (e.event === "tip-verify" && e.data.pass === false))
39
39
  this.tipFailed = true;
40
40
  if (e.event === "run-end") {
@@ -146,13 +146,9 @@ export class OperatorStateFold {
146
146
  comparableTo(hash) {
147
147
  if (!hash)
148
148
  return false;
149
- const events = [this.startEvent, this.latestGraphRehash].filter((e) => e !== undefined);
150
- const baseline = engagementComparable(events, hash);
151
- if (this.latestGraphRehash) {
152
- const from = baseline.comparable ? baseline.recorded : baseline.reason === "mismatch" ? baseline.recorded : null;
153
- return this.latestGraphRehash.data.to === hash && this.latestGraphRehash.data.from === from;
154
- }
155
- return baseline.comparable;
149
+ // The shared comparator audits the rehash chain; the fold keeps only the rows it reads.
150
+ const events = [this.startEvent, ...this.rehashes].filter((e) => e !== undefined);
151
+ return engagementComparable(events, hash).comparable;
156
152
  }
157
153
  }
158
154
  /** C1/C6 share this pure reader; callers supply the same observation and journal snapshot. */
@@ -467,18 +467,20 @@ export async function runConsolidatedCockpit(options) {
467
467
  const reader = createPointerReportReader();
468
468
  const stdin = new Proxy(input, { get(target, property) {
469
469
  if (property === "read")
470
- return (...args) => {
470
+ return () => {
471
471
  try {
472
- const chunk = target.read(...args);
473
- if (chunk == null)
474
- return null;
475
- const result = reader(String(chunk));
476
472
  let keys = "";
477
- for (const token of result.tokens) {
478
- if (token.type === "pointer")
479
- pointer(token.report);
480
- else
481
- keys += token.bytes;
473
+ // OBS-965: drain until the stream itself says empty. A real tty (highWaterMark 0) stops its
474
+ // handle after every chunk and only a read() that finds the buffer EMPTY restarts it; Ink's
475
+ // loop stops at the first null, so returning null after ONE pointer-only chunk left that
476
+ // empty read unmade and every later key queued in the kernel — the deaf board.
477
+ for (let chunk = target.read(); chunk != null; chunk = target.read()) {
478
+ for (const token of reader(String(chunk)).tokens) {
479
+ if (token.type === "pointer")
480
+ pointer(token.report);
481
+ else
482
+ keys += token.bytes;
483
+ }
482
484
  }
483
485
  // A standalone Escape is unambiguous after Ink's own input grace.
484
486
  if (reader.pending() === "\x1b")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@ After sending, **confirm delivery** by reading the target pane and verifying the
92
92
  1. **Prepare** — confirm the target list. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
93
93
  2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
94
94
  3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
95
- 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal events rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
95
+ 4. **Run** — run `tickmarkr run`. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
96
96
  5. **Verify and consolidate** — continue only after a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", and the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty — a run with a parked task is partial, not green. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
97
97
  6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout; redirect explicitly beside the spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records.
98
98
  7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
@@ -88,7 +88,7 @@ When spawning consultants (agents gathering synthesis input for decisions like S
88
88
  1. **Prepare** — start from the requested spec. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
89
89
  2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
90
90
  3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
91
- 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal events rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
91
+ 4. **Run** — run `tickmarkr run`. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
92
92
  5. **Verify and consolidate** — accept only a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", and the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty — a run with a parked task is partial, not green. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
93
93
  6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout. Redirect it explicitly beside the source spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
94
94
 
@@ -149,6 +149,35 @@ through brief lineage. **An executor choice nobody made is still an executor cho
149
149
  guidance belongs in the memory file or the shipped docs.
150
150
  4. Arm the watcher and your own supervision beat (Supervision). Report the hierarchy map (pane ids + names) to the user.
151
151
 
152
+ ### Restart, seat identity, and verified launch law
153
+
154
+ These rules apply after **ANY restart**, whether the overseer itself or Herdr restarted. First re-read the
155
+ overseer's own pane id from Herdr; never continue with an id remembered before the restart. Then re-announce
156
+ that fresh pane id to **every live seat** and re-arm **every watcher** with it, verifying each announcement and
157
+ arm by reading back the resulting pane/process state. A live seat must never keep a pre-restart overseer pane id.
158
+ When briefing a seat, take the overseer's address from the handoff file; never hardcode a pane id in a
159
+ brief or command template.
160
+
161
+ The orchestrator's run log, handoff, and plan live inside its sandbox root and are artifacts of that orchestrator.
162
+ The overseer reads them from there, using the path the orchestrator reports; do not substitute the
163
+ overseer's repository or a machine-global planning directory. The orchestrator brief must say: if the seat's
164
+ sandbox denies writes under `.git`, report the denial to the overseer and stop; the overseer launches the daemon itself;
165
+ and the daemon host is never sandboxed.
166
+
167
+ After every `agent start`, wait for and read the model banner. Only then deliver the brief with `pane run`,
168
+ then read the transcript back and verify that the brief's first words are present before any deadline is armed.
169
+ That read-back proves the brief was sent; a brief absent from the transcript was not sent. Every Claude
170
+ seat is started with `--effort high` and has its banner read. Inventories retain the **full suite log**, not a
171
+ tail or summary, and no one runs `git checkout` in a clone while that clone's suite is running.
172
+
173
+ ### Seat-spawn and Leg-2 recipes
174
+
175
+ Every mission to a Claude or Grok seat is delivered only with `herdr pane run <pane> "<message>"` and
176
+ verified by reading the pane back; never use `agent prompt` for mission delivery. Launch a Grok seat with
177
+ `herdr agent start <seat> --kind grok --pane <pane> -- -m grok-4.6`. For Leg-2, a Codex reviewer under
178
+ `workspace-write` must be briefed with an in-worktree verdict path such as
179
+ `<repo>/.tickmarkr/overseer/verdicts/<task>.md`, and its verdict must be written there before it is read.
180
+
152
181
  ## Supervising tickmarkr as the executor — WHO DOES WHAT
153
182
 
154
183
  When the mission runs `/tickmarkr-auto` (tickmarkr dispatches the workers), supervision changes shape —
@@ -204,6 +233,9 @@ journal tail to decide what happens next, or sweeping orphans — you have taken
204
233
  - **The journal is the source of truth**, not panes. Watchers go on `run-end` / `task-human` /
205
234
  `task-failed` / `consult-verdict`; never sleep-poll inside an agent turn. **Never key a watcher on an
206
235
  agent's `done`** — that is turn end and fires the moment a seat finishes acknowledging you.
236
+ A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for those terminal
237
+ events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and
238
+ ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake.
207
239
  **All four are covered by one shipped instrument** — `scripts/watch-journal.sh <runs-dir> [poll] [cap]
208
240
  [events-csv]` — which arms on a line baseline, wakes once, and grades a `run-end` against every green
209
241
  clause. `scripts/watch-parks.sh` stays the park-specific wake for THIS seat (it counts parks and speaks
@@ -476,6 +508,52 @@ number — an unmeasured budget is not a small budget.
476
508
  .claude/skills/tickmarkr-overseer/scripts/watch-context.sh overseer <overseer-agent-or-pane> 50 50 <handoff-file>
477
509
  ```
478
510
 
511
+ ### A GO has a deadline — arm `watch-launch.sh` in the same act as the GO
512
+
513
+ A GO that produces no run is a silent failure until someone notices; on 2026-09-11 an orchestrator's codex
514
+ sandbox was rooted at the main repo, the spec worktree was outside its writable roots, it stopped at the
515
+ denial without reporting, the overseer's 10-minute wake expired un-re-armed, and three hours passed.
516
+ Two rules close that hole:
517
+
518
+ - **Every orchestrator seat is sandbox-rooted at the worktree it will run in** (`cd <worktree>` before
519
+ `herdr agent start … --sandbox workspace-write`), and its brief says: *a denied path or refused command
520
+ is reported to the overseer pane within 60 s — never a silent stop.*
521
+ - **The overseer arms the launch watcher in the SAME act as the GO**, with the lock path the run will
522
+ create, and treats `LAUNCH_OVERDUE` as a first-class event (read the orchestrator pane, fix the seat,
523
+ re-issue the GO):
524
+
525
+ ```bash
526
+ .claude/skills/tickmarkr-overseer/scripts/watch-launch.sh <worktree>/.tickmarkr/graph.lock 900 <overseer-pane> &
527
+ ```
528
+
529
+ It prints `LAUNCH_OK` with the lock's contents when the run starts (exit 0) and, past the deadline, delivers
530
+ `LAUNCH OVERDUE …` to the overseer pane AND as an OS notification (exit 3). Any wake you arm yourself with
531
+ a cap (a background `until` loop) must be RE-ARMED on every expiry; an expired wake is not a watch.
532
+
533
+
534
+ ### A GO has a deadline — arm `watch-launch.sh` in the same act as the GO
535
+
536
+ A GO that produces no run is a silent failure until someone notices; on 2026-09-11 an orchestrator's codex
537
+ sandbox was rooted at the main repo, the spec worktree was outside its writable roots, it stopped at the
538
+ denial without reporting, the overseer's 10-minute wake expired un-re-armed, and three hours passed.
539
+ Two rules close that hole:
540
+
541
+ - **Every orchestrator seat is sandbox-rooted at the worktree it will run in** (`cd <worktree>` before
542
+ `herdr agent start … --sandbox workspace-write`), and its brief says: *a denied path or refused command
543
+ is reported to the overseer pane within 60 s — never a silent stop.*
544
+ - **The overseer arms the launch watcher in the SAME act as the GO**, with the lock path the run will
545
+ create, and treats `LAUNCH_OVERDUE` as a first-class event (read the orchestrator pane, fix the seat,
546
+ re-issue the GO):
547
+
548
+ ```bash
549
+ .claude/skills/tickmarkr-overseer/scripts/watch-launch.sh <worktree>/.tickmarkr/graph.lock 900 <overseer-pane> &
550
+ ```
551
+
552
+ It prints `LAUNCH_OK` with the lock's contents when the run starts (exit 0) and, past the deadline, delivers
553
+ `LAUNCH OVERDUE …` to the overseer pane AND as an OS notification (exit 3). Any wake you arm yourself with
554
+ a cap (a background `until` loop) must be RE-ARMED on every expiry; an expired wake is not a watch.
555
+
556
+
479
557
  The first argument chooses the closed per-seat tier (`orchestrator-context` or `overseer-context`),
480
558
  and every beat names the second argument as that tier's seat. The watcher beats only after reading a
481
559
  rendered percentage, keeps beating on the supervision cadence even when its requested poll is slower,
@@ -580,9 +658,9 @@ they are left implicit:
580
658
  Send only when the seat is idle and the ANSI prompt line is empty or dim-only (the Esc/SGR discriminator
581
659
  separates an autosuggest ghost from typed input), then read back activity or an ACK; presence is not
582
660
  delivery. If a stale draft must be replaced, supersede it explicitly with
583
- `agent prompt " <-- disregard … ACTUAL: …"` instead of stacking another instruction behind it.
661
+ `herdr pane run <pane> "<-- disregard … ACTUAL: …"` instead of stacking another instruction behind it.
584
662
  - **A MESSAGE TO A WORKING SEAT IS A QUEUED MESSAGE, AND THE QUEUE DRAINS ONLY AT TURN BOUNDARIES.**
585
- Delivery is not arrival: `agent prompt` to a `working` claude seat lands in its queue (`Press up to
663
+ Delivery is not arrival: a message sent to a `working` claude seat lands in its queue (`Press up to
586
664
  edit queued messages` on the seat's prompt line is the tell) and is READ only when the current turn
587
665
  ends — and with in-process teammates a turn runs 20–40 minutes, so steering latency equals subagent
588
666
  runtime. Measured 2026-08-17/18 (P98 leg 1): a FREEZE HOLD and a checker-release directive stacked
@@ -622,7 +700,7 @@ they are left implicit:
622
700
  - **AGENT NAMES ARE GLOBAL ACROSS WORKSPACES — verify a seat you spawned by PANE ID, never by name.**
623
701
  Names must be unique among live agents *everywhere*, not within your workspace, so another workspace can
624
702
  already hold `opus`, `sol`, `reviewer` or `orch`. When it does, your `agent start` **fails**, your pane
625
- is left a bare shell, and `agent list` / `agent read` / `agent prompt` for that name then resolve to the
703
+ is left a bare shell, and `agent list` / `agent read` for that name then resolve to the
626
704
  **stranger's seat**. Measured 2026-08-06 (OBS-392): a spawn of `fable` collided with a live seat in
627
705
  another workspace; `agent list` reported `fable -> blocked` and it was read as *this* seat coming up
628
706
  blocked. It was an operator research session sitting on a *"Resume full session?"* prompt. One more
@@ -644,7 +722,7 @@ they are left implicit:
644
722
  Re-arm name-keyed watchers in the same act as the rename; file-keyed artifact watchers are
645
723
  unaffected (one more reason to prefer them).
646
724
  - Stale typed input is unclearable via CLI — supersede it:
647
- `pane run "<-- disregard everything before this arrow (stale draft). ACTUAL: <message>"`.
725
+ `herdr pane run <pane> "<-- disregard everything before this arrow (stale draft). ACTUAL: <message>"`.
648
726
  **But DISCRIMINATE before you supersede or file it: text on an idle seat's prompt line has FOUR
649
727
  authors** — the seat's own draft, an operator, another agent's `agent send` (writes WITHOUT Enter),
650
728
  and claude-code's AUTOSUGGEST, which renders context-plausible ghost text BYTE-IDENTICAL to a typed
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # watch-launch.sh — a GO that produced no run is a silent failure until someone notices. This watcher
3
+ # notices. Arm it in the SAME act as the GO (orchestrator briefed to compile → plan → run) and it waits
4
+ # for the run's lock; when the lock has not appeared by the deadline it delivers LAUNCH OVERDUE to the
5
+ # overseer's pane AND as an OS notification, so the wake reaches a seat instead of a log nobody reads.
6
+ #
7
+ # Why it exists (2026-09-11): an orchestrator's codex sandbox was rooted at the main repo, the spec
8
+ # worktree was outside its writable roots, it stopped at the denial without reporting, and the overseer's
9
+ # own 10-minute wake expired un-re-armed. Three hours passed before anyone looked. A launch has a
10
+ # deadline; silence past it is the event.
11
+ #
12
+ # usage: watch-launch.sh <lock-path> <deadline-s> <overseer-pane> [poll-s]
13
+ # <lock-path> the run's .tickmarkr/graph.lock in the worktree the run will be launched in
14
+ # <deadline-s> seconds from now by which the lock must exist (a compile+plan+launch takes minutes,
15
+ # never hours; 900 is a generous default for a 7-task spec)
16
+ # <overseer-pane> the pane that must hear about it (herdr pane id), e.g. wZ:p18S
17
+ # [poll-s] poll interval, default 15
18
+ # exit 0 LAUNCH_OK (lock seen; prints its contents) · exit 3 LAUNCH_OVERDUE (delivered) · exit 64 usage
19
+ set -u
20
+ LOCK="${1:-}"; DEADLINE="${2:-}"; PANE="${3:-}"; POLL="${4:-15}"
21
+ [ -n "$LOCK" ] && [ -n "$DEADLINE" ] && [ -n "$PANE" ] || { echo "usage: watch-launch.sh <lock-path> <deadline-s> <overseer-pane> [poll-s]" >&2; exit 64; }
22
+ start=$(date +%s)
23
+ while :; do
24
+ if [ -f "$LOCK" ]; then
25
+ printf 'LAUNCH_OK %s %s\n' "$(date -u +%H:%M:%SZ)" "$(cat "$LOCK" 2>/dev/null | tr -d '\n')"
26
+ exit 0
27
+ fi
28
+ now=$(date +%s)
29
+ if [ $((now - start)) -ge "$DEADLINE" ]; then
30
+ msg="LAUNCH OVERDUE $(date -u +%H:%M:%SZ): no lock at $LOCK after ${DEADLINE}s — read the orchestrator pane NOW (sandbox denial? preflight refusal? unsubmitted GO?)"
31
+ echo "LAUNCH_OVERDUE $msg"
32
+ # Both deliveries, always: a pane the overseer reads AND a notification the operator sees.
33
+ herdr pane run "$PANE" "$msg" >/dev/null 2>&1 || echo " (pane delivery failed — the notification is the only path)"
34
+ herdr notification show "$msg" >/dev/null 2>&1 || true
35
+ exit 3
36
+ fi
37
+ sleep "$POLL"
38
+ done