tickmarkr 2.2.0 → 2.3.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 +10 -9
- package/dist/adapters/model-lints.js +9 -0
- package/dist/adapters/pi.d.ts +1 -0
- package/dist/adapters/pi.js +15 -1
- package/dist/adapters/prompt.js +11 -3
- package/dist/adapters/registry.js +13 -4
- package/dist/adapters/types.d.ts +23 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.d.ts +3 -7
- package/dist/cli/commands/approve.js +26 -20
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/doctor.d.ts +6 -2
- package/dist/cli/commands/doctor.js +79 -9
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +20 -3
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +142 -25
- package/dist/cli/commands/version.d.ts +2 -1
- package/dist/cli/commands/version.js +25 -4
- package/dist/compile/collateral.js +15 -9
- package/dist/compile/native.js +5 -3
- package/dist/config/config.js +1 -1
- package/dist/drivers/index.d.ts +7 -0
- package/dist/drivers/index.js +40 -10
- package/dist/drivers/orca.d.ts +41 -1
- package/dist/drivers/orca.js +192 -15
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/baseline.d.ts +4 -0
- package/dist/gates/baseline.js +68 -16
- package/dist/gates/llm.d.ts +7 -1
- package/dist/gates/llm.js +66 -35
- package/dist/gates/review.d.ts +3 -1
- package/dist/gates/review.js +42 -12
- package/dist/gates/run-gates.d.ts +6 -0
- package/dist/gates/run-gates.js +25 -10
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/run/consult.d.ts +7 -0
- package/dist/run/consult.js +21 -3
- package/dist/run/daemon.d.ts +14 -0
- package/dist/run/daemon.js +249 -27
- package/dist/run/git.d.ts +1 -0
- package/dist/run/git.js +4 -0
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +70 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -0
- package/skills/tickmarkr-overseer/SKILL.md +88 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +35 -9
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
package/dist/run/git.js
CHANGED
|
@@ -13,6 +13,7 @@ export { ROUTING_ENV_SEAMS };
|
|
|
13
13
|
// Default to a modest cap through the environment so vitest honors it natively; never pass it
|
|
14
14
|
// as argv (OBS-55) so child test oracles stay intact. The operator's own export wins.
|
|
15
15
|
export const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
16
|
+
export const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
16
17
|
export const DEFAULT_FORK_CAP = "6";
|
|
17
18
|
/**
|
|
18
19
|
* The fork budget belongs to ONE run: how many gate suites can be in flight at once is exactly that
|
|
@@ -150,6 +151,9 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
150
151
|
// OBS-110: apply the run's own fork cap only when the operator has not already set one.
|
|
151
152
|
if (!(FORK_CAP_ENV in env))
|
|
152
153
|
env[FORK_CAP_ENV] = resolvedForkCap();
|
|
154
|
+
// OBS-854: descendants can leave the checkout (nested fixture suites do), so cwd alone cannot
|
|
155
|
+
// attribute them. Every daemon shell exports the daemon pid as their durable parentage marker.
|
|
156
|
+
env[SUITE_PARENT_ENV] = String(process.pid);
|
|
153
157
|
// T7: the capacity every result of this shell carries, read HERE — off the environment the child
|
|
154
158
|
// is about to receive, after the precedence above has settled. An operator export is already in
|
|
155
159
|
// `env`, so what gets recorded is the operator's number, which is the case a release was re-taken
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -116,8 +116,21 @@ export declare function formatPriorFindingEvidence(evidence: PriorFindingEvidenc
|
|
|
116
116
|
export declare function normalizeGateFailure(details: string): string;
|
|
117
117
|
export declare const GATE_FINGERPRINT_CAP = 2;
|
|
118
118
|
export declare function identicalGateFailures(events: JournalEvent[], taskId: string, gate: string, normalized: string): number;
|
|
119
|
-
/**
|
|
119
|
+
/** What each funded repair's next battery actually reached. A repair spends its budget only
|
|
120
|
+
* when that battery reaches one of the gates it was funded to fix; an earlier red is evidence that
|
|
121
|
+
* this repair never got its funded turn, not a charge against the repair ladder. */
|
|
122
|
+
export interface RepairReach {
|
|
123
|
+
repair: number;
|
|
124
|
+
funded: string[];
|
|
125
|
+
reached: string[];
|
|
126
|
+
diedAt?: string;
|
|
127
|
+
charged: boolean;
|
|
128
|
+
}
|
|
129
|
+
export declare function repairReachSinceApproval(events: JournalEvent[], taskId: string): RepairReach[];
|
|
130
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
120
131
|
export declare function repairsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
132
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
133
|
+
export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
121
134
|
/**
|
|
122
135
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
123
136
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -242,9 +255,9 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
242
255
|
quotaFailover: z.ZodOptional<z.ZodLiteral<true>>;
|
|
243
256
|
overrun: z.ZodOptional<z.ZodLiteral<true>>;
|
|
244
257
|
retryMode: z.ZodOptional<z.ZodEnum<{
|
|
258
|
+
repair: "repair";
|
|
245
259
|
resume: "resume";
|
|
246
260
|
fresh: "fresh";
|
|
247
|
-
repair: "repair";
|
|
248
261
|
}>>;
|
|
249
262
|
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
250
263
|
signalBasis: z.ZodOptional<z.ZodEnum<{
|
package/dist/run/journal.js
CHANGED
|
@@ -72,8 +72,8 @@ export const REVIEW_UPHELD_RELEASE = "review-upheld";
|
|
|
72
72
|
// OBS-203: the third decision a gate-fail park needs. Plain approve WAIVES the failed gate (and every
|
|
73
73
|
// gate before it — daemon.ts remainingGates), which is wrong when the gate failed against a stale task
|
|
74
74
|
// DECLARATION rather than a bad diff: amend the spec's files[], recompile, and the gate now passes
|
|
75
|
-
// honestly. This release
|
|
76
|
-
//
|
|
75
|
+
// honestly. This release runs the whole gate suite over the parked commit before any worker and
|
|
76
|
+
// marks no gate satisfied: green merges directly, red returns to the ladder. Budget semantics match attempt-cap
|
|
77
77
|
// (fresh attempts, tried survives) because the park cost the task its remaining budget.
|
|
78
78
|
export const RECHECK_RELEASE = "recheck";
|
|
79
79
|
// OBS-738: one authority for every recovery surface. The ref is accepted only from the row that
|
|
@@ -547,18 +547,65 @@ export function identicalGateFailures(events, taskId, gate, normalized) {
|
|
|
547
547
|
}
|
|
548
548
|
return n;
|
|
549
549
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let n = 0;
|
|
550
|
+
export function repairReachSinceApproval(events, taskId) {
|
|
551
|
+
const repairs = [];
|
|
553
552
|
for (const e of events) {
|
|
554
553
|
if (e.taskId !== taskId)
|
|
555
554
|
continue;
|
|
556
|
-
if (e.event === "task-approved")
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
555
|
+
if (e.event === "task-approved") {
|
|
556
|
+
repairs.length = 0;
|
|
557
|
+
}
|
|
558
|
+
else if (e.event === "repair-attempt") {
|
|
559
|
+
const funded = Array.isArray(e.data.gates)
|
|
560
|
+
? e.data.gates.filter((gate) => typeof gate === "string")
|
|
561
|
+
: [];
|
|
562
|
+
repairs.push({
|
|
563
|
+
repair: typeof e.data.repair === "number" ? e.data.repair : repairs.length + 1,
|
|
564
|
+
funded,
|
|
565
|
+
reached: [],
|
|
566
|
+
charged: false,
|
|
567
|
+
launched: false,
|
|
568
|
+
closed: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const repair = repairs.at(-1);
|
|
573
|
+
if (!repair || repair.closed)
|
|
574
|
+
continue;
|
|
575
|
+
if (e.event === "worker-launch") {
|
|
576
|
+
if (repair.launched)
|
|
577
|
+
repair.closed = true;
|
|
578
|
+
else
|
|
579
|
+
repair.launched = true;
|
|
580
|
+
}
|
|
581
|
+
else if (repair.launched && e.event === "gate-result" && typeof e.data.gate === "string") {
|
|
582
|
+
if (!repair.reached.includes(e.data.gate))
|
|
583
|
+
repair.reached.push(e.data.gate);
|
|
584
|
+
if (e.data.pass === false && repair.diedAt === undefined)
|
|
585
|
+
repair.diedAt = e.data.gate;
|
|
586
|
+
if (repair.funded.includes(e.data.gate))
|
|
587
|
+
repair.charged = true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
560
590
|
}
|
|
561
|
-
return
|
|
591
|
+
return repairs.map(({ launched: _launched, closed: _closed, ...repair }) => repair);
|
|
592
|
+
}
|
|
593
|
+
/** Repair slots actually spent this engagement — journal-derived, so a resume inherits it. */
|
|
594
|
+
export function repairsSinceApproval(events, taskId) {
|
|
595
|
+
return repairReachSinceApproval(events, taskId).filter((repair) => repair.charged).length;
|
|
596
|
+
}
|
|
597
|
+
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
598
|
+
export function pendingRechecks(events) {
|
|
599
|
+
const pending = new Set();
|
|
600
|
+
for (const e of events) {
|
|
601
|
+
if (!e.taskId)
|
|
602
|
+
continue;
|
|
603
|
+
if (e.event === "task-approved" && e.data.release === RECHECK_RELEASE)
|
|
604
|
+
pending.add(e.taskId);
|
|
605
|
+
else if (e.event === "recheck-battery" || e.event === "worker-launch")
|
|
606
|
+
pending.delete(e.taskId);
|
|
607
|
+
}
|
|
608
|
+
return pending;
|
|
562
609
|
}
|
|
563
610
|
// Both retry decisions below govern exactly ONE dispatch: the next one. So both are read back from the
|
|
564
611
|
// journal at the moment that dispatch is built, never carried in a process variable — a stop between
|
|
@@ -695,8 +742,19 @@ export function pendingRepairFindings(events, taskId) {
|
|
|
695
742
|
* a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
|
|
696
743
|
*/
|
|
697
744
|
export function activeRetryBan(events, taskId, channel) {
|
|
698
|
-
|
|
699
|
-
|
|
745
|
+
let pending;
|
|
746
|
+
for (const e of events) {
|
|
747
|
+
if (e.taskId !== taskId)
|
|
748
|
+
continue;
|
|
749
|
+
if (e.event === "gate-fingerprint-cap")
|
|
750
|
+
pending = e;
|
|
751
|
+
else if (e.event === DECISION_SPENT
|
|
752
|
+
|| (e.event === "task-approved" && e.data.release === RECHECK_RELEASE))
|
|
753
|
+
pending = undefined;
|
|
754
|
+
}
|
|
755
|
+
return pending && pending.data.channel === channel && typeof pending.data.gate === "string"
|
|
756
|
+
? pending.data.gate
|
|
757
|
+
: undefined;
|
|
700
758
|
}
|
|
701
759
|
// Fail-closed shape for a dispatched assignment (journal.ts:75-90 posture): a malformed assignment in
|
|
702
760
|
// one dispatch degrades that single task toward today's behavior — counts toward attempts, contributes
|
|
@@ -7,6 +7,12 @@ export declare const SUPERVISION_FUTURE_GRACE_MS = 1000;
|
|
|
7
7
|
export declare const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
8
8
|
export declare const SUPERVISION_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch"];
|
|
9
9
|
export type SupervisionTier = (typeof SUPERVISION_TIERS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
12
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
13
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSupervisionRoot(cwd: string): string;
|
|
10
16
|
export declare const SUPERVISION_SEAT_TIERS: readonly ["orchestrator", "orchestrator-context", "overseer", "overseer-context"];
|
|
11
17
|
export type SeatTier = (typeof SUPERVISION_SEAT_TIERS)[number];
|
|
12
18
|
/** Does this tier's record have to name the seat it speaks for? */
|
package/dist/run/supervision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { stateDirName, tickmarkrDir } from "../graph/graph.js";
|
|
5
5
|
// SUP-01: supervision liveness as FILE STATE, not as a report — lock.ts's proven shape, one file per
|
|
6
6
|
// tier. A watcher beats its OWN file; every other seat derives that tier's state by stat()ing it. So
|
|
@@ -41,6 +41,34 @@ export const SUPERVISION_DEFAULT_THRESHOLD_PCT = 75;
|
|
|
41
41
|
export const SUPERVISION_TIERS = [
|
|
42
42
|
"orchestrator", "orchestrator-context", "overseer", "overseer-context", "watch",
|
|
43
43
|
];
|
|
44
|
+
const pathKind = (path) => {
|
|
45
|
+
try {
|
|
46
|
+
return statSync(path).isDirectory() ? "directory" : "present";
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "absent";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a one-shot supervision writer to the repository state it is claiming to supervise.
|
|
54
|
+
* The nearest state directory wins; reaching a git root without one is a refusal, not permission
|
|
55
|
+
* to create fresh state there or to wander into an enclosing repository. This function is read-only.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveSupervisionRoot(cwd) {
|
|
58
|
+
let current = resolve(cwd);
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (pathKind(join(current, stateDirName(current))) === "directory")
|
|
61
|
+
return current;
|
|
62
|
+
if (pathKind(join(current, ".git")) !== "absent") {
|
|
63
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir at repository root ${current}`);
|
|
64
|
+
}
|
|
65
|
+
const parent = dirname(current);
|
|
66
|
+
if (parent === current) {
|
|
67
|
+
throw new Error(`missing ${stateDirName(current)}/ state dir — no tickmarkr repository found from ${resolve(cwd)}`);
|
|
68
|
+
}
|
|
69
|
+
current = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
44
72
|
// The tiers whose records must NAME the seat behind them. A one-shot `tickmarkr beat` records a pid
|
|
45
73
|
// that has already exited by the time anyone reads it, and an instant — nothing a reader can attribute
|
|
46
74
|
// to a seat. Measured 2026-08-26: a consult seat of another tier ran the documented beat loop and the
|
package/dist/tui/ink/init-app.js
CHANGED
|
@@ -5,13 +5,13 @@ import { ToggleMark } from "./components.js";
|
|
|
5
5
|
import { clip, INK, inkInput, inkOutput, KeyBar, padCell, Pointer } from "./frame.js";
|
|
6
6
|
const DRIVERS = ["auto", "herdr", "subprocess", "orca"];
|
|
7
7
|
// One line per driver, shown for the value currently selected: four environments no longer fit on
|
|
8
|
-
// a single description row at 80 columns (the shipped three already clipped)
|
|
9
|
-
//
|
|
8
|
+
// a single description row at 80 columns (the shipped three already clipped). Auto identifies its
|
|
9
|
+
// innermost host from environment markers only; outside an Orca terminal, Orca is named explicitly.
|
|
10
10
|
const DRIVER_DESC = {
|
|
11
|
-
auto: "auto: herdr when HERDR_ENV=1, else
|
|
11
|
+
auto: "auto: herdr when HERDR_ENV=1, else orca inside an Orca terminal, else subprocess",
|
|
12
12
|
herdr: "herdr: every worker runs in a visible pane you can watch and unblock",
|
|
13
13
|
subprocess: "subprocess: headless child processes — no cockpit, same fail-closed gates",
|
|
14
|
-
orca: "orca: visible terminals in the Orca app —
|
|
14
|
+
orca: "orca: visible terminals in the Orca app — picked by auto inside one; name it elsewhere",
|
|
15
15
|
};
|
|
16
16
|
const VISIBILITY = ["pane", "headless"];
|
|
17
17
|
// Three fields, one toggle, one action — a descriptor array, deliberately not a forms framework.
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ Outside a multi-agent terminal environment, run the loop directly.
|
|
|
29
29
|
- Do not edit the compiled graph to force an outcome; fix the source spec and compile again.
|
|
30
30
|
- Gates verify commits, diffs, acceptance criteria, and reviews independently. Never trust a worker's claim that work is complete.
|
|
31
31
|
- Treat missing or unparseable machine results and verdicts as failures. Do not release, resume, or merge around failed gates.
|
|
32
|
+
- A task changing what the daemon DOES must own every surface that TELLS the operator what the daemon does.
|
|
32
33
|
|
|
33
34
|
## Act by default
|
|
34
35
|
|
|
@@ -48,6 +48,15 @@ through brief lineage. **An executor choice nobody made is still an executor cho
|
|
|
48
48
|
a watcher you know is alive. Earned 2026-08-25 (OBS-622): a handoff recorded *"artifact watcher armed"*
|
|
49
49
|
over two live consult verdicts; at adopt the only `watch-artifacts.sh` on the machine belonged to a
|
|
50
50
|
different repository, and nothing had been watching either file.
|
|
51
|
+
**WATCHER OWNERSHIP IS THE ARMING SEAT'S RECORDED PID, NEVER A NAME PATTERN.** Every bundled
|
|
52
|
+
`watch-*.sh` arm sets `TKR_ARMING_SEAT=<seat>` and writes its own pid under
|
|
53
|
+
`<state-dir>/overseer/pids/<arming-seat>-<script>-<pid>.pid`. A seat retires only watchers it armed,
|
|
54
|
+
by reading those files and killing the exact recorded pids; it never uses `pkill -f`, `pgrep -f`, or
|
|
55
|
+
any argv/path pattern. A journal path is shared by partner tiers and therefore cannot prove ownership.
|
|
56
|
+
Verify each executing pid in two process-table reads before acting, kill-by-pid, arm the replacement,
|
|
57
|
+
then verify its new pid in two process-table reads. A stand-down order inventories both sets: the
|
|
58
|
+
ordering seat's recorded pids to retire, and the partner's watchers armed on the ordering seat that
|
|
59
|
+
must survive it. This is the stand-down order of 222-11, not a best-effort sweep.
|
|
51
60
|
**An adopted seat ANNOUNCES itself, in the same act as re-arming:** tell the adopted orchestrator the
|
|
52
61
|
fresh seat is live (verified send: probe token + read-back). Through the gap its view of your tier read
|
|
53
62
|
STALE, and a tier that believes it is unsupervised escalates into a file nobody is reading. Earned
|
|
@@ -289,8 +298,11 @@ on its owner being free is scheduled, not armed* — applies to itself:
|
|
|
289
298
|
.claude/skills/tickmarkr-overseer/scripts/watch-contamination.sh <journal> <load-ceiling> <poll-s> <cap-s>
|
|
290
299
|
```
|
|
291
300
|
|
|
292
|
-
It wakes on a new **failed
|
|
293
|
-
|
|
301
|
+
It wakes on a new **failed runner `test`, `build`, or `lint`** `gate-result` carrying an infrastructure
|
|
302
|
+
fingerprint in the runner-emitted failure lines, or on sustained load above a ceiling, and prints one
|
|
303
|
+
wake reason. It deliberately ignores the same tokens inside review findings, acceptance prose, and the
|
|
304
|
+
secondary fingerprint echo list: quoted discussion of contamination is not contamination. **Two triggers,
|
|
305
|
+
because one is provably not enough:** run
|
|
294
306
|
against the four reds above, the fingerprint trigger caught the `vitest-worker` timeout and correctly
|
|
295
307
|
refused to launder either review rejection — and **missed** the 1-of-3074 case, whose text looks like an
|
|
296
308
|
ordinary assertion failure. Only the load ceiling catches that one. A single-signal version reads as
|
|
@@ -323,7 +335,10 @@ when the run's own status text pushed the percentage off the statusline; and an
|
|
|
323
335
|
The protocol, in both directions:
|
|
324
336
|
|
|
325
337
|
1. **Overseer sees orch at ≥50%** → nudge it: write `HANDOFF-ORCH-<ver>.md`, then `/clear`, then re-read
|
|
326
|
-
its brief **and** its handoff
|
|
338
|
+
its brief **and** its handoff. **A same-process `/clear` keeps every background task alive**; it clears
|
|
339
|
+
conversation context, not processes. Re-arm is therefore a controlled replacement: read only this
|
|
340
|
+
seat's pid files, verify each pid in two process-table reads, kill those exact pids, arm replacements,
|
|
341
|
+
and verify every new pid in two reads. Never retire by pattern.
|
|
327
342
|
2. **The returning orch, now fresh, checks the OVERSEER.** If the overseer is at ≥50%, it directs the
|
|
328
343
|
overseer to write its handoff and clear, and **points it at `HANDOFF-OVERSEER-<ver>.md` by path**.
|
|
329
344
|
3. Whichever seat is fresh performs the check. **Never both at once** — the run keeps one supervising tier
|
|
@@ -345,8 +360,9 @@ The protocol, in both directions:
|
|
|
345
360
|
herdr pane run <my-pane> "/clear"
|
|
346
361
|
# read the pane back — a cleared claude session shows an empty prompt and a reset context gauge
|
|
347
362
|
# 4. and only THEN, as a SEPARATE send, the re-orientation:
|
|
348
|
-
herdr pane run <my-pane> "You were cleared at <N>%. Read <handoff> and <brief
|
|
349
|
-
|
|
363
|
+
herdr pane run <my-pane> "You were cleared at <N>%. Read <handoff> and <brief>. Same-process clear kept
|
|
364
|
+
every background task alive: retire only your recorded watcher pids, verify twice, kill-by-pid, arm,
|
|
365
|
+
verify every new pid twice, preserve the partner-owned watchers the stand-down lists, then confirm."
|
|
350
366
|
```
|
|
351
367
|
|
|
352
368
|
⚠ **Steps 3 and 4 are two sends, never one.** A pointer batched with the clear lands *during* it and is
|
|
@@ -507,7 +523,11 @@ they are left implicit:
|
|
|
507
523
|
sleep 2–3s → send-keys Enter → read back (input empty / agent `working`). Never report "briefed" without
|
|
508
524
|
the read-back. Long content goes in a brief file, never pane text. `scripts/seat-send.sh` encodes
|
|
509
525
|
this whole path — size guard, atomic prompt, prompt-line read-back, optional interrupt — and never
|
|
510
|
-
auto-resends.
|
|
526
|
+
auto-resends. Each adapter declares its prompt glyph beside its input-box matchers; `seat-send.sh` reads
|
|
527
|
+
that declaration rather than assuming Claude's `❯`. **Probe the prompt line before writing:** if it
|
|
528
|
+
already holds a non-ghost draft, wait only the script's bounded window and return `SEND_DEFERRED`
|
|
529
|
+
without writing a byte. An unsubmitted post-send draft returns `SEND_UNSUBMITTED`. This protects both
|
|
530
|
+
Codex's `›` prompt and a HUMAN typing in a pane; delivery is never allowed to glue onto either draft.
|
|
511
531
|
**PROBE THE READ-BACK WITH THE SHORTEST DISTINCTIVE TOKEN — a commit hash, a pid, an OBS id — NEVER a
|
|
512
532
|
sentence.** A long phrase crosses the pane's render wrap boundary, so grepping for it returns zero on a
|
|
513
533
|
message that arrived intact, and **a badly-probed successful send is byte-identical to a truncated one.**
|
|
@@ -755,6 +775,10 @@ once** — an overseer ran nine hours at 86% unable to read its own number. Arm
|
|
|
755
775
|
Herdr's `agent_status`, which is unreliable for vendors whose screen detection is skipped (kimi) — for
|
|
756
776
|
those, the spawn-time auto-approve flag is the control, not this watcher.
|
|
757
777
|
|
|
778
|
+
For `consult`, `surgeon`, and `reviewer` seats, the CONTEXT watcher is wake-only: arm it with
|
|
779
|
+
`TKR_AUTO_CLEAR=0 .claude/skills/tickmarkr-overseer/scripts/watch-context.sh <consult|surgeon|reviewer>
|
|
780
|
+
<agent|pane> 50 50 <handoff-file>`. Only `orchestrator` and `overseer` seats may use auto-clear.
|
|
781
|
+
|
|
758
782
|
```bash
|
|
759
783
|
.claude/skills/tickmarkr-overseer/scripts/watch-pending-input.sh <agent|pane> [poll-s] [cap-s] [confirm-polls]
|
|
760
784
|
```
|
|
@@ -798,12 +822,17 @@ nowhere.
|
|
|
798
822
|
**The artifact watcher** — bundled, and keyed on the deliverable rather than the seat:
|
|
799
823
|
|
|
800
824
|
```bash
|
|
801
|
-
.claude/skills/tickmarkr-overseer/scripts/watch-artifacts.sh
|
|
825
|
+
TKR_ARMING_SEAT=<this-seat> .claude/skills/tickmarkr-overseer/scripts/watch-artifacts.sh \
|
|
826
|
+
[--changed-from <content-sha-or-now>] <MARKER> <cap-s> <poll-s> <file>...
|
|
802
827
|
```
|
|
803
828
|
|
|
804
829
|
It wakes when every named file exists AND ends with its terminal marker, and on timeout it reports each
|
|
805
830
|
file as READY / PARTIAL / ABSENT so a quiet arm still proves the watcher was alive. Tell each seat, in its
|
|
806
831
|
brief, the exact marker its report must end with — you cannot watch for a marker you never demanded.
|
|
832
|
+
**A re-arm over an artifact that already has its marker uses `--changed-from now` (or its captured SHA).**
|
|
833
|
+
The existing marked bytes are the baseline, not a fresh delivery: the watcher fires only after content
|
|
834
|
+
differs from that baseline and then stabilizes, once. Without the changed-from baseline, re-arm is an
|
|
835
|
+
instant replay of old completion rather than observation of new work.
|
|
807
836
|
**Arm on the marker YOU demanded, verified against the FILE — never on the seat's report of its own
|
|
808
837
|
marker.** Measured 2026-08-17: a seat reported its sweep "ends `SWEEP-END`"; the file on disk ended
|
|
809
838
|
`ORDER4-END`. A watcher armed on the reported marker never fires while the artifact sits COMPLETE, and
|
|
@@ -937,9 +966,18 @@ orchestrator turn boundary.
|
|
|
937
966
|
filed a *different* finding as OBS-437, then repeated it as OBS-438 and OBS-439 — and a sweep of the
|
|
938
967
|
ledger's history found **twelve** more. A duplicated id makes every citation ambiguous, and this project
|
|
939
968
|
cites them in rulings, handoffs, memory entries and shipped source comments. **Allocate from the current
|
|
940
|
-
maximum and then
|
|
941
|
-
nothing** —
|
|
942
|
-
|
|
969
|
+
maximum and then run the duplicate-id check against the committed historical baseline below; it must
|
|
970
|
+
print nothing NEW** — the raw `uniq -d` list is intentionally non-empty history and cannot be used as a
|
|
971
|
+
zero-output gate:
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
comm -13 \
|
|
975
|
+
<(printf '%s\n' OBS-106 OBS-12 OBS-129 OBS-148 OBS-24 OBS-26 OBS-29 OBS-35 OBS-36 OBS-40 OBS-41 OBS-415 OBS-459 OBS-463 OBS-466 OBS-470 OBS-472 OBS-542 OBS-548 OBS-552 OBS-562 OBS-563 OBS-564 OBS-592 OBS-785 OBS-791 | sort -u) \
|
|
976
|
+
<(grep -o '^## OBS-[0-9]*' <ledger> | sed 's/^## //' | sort | uniq -d)
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
Allocation alone is a guess about what the other tier is doing, and only this baseline-relative check
|
|
980
|
+
catches you both guessing the same. Renumber the LATER entry and say so in its heading. **Never renumber a
|
|
943
981
|
historical id**: every record already citing it would then point at the wrong finding.
|
|
944
982
|
7. **Every fix is evaluated for shipping.** The tarball is `files: [dist, schema, skills, fixtures]` — so
|
|
945
983
|
`src/**` and `skills/**` reach users while `.overseer/**` and `.tickmarkr/**` reach nobody. Before
|
|
@@ -953,6 +991,13 @@ orchestrator turn boundary.
|
|
|
953
991
|
leave clean state after a version is shipped"***. Every line below is a defect that actually happened
|
|
954
992
|
on the release that produced this rule.
|
|
955
993
|
|
|
994
|
+
- **RUN BOTH RELEASE PROOFS BEFORE STANDING DOWN.** The first is the exported-tree suite, which
|
|
995
|
+
maintainers run from the private repo with its `verify:export` script — that tooling is deliberately
|
|
996
|
+
not part of this package, so the command does not exist in a public checkout; it must install,
|
|
997
|
+
build, and run the suite in the exported tree; then
|
|
998
|
+
`TICKMARKR_E2E=1 npx vitest run tests/e2e/orca-smoke.e2e.test.ts` must exercise the installed-Orca
|
|
999
|
+
smoke. A skipped smoke is not proof. Record both results before declaring the release complete.
|
|
1000
|
+
|
|
956
1001
|
- **REWRITE THE MEMORY INDEX FIRST, and read it back.** Minutes after `2.1.1` hit npm, the index line
|
|
957
1002
|
a fresh session loads still read *"⛔ 2.1.1 CANNOT ship from run …2011"* — true when written, and by
|
|
958
1003
|
then the exact opposite of the truth. **The index is what everyone loads and the body is what nobody
|
|
@@ -1018,6 +1063,29 @@ Distilled from a v1.86 spec-repair mission that produced 31 numbered rules, ~90
|
|
|
1018
1063
|
errors authored by the supervising seat itself. **Every line below was earned by a defect, most of them
|
|
1019
1064
|
twice.** They are mission-independent on purpose: nothing here names a task, a line number or a figure.
|
|
1020
1065
|
|
|
1066
|
+
**Five queue-and-handoff method laws — execute them; citing their OBS id is not execution:**
|
|
1067
|
+
|
|
1068
|
+
1. **CITE-IS-NOT-READ.** A queue, handoff, finding, or brief that cites an observation does not prove its
|
|
1069
|
+
author opened it. Before acting on a cited premise, open the primary record, quote the operative bytes,
|
|
1070
|
+
and state the qualifier or falsifier the citation would otherwise hide.
|
|
1071
|
+
2. **EXECUTING-FORM PROBE.** Process ownership starts from the watcher pid file. Where legacy discovery is
|
|
1072
|
+
unavoidable, match the executing form — interpreter plus exact script path and arguments — not a journal
|
|
1073
|
+
path or name substring, resolve the candidate's cwd/parent, read the arm log for startup failure, then
|
|
1074
|
+
require the same pid in two reads.
|
|
1075
|
+
3. **RESUME-AWARE CONTAMINATION.** Capture the current engagement boundary before watching a resumable
|
|
1076
|
+
journal. Never terminate on a historical `run-end`: continue across `run-resume`, and stop only at the
|
|
1077
|
+
cap or after current lock release proves no later engagement follows. Classify only runner-emitted lines
|
|
1078
|
+
of new failed `test`, `build`, or `lint` gates; review prose and the secondary fingerprint echo list are
|
|
1079
|
+
evidence about findings, not contamination occurrences.
|
|
1080
|
+
4. **THE QUEUE-INHERITANCE DIFF.** In the same act that opens a queue, extract observation ids from the
|
|
1081
|
+
previous queue and the new queue, run `comm -23 <previous-queue-ids> <new-queue-ids>`, paste its output
|
|
1082
|
+
into the new queue, and classify every row as queued, shipped, or no-ship with a falsifiable removal
|
|
1083
|
+
condition. Reading the previous queue is not a diff.
|
|
1084
|
+
5. **RE-RUN A PREMISE.** Before inheriting any queue item or handoff conclusion, run its named positive and
|
|
1085
|
+
negative controls on the current tree. For an executable premise, execute it with a negative control;
|
|
1086
|
+
for a call-site premise, open the code line and read any rationale through its rejected alternatives.
|
|
1087
|
+
Record the current output; if the premise dissolved, mark it superseded rather than carrying it forward.
|
|
1088
|
+
|
|
1021
1089
|
**Rot**
|
|
1022
1090
|
|
|
1023
1091
|
1. **A quotation is exact bytes.** `grep` it before attributing it; if it does not hit, it is not a
|
|
@@ -1101,11 +1169,12 @@ twice.** They are mission-independent on purpose: nothing here names a task, a l
|
|
|
1101
1169
|
owns it** — "watchers alive" is the one claim a seat cannot verify about itself. Measured 2026-08-06:
|
|
1102
1170
|
an orchestrator sat `idle` through three merges and two dispatches with no journal watcher in the
|
|
1103
1171
|
process table, while its own last report read *"daemon, board, sweeper, watcher all alive"* (OBS-366).
|
|
1104
|
-
**STATE THE LIFETIME, because an unstated one is read as the mission's
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1172
|
+
**STATE THE LIFETIME, because an unstated one is read as the mission's.** Every watcher a seat arms —
|
|
1173
|
+
journal, artifact, dialog, beat loop — records its own pid and arming seat. **A same-process `/clear`
|
|
1174
|
+
keeps every background task alive**; it does not retire even one watcher. A crash may orphan or kill
|
|
1175
|
+
tasks according to how they were launched, while adopt and stand-down are explicit kill-by-recorded-pid
|
|
1176
|
+
transitions. A handoff must therefore inventory real pids, not infer process lifetime from the context
|
|
1177
|
+
lifecycle. The inverse failure is
|
|
1109
1178
|
the same root read the other way: a DETACHED loop outlives its seat and holds a tier `ARMED` with
|
|
1110
1179
|
nobody home (OBS-583). Neither direction may be assumed; the lifetime is a property of how the watcher
|
|
1111
1180
|
was launched, and it belongs in writing next to every claim that one is armed.
|
|
@@ -1162,9 +1231,10 @@ twice.** They are mission-independent on purpose: nothing here names a task, a l
|
|
|
1162
1231
|
The general rule: **an exclusion filter is exactly as
|
|
1163
1232
|
dangerous as an over-broad inclusion filter, and it fails in the direction that reads as "not there" —
|
|
1164
1233
|
which is the direction that gets acted on.**
|
|
1165
|
-
Two corollaries: **
|
|
1166
|
-
|
|
1167
|
-
handoff that re-arms one tier's watchers must say which
|
|
1234
|
+
Two corollaries: **handling a fire-and-exit watcher's wake and re-arming it are the same act.** The wake
|
|
1235
|
+
handling line is incomplete until it names the new recorded pid and two successful process-table reads;
|
|
1236
|
+
never defer the arm to the turn's end. And **a handoff that re-arms one tier's watchers must say which
|
|
1237
|
+
tier's it did NOT re-arm.**
|
|
1168
1238
|
**That first corollary prescribes DISCIPLINE, and discipline is the wrong fix — measured 2026-08-06.**
|
|
1169
1239
|
One orchestrator lapsed its journal tier **31 minutes**, then, after diagnosing it and fully intending
|
|
1170
1240
|
to re-arm, lapsed it again for 3 minutes **while actively thinking about watchers**. Its own diagnosis
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# under load; this encodes the steps.
|
|
6
6
|
#
|
|
7
7
|
# seat-send.sh <agent-name-or-pane> <message>
|
|
8
|
+
# TKR_ADAPTER=<id> supplies the adapter when a bare pane/name does not expose it in `agent get`.
|
|
8
9
|
#
|
|
9
10
|
# TKR_INTERRUPT=1 end a `working` seat's turn first (Esc, at most twice, verified between) so the
|
|
10
11
|
# directive is READ now instead of queued behind the turn. A message to a working
|
|
@@ -24,8 +25,8 @@
|
|
|
24
25
|
# alone, or nothing — a decorative check beside a real one reads as corroboration.
|
|
25
26
|
#
|
|
26
27
|
# First output line is the machine-greppable verdict:
|
|
27
|
-
# DELIVERED_SUBMITTED | QUEUED_BEHIND_TURN | SEND_UNSUBMITTED |
|
|
28
|
-
# |
|
|
28
|
+
# DELIVERED_SUBMITTED | QUEUED_BEHIND_TURN | SEND_UNSUBMITTED | SEND_DEFERRED
|
|
29
|
+
# | INTERRUPT_FAILED | TARGET_GONE | REFUSED_SIZE | SEND_UNVERIFIED | SEND_REFUSED | SEND_FAILED
|
|
29
30
|
#
|
|
30
31
|
# A success verdict names the surface that delivered when it was not the default one, so a fallback
|
|
31
32
|
# is visible in the transcript rather than inferred (OBS-552).
|
|
@@ -36,6 +37,7 @@ set -u
|
|
|
36
37
|
|
|
37
38
|
TARGET="${1:?usage: seat-send.sh <agent-name-or-pane> <message>}"
|
|
38
39
|
MSG="${2:?message required}"
|
|
40
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
39
41
|
|
|
40
42
|
# PTY input front-truncates around 1KB and a truncated brief silently drops policy — the drop is at
|
|
41
43
|
# the FRONT, so what survives still reads as a complete message. Long content goes in a FILE; the
|
|
@@ -57,9 +59,68 @@ pane_of() {
|
|
|
57
59
|
| sed -n 's/.*"pane_id":"\([^"]*\)".*/\1/p' | head -1
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
62
|
+
# Adapter identity comes from the worker name or Herdr's agent field; a caller targeting a bare pane
|
|
63
|
+
# supplies TKR_ADAPTER. The glyph itself is never duplicated here: this parser reads the declaration
|
|
64
|
+
# beside the input-box matchers in adapters/types.ts (or its built dist twin in an installed package).
|
|
65
|
+
adapter_of() {
|
|
66
|
+
if [ -n "${TKR_ADAPTER:-}" ]; then printf '%s\n' "$TKR_ADAPTER"; return 0; fi
|
|
67
|
+
local record identity adapter
|
|
68
|
+
record=$(herdr agent get "$TARGET" 2>/dev/null)
|
|
69
|
+
identity=$(printf '%s\n' "$record" |
|
|
70
|
+
sed -n 's/.*"agent":"\([^"]*\)".*/\1/p; s/.*"name":"\([^"]*\)".*/\1/p' | head -1)
|
|
71
|
+
[ -n "$identity" ] || identity="$TARGET"
|
|
72
|
+
adapter=$(printf '%s\n' "$identity" |
|
|
73
|
+
sed -n 's/^.*-worker-\(.*\)-a[0-9][0-9]*-.*$/\1/p' | head -1)
|
|
74
|
+
[ -n "$adapter" ] || adapter="$identity"
|
|
75
|
+
case "$adapter" in claude) adapter="claude-code" ;; cursor) adapter="cursor-agent" ;; esac
|
|
76
|
+
printf '%s\n' "$adapter"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
prompt_glyph_of() {
|
|
80
|
+
local adapter="$1" source glyph root tickmarkr_bin tickmarkr_entry
|
|
81
|
+
# The canonical script is <repo>/skills/.../scripts; the independently tracked installed copy is
|
|
82
|
+
# <repo>/.claude/skills/.../scripts. Search both ancestor shapes so identical bytes work from either
|
|
83
|
+
# shipped location. TKR_ADAPTER_TYPES remains the explicit consumer-install override.
|
|
84
|
+
for source in ${TKR_ADAPTER_TYPES:+"$TKR_ADAPTER_TYPES"}; do
|
|
85
|
+
[ -f "$source" ] || continue
|
|
86
|
+
glyph=$(awk -F'"' -v adapter="$adapter" '$2 == adapter && $3 ~ /^[[:space:]]*:[[:space:]]*$/ { print $4; exit }' "$source")
|
|
87
|
+
if [ -n "$glyph" ]; then printf '%s\n' "$glyph"; return 0; fi
|
|
88
|
+
done
|
|
89
|
+
for root in "$SCRIPT_DIR/../../.." "$SCRIPT_DIR/../../../.."; do
|
|
90
|
+
root=$(CDPATH= cd -- "$root" 2>/dev/null && pwd) || continue
|
|
91
|
+
for source in "$root/src/adapters/types.ts" "$root/dist/adapters/types.js"; do
|
|
92
|
+
[ -f "$source" ] || continue
|
|
93
|
+
glyph=$(awk -F'"' -v adapter="$adapter" '$2 == adapter && $3 ~ /^[[:space:]]*:[[:space:]]*$/ { print $4; exit }' "$source")
|
|
94
|
+
if [ -n "$glyph" ]; then printf '%s\n' "$glyph"; return 0; fi
|
|
95
|
+
done
|
|
96
|
+
done
|
|
97
|
+
# `tickmarkr init` copies this skill into a consumer repository, so neither ancestor above belongs
|
|
98
|
+
# to the package. The executable is shipped from <package>/dist/cli/index.js; resolve its symlink and
|
|
99
|
+
# read the adjacent compiled declaration. Node is already a runtime requirement of that executable.
|
|
100
|
+
tickmarkr_bin=$(command -v tickmarkr 2>/dev/null) || tickmarkr_bin=""
|
|
101
|
+
if [ -n "$tickmarkr_bin" ]; then
|
|
102
|
+
tickmarkr_entry=$(node -e 'try { process.stdout.write(require("fs").realpathSync(process.argv[1])) } catch {}' "$tickmarkr_bin" 2>/dev/null)
|
|
103
|
+
if [ -n "$tickmarkr_entry" ]; then
|
|
104
|
+
root=$(CDPATH= cd -- "$(dirname -- "$tickmarkr_entry")/../.." 2>/dev/null && pwd) || root=""
|
|
105
|
+
source="$root/dist/adapters/types.js"
|
|
106
|
+
if [ -n "$root" ] && [ -f "$source" ]; then
|
|
107
|
+
glyph=$(awk -F'"' -v adapter="$adapter" '$2 == adapter && $3 ~ /^[[:space:]]*:[[:space:]]*$/ { print $4; exit }' "$source")
|
|
108
|
+
if [ -n "$glyph" ]; then printf '%s\n' "$glyph"; return 0; fi
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ADAPTER=$(adapter_of)
|
|
116
|
+
if ! PROMPT_GLYPH=$(prompt_glyph_of "$ADAPTER"); then
|
|
117
|
+
echo "SEND_UNVERIFIED $TARGET — adapter '$ADAPTER' has no prompt glyph declaration; nothing was typed"
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# The adapter-declared prompt line is the only state that discriminates submitted from sitting.
|
|
122
|
+
# tail -1 takes the LAST matching line: a wrapped draft's continuation lines carry no prompt glyph,
|
|
123
|
+
# so the last one is still that draft's FIRST rendered line, i.e. a PREFIX of the draft —
|
|
63
124
|
# callers compare prefixes, never whole sentences (OBS-396: a full-sentence match returns false on a
|
|
64
125
|
# message that arrived intact).
|
|
65
126
|
#
|
|
@@ -84,7 +145,11 @@ pane_of() {
|
|
|
84
145
|
prompt_line() {
|
|
85
146
|
herdr agent read "$TARGET" --source visible --lines 14 2>/dev/null \
|
|
86
147
|
| sed '/↑ to edit/{/steer/d;}' \
|
|
87
|
-
|
|
|
148
|
+
| awk -v glyph="$PROMPT_GLYPH" '
|
|
149
|
+
{ pos = index($0, glyph); if (!pos) next
|
|
150
|
+
prefix = substr($0, 1, pos - 1); gsub(/[[:space:]|]/, "", prefix); gsub(/│/, "", prefix)
|
|
151
|
+
if (prefix == "") print substr($0, pos + length(glyph)) }
|
|
152
|
+
' | tail -1 | sed 's/^[[:space:]]*//; s/[[:space:]│|]*$//'
|
|
88
153
|
}
|
|
89
154
|
|
|
90
155
|
# STAGED-QUEUE DISCRIMINATOR (OBS-552 addendum, captured 2026-08-19 on a live kimi seat). After its
|
|
@@ -114,7 +179,7 @@ is_ours() {
|
|
|
114
179
|
# wrapped in SGR dim; typed text renders default. A dim-wrapped line is rendering, not state.
|
|
115
180
|
is_ghost() {
|
|
116
181
|
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
117
|
-
| grep -F -- "
|
|
182
|
+
| grep -F -- "$PROMPT_GLYPH" | grep -F -- "$1" | head -1 | grep -q "$(printf '\033')\[2m"
|
|
118
183
|
}
|
|
119
184
|
|
|
120
185
|
S=$(status_of)
|
|
@@ -123,17 +188,22 @@ if [ -z "$S" ]; then
|
|
|
123
188
|
exit 1
|
|
124
189
|
fi
|
|
125
190
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
191
|
+
# Probe BEFORE writing: `agent prompt` and `pane run` both append to a live draft and submit the
|
|
192
|
+
# concatenation. Give a human a short bounded window to finish or clear; if it remains, defer without
|
|
193
|
+
# touching the pane. Re-running the receipt is the retry, so SEND_DEFERRED remains safe indefinitely.
|
|
194
|
+
DRAFT_WAIT="${TKR_DRAFT_WAIT_S:-10}"
|
|
195
|
+
DRAFT_POLL="${TKR_DRAFT_POLL_S:-1}"
|
|
196
|
+
waited=0
|
|
197
|
+
while :; do
|
|
198
|
+
PRE=$(prompt_line)
|
|
199
|
+
if [ -z "$PRE" ] || is_ghost "$PRE"; then break; fi
|
|
200
|
+
if [ "$waited" -ge "$DRAFT_WAIT" ] 2>/dev/null; then
|
|
201
|
+
echo "SEND_DEFERRED $TARGET — draft present; nothing was typed: $PRE"
|
|
202
|
+
exit 2
|
|
203
|
+
fi
|
|
204
|
+
sleep "$DRAFT_POLL"
|
|
205
|
+
waited=$((waited + DRAFT_POLL))
|
|
206
|
+
done
|
|
137
207
|
|
|
138
208
|
QUEUED=""
|
|
139
209
|
if [ "$S" = "working" ]; then
|