tickmarkr 2.1.1 → 2.1.3
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/adapters/claude-code.js +18 -3
- package/dist/adapters/pi.d.ts +6 -0
- package/dist/adapters/pi.js +99 -18
- package/dist/cli/commands/beat.js +28 -8
- package/dist/cli/commands/init.js +29 -2
- package/dist/cli/commands/status.js +17 -15
- package/dist/cli/commands/verify.js +9 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/gates/baseline.d.ts +10 -0
- package/dist/gates/baseline.js +46 -3
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +52 -4
- package/dist/run/lock.d.ts +1 -0
- package/dist/run/lock.js +23 -13
- package/dist/run/supervision.d.ts +9 -3
- package/dist/run/supervision.js +77 -21
- package/dist/tui/cockpit/derive.js +5 -10
- package/package.json +2 -2
- package/skills/tickmarkr-auto/SKILL.md +19 -2
- package/skills/tickmarkr-loop/SKILL.md +19 -2
- package/skills/tickmarkr-overseer/SKILL.md +108 -14
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +79 -30
|
@@ -35,6 +35,16 @@ through brief lineage. **An executor choice nobody made is still an executor cho
|
|
|
35
35
|
status, and either ADOPT the
|
|
36
36
|
existing orchestrator (updated brief, re-armed watchers) or, if the old hierarchy is dead, archive the
|
|
37
37
|
stale brief and build fresh.
|
|
38
|
+
⚠ **VERIFY EVERY INHERITED WATCHER FROM THE PROCESS TABLE BEFORE YOU TRUST IT — re-arming your own
|
|
39
|
+
watchers is NOT enough, and a seat told only to re-arm its own is told the wrong thing.** An inherited
|
|
40
|
+
*"watcher armed"* line is a claim, not a watcher: it is a report by a seat that no longer exists, which
|
|
41
|
+
is strictly WEAKER than the live seat's report rule 11 already forbids trusting — and it reads as
|
|
42
|
+
settled fact. So at every adopt, walk the predecessor's watchers by class — **journal watchers,
|
|
43
|
+
artifact watchers, dialog watchers and beat loops, which is the closed set a session owns** — probe
|
|
44
|
+
each from the process table yourself (`pgrep -f <token>`, discriminated per rule 11), and re-arm every
|
|
45
|
+
one the table does not show. Earned 2026-08-25 (OBS-622): a handoff recorded *"artifact watcher armed"*
|
|
46
|
+
over two live consult verdicts; at adopt the only `watch-artifacts.sh` on the machine belonged to a
|
|
47
|
+
different repository, and nothing had been watching either file.
|
|
38
48
|
**An adopted seat ANNOUNCES itself, in the same act as re-arming:** tell the adopted orchestrator the
|
|
39
49
|
fresh seat is live (verified send: probe token + read-back). Through the gap its view of your tier read
|
|
40
50
|
STALE, and a tier that believes it is unsupervised escalates into a file nobody is reading. Earned
|
|
@@ -93,7 +103,7 @@ through brief lineage. **An executor choice nobody made is still an executor cho
|
|
|
93
103
|
fraction (`ORCH · v1.19 4/5`, updated on every task-done); tickmarkr opens ONE TAB PER TASK, labelled
|
|
94
104
|
with the task id and holding that task's worker plus its judge/review/consult panes (tickmarkr
|
|
95
105
|
updates it). Never long context strings or ✓-chains.
|
|
96
|
-
2. **Orchestrator**: Launch the orchestrator with your agent host. Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <version>"` and parse `result.root_pane.pane_id` from its JSON, then start the agent in it. For Claude Code, use `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions` (append `--model <m>` after the `--` if the operator has a policy). For Codex, use `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` (add `--model <m>` to specify the model). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do 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`, and a read-only codex consultant may use `--sandbox read-only`.
|
|
106
|
+
2. **Orchestrator**: Launch the orchestrator with your agent host. Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <version>"` and parse `result.root_pane.pane_id` from its JSON, then start the agent in it. For Claude Code, use `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions` (append `--model <m>` after the `--` if the operator has a policy). For Codex, use `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` (add `--model <m>` to specify the model). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do 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 --settings '{"promptSuggestionEnabled":false}'`, and a read-only codex consultant may use `--sandbox read-only`. **That `--settings` pair is not cosmetic and it is not optional:** claude-code's AUTOSUGGEST renders context-plausible ghost text into an idle seat's prompt line that is BYTE-IDENTICAL to a typed draft in text-format reads (OBS-482), so a supervising tier cannot tell a seat's own unsent work from a rendering artifact without `agent read --format ansi`. Turning the suggester off at spawn removes the ambiguity at its source instead of paying for the discrimination at every read. Verified against the shipped binary: `claude --settings '{"promptSuggestionEnabled":false}' -p …` exits 0 with a real response, and the key appears in the binary's own settings schema. **For kimi, pass `-y`** (`herdr agent start <name> --kind kimi --pane <id> -- -y`) — the adapter already launches its own workers that way (`src/adapters/kimi.ts:204`), and a kimi seat spawned without it sits on an approval prompt having done nothing. **Herdr cannot see that state**: it reports a kimi pane as `agent_status: working` with `screen_detection_skipped: true` while the prompt is up, so the BLOCKED-STATE watcher below is blind on this vendor and the spawn flag is the ONLY control. Every vendor you spawn needs its auto-approve form named here; a vendor absent from this list is a seat that will hang.
|
|
97
107
|
3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
|
|
98
108
|
truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
|
|
99
109
|
(inside the tickmarkr state dir — already self-gitignored, no exclude step needed), then send one line:
|
|
@@ -245,6 +255,17 @@ is a lossy summary nobody trusts while a clean session re-oriented from disk-ver
|
|
|
245
255
|
**Do the same for yourself before you are forced to**: write the handoff while your judgment is still
|
|
246
256
|
good, not after. If your own context cannot be read by the watcher, say so to the operator and ask for the
|
|
247
257
|
number — an unmeasured budget is not a small budget.
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
.claude/skills/tickmarkr-overseer/scripts/watch-context.sh orchestrator <orchestrator-agent-or-pane> 60 75 <handoff-file>
|
|
261
|
+
.claude/skills/tickmarkr-overseer/scripts/watch-context.sh overseer <overseer-agent-or-pane> 60 75 <handoff-file>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The first argument chooses the closed per-seat tier (`orchestrator-context` or `overseer-context`),
|
|
265
|
+
and every beat names the second argument as that tier's seat. The watcher beats only after reading a
|
|
266
|
+
rendered percentage, keeps beating on the supervision cadence even when its requested poll is slower,
|
|
267
|
+
continues past WARN to ACT, and records a stand-down on each controlled exit. A killed watcher alone
|
|
268
|
+
leaves its last beat to age into `STALE`.
|
|
248
269
|
**Every handoff's re-arm list ends with the announce step from Setup 0** — inform the surviving
|
|
249
270
|
orchestrator the fresh seat is live — or the next seat re-arms silently beside a tier that still
|
|
250
271
|
believes it is alone.
|
|
@@ -396,7 +417,7 @@ they are left implicit:
|
|
|
396
417
|
authors** — the seat's own draft, an operator, another agent's `agent send` (writes WITHOUT Enter),
|
|
397
418
|
and claude-code's AUTOSUGGEST, which renders context-plausible ghost text BYTE-IDENTICAL to a typed
|
|
398
419
|
draft in text-format reads (OBS-482). The check is mechanical and only works at observation time:
|
|
399
|
-
`agent read --format ansi` — dim/grey SGR around the text = autosuggest ghost, NOT input. Measured
|
|
420
|
+
`agent read --format ansi --source visible` — dim/grey SGR (`ESC[2m`) around the text = autosuggest ghost, NOT input. ⚠ **`--source` is load-bearing and its natural choice is the wrong one.** `--source detection` is the plain-text buffer used for agent detection: it strips ANSI *entirely*, so `--format ansi --source detection` returns ZERO escape sequences and every string reads as un-styled — i.e. as real typed input. Measured 2026-08-26 on a live orchestrator: `detection` returned 0 escapes and the ghost read as a genuine unsubmitted draft; `visible` returned 100 escapes and the same line came back `ESC[0mESC[2m…`, dim, ghost. **A probe that cannot render the evidence cannot fail**, so check the capture contains escapes at all before believing its answer — that is rule 11 aimed at your own instrument. Measured
|
|
400
421
|
2026-08-17 (D-206): an unattributed instruction was found in an orchestrator's box, superseded
|
|
401
422
|
defensively, and its origin stayed UNRESOLVED — the one probe that discriminates was not taken while
|
|
402
423
|
the text still sat there. An origin question you can close in ten seconds at the pane becomes
|
|
@@ -414,17 +435,24 @@ The beat is one shipped command and the loop is yours, run from the repo root as
|
|
|
414
435
|
`run_in_background` Bash call:
|
|
415
436
|
|
|
416
437
|
```bash
|
|
417
|
-
cd <repo> && while :; do tickmarkr beat overseer
|
|
418
|
-
tickmarkr beat overseer --
|
|
438
|
+
cd <repo> && while :; do tickmarkr beat overseer --seat <overseer-agent-or-pane>; sleep 10; done
|
|
439
|
+
tickmarkr beat overseer --seat <overseer-agent-or-pane> --stand-down # after stopping that loop
|
|
419
440
|
```
|
|
420
441
|
|
|
442
|
+
The pre-2.1.3 forms `while :; do tickmarkr beat overseer; sleep 10; done` and
|
|
443
|
+
`tickmarkr beat overseer --stand-down` are preserved here only as migration warnings: both are now
|
|
444
|
+
rejected because neither declares which seat the tier speaks for. Do not copy or run them.
|
|
445
|
+
|
|
421
446
|
One beat per invocation, deliberately: the loop is what proves the seat is alive, so a command that
|
|
422
447
|
kept beating on its own would keep reporting a dead seat as healthy. Stop the loop — or die — and the
|
|
423
448
|
tier ages to `STALE` (never `ABSENT`) within six beats, which is the state that says *armed, then lost*.
|
|
424
449
|
Stand down explicitly when you hand off, or a deliberate exit reads as a death. Same rule as rule 29
|
|
425
450
|
below, now with a conventional path the other tier already reads: `tickmarkr status` shows it.
|
|
426
451
|
|
|
427
|
-
⚠ **THE LOOP ABOVE
|
|
452
|
+
⚠ **THE LOOP ABOVE NAMES A SEAT BUT STILL BINDS ITS LIFETIME TO A PROCESS — and that distinction is
|
|
453
|
+
load-bearing.** The command refuses an anonymous beat, and `status` renders the declared seat beside
|
|
454
|
+
the tier state; a legacy tier+pid+instant record cannot be attributed and reads `UNREADABLE`, never
|
|
455
|
+
`ARMED`. Naming the seat does not make the shell loop stop when that seat leaves.
|
|
428
456
|
The beat keeps running while its *session* lives, so a loop started by a seat that has since been
|
|
429
457
|
cleared, re-briefed, or replaced keeps beating that tier's file forever. Measured 2026-08-24
|
|
430
458
|
(OBS-583): a **2d20h** orphan loop from a predecessor seat held `orchestrator ARMED` through a
|
|
@@ -434,13 +462,13 @@ one owned by an unrelated session. So:
|
|
|
434
462
|
- **At every adopt, clear, or re-brief, sweep for pre-existing loops on YOUR tier before arming one**
|
|
435
463
|
(`pgrep -f "tickmarkr beat <tier>"`), trace each to its parent session, and kill the **loop only**
|
|
436
464
|
— never the parent — then verify the parent survived.
|
|
437
|
-
- **`ARMED` is
|
|
438
|
-
whose session owns the beater;
|
|
439
|
-
|
|
465
|
+
- **`ARMED (<seat>)` is an attributable claim, not proof that the named seat is still alive.** Before
|
|
466
|
+
trusting it, ask whose session owns the beater; an orphan loop can keep naming a departed seat
|
|
467
|
+
(rule 11's outliving-its-trigger failure, in beat form).
|
|
440
468
|
- Stand-down must kill the loop **and** run `--stand-down`; the second without the first is undone
|
|
441
469
|
by the next tick.
|
|
442
|
-
The product fix (a
|
|
443
|
-
|
|
470
|
+
The remaining product fix (a sentinel-terminated beat, armed and stood down in one act) is queued;
|
|
471
|
+
until it ships, this sweep is the guard.
|
|
444
472
|
|
|
445
473
|
Arm the bundled watcher as its OWN Bash call with `run_in_background` — chaining it after other commands
|
|
446
474
|
with `&` orphans it from the wake chain. It prints one wake reason and exits; re-arm after every wake.
|
|
@@ -489,10 +517,15 @@ Two keys that do not lie, in order of strength:
|
|
|
489
517
|
will keep producing this stall, and a sweeper that has been running since 04:40 is evidence the gap was
|
|
490
518
|
visible and got swept instead of fixed.
|
|
491
519
|
|
|
492
|
-
**Every seat you spawn gets
|
|
493
|
-
BLOCKED-STATE,
|
|
494
|
-
a stall, the blocked watcher cannot see a finish,
|
|
495
|
-
unsubmitted text in its own prompt
|
|
520
|
+
**Every seat you spawn gets FOUR watchers armed in the SAME call that spawns it — ARTIFACT,
|
|
521
|
+
BLOCKED-STATE, PENDING-INPUT, and CONTEXT.** Each is blind to what the others catch: the artifact watcher cannot
|
|
522
|
+
see a stall, the blocked watcher cannot see a finish, neither can see a seat sitting **idle with
|
|
523
|
+
unsubmitted text in its own prompt**, and none of them can see a seat running out of context.
|
|
524
|
+
**CONTEXT was mandated in prose above and omitted from this list, so it shipped in 2.1.2 and was never armed
|
|
525
|
+
once** — an overseer ran nine hours at 86% unable to read its own number. Arm
|
|
526
|
+
`scripts/watch-context.sh` here, by name, like the other three. Note also that BLOCKED-STATE relies on
|
|
527
|
+
Herdr's `agent_status`, which is unreliable for vendors whose screen detection is skipped (kimi) — for
|
|
528
|
+
those, the spawn-time auto-approve flag is the control, not this watcher.
|
|
496
529
|
|
|
497
530
|
```bash
|
|
498
531
|
.claude/skills/tickmarkr-overseer/scripts/watch-pending-input.sh <agent|pane> [poll-s] [cap-s] [confirm-polls]
|
|
@@ -550,6 +583,15 @@ that hang is byte-identical to a seat still working. The script prints each unfi
|
|
|
550
583
|
last line on every timeout heartbeat — read it there, and when in doubt `tail -1` the artifact, never
|
|
551
584
|
the transcript's claim about it.
|
|
552
585
|
|
|
586
|
+
**An ABSENT artifact ALONE cannot discriminate a working producer from a dead watcher.** A producer still
|
|
587
|
+
working and a watcher that died with its seat write byte-identical evidence — nothing — so a missing file
|
|
588
|
+
is one signal carrying at least three meanings (still working, watcher dead, producer dead), and it is
|
|
589
|
+
**never** evidence that the watcher is still waiting. Reading it that way infers an instrument's liveness
|
|
590
|
+
from the silence it was built to sit through. Settle it with two probes that do not share a failure:
|
|
591
|
+
the watcher from the process table, the producer from its pane or seat status. Measured 2026-08-25
|
|
592
|
+
(OBS-622): both consultants were live and had written nothing, so the artifact side could not see that
|
|
593
|
+
nothing was watching them.
|
|
594
|
+
|
|
553
595
|
**Arm it in the same call as the spawn, not the next one.** A watcher armed "after I finish this step"
|
|
554
596
|
leaves a gap exactly as wide as however long you stay busy, and you will be busy — you just spawned work.
|
|
555
597
|
**Measured 2026-08-06 (OBS-369): two consult verdicts, 30KB and 12.8KB, sat COMPLETE with their markers
|
|
@@ -677,6 +719,50 @@ orchestrator turn boundary.
|
|
|
677
719
|
fix helps ONE operator and leaves every other user with the defect. If an overlay is the interim, it
|
|
678
720
|
says so in writing and names its removal condition.
|
|
679
721
|
|
|
722
|
+
8. **A SHIPPED VERSION IS NOT DONE UNTIL THE STATE IT LEAVES BEHIND IS CLEAN.** Publishing is the loud
|
|
723
|
+
half; the quiet half is that the NEXT seat inherits either the truth or a confident lie. Run this
|
|
724
|
+
before you stand down from any release — **operator instruction, 2026-08-25: *"overseer should always
|
|
725
|
+
leave clean state after a version is shipped"***. Every line below is a defect that actually happened
|
|
726
|
+
on the release that produced this rule.
|
|
727
|
+
|
|
728
|
+
- **REWRITE THE MEMORY INDEX FIRST, and read it back.** Minutes after `2.1.1` hit npm, the index line
|
|
729
|
+
a fresh session loads still read *"⛔ 2.1.1 CANNOT ship from run …2011"* — true when written, and by
|
|
730
|
+
then the exact opposite of the truth. **The index is what everyone loads and the body is what nobody
|
|
731
|
+
opens** (Evidence rule 25), so a stale index is not a cosmetic lag; it is the most-read wrong
|
|
732
|
+
sentence in the project. State what shipped, what did NOT, and the first three things the next seat
|
|
733
|
+
should do.
|
|
734
|
+
- **VERIFY EVERY ID THE CODE NOW CITES ACTUALLY EXISTS.** A release lands source comments citing
|
|
735
|
+
ledger ids. One of that release's entries was written by a heredoc in a command that then timed out
|
|
736
|
+
— the entry survived, but nothing had checked. `grep -c '^## OBS-<id>'` for each id the diff
|
|
737
|
+
introduced. A citation pointing at nothing is the defect the ledger itself files (OBS-604), shipped
|
|
738
|
+
into `src/`.
|
|
739
|
+
- **KILL THE BEAT LOOP *AND* RUN `--stand-down`.** Either alone is worse than neither: the loop
|
|
740
|
+
without the stand-down re-arms a tier you retired within 10s, and the stand-down without the loop
|
|
741
|
+
is undone by the next tick. Verify `status` reads `DISARMED` — which means *handed off*, distinct
|
|
742
|
+
from `STALE` (armed then died) and `ABSENT` (never armed).
|
|
743
|
+
- **RECORD YOUR WATCHERS AS DYING WITH THIS SESSION — never as "armed".** A written stand-down or
|
|
744
|
+
handoff may NOT carry the bare wording *"watcher armed"* for anything this seat owns: that form
|
|
745
|
+
states an act and lets the successor read a fact, and it survived into a handoff exactly once before
|
|
746
|
+
costing two unwatched consult verdicts (OBS-622). The admissible form names the lifetime and the
|
|
747
|
+
work it leaves the successor — *"watchers armed by this session (journal, artifact, dialog, beat);
|
|
748
|
+
they die with it — re-arm on adopt"* — and, per rule 11, says which tier's watchers were NOT armed.
|
|
749
|
+
A detached watcher is the one exception and must be labelled as such, with its heartbeat file, since
|
|
750
|
+
it outlives the seat instead.
|
|
751
|
+
- **SWEEP THE PANES THE RUN LEFT.** A daemon killed by a signal flushes its journal and releases its
|
|
752
|
+
lock but **does not clean up its worker panes or its board**. Two orphaned worker panes and a dead
|
|
753
|
+
board pane sat in the operator's tab bar until he screenshotted them. Verify each is inert first
|
|
754
|
+
(no agent, nothing running in its worktree) and confirm the WORK is on its branch — then close.
|
|
755
|
+
Emptied tabs disappear on their own.
|
|
756
|
+
- **CORRECT EVERY TAB LABEL.** `ORCH · 2.1.1 T1 regate` was still on screen hours after that regate
|
|
757
|
+
ended. Tab labels are how the operator reads fleet state; a stale one is a false status report.
|
|
758
|
+
- **LEAVE THE TREE CLEAN AND SAY WHAT IS UNMERGED.** Name the branches that hold real but ungated
|
|
759
|
+
work, so the next seat neither discards nor trusts them. *"Zero merges, T1/T3 branches ungated, T5
|
|
760
|
+
never dispatched"* is a handoff; *"the run ended"* is not.
|
|
761
|
+
|
|
762
|
+
⚠ **The half of this that is NOT operator discipline must be QUEUED, not absorbed:** a daemon that
|
|
763
|
+
orphans its panes on SIGTERM is a PRODUCT defect and belongs in `src/**`. Sweeping by hand every time
|
|
764
|
+
is the local remedy, and per rule 7 it says so in writing and names its removal condition.
|
|
765
|
+
|
|
680
766
|
---
|
|
681
767
|
|
|
682
768
|
## Briefing a seat to audit a security-shaped check — phrasing matters
|
|
@@ -787,6 +873,14 @@ twice.** They are mission-independent on purpose: nothing here names a task, a l
|
|
|
787
873
|
owns it** — "watchers alive" is the one claim a seat cannot verify about itself. Measured 2026-08-06:
|
|
788
874
|
an orchestrator sat `idle` through three merges and two dispatches with no journal watcher in the
|
|
789
875
|
process table, while its own last report read *"daemon, board, sweeper, watcher all alive"* (OBS-366).
|
|
876
|
+
**STATE THE LIFETIME, because an unstated one is read as the mission's: a session-scoped watcher DIES
|
|
877
|
+
WITH THE SEAT THAT ARMED IT.** Every watcher a seat arms — journal, artifact, dialog, beat loop — is
|
|
878
|
+
session-scoped unless it was deliberately detached (`ppid 1`, the heartbeat form below), so `/clear`,
|
|
879
|
+
a crash, an adopt or a stand-down ends it, and **a handoff is the one moment the arming seat stops
|
|
880
|
+
existing** — which is exactly when its watchers are most likely to be believed. The inverse failure is
|
|
881
|
+
the same root read the other way: a DETACHED loop outlives its seat and holds a tier `ARMED` with
|
|
882
|
+
nobody home (OBS-583). Neither direction may be assumed; the lifetime is a property of how the watcher
|
|
883
|
+
was launched, and it belongs in writing next to every claim that one is armed.
|
|
790
884
|
**And the process-table probe has a standard idiom that DEFEATS it, so the rule above needs one more
|
|
791
885
|
line to be usable.** Never probe for a watcher with `ps … | grep <token> | grep -v grep`: a poll-grep
|
|
792
886
|
watcher carries the word `grep` in its own argv, so the filter whose job is removing the *probing* grep
|
|
@@ -14,26 +14,66 @@
|
|
|
14
14
|
# handoff is stale, the seat is holding state that a clear would destroy, and the correct move is to wake
|
|
15
15
|
# a supervisor — never to clear and hope.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
17
|
+
# THIS WATCHER IS SUPERVISED, and the tier is PER SEAT: `<role>-context`, one per supervising seat, so a
|
|
18
|
+
# live overseer watcher can never make a dead orchestrator one read as covered. Every beat declares the
|
|
19
|
+
# seat it watches (`--seat`), because a tier that is armed and seatless reads as coverage, which is worse
|
|
20
|
+
# than absent. Four rules the shipped version broke, each of which made the tier lie:
|
|
21
|
+
# 1. BEAT ON THE SUPERVISION CADENCE, NOT ON THE POLL. Beats gap by TICK (below), never by POLL, so a
|
|
22
|
+
# poll interval above the supervision beat interval cannot leave the tier stale half of every cycle.
|
|
23
|
+
# 2. BEAT ONLY AFTER A SUCCESSFUL READ. A watcher that cannot see its seat's percentage is not
|
|
24
|
+
# watching it; beating anyway reports coverage it is not providing, and the tier must age out.
|
|
25
|
+
# 3. WARN DOES NOT EXIT. Warn precedes act, so exiting at warn meant the act was never reached and the
|
|
26
|
+
# last beat aged into a permanent stale — gradual growth never reached the auto-clear path at all.
|
|
27
|
+
# 4. EVERY TERMINAL EXIT STANDS THE TIER DOWN, so a watcher that finished reads DISARMED, not dead.
|
|
28
|
+
# Only a killed watcher reads STALE, which is exactly what STALE means.
|
|
29
|
+
#
|
|
30
|
+
# usage: watch-context.sh <orchestrator|overseer> <agent|pane> <warn-pct> <act-pct> [handoff-file] [poll-s] [cap-s]
|
|
18
31
|
# TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff, send /clear and re-brief instead of waking.
|
|
19
32
|
# TKR_REBRIEF=<path> the file the re-briefed seat is told to read (defaults to the handoff).
|
|
20
33
|
# TKR_HANDOFF_MAX_AGE_S how fresh "fresh" is (default 900).
|
|
34
|
+
# TKR_CLEAR_SETTLE_S seconds to let a cleared seat settle before the re-brief (default 6).
|
|
21
35
|
|
|
22
36
|
set -u
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
ROLE="${1:?supervising seat role required: orchestrator|overseer}"
|
|
38
|
+
TARGET="${2:?agent name or pane id required}"
|
|
39
|
+
WARN="${3:-60}"
|
|
40
|
+
ACT="${4:-75}"
|
|
41
|
+
HANDOFF="${5:-}"
|
|
42
|
+
POLL="${6:-120}"
|
|
43
|
+
CAP="${7:-28800}"
|
|
29
44
|
MAXAGE="${TKR_HANDOFF_MAX_AGE_S:-900}"
|
|
30
45
|
REBRIEF="${TKR_REBRIEF:-$HANDOFF}"
|
|
46
|
+
SETTLE="${TKR_CLEAR_SETTLE_S:-6}"
|
|
47
|
+
|
|
48
|
+
case "$ROLE" in
|
|
49
|
+
orchestrator|overseer) TIER="${ROLE}-context" ;;
|
|
50
|
+
*) echo "watch-context.sh: unknown seat role '$ROLE' — expected orchestrator or overseer" >&2; exit 64 ;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
# The supervision beat interval (SUPERVISION_BEAT_MS = 10s). The loop ticks at the beat cadence or the
|
|
54
|
+
# caller's poll, whichever is SHORTER: a beat may only follow a successful read (rule 2), so the read
|
|
55
|
+
# cadence is the floor on the beat cadence, and the tier's freshness is never the caller's to widen.
|
|
56
|
+
BEAT_EVERY=5
|
|
57
|
+
TICK=$(( POLL < BEAT_EVERY ? POLL : BEAT_EVERY ))
|
|
58
|
+
[ "$TICK" -ge 1 ] 2>/dev/null || TICK=1 # a zero or junk poll would spin, not watch
|
|
59
|
+
SEAT="$TARGET"
|
|
60
|
+
|
|
61
|
+
beat() { tickmarkr beat "$TIER" --seat "$SEAT" >/dev/null 2>&1; }
|
|
62
|
+
stand_down() { tickmarkr beat "$TIER" --stand-down --seat "$SEAT" >/dev/null 2>&1; }
|
|
63
|
+
# EVERY terminal exit — act, unsafe-act, cap — leaves through here, so none of them can forget to
|
|
64
|
+
# record the hand-off. A killed watcher never runs it, which is the one case that must read STALE.
|
|
65
|
+
trap stand_down EXIT
|
|
31
66
|
|
|
32
|
-
# The seat's own rendered truth.
|
|
33
|
-
#
|
|
67
|
+
# The seat's own rendered truth. Prefer a line carrying a context marker so a percentage elsewhere on
|
|
68
|
+
# screen cannot be mistaken for the gauge — but NEVER require one: the ✳ marker the shipped version
|
|
69
|
+
# anchored on is rendered by a single vendor, so every other seat read empty, beat anyway and slept to
|
|
70
|
+
# its cap while its tier claimed coverage. The last percentage in the statusline window is the fallback.
|
|
34
71
|
context_pct() {
|
|
35
|
-
|
|
36
|
-
|
|
72
|
+
local screen marked
|
|
73
|
+
screen=$(herdr agent read "$TARGET" --source visible --lines 8 2>/dev/null) || return 1
|
|
74
|
+
marked=$(printf '%s\n' "$screen" | grep -iE '✳|context' | grep -oE '[0-9]+%' | tail -1 | tr -d '%')
|
|
75
|
+
[ -n "$marked" ] && { printf '%s\n' "$marked"; return 0; }
|
|
76
|
+
printf '%s\n' "$screen" | grep -oE '[0-9]+%' | tail -1 | tr -d '%'
|
|
37
77
|
}
|
|
38
78
|
|
|
39
79
|
handoff_fresh() {
|
|
@@ -46,41 +86,50 @@ handoff_fresh() {
|
|
|
46
86
|
[ "$age" -le "$MAXAGE" ]
|
|
47
87
|
}
|
|
48
88
|
|
|
89
|
+
act_on() {
|
|
90
|
+
local P="$1"
|
|
91
|
+
if handoff_fresh; then
|
|
92
|
+
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
|
|
93
|
+
herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
|
|
94
|
+
sleep "$SETTLE"
|
|
95
|
+
herdr agent prompt "$TARGET" "Read ${REBRIEF} and continue exactly where it says. Your context was cleared at ${P}% against that handoff; it is current as of $(date '+%H:%M'). Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
|
|
96
|
+
echo "CONTEXT_CLEARED $TARGET at ${P}% — handoff fresh, re-briefed from ${REBRIEF}"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
echo "CONTEXT_ACT $TARGET ${P}% (>= ${ACT}) — handoff is FRESH, a clear is SAFE now"
|
|
100
|
+
echo " herdr agent prompt $TARGET \"/clear\" then re-brief from ${REBRIEF}"
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
echo "CONTEXT_ACT_UNSAFE $TARGET ${P}% (>= ${ACT}) — NO FRESH HANDOFF (${HANDOFF:-none})"
|
|
104
|
+
echo " the seat is holding state that exists only in its head; a clear would destroy it"
|
|
105
|
+
echo " make it write the handoff FIRST, then clear"
|
|
106
|
+
exit 0
|
|
107
|
+
}
|
|
108
|
+
|
|
49
109
|
warned=0
|
|
50
110
|
elapsed=0
|
|
51
111
|
while [ "$elapsed" -lt "$CAP" ]; do
|
|
52
112
|
P=$(context_pct)
|
|
53
113
|
if [ -z "$P" ]; then
|
|
54
|
-
|
|
114
|
+
# Rule 2: no reading, no beat. The tier ages to STALE and a supervisor comes looking, which is the
|
|
115
|
+
# truth about a watcher that cannot see the seat it was armed on.
|
|
116
|
+
sleep "$TICK"; elapsed=$((elapsed + TICK)); continue
|
|
55
117
|
fi
|
|
118
|
+
beat
|
|
56
119
|
|
|
57
120
|
if [ "$P" -ge "$ACT" ] 2>/dev/null; then
|
|
58
|
-
|
|
59
|
-
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
|
|
60
|
-
herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
|
|
61
|
-
sleep 6
|
|
62
|
-
herdr agent prompt "$TARGET" "Read ${REBRIEF} and continue exactly where it says. Your context was cleared at ${P}% against that handoff; it is current as of $(date '+%H:%M'). Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
|
|
63
|
-
echo "CONTEXT_CLEARED $TARGET at ${P}% — handoff fresh, re-briefed from ${REBRIEF}"
|
|
64
|
-
exit 0
|
|
65
|
-
fi
|
|
66
|
-
echo "CONTEXT_ACT $TARGET ${P}% (>= ${ACT}) — handoff is FRESH, a clear is SAFE now"
|
|
67
|
-
echo " herdr agent prompt $TARGET \"/clear\" then re-brief from ${REBRIEF}"
|
|
68
|
-
exit 0
|
|
69
|
-
fi
|
|
70
|
-
echo "CONTEXT_ACT_UNSAFE $TARGET ${P}% (>= ${ACT}) — NO FRESH HANDOFF (${HANDOFF:-none})"
|
|
71
|
-
echo " the seat is holding state that exists only in its head; a clear would destroy it"
|
|
72
|
-
echo " make it write the handoff FIRST, then clear"
|
|
73
|
-
exit 0
|
|
121
|
+
act_on "$P"
|
|
74
122
|
fi
|
|
75
123
|
|
|
76
124
|
if [ "$P" -ge "$WARN" ] 2>/dev/null && [ "$warned" -eq 0 ]; then
|
|
125
|
+
# Rule 3: warn is a LINE, not an exit — the act is on the far side of it.
|
|
77
126
|
warned=1
|
|
78
127
|
echo "CONTEXT_WARN $TARGET ${P}% (>= ${WARN}) — write the handoff NOW, while judgement is still good"
|
|
128
|
+
echo " re-brief target when it acts: ${REBRIEF:-none}"
|
|
79
129
|
echo " a handoff written at ${ACT}% is written by a seat already degraded; that is the wrong time"
|
|
80
|
-
exit 0
|
|
81
130
|
fi
|
|
82
131
|
|
|
83
|
-
sleep "$
|
|
132
|
+
sleep "$TICK"; elapsed=$((elapsed + TICK))
|
|
84
133
|
done
|
|
85
134
|
|
|
86
135
|
echo "WATCH_CAP_REACHED $TARGET context=$(context_pct)% — no threshold crossed in ${CAP}s"
|