tickmarkr 1.92.0 → 1.93.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/dist/config/fleet-overlay.js +49 -20
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -1
- package/skills/tickmarkr-loop/SKILL.md +2 -1
- package/skills/tickmarkr-overseer/SKILL.md +156 -6
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +140 -0
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +5 -2
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +17 -1
|
@@ -88,7 +88,7 @@ function setScalarPreservingComment(doc, path, value, authoredComment) {
|
|
|
88
88
|
if (isScalar(existing))
|
|
89
89
|
existing.value = value;
|
|
90
90
|
else
|
|
91
|
-
doc
|
|
91
|
+
setAt(doc, path, doc.createNode(value));
|
|
92
92
|
if (authoredComment !== undefined) {
|
|
93
93
|
const written = doc.getIn(path, true);
|
|
94
94
|
if (isScalar(written))
|
|
@@ -112,11 +112,11 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
112
112
|
if (comments.length)
|
|
113
113
|
tombstone.comment = comments.join("\n");
|
|
114
114
|
}
|
|
115
|
-
doc
|
|
115
|
+
setAt(doc, path, tombstone);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
if (!isSeq(existing)) {
|
|
119
|
-
doc
|
|
119
|
+
setAt(doc, path, doc.createNode(values));
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
const available = [...existing.items];
|
|
@@ -127,6 +127,34 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
127
127
|
return doc.createNode(value);
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
// OBS-533: yaml's setIn/deleteIn throw on any non-collection intermediate ("Expected YAML
|
|
131
|
+
// collection at spec. Remaining path: pin"), and a v1.1 null tombstone (`spec:` under
|
|
132
|
+
// routing.map) is exactly that scalar — deepMerge prunes it before schema validation, so the
|
|
133
|
+
// overlay loads clean and the first fleet write under the tombstoned key crashed the TUI.
|
|
134
|
+
// Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
|
|
135
|
+
// denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
|
|
136
|
+
// the routing.allow precedent below.
|
|
137
|
+
function deleteAt(doc, path) {
|
|
138
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
139
|
+
if (!isMap(doc.getIn(path.slice(0, depth), true)))
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// deleteIn throws on an empty document — only delete keys that exist.
|
|
143
|
+
if (doc.getIn(path) !== undefined)
|
|
144
|
+
doc.deleteIn(path);
|
|
145
|
+
}
|
|
146
|
+
function setAt(doc, path, value) {
|
|
147
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
148
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
149
|
+
if (node === undefined)
|
|
150
|
+
break;
|
|
151
|
+
if (!isMap(node)) {
|
|
152
|
+
doc.deleteIn(path.slice(0, depth));
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
doc.setIn(path, value);
|
|
157
|
+
}
|
|
130
158
|
function deleteEmptyMap(doc, path) {
|
|
131
159
|
const node = doc.getIn(path, true);
|
|
132
160
|
if (isMap(node) && node.items.length === 0)
|
|
@@ -150,27 +178,26 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
150
178
|
const allowNode = doc.getIn(["routing", "allow"], true);
|
|
151
179
|
if (allowNode !== undefined && !isMap(allowNode))
|
|
152
180
|
doc.deleteIn(["routing", "allow"]);
|
|
153
|
-
// deleteIn throws on an empty document — only delete keys that exist.
|
|
154
181
|
if (form.adapters.length) {
|
|
155
182
|
setStringSequencePreservingComments(doc, ["routing", "allow", "adapters"], form.adapters);
|
|
156
183
|
}
|
|
157
|
-
else
|
|
158
|
-
doc
|
|
184
|
+
else {
|
|
185
|
+
deleteAt(doc, ["routing", "allow", "adapters"]);
|
|
159
186
|
}
|
|
160
187
|
if (form.models.length) {
|
|
161
188
|
setStringSequencePreservingComments(doc, ["routing", "allow", "models"], form.models);
|
|
162
189
|
}
|
|
163
|
-
else
|
|
164
|
-
doc
|
|
190
|
+
else {
|
|
191
|
+
deleteAt(doc, ["routing", "allow", "models"]);
|
|
165
192
|
}
|
|
166
193
|
// Whole fleet out: allow stays present but empty — fail-closed, nothing admitted.
|
|
167
194
|
if (doc.getIn(["routing", "allow"]) === undefined) {
|
|
168
|
-
doc
|
|
195
|
+
setAt(doc, ["routing", "allow"], doc.createNode({}));
|
|
169
196
|
}
|
|
170
197
|
}
|
|
171
|
-
else
|
|
198
|
+
else {
|
|
172
199
|
// Whole fleet in: no restriction to express — the allow block goes away entirely.
|
|
173
|
-
doc
|
|
200
|
+
deleteAt(doc, ["routing", "allow"]);
|
|
174
201
|
}
|
|
175
202
|
// Residuals stay in deny; covered scopes are tombstoned so a lower layer can never
|
|
176
203
|
// re-exclude behind the operator's back (workers untouched).
|
|
@@ -189,9 +216,9 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
189
216
|
const after = edited.map[shape];
|
|
190
217
|
if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
|
|
191
218
|
if (after?.pin === undefined)
|
|
192
|
-
doc
|
|
219
|
+
deleteAt(doc, ["routing", "map", shape, "pin"]);
|
|
193
220
|
else
|
|
194
|
-
doc
|
|
221
|
+
setAt(doc, ["routing", "map", shape, "pin"], doc.createNode(after.pin));
|
|
195
222
|
}
|
|
196
223
|
if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
|
|
197
224
|
const path = ["routing", "map", shape, "pool"];
|
|
@@ -200,9 +227,6 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
200
227
|
setStringSequencePreservingComments(doc, path, null);
|
|
201
228
|
}
|
|
202
229
|
else {
|
|
203
|
-
const existing = doc.getIn(path, true);
|
|
204
|
-
if (existing !== undefined && !isMap(existing))
|
|
205
|
-
doc.deleteIn(path);
|
|
206
230
|
setScalarPreservingComment(doc, [...path, "mode"], after.pool.mode);
|
|
207
231
|
// Channel order is semantic (ordered walks it; any breaks ties by it) — dedupe, never sort.
|
|
208
232
|
setStringSequencePreservingComments(doc, [...path, "channels"], [...new Set(after.pool.channels)]);
|
|
@@ -213,7 +237,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
213
237
|
// the pin/pool declaration owns the whole slot at merge (deepMerge drops the lower
|
|
214
238
|
// layer's pin/pool/prefer atomically) — an [] mask here would re-declare prefer BESIDE
|
|
215
239
|
// the pool in one document and fail the exclusivity refine; delete the key instead
|
|
216
|
-
doc
|
|
240
|
+
deleteAt(doc, ["routing", "map", shape, "prefer"]);
|
|
217
241
|
}
|
|
218
242
|
else {
|
|
219
243
|
// Clearing a resolved list with no replacing declaration must mask the lower layer
|
|
@@ -223,7 +247,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
223
247
|
}
|
|
224
248
|
if (before?.escalate !== after?.escalate) {
|
|
225
249
|
if (after?.escalate === undefined)
|
|
226
|
-
doc
|
|
250
|
+
deleteAt(doc, ["routing", "map", shape, "escalate"]);
|
|
227
251
|
else
|
|
228
252
|
setScalarPreservingComment(doc, ["routing", "map", shape, "escalate"], after.escalate);
|
|
229
253
|
}
|
|
@@ -233,7 +257,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
233
257
|
continue;
|
|
234
258
|
const tier = edited.floors[shape];
|
|
235
259
|
if (tier === undefined)
|
|
236
|
-
doc
|
|
260
|
+
deleteAt(doc, ["routing", "floors", shape]);
|
|
237
261
|
else
|
|
238
262
|
setScalarPreservingComment(doc, ["routing", "floors", shape], tier);
|
|
239
263
|
}
|
|
@@ -283,7 +307,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
283
307
|
if (JSON.stringify(before) === JSON.stringify(after))
|
|
284
308
|
continue;
|
|
285
309
|
if (after === undefined) {
|
|
286
|
-
doc
|
|
310
|
+
deleteAt(doc, [key, "prefer"]);
|
|
287
311
|
deleteEmptyMap(doc, [key]);
|
|
288
312
|
}
|
|
289
313
|
else {
|
|
@@ -291,6 +315,11 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
291
315
|
}
|
|
292
316
|
}
|
|
293
317
|
}
|
|
318
|
+
// Every mutation no-oped against an empty or comment-only overlay (contents stays null;
|
|
319
|
+
// deleteAt never creates content): return the prior bytes verbatim — stringifying a null
|
|
320
|
+
// document would write a literal `null` line into the file.
|
|
321
|
+
if (doc.contents === null)
|
|
322
|
+
return priorBytes;
|
|
294
323
|
// OBS-505: fleet's two-space note style (`tier: mid # note`) applies to INLINE comments only.
|
|
295
324
|
// The previous commentString prefixed " #" onto EVERY comment line, so block comments — the
|
|
296
325
|
// whole init scaffold at column 0, and indented operator essays — each gained a stray leading
|
package/package.json
CHANGED
|
@@ -19,12 +19,13 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
19
19
|
- **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
|
|
20
20
|
- **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
21
21
|
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
22
|
+
- **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
|
|
22
23
|
|
|
23
24
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
24
25
|
|
|
25
26
|
## Stand-down (mission end and retirement)
|
|
26
27
|
|
|
27
|
-
On each mission's terminal state, after the record commit and operator notification: the orchestrator stops every monitor and background task it started, prints one final stand-down line, and leaves nothing queued in its input box. A finished session with an armed watcher or pre-filled input is a loaded gun.
|
|
28
|
+
On each mission's terminal state, after the record commit and operator notification: the orchestrator stops every monitor and background task it started, sweeps the heartbeat/beat files its watchers wrote (a stale beat beside a live one reads as coverage to whoever globs the directory), prints one final stand-down line, and leaves nothing queued in its input box. A finished session with an armed watcher or pre-filled input is a loaded gun.
|
|
28
29
|
|
|
29
30
|
## Invariants
|
|
30
31
|
|
|
@@ -18,6 +18,7 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
18
18
|
- **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
|
|
19
19
|
- **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
20
20
|
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
21
|
+
- **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
|
|
21
22
|
|
|
22
23
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
23
24
|
|
|
@@ -60,7 +61,7 @@ When spawning consultants (agents gathering synthesis input for decisions like S
|
|
|
60
61
|
|
|
61
62
|
## Stand-down (mission end and retirement)
|
|
62
63
|
|
|
63
|
-
- **Orchestrator, on terminal state** (green, failed, or parked), after the record commit and operator notification: stop every monitor and background task you started, print one final stand-down line, and leave NOTHING queued in your input box. A finished session with an armed watcher or pre-filled input is a loaded gun — a retired v1.40 orchestrator sat idle with "merge … tag, publish" unsent in its input; one stray Enter would have shipped a duplicate release.
|
|
64
|
+
- **Orchestrator, on terminal state** (green, failed, or parked), after the record commit and operator notification: stop every monitor and background task you started, sweep the heartbeat/beat files your watchers wrote (a stale beat beside a live one reads as coverage to whoever globs the directory), print one final stand-down line, and leave NOTHING queued in your input box. A finished session with an armed watcher or pre-filled input is a loaded gun — a retired v1.40 orchestrator sat idle with "merge … tag, publish" unsent in its input; one stray Enter would have shipped a duplicate release.
|
|
64
65
|
- **Supervisor, when a mission completes** (and always before spawning the next orchestrator): verify the orchestrator stood down, then close its tab. Seeming input-box text in a retired pane can be the TUI's dim ghost-text suggestion, not queued input — confirm with an ANSI read (dim escape around the text) or type-one-char-and-read-back before treating it as the loaded gun; close the tab either way. The journal, execution record, OBS ledger, and memory hold the story; pane scrollback is disposable. Never leave a retired agent idle with watchers armed.
|
|
65
66
|
|
|
66
67
|
## The loop
|
|
@@ -12,6 +12,19 @@ to the user with evidence.
|
|
|
12
12
|
The mission is the skill argument. If empty, ask the user what to run end-to-end before doing anything else.
|
|
13
13
|
Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
14
14
|
|
|
15
|
+
**THE ENGINE IS THE DEFAULT EXECUTOR.** A mission that names a milestone, a phase, or a spec runs the
|
|
16
|
+
loop: `tickmarkr compile` → `plan` → `run` → `report`, and the JOURNAL is the record. `compile` ingests
|
|
17
|
+
GSD phase plans (`src/compile/gsd.ts`), so *"this repo uses GSD"* is not a reason to bypass it. The
|
|
18
|
+
supervised GSD flow (below) is the EXCEPTION: it exists only on an explicit operator order, recorded in
|
|
19
|
+
the brief WITH its costs named — no journal (so no per-task adapter/model record), no routing, no
|
|
20
|
+
enforced gate battery, no enforced cross-vendor review; each is re-implemented by hand or silently lost.
|
|
21
|
+
**Measured 2026-08-18 (P98):** the operator triggered this skill expecting the engine; the mission ran
|
|
22
|
+
as GSD legs instead — 12 seats, 11 of them one model checking that same model's work, and *"which model
|
|
23
|
+
ran each task"* was unanswerable from every mission artifact (no ruling, log, or brief named a model;
|
|
24
|
+
the answer took session-file archaeology). The flip from the engine (P89, journaled runs on disk) to
|
|
25
|
+
GSD legs (P92) had been RULED NOWHERE — no ledger entry decides it — and then propagated for six phases
|
|
26
|
+
through brief lineage. **An executor choice nobody made is still an executor choice, and it compounds.**
|
|
27
|
+
|
|
15
28
|
## Setup
|
|
16
29
|
|
|
17
30
|
0. **Adopt before you build.** If this workspace already has a supervision hierarchy — an
|
|
@@ -22,11 +35,17 @@ Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
|
22
35
|
status, and either ADOPT the
|
|
23
36
|
existing orchestrator (updated brief, re-armed watchers) or, if the old hierarchy is dead, archive the
|
|
24
37
|
stale brief and build fresh.
|
|
38
|
+
⚠ **Adopting a hierarchy silently adopts its EXECUTOR CHOICE.** The P92→P98 GSD drift propagated
|
|
39
|
+
exactly this way: each overseer read the prior brief, reproduced "the same two-leg pattern as the
|
|
40
|
+
last three phases", and the unruled bypass of the engine became load-bearing through repetition.
|
|
41
|
+
At every adopt, re-derive the executor question — *"why is this milestone not compiled?"* — and if
|
|
42
|
+
the answer is not a recorded operator order, route the mission back through the engine.
|
|
25
43
|
0a. **READ THE PROJECT MEMORY BEFORE YOU START — it already contains discipline you are about to re-earn.**
|
|
26
44
|
`~/.claude/projects/<cwd-slug>/memory/` (slug = the absolute cwd with `/` → `-`). Read `MEMORY.md`, then
|
|
27
|
-
`ls` the topic entries and open every one whose name concerns METHOD
|
|
28
|
-
milestone — names like `*-discipline`, `*-drill`,
|
|
29
|
-
`consults-*`, `agent
|
|
45
|
+
`ls` the topic entries and open every one whose name concerns METHOD, DISCIPLINE, or a STANDING
|
|
46
|
+
OPERATOR LAYOUT/CONVENTION rather than a shipped milestone — names like `*-discipline`, `*-drill`,
|
|
47
|
+
`*-parity`, `*-least-permission`, `context-reset-*`, `consults-*`, `agent-*`, `*-tab-layout`,
|
|
48
|
+
`*-visible-*`, `*-panes*`, `user-tabs-*`.
|
|
30
49
|
**Earned 2026-08-04, expensively.** That directory held `…-falsification-drill-discipline.md`, written
|
|
31
50
|
three weeks earlier: *"a gate or grep-pin is assumed WRONG until a falsification drill proves it bites…
|
|
32
51
|
run the drill that should redden it and SEE the red before trusting green."* That is Evidence discipline
|
|
@@ -35,8 +54,33 @@ Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
|
35
54
|
mission-scoped brief. **A memory that exists and is never opened costs more than one that was never
|
|
36
55
|
written, because everyone assumes the lesson is somewhere.** Entries may predate a project rename; search
|
|
37
56
|
by concept, not by the current product name.
|
|
57
|
+
**And the sweep scope is itself a recorded defect: a filter limited to discipline names SKIPS the
|
|
58
|
+
layout canon, and that skip is paid.** The layout entry records a 2026-08-05 correction it caused —
|
|
59
|
+
*"widen the start-of-session read to include layout/convention entries, not only discipline ones"* —
|
|
60
|
+
and on 2026-08-17 the same skip put a planning seat inside the ORCH tab. A standing operator layout is
|
|
61
|
+
not cosmetic; it is how the operator reads the fleet, and it binds exactly like a discipline rule.
|
|
38
62
|
1. Load the `herdr` skill. `herdr pane list` to map the workspace — the focused pane is yours. Rename your
|
|
39
63
|
tab OVERSEER; create ONE tab ORCHESTRATOR.
|
|
64
|
+
**FIVE-TAB CANON (standing operator layout — corrected three times on 2026-07-27, layout approved
|
|
65
|
+
2026-07-29, re-earned 2026-08-17):**
|
|
66
|
+
- `OVERSEER` — you, plus a visible pipeline watch pane running the SHIPPED surface (`tickmarkr ui`,
|
|
67
|
+
newest-run default; never a hand-rolled status loop — the operator flagged an ad-hoc shell-loop
|
|
68
|
+
watcher as "strange watch design"). A supervision watcher buried in an invisible background task
|
|
69
|
+
reads as "no watcher" to the operator even when armed.
|
|
70
|
+
- `ORCH` — the orchestrator, the run's watch board (split `--direction right`, ~0.6 to the board —
|
|
71
|
+
never `down`; the board needs full height for its task matrix), and the raw journal feed. **Nothing
|
|
72
|
+
else, ever: a work seat NEVER splits into the ORCH tab**, even when width arithmetic allows a second
|
|
73
|
+
column. Operator verbatim: *"in orch tab should be the orch and the watcher only."* Re-earned
|
|
74
|
+
2026-08-17: a planning seat split beside the orchestrator, and the operator caught it, again.
|
|
75
|
+
- Worker/seat tabs — tickmarkr opens ONE TAB PER TASK itself; GSD-leg seats get the same treatment
|
|
76
|
+
(own tab, or a shared WORKERS tab), never the ORCH tab.
|
|
77
|
+
- `CONSULT · <topic>` — ONE shared tab for ALL consultants of a round, side-by-side splits; never one
|
|
78
|
+
tab per consultant; do NOT auto-close after adjudication (operator, 2026-08-09 — keep the round's
|
|
79
|
+
panes until the thread is confirmed finished or a successor round supersedes them).
|
|
80
|
+
- `REVIEW <task>` — reviewer panes.
|
|
81
|
+
Never multiply beyond these without asking. **Tabs you did not create are the OPERATOR'S — provenance
|
|
82
|
+
decides: never close, rename, reuse, or send input to one, however idle it looks.** An "idle
|
|
83
|
+
stale-looking" tab an overseer once swept was the operator's in-progress thinking (2026-07-14).
|
|
40
84
|
**Live tab labels (standing operator rule, 2026-07-12):** on every decision or state change (role
|
|
41
85
|
handoff, task done/merged, run end) rename the affected tabs — and keep labels SHORT: the role as the
|
|
42
86
|
main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
|
|
@@ -48,8 +92,9 @@ Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
|
48
92
|
truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
|
|
49
93
|
(inside the tickmarkr state dir — already self-gitignored, no exclude step needed), then send one line:
|
|
50
94
|
`herdr pane run <orch> "Read .tickmarkr/overseer/ORCH-BRIEF.md and follow it exactly."` The brief MUST contain: the
|
|
51
|
-
mission, the pane mechanics below, rules 1–2,
|
|
52
|
-
|
|
95
|
+
mission, the five-tab canon from step 1, the pane mechanics below, rules 1–2, the GSD-leg rules
|
|
96
|
+
(below) whenever the mission dispatches `/gsd:*` legs, and require a verbatim one-sentence
|
|
97
|
+
acknowledgment of the human-checkpoint rule before anything is dispatched.
|
|
53
98
|
**⚠ HARVEST BEFORE YOU DELETE.** At mission end the brief dir goes — but a long mission accumulates
|
|
54
99
|
*method guards* in that brief (how to know a thing, not what is true of this spec), and deleting them
|
|
55
100
|
re-earns each one at full price on the next mission. So before removing the dir: lift every durable,
|
|
@@ -192,12 +237,68 @@ is a lossy summary nobody trusts while a clean session re-oriented from disk-ver
|
|
|
192
237
|
good, not after. If your own context cannot be read by the watcher, say so to the operator and ask for the
|
|
193
238
|
number — an unmeasured budget is not a small budget.
|
|
194
239
|
|
|
240
|
+
## Supervising GSD legs — when the mission dispatches `/gsd:*` instead of `tickmarkr run`
|
|
241
|
+
|
|
242
|
+
Milestones that alternate GSD legs (`/gsd:plan-phase N`, `/gsd:execute-phase N`) under this hierarchy get
|
|
243
|
+
none of the engine's dispatcher, routing, or gates — every guarantee `tickmarkr run` provides has to be
|
|
244
|
+
demanded in the brief instead. **This path is the EXCEPTION and requires the recorded operator order
|
|
245
|
+
named in the default-executor rule at the top of this skill.** Five guarantees get dropped every time
|
|
246
|
+
they are left implicit:
|
|
247
|
+
|
|
248
|
+
0. **A seats ledger stands in for the journal's assignment records.** Every seat spawn — orchestrator,
|
|
249
|
+
planner, checker, executor, verifier, consult — appends ONE JSON line to
|
|
250
|
+
`<state-dir>/overseer/seats.jsonl` in the same act as the spawn:
|
|
251
|
+
`{"ts":"<iso>","seat":"<name>","pane":"<id>","tab":"<label>","adapter":"<cli>","model":"<model>","role":"<role>","brief":"<path>"}`.
|
|
252
|
+
The journal answers *"which model ran each task"* in one line; without this file the answer is
|
|
253
|
+
session-file archaeology, and the model monoculture it would have exposed stays invisible (measured
|
|
254
|
+
2026-08-18: 12 seats, no artifact naming any seat's model). ⚠ Interim per rule 27 — removal
|
|
255
|
+
condition: the engine runs the milestone and the journal is the record.
|
|
256
|
+
|
|
257
|
+
1. **GSD's Agent-tool default is OVERRIDDEN — name the trap in every seat brief.** `/gsd:plan-phase` and
|
|
258
|
+
`/gsd:execute-phase` fan their work out to in-process Task subagents: invisible to every watcher tier,
|
|
259
|
+
billed to the seat's own context, visible only in the seat's status footer. Measured 2026-08-17 (P98
|
|
260
|
+
leg 1): one planning seat ran FIVE in-process subagents to ≈855k tokens — two of them an unexplained
|
|
261
|
+
duplicate respawn pair — and the operator caught it from the footer, not from any tier of supervision.
|
|
262
|
+
The violation was first recorded 2026-07-10, and that record already states the fix: *"generic
|
|
263
|
+
'visible panes' wording is not enough — name the GSD trap."* The concrete mechanism is claude-code
|
|
264
|
+
TEAMMATES (the seat's footer roster; `Teammate @<name> finished` notices): they run INSIDE the
|
|
265
|
+
parent seat's turn, so the seat cannot drain its message queue until every teammate returns —
|
|
266
|
+
**unwatchable and unsteerable are the same defect** (the queued-message law, Pane mechanics).
|
|
267
|
+
Measured 2026-08-18: two freeze-class directives sat queued behind a planner's teammate fan-out
|
|
268
|
+
while the plan set they froze was still editable from that queue, and THE OPERATOR ended the turn
|
|
269
|
+
by hand (Esc, twice) because no seat owned the interrupt. Every GSD seat brief states: subagents
|
|
270
|
+
run as visible herdr panes in per-task tabs (`herdr agent start …`), and a seat report whose work
|
|
271
|
+
was produced by invisible subagents is rejected on read.
|
|
272
|
+
2. **Seats are interactive TUI, never headless `-p`.** The P92–P96 exec lane —
|
|
273
|
+
`cat brief | claude -p … ` in a visible pane — satisfied visibility in the letter only: `claude -p`
|
|
274
|
+
buffers output until exit, so the pane renders idle for the entire run; it is blind to SessionStart
|
|
275
|
+
hook errors (a broken and a fixed hook both return green); and its silence has no midpoint for a
|
|
276
|
+
stall watcher to catch — silent-time equals lifetime. Standing operator rule since 2026-07-13:
|
|
277
|
+
consults and one-off LLM calls run as the CLI's real interactive TUI in a visible named pane.
|
|
278
|
+
Headless is for exit-code probes — a quota check that wants `rc`, never work anyone must watch.
|
|
279
|
+
3. **Buy seat diversity from the live capability matrix, at every dispatch.** When one vendor's model
|
|
280
|
+
quota collapses, the reflex is to collapse every seat onto the surviving model and hold the
|
|
281
|
+
cross-vendor CLI back for a late probe — P97 ran planner, checker and verifier as one family that
|
|
282
|
+
way, and three same-family passes confirmed one wrong anchored conclusion with the refuting fact in
|
|
283
|
+
the room. `<state-dir>/doctor.json` already lists every installed+authed adapter and its models (nine
|
|
284
|
+
were authed on 2026-08-17 while every seat ran claude). Priority when independence is scarce:
|
|
285
|
+
**verifier > checker > planner > executors** — the independent seat goes cross-vendor
|
|
286
|
+
(`herdr agent start … --kind codex`), ruled at dispatch, never debated under time pressure.
|
|
287
|
+
4. **Gate every exec lane with the shipped battery, not hand-rolled greps.**
|
|
288
|
+
`tickmarkr verify --base <ref> --criteria <file>` is the standalone form of the engine's own gates —
|
|
289
|
+
build/test/lint diffed against a recorded baseline, evidence, scope, plus the semantic judges — one
|
|
290
|
+
fail-closed verdict, no daemon, no retries. A per-lane grep gate re-implements a weaker version of
|
|
291
|
+
this and passes on source text the screen never renders, which is exactly the class the acceptance
|
|
292
|
+
judge exists to reject. One command per lane, named in the lane's own brief.
|
|
293
|
+
|
|
195
294
|
## Pane mechanics that bite
|
|
196
295
|
|
|
197
296
|
- **Verified send protocol**: `herdr agent send` writes WITHOUT Enter, and `pane run`'s Enter can be swallowed
|
|
198
297
|
by bracketed-paste on long payloads. Robust sequence: read the pane (bare prompt required) → send-text →
|
|
199
298
|
sleep 2–3s → send-keys Enter → read back (input empty / agent `working`). Never report "briefed" without
|
|
200
|
-
the read-back. Long content goes in a brief file, never pane text.
|
|
299
|
+
the read-back. Long content goes in a brief file, never pane text. `scripts/seat-send.sh` encodes
|
|
300
|
+
this whole path — size guard, atomic prompt, prompt-line read-back, optional interrupt — and never
|
|
301
|
+
auto-resends.
|
|
201
302
|
**PROBE THE READ-BACK WITH THE SHORTEST DISTINCTIVE TOKEN — a commit hash, a pid, an OBS id — NEVER a
|
|
202
303
|
sentence.** A long phrase crosses the pane's render wrap boundary, so grepping for it returns zero on a
|
|
203
304
|
message that arrived intact, and **a badly-probed successful send is byte-identical to a truncated one.**
|
|
@@ -206,6 +307,35 @@ number — an unmeasured budget is not a small budget.
|
|
|
206
307
|
(OBS-396): a grep for the full sentence returned 0 while a grep for one word of the same sentence
|
|
207
308
|
returned 1. This trap lives *inside* the verification step above, which is why it survives — the rule
|
|
208
309
|
that is supposed to catch dropped sends is the rule that manufactures the phantom.
|
|
310
|
+
**AND A CONTENT PROBE — TOKEN, TAIL, FULL TEXT — CANNOT VERIFY DELIVERY AT ALL, only presence.**
|
|
311
|
+
Measured 2026-08-18: `herdr pane read` includes the INPUT BOX, so an unsubmitted message renders
|
|
312
|
+
identically to a submitted one and every content-based probe returns the same answer in both
|
|
313
|
+
states. A freeze hold was "verified delivered" by three independent content methods and had never
|
|
314
|
+
been submitted; the receiving seat stopped 19 minutes later without ever reading it — the frozen
|
|
315
|
+
set was protected by nothing but the operator's manual Esc. **The prompt line is the only state
|
|
316
|
+
that discriminates** — text sitting on `❯` is exactly what will not run — and a delivery report
|
|
317
|
+
is prompt-line state alone, or nothing: a decorative check beside a real one reads as
|
|
318
|
+
corroboration (two greens, one of which was never capable of disagreeing).
|
|
319
|
+
- **A MESSAGE TO A WORKING SEAT IS A QUEUED MESSAGE, AND THE QUEUE DRAINS ONLY AT TURN BOUNDARIES.**
|
|
320
|
+
Delivery is not arrival: `agent prompt` to a `working` claude seat lands in its queue (`Press up to
|
|
321
|
+
edit queued messages` on the seat's prompt line is the tell) and is READ only when the current turn
|
|
322
|
+
ends — and with in-process teammates a turn runs 20–40 minutes, so steering latency equals subagent
|
|
323
|
+
runtime. Measured 2026-08-17/18 (P98 leg 1): a FREEZE HOLD and a checker-release directive stacked
|
|
324
|
+
behind a planner's teammate fan-out — the freeze forbade edits its own queue could still trigger —
|
|
325
|
+
and the OPERATOR ended the turn by hand. Three consequences:
|
|
326
|
+
- **A queued hold is not a hold.** A freeze-class or superseding directive to a `working` seat is
|
|
327
|
+
delivered by INTERRUPT, and the interrupt is the SUPERVISING tier's move, never left to the
|
|
328
|
+
operator: `send-keys esc` → re-read status → esc once more if still working (two, bounded) →
|
|
329
|
+
verify idle → prompt → verify. The interrupt loses the seat's in-flight step; for a
|
|
330
|
+
correctness-class directive that is the price, and it is cheaper than a voided verdict.
|
|
331
|
+
- **Never stack a correction behind a stale directive.** A queued message executes in a context that
|
|
332
|
+
no longer matches the one it was written in. When conditions change, do not append another
|
|
333
|
+
message — interrupt, then send ONE directive that NAMES the queue and overrides it (*"your queue
|
|
334
|
+
holds X and Y; act on neither; current state is Z"*), and require the seat to report what its
|
|
335
|
+
queue held before acting. The receiving seat re-validates every queued item against CURRENT state.
|
|
336
|
+
- Five distinct send failures in one leg — front-truncation, sitting unsubmitted, two silent losses,
|
|
337
|
+
a probe that mistook its own echo for a reply — is what a prose send protocol costs under load:
|
|
338
|
+
run `scripts/seat-send.sh` instead.
|
|
209
339
|
- **Guard-before-Enter** (race-safe prompt answering): chain with `&&` — pane get shows `blocked` && pane
|
|
210
340
|
read shows the expected option under the cursor && only then send-keys. If no longer `blocked`, someone
|
|
211
341
|
already answered; do nothing.
|
|
@@ -235,6 +365,15 @@ number — an unmeasured budget is not a small budget.
|
|
|
235
365
|
unaffected (one more reason to prefer them).
|
|
236
366
|
- Stale typed input is unclearable via CLI — supersede it:
|
|
237
367
|
`pane run "<-- disregard everything before this arrow (stale draft). ACTUAL: <message>"`.
|
|
368
|
+
**But DISCRIMINATE before you supersede or file it: text on an idle seat's prompt line has FOUR
|
|
369
|
+
authors** — the seat's own draft, an operator, another agent's `agent send` (writes WITHOUT Enter),
|
|
370
|
+
and claude-code's AUTOSUGGEST, which renders context-plausible ghost text BYTE-IDENTICAL to a typed
|
|
371
|
+
draft in text-format reads (OBS-482). The check is mechanical and only works at observation time:
|
|
372
|
+
`agent read --format ansi` — dim/grey SGR around the text = autosuggest ghost, NOT input. Measured
|
|
373
|
+
2026-08-17 (D-206): an unattributed instruction was found in an orchestrator's box, superseded
|
|
374
|
+
defensively, and its origin stayed UNRESOLVED — the one probe that discriminates was not taken while
|
|
375
|
+
the text still sat there. An origin question you can close in ten seconds at the pane becomes
|
|
376
|
+
permanently open the moment anyone clears the box.
|
|
238
377
|
|
|
239
378
|
## Supervision watcher
|
|
240
379
|
|
|
@@ -324,6 +463,12 @@ nowhere.
|
|
|
324
463
|
It wakes when every named file exists AND ends with its terminal marker, and on timeout it reports each
|
|
325
464
|
file as READY / PARTIAL / ABSENT so a quiet arm still proves the watcher was alive. Tell each seat, in its
|
|
326
465
|
brief, the exact marker its report must end with — you cannot watch for a marker you never demanded.
|
|
466
|
+
**Arm on the marker YOU demanded, verified against the FILE — never on the seat's report of its own
|
|
467
|
+
marker.** Measured 2026-08-17: a seat reported its sweep "ends `SWEEP-END`"; the file on disk ended
|
|
468
|
+
`ORDER4-END`. A watcher armed on the reported marker never fires while the artifact sits COMPLETE, and
|
|
469
|
+
that hang is byte-identical to a seat still working. The script prints each unfinished file's actual
|
|
470
|
+
last line on every timeout heartbeat — read it there, and when in doubt `tail -1` the artifact, never
|
|
471
|
+
the transcript's claim about it.
|
|
327
472
|
|
|
328
473
|
**Arm it in the same call as the spawn, not the next one.** A watcher armed "after I finish this step"
|
|
329
474
|
leaves a gap exactly as wide as however long you stay busy, and you will be busy — you just spawned work.
|
|
@@ -713,6 +858,11 @@ twice.** They are mission-independent on purpose: nothing here names a task, a l
|
|
|
713
858
|
**Write beats to a conventional path inside the repository the other tier already reads**, one file per
|
|
714
859
|
tier, and state the path when you report. Then have the reader name the tiers that are ABSENT, never
|
|
715
860
|
the ones present: a list of what IS armed is producible by a seat whose watchers are all dead.
|
|
861
|
+
**And a beat OUTLIVES its mission unless something sweeps it.** Namespace beats per phase/leg and
|
|
862
|
+
sweep them at stand-down: measured 2026-08-17, a beats directory held SEVEN stale beats from prior
|
|
863
|
+
phases beside four live tiers, and a stale beat beside live ones reads as coverage to anyone globbing
|
|
864
|
+
the directory — the outliving-its-trigger failure (rule 11) in file form. Only age distinguishes a
|
|
865
|
+
frozen beat from a live one, so the reader states ages, and the writer removes what it retires.
|
|
716
866
|
30. **A JOURNAL WATCHER ON A RESUMABLE RUN MUST SCOPE TO THE CURRENT ENGAGEMENT.** A resumed run's journal
|
|
717
867
|
still contains the PREVIOUS `run-end`. A watcher that greps the whole file for its terminal event finds
|
|
718
868
|
that old one immediately, concludes the run is over, and exits — on every resume, which is exactly when
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Deliver ONE directive to a seat with VERIFIED submission — the send path that failed five distinct
|
|
3
|
+
# ways in one leg (2026-08-17, P98): front-truncation at the PTY (~1024B), text sitting unsubmitted,
|
|
4
|
+
# two silent losses, and probes that mistook presence for delivery. A prose protocol loses one step
|
|
5
|
+
# under load; this encodes the steps.
|
|
6
|
+
#
|
|
7
|
+
# seat-send.sh <agent-name-or-pane> <message>
|
|
8
|
+
#
|
|
9
|
+
# TKR_INTERRUPT=1 end a `working` seat's turn first (Esc, at most twice, verified between) so the
|
|
10
|
+
# directive is READ now instead of queued behind the turn. A message to a working
|
|
11
|
+
# claude seat lands in its queue and drains only at the turn boundary — with
|
|
12
|
+
# in-process teammates that is 20–40 minutes, so A QUEUED HOLD IS NOT A HOLD.
|
|
13
|
+
# Measured 2026-08-18: two freeze-class directives sat queued behind a planner's
|
|
14
|
+
# teammate fan-out and THE OPERATOR pressed Esc by hand because no seat owned the
|
|
15
|
+
# interrupt. The interrupt loses the seat's in-flight step; for a correctness-class
|
|
16
|
+
# directive that is the price, and it is cheaper than a voided verdict.
|
|
17
|
+
#
|
|
18
|
+
# ⚠ WHY THE READ-BACK IS THE PROMPT LINE AND NOTHING ELSE (measured 2026-08-18, three methods failing
|
|
19
|
+
# identically): `herdr pane read` INCLUDES THE INPUT BOX, so an unsubmitted message renders exactly
|
|
20
|
+
# like a submitted one and every content-based probe — token grep, tail token, full-text match —
|
|
21
|
+
# returns the same answer in both states. A freeze hold was "verified delivered" three ways and had
|
|
22
|
+
# never been submitted; the receiving seat stopped 19 minutes later without ever reading it. Only the
|
|
23
|
+
# PROMPT LINE discriminates: text sitting on `❯` is exactly what will not run. Prompt-line state
|
|
24
|
+
# alone, or nothing — a decorative check beside a real one reads as corroboration.
|
|
25
|
+
#
|
|
26
|
+
# First output line is the machine-greppable verdict:
|
|
27
|
+
# DELIVERED_SUBMITTED | QUEUED_BEHIND_TURN | SEND_UNSUBMITTED | INTERRUPT_FAILED | TARGET_GONE
|
|
28
|
+
# | REFUSED_SIZE | REFUSED_BOX_OCCUPIED
|
|
29
|
+
#
|
|
30
|
+
# This verifies SUBMISSION — the text left the input box — never that the seat understood or acted.
|
|
31
|
+
# Read the seat's ARTIFACT for that; a send receipt is not a response.
|
|
32
|
+
set -u
|
|
33
|
+
|
|
34
|
+
TARGET="${1:?usage: seat-send.sh <agent-name-or-pane> <message>}"
|
|
35
|
+
MSG="${2:?message required}"
|
|
36
|
+
|
|
37
|
+
# PTY input front-truncates around 1KB and a truncated brief silently drops policy — the drop is at
|
|
38
|
+
# the FRONT, so what survives still reads as a complete message. Long content goes in a FILE; the
|
|
39
|
+
# pane gets one line pointing at it.
|
|
40
|
+
if [ "${#MSG}" -gt 900 ]; then
|
|
41
|
+
echo "REFUSED_SIZE ${#MSG} bytes — write a brief file and send a one-line pointer instead"; exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
status_of() {
|
|
45
|
+
herdr agent get "$TARGET" 2>/dev/null \
|
|
46
|
+
| sed -n 's/.*"agent_status":"\([a-z_]*\)".*/\1/p' | head -1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# The rendered `❯` line — the only state that discriminates submitted from sitting. head -1 returns
|
|
50
|
+
# the first rendered line, i.e. a PREFIX of a wrapped draft, so callers compare prefixes, never
|
|
51
|
+
# whole sentences (OBS-396: a full-sentence match returns false on a message that arrived intact).
|
|
52
|
+
prompt_line() {
|
|
53
|
+
herdr agent read "$TARGET" --source visible --lines 14 2>/dev/null \
|
|
54
|
+
| sed -n 's/^[[:space:]]*❯[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Is $1 a rendered prefix of our message? (first line of a wrapped draft = prefix of MSG)
|
|
58
|
+
is_ours() {
|
|
59
|
+
case "$MSG" in "$1"*) return 0 ;; *) return 1 ;; esac
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# GHOST DISCRIMINATOR (OBS-482): claude-code AUTOSUGGEST renders its suggestion on the prompt line
|
|
63
|
+
# wrapped in SGR dim; typed text renders default. A dim-wrapped line is rendering, not state.
|
|
64
|
+
is_ghost() {
|
|
65
|
+
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
66
|
+
| grep -F -- "❯" | grep -F -- "$1" | head -1 | grep -q "$(printf '\033')\[2m"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
S=$(status_of)
|
|
70
|
+
if [ -z "$S" ]; then
|
|
71
|
+
echo "TARGET_GONE $TARGET — agent get returned nothing (pane closed, or the name resolves nowhere)"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# A pre-existing draft in the box is a decision, not an obstacle: `agent prompt` would APPEND to it
|
|
76
|
+
# and submit both. Superseding someone else's text needs an author and the arrow form — refuse and
|
|
77
|
+
# surface it rather than silently stomping it (D-206: an unattributed line in a box, origin never
|
|
78
|
+
# resolved because it was cleared).
|
|
79
|
+
PRE=$(prompt_line)
|
|
80
|
+
if [ -n "$PRE" ] && ! is_ghost "$PRE"; then
|
|
81
|
+
echo "REFUSED_BOX_OCCUPIED $TARGET — input box already holds: $PRE"
|
|
82
|
+
echo " ANSI-verify first (dim SGR = autosuggest ghost, ignorable). A real draft is superseded by a"
|
|
83
|
+
echo " DECISION, not by this script: send the arrow form naming what you are overriding."
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
QUEUED=""
|
|
88
|
+
if [ "$S" = "working" ]; then
|
|
89
|
+
if [ "${TKR_INTERRUPT:-0}" = "1" ]; then
|
|
90
|
+
# Esc at most twice, VERIFIED between: the first Esc can merely dismiss a UI surface or stop one
|
|
91
|
+
# step (the operator needed two on 2026-08-18). Unbounded Esc into an idle seat starts clearing
|
|
92
|
+
# state that is not yours to clear.
|
|
93
|
+
for _ in 1 2; do
|
|
94
|
+
herdr agent send-keys "$TARGET" esc >/dev/null 2>&1
|
|
95
|
+
sleep 3
|
|
96
|
+
S=$(status_of)
|
|
97
|
+
[ "$S" != "working" ] && break
|
|
98
|
+
done
|
|
99
|
+
if [ "$S" = "working" ]; then
|
|
100
|
+
echo "INTERRUPT_FAILED $TARGET still working after 2x esc — NOT sending; a directive queued now lands after the turn, which is the failure you set TKR_INTERRUPT to avoid"
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
else
|
|
104
|
+
QUEUED=1
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Atomic text+Enter honouring the pane's live bracketed-paste mode — never send-text + separate
|
|
109
|
+
# Enter, which is the "sitting unsubmitted" failure by construction.
|
|
110
|
+
herdr agent prompt "$TARGET" "$MSG" >/dev/null 2>&1
|
|
111
|
+
|
|
112
|
+
sleep 2
|
|
113
|
+
PL=$(prompt_line)
|
|
114
|
+
if [ -n "$PL" ] && is_ours "$PL"; then
|
|
115
|
+
# The text is SITTING, not sent — the swallowed-Enter shape. Submitting the existing draft is not a
|
|
116
|
+
# re-send (nothing is duplicated); one bounded Enter, then re-read.
|
|
117
|
+
herdr agent send-keys "$TARGET" enter >/dev/null 2>&1
|
|
118
|
+
sleep 2
|
|
119
|
+
PL=$(prompt_line)
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if [ -n "$PL" ] && is_ours "$PL"; then
|
|
123
|
+
echo "SEND_UNSUBMITTED $TARGET — the message is SITTING on the prompt line after send + one Enter."
|
|
124
|
+
echo " Do NOT re-send (it appends and submits both). Read the pane; supersede with the arrow form."
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if [ -n "$PL" ]; then
|
|
129
|
+
# Not ours: either autosuggest ghost (dim SGR — benign) or a draft that appeared under us.
|
|
130
|
+
echo "DELIVERED_SUBMITTED $TARGET — prompt line clear of our text; NOTE it now holds: $PL"
|
|
131
|
+
echo " ANSI-verify (dim = ghost). If typed, treat per D-206: discriminate before anyone clears it."
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [ -n "$QUEUED" ]; then
|
|
136
|
+
echo "QUEUED_BEHIND_TURN $TARGET — submitted into a working seat's queue; it is READ at turn end. A freeze-class or superseding directive belongs behind TKR_INTERRUPT=1."
|
|
137
|
+
else
|
|
138
|
+
echo "DELIVERED_SUBMITTED $TARGET"
|
|
139
|
+
fi
|
|
140
|
+
exit 0
|
|
@@ -115,7 +115,7 @@ while :; do
|
|
|
115
115
|
echo "WAKE: ${#ready[@]} of $# artifact(s) complete with marker '$MARKER' — ${#pending[@]} still outstanding"
|
|
116
116
|
for f in ${ready[@]+"${ready[@]}"}; do echo " READY $(wc -c <"$f" | tr -d ' ') bytes sha1 $(shasum -a 1 "$f" | cut -c1-12) $f"; done
|
|
117
117
|
for f in ${pending[@]+"${pending[@]}"}; do
|
|
118
|
-
if [ -s "$f" ]; then echo " PARTIAL $(wc -c <"$f" | tr -d ' ') bytes, no marker yet $f"
|
|
118
|
+
if [ -s "$f" ]; then echo " PARTIAL $(wc -c <"$f" | tr -d ' ') bytes, no marker yet $f last-line: $(tail -n 1 "$f" | tr -d '\0' | cut -c1-72)"
|
|
119
119
|
else echo " NOT STARTED $f <- check whether that seat is BLOCKED; a stalled seat writes nothing"; fi
|
|
120
120
|
done
|
|
121
121
|
exit 0
|
|
@@ -147,7 +147,10 @@ while :; do
|
|
|
147
147
|
echo "WAKE: cap ${CAP}s reached — ${#ready[@]} of $# complete, re-arm"
|
|
148
148
|
for f in ${ready[@]+"${ready[@]}"}; do echo " READY $(wc -c <"$f" | tr -d ' ') bytes $f"; done
|
|
149
149
|
for f in ${pending[@]+"${pending[@]}"}; do
|
|
150
|
-
|
|
150
|
+
# PARTIAL prints the file's ACTUAL last line: a seat that reports the wrong marker for its own
|
|
151
|
+
# artifact leaves the file complete while this watcher waits forever, and the mismatch is visible
|
|
152
|
+
# only here (measured 2026-08-17: demanded/reported `SWEEP-END`, written `ORDER4-END`).
|
|
153
|
+
if [ -s "$f" ]; then echo " PARTIAL $(wc -c <"$f" | tr -d ' ') bytes, no '$MARKER' yet $f last-line: $(tail -n 1 "$f" | tr -d '\0' | cut -c1-72)"
|
|
151
154
|
else echo " ABSENT $f"; fi
|
|
152
155
|
done
|
|
153
156
|
exit 0
|
|
@@ -162,7 +162,7 @@ while [ "$elapsed" -lt "$CAP" ]; do
|
|
|
162
162
|
&& printf '%s' "$T" | grep -qEi -- "$AUTO_ALLOW_RE"; then
|
|
163
163
|
auto=$((auto + 1))
|
|
164
164
|
herdr agent prompt "$TARGET" \
|
|
165
|
-
" <-- disregard everything before this arrow (stale unsubmitted draft, auto-detected). ACTUAL: proceed exactly as that draft said: ${T}" \
|
|
165
|
+
" <-- [watch-pending-input auto-supersede] disregard everything before this arrow (stale unsubmitted draft, auto-detected). ACTUAL: proceed exactly as that draft said: ${T}" \
|
|
166
166
|
>/dev/null 2>&1
|
|
167
167
|
echo "$(date '+%H:%M:%S') auto-superseded #${auto}: ${T}" >> "${AUTO_LOG}"
|
|
168
168
|
streak=0
|
|
@@ -198,4 +198,20 @@ while [ "$elapsed" -lt "$CAP" ]; do
|
|
|
198
198
|
esac
|
|
199
199
|
done
|
|
200
200
|
|
|
201
|
+
# The negative alone is not evidence: a draft can arrive inside the final poll window, and a draft
|
|
202
|
+
# sitting in the box while the seat is WORKING is invisible to the loop above by design (working
|
|
203
|
+
# resets the streak). Measured 2026-08-17 (D-206): this watcher capped with "no sustained pending
|
|
204
|
+
# input in 1800s" while an unattributed instruction sat in the target's box, and blind-vs-timing
|
|
205
|
+
# could not be distinguished afterwards. So the cap line carries one FINAL unsustained read — the
|
|
206
|
+
# last frame of the window, whatever the seat's status — plus the styled bytes so ghost text is
|
|
207
|
+
# discriminable at the only moment it can be.
|
|
201
208
|
echo "WATCH_CAP_REACHED $TARGET status=$(status_of) — no sustained pending input in ${CAP}s"
|
|
209
|
+
FINAL=$(pending_text)
|
|
210
|
+
if [ -n "$FINAL" ]; then
|
|
211
|
+
echo " final read (UNSUSTAINED — present at cap, not confirmed across polls): $FINAL"
|
|
212
|
+
echo " styling evidence (dim/grey SGR = autosuggest ghost, NOT a draft):"
|
|
213
|
+
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
214
|
+
| grep -F -- "$FINAL" | head -2 | cat -v | sed 's/^/ /'
|
|
215
|
+
else
|
|
216
|
+
echo " final read: input box empty"
|
|
217
|
+
fi
|