kijito-tools 0.2.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.
Files changed (104) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +220 -0
  4. package/bin/cli.js +34 -0
  5. package/install.sh +161 -0
  6. package/package.json +50 -0
  7. package/providers/_shared/wake-core.mjs +358 -0
  8. package/providers/claude/CLAUDE.md.snippet +31 -0
  9. package/providers/claude/install.sh +186 -0
  10. package/providers/claude/scripts/arm-session.sh +66 -0
  11. package/providers/claude/scripts/claude-armed.sh +101 -0
  12. package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
  13. package/providers/claude/scripts/inbox-selftest.sh +277 -0
  14. package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
  15. package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
  16. package/providers/claude/scripts/lifecycle-lib.sh +167 -0
  17. package/providers/claude/scripts/myctx.sh +46 -0
  18. package/providers/claude/scripts/self-clear.sh +134 -0
  19. package/providers/claude/scripts/session-autosend.sh +85 -0
  20. package/providers/claude/scripts/session-catchup-hint.sh +330 -0
  21. package/providers/claude/scripts/statusline-context.sh +67 -0
  22. package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
  23. package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
  24. package/providers/claude/skills/kijito-start/SKILL.md +122 -0
  25. package/providers/claude/wiring/README.md +93 -0
  26. package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
  27. package/providers/codex/README.md +75 -0
  28. package/providers/codex/codex-kijito-parity-plan.md +265 -0
  29. package/providers/codex/docs-codex-setup.md +64 -0
  30. package/providers/codex/install.mjs +184 -0
  31. package/providers/codex/n0-capability-probe-protocol.md +472 -0
  32. package/providers/codex/n0-harness/README.md +57 -0
  33. package/providers/codex/n0-harness/cli.mjs +58 -0
  34. package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
  35. package/providers/codex/n0-harness/fixture.mjs +279 -0
  36. package/providers/codex/n0-harness/lib.mjs +141 -0
  37. package/providers/codex/n0-harness/manifest.mjs +36 -0
  38. package/providers/codex/n0-harness/oracle.mjs +464 -0
  39. package/providers/codex/n0-harness/parser.mjs +200 -0
  40. package/providers/codex/n0-harness/prompt.mjs +43 -0
  41. package/providers/codex/n0-harness/snapshot.mjs +120 -0
  42. package/providers/codex/n0-harness/specimen.mjs +37 -0
  43. package/providers/codex/notify/kijito-notify-count.mjs +117 -0
  44. package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
  45. package/providers/codex/plans/gate4-battery-log.md +184 -0
  46. package/providers/codex/plans/gate4-battery-protocol.md +43 -0
  47. package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
  48. package/providers/codex/plans/hive-user-first-plan.md +247 -0
  49. package/providers/codex/release-manifest.json +53 -0
  50. package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
  51. package/providers/codex/same-chat-continuation-plan.md +458 -0
  52. package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
  53. package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
  54. package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
  55. package/providers/codex/skills/kijito-start/SKILL.md +173 -0
  56. package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
  57. package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
  58. package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
  59. package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
  60. package/providers/codex/test/n0-guard-census.json +35479 -0
  61. package/providers/codex/test/n0-guard-census.mjs +31 -0
  62. package/providers/codex/test/n0-guard-census.test.mjs +374 -0
  63. package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
  64. package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
  65. package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
  66. package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
  67. package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
  68. package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
  69. package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
  70. package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
  71. package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
  72. package/providers/codex/test/n0-harness.test.mjs +553 -0
  73. package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
  74. package/providers/codex/test/n0-subject-boundary.mjs +97 -0
  75. package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
  76. package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
  77. package/providers/codex/tools/refresh-manifest.mjs +81 -0
  78. package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
  79. package/providers/codex/wake-helper/integration.test.mjs +285 -0
  80. package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
  81. package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
  82. package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
  83. package/providers/codex/wake-helper/status-probe.mjs +45 -0
  84. package/providers/codex/wake-helper/ws-uds.mjs +153 -0
  85. package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
  86. package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
  87. package/providers/monitor/CHANGELOG.md +465 -0
  88. package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
  89. package/providers/monitor/LICENSE +202 -0
  90. package/providers/monitor/NOTICE +7 -0
  91. package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
  92. package/providers/monitor/README.md +571 -0
  93. package/providers/monitor/RELEASING.md +125 -0
  94. package/providers/monitor/arm-hive-monitor.sh +13 -0
  95. package/providers/monitor/bin/cli.js +43 -0
  96. package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
  97. package/providers/monitor/docs/DESIGN.md +984 -0
  98. package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
  99. package/providers/monitor/kijito_inbox_monitor.py +3666 -0
  100. package/providers/monitor/package.json +35 -0
  101. package/providers/monitor/pyproject.toml +42 -0
  102. package/providers/monitor/scripts/mutation-check.py +399 -0
  103. package/providers/monitor/scripts/prepublish-gate.sh +284 -0
  104. package/providers/monitor/test_kijito_monitor.py +5609 -0
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # AGENT-INVOKED self-/clear — the agent's FINAL action, only after /kijito-qa-memory passed.
3
+ # Hard gates (all must hold): kill-switch off · armed · not-a-subagent · in tmux · pane alive ·
4
+ # FRESH qa-pass token. Never auto-fired. (Cycle cap + every-5 checkpoint REMOVED 2026-07-29 — see C2.)
5
+ set -u
6
+ # Resolve the shared lib NEXT TO THIS SCRIPT so the repo copy is runnable/testable in place, and
7
+ # fall back to the installed location for a stray single-file copy. KIJITO_LC_LIB overrides both.
8
+ _kjt_lib="${KIJITO_LC_LIB:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lifecycle-lib.sh}"
9
+ [ -f "$_kjt_lib" ] || _kjt_lib="$HOME/.claude/lifecycle-lib.sh"
10
+ . "$_kjt_lib"
11
+ refuse(){ echo "self-clear REFUSED: $1" >&2; lc_log SELFCLEAR_REFUSED "$1"; exit "${2:-3}"; }
12
+
13
+ lc_stopped && refuse "kill switch present ($KIJITO_LC_STOP) — rm it to re-enable" 9
14
+ lc_is_child && refuse "subagent marker set — would clear the PARENT pane" 6
15
+ # ⚠️ THE TMUX CHECK MUST PRECEDE THE ARMED CHECK, AND THE ORDER USED TO BE REVERSED (argus, 2026-08-01).
16
+ # Outside tmux there is no TMUX_PANE, so lc_is_armed falls back to a placeholder and refuses with
17
+ # "not an armed pane — launch via claude-armed.sh". But arming is PANE-KEYED and arm-session.sh
18
+ # itself exits 1 outside tmux, so that refusal pointed at a remedy which cannot work. It is the
19
+ # familiar shape: COULD-NOT-MEASURE wearing the costume of THE-CLAIM-IS-FALSE. Ask the question
20
+ # that can actually be answered first.
21
+ { [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ]; } || refuse "not in tmux (TMUX/TMUX_PANE unset)" 4
22
+ lc_pane_alive "$TMUX_PANE" || refuse "target pane $TMUX_PANE no longer exists" 4
23
+ lc_is_armed "${TMUX_PANE:-}" || refuse "not an armed pane — self-clear only runs in autonomous sessions (launch via ~/.claude/claude-armed.sh, or ~/.claude/arm-session.sh on); plain 'claude' is human-managed" 3
24
+ # (no pane_current_command gate — unreliable label; send-keys reaches the TTY regardless)
25
+
26
+ # C1 — require a FRESH /kijito-qa-memory pass for THIS session
27
+ tok="$(lc_qa_token)"; ttl="${KIJITO_QA_TTL:-1800}"
28
+ [ -f "$tok" ] || refuse "no kijito-qa-memory pass — run /kijito-qa-memory first (it cold-boot-verifies, then records the token)" 5
29
+ age=$(( $(lc_now) - $(cat "$tok" 2>/dev/null || echo 0) ))
30
+ [ "$age" -le "$ttl" ] || refuse "kijito-qa-memory pass is stale (${age}s > ${ttl}s) — re-run /kijito-qa-memory" 5
31
+
32
+ # C2 — cycle COUNTER, telemetry only. NOT a gate.
33
+ #
34
+ # ⛔ REMOVED 2026-07-29 ON JASON'S EXPLICIT INSTRUCTION: the cycle cap (default 12)
35
+ # and the every-5 human checkpoint. Both were COUNT-based, and a count cannot
36
+ # distinguish a runaway loop from a productive day — it measures UPTIME.
37
+ #
38
+ # MEASURED over the whole log before removing (88 successful cycles, 24 refusals):
39
+ # 15 checkpoint at cycle N (every 5) <- count-based, never caught a loop
40
+ # 4 cycle cap hit <- count-based, never caught a loop
41
+ # 3 kijito-qa-memory pass is stale <- PROPERTY-based, genuinely useful
42
+ # 1 not an armed pane <- PROPERTY-based, genuinely useful
43
+ # 1 no kijito-qa-memory pass <- PROPERTY-based, genuinely useful
44
+ # 19 of 24 refusals were the two count gates. Jason: "it's never once been useful,
45
+ # it's just gotten you to high context usage and blocked on something that isn't
46
+ # useful." Correct: they fired hardest on the most productive days, and the cost
47
+ # was a degraded session sitting at ~70% context waiting for a human to type rm.
48
+ #
49
+ # ★ THE REAL GATES ARE ALL STILL ABOVE and none of them is a counter: kill-switch,
50
+ # armed pane, not-a-subagent, in-tmux, pane alive, and a FRESH cold-boot-verified
51
+ # qa-pass token. Those check a PROPERTY of this clear ("is the handoff good enough
52
+ # to survive it?") rather than how many times it has happened before. A thin
53
+ # handoff still cannot self-clear, which is the protection that ever mattered.
54
+ #
55
+ # ⚠️ If a genuine runaway ever needs catching, detect the LOOP, not the count:
56
+ # consecutive cycles that land no commits and no memories. Do not reintroduce a
57
+ # counter — it is the same defect class as a stranded-mail check that measures
58
+ # broadcast cadence instead of neglect.
59
+ cf="$(lc_cycle_file)"; cyc=$(( $(cat "$cf" 2>/dev/null || echo 0) + 1 )); echo "$cyc" > "$cf"
60
+
61
+ # C5 — consume the token (one clear per QA pass) and fire as the LAST action
62
+ rm -f "$tok"
63
+ delay="${KIJITO_SELFCLEAR_DELAY:-3.0}"
64
+
65
+ # C6 — RECORD THE CONTEXT LEVEL WE RECYCLED AT. OBSERVABILITY, NOT A GATE.
66
+ # ⛔ THE RULE IS NUMERIC AND WE WERE RECORDING NO INSTANCE OF THE NUMBER. `lifecycle.log` could
67
+ # say THAT a seat recycled and WHEN, but not whether it went at 20% or 78% — so "does the fleet
68
+ # actually recycle near the target?" was unanswerable, including retrospectively. You could not
69
+ # spot a seat looping at 15%, nor one running to 80% and doing its worst work in the tail.
70
+ # (Found by ladybug 2026-08-01 while auditing the myctx residual; this also gives myctx's
71
+ # non-zero exit its first real consumer — until now its only "consumer" was a sentence of prose
72
+ # telling an agent to run it.)
73
+ #
74
+ # ⛔ AND IT IS DELIBERATELY NOT A GATE, which is the more important half. The obvious version —
75
+ # "refuse to self-clear when context is UNMEASURABLE" — makes jq, $CLAUDE_CODE_SESSION_ID and a
76
+ # readable transcript into three new fleet-wide halt conditions for the autonomous loop. That is
77
+ # a STRICTLY LARGER outage than the risk it removes. ★ The blast radius a new gate on this path
78
+ # may have is "can stop ONE cycle"; that one is "can stop EVERY cycle on EVERY seat". ladybug
79
+ # proposed it against their own instinct for exactly this reason, hours after nearly shipping
80
+ # `lc_is_child` — a real defect with an invented fix that would have refused forever.
81
+ #
82
+ # ✅ THE CAVEAT THEY FLAGGED IS RESOLVED BY MEASUREMENT, NOT BY ARGUMENT: myctx inside a SUBAGENT
83
+ # reports the PARENT's context (a real number about the wrong subject), so this would be worse
84
+ # than useless if this script ran in a different session context than the pane's agent. Verified
85
+ # 2026-08-01 on the VM: the shell this script runs in resolves the same CLAUDE_CODE_SESSION_ID as
86
+ # the pane's agent, and myctx there returned 32.2% / ~677557 free against the agent's own live
87
+ # counter of 677261 remaining — agreement to ~300 tokens, which is just the tokens spent between
88
+ # the two reads. ⚠️ A WRONG number in an audit log is worse than an absent one, so if this ever
89
+ # moves to a different execution context, RE-MEASURE that agreement before trusting the field.
90
+ ctx="UNMEASURABLE"
91
+ # KIJITO_MYCTX exists so the SUCCESS branch is testable. Without it a test can only ever exercise
92
+ # the failure path (a fixture session has no transcript), and a branch that is only ever tested in
93
+ # the direction it fails is not tested — the exact defect that let lc_pane_alive return TRUE for
94
+ # every input for months.
95
+ _myctx="${KIJITO_MYCTX:-$HOME/.claude/myctx.sh}"
96
+ if [ -x "$_myctx" ]; then
97
+ _m=$("$_myctx" 2>/dev/null) && case "$_m" in
98
+ *%*) ctx=$(printf '%s' "$_m" | sed -n 's/.*= *\([0-9.]*%\).*/\1/p'); [ -n "$ctx" ] || ctx="UNPARSED" ;;
99
+ esac
100
+ fi
101
+ lc_log SELFCLEAR_FIRE "cycle=$cyc delay=$delay ctx=$ctx"
102
+ ( sleep "$delay"
103
+ lc_stopped && { lc_log SELFCLEAR_ABORT "stop during delay"; exit 0; }
104
+ lc_pane_alive "$TMUX_PANE" || { lc_log SELFCLEAR_ABORT "pane gone during delay"; exit 0; }
105
+ # ⛔ BRANCH ON DELIVERY. `SELFCLEAR_DONE` used to be logged UNCONDITIONALLY, with both send-keys
106
+ # calls discarding stderr and nothing reading their status — so a REFUSED delivery still wrote
107
+ # DONE (argus, 2026-08-01). An audit log asserting an action that did not occur is the one thing
108
+ # an audit log must never do, and it chained with the decorative lc_pane_alive: gate passes,
109
+ # delivery fails, log says DONE, token is consumed — the loop believes it recycled and did not.
110
+ #
111
+ # ★ THE TWO FAILURES ARE LOGGED SEPARATELY ON PURPOSE: "not typed" and "typed but not submitted"
112
+ # are different states with different causes, and the second is exactly what Jason observed for
113
+ # session-autosend. Collapsing them would hide the one the settle-sleep below addresses.
114
+ if tmux send-keys -t "$TMUX_PANE" -l -- "/clear" 2>/dev/null; then
115
+ # Same paste-buffer race that broke session-autosend — an Enter arriving inside the TUI's ingest
116
+ # burst is taken as a newline rather than as submit. "/clear" is short and has fired ~88 times
117
+ # successfully, so this gap is hardening rather than a repair; the cost of failing here is the
118
+ # same dead loop.
119
+ # ⛔ NO RETRY LOOP, DELIBERATELY — THE OPPOSITE CHOICE FROM session-autosend. There a second
120
+ # Enter is free. Here a second "/clear" would land in the session the first already cleared,
121
+ # wiping the auto-resume prompt the SessionStart hook had just injected — stopping the loop by
122
+ # way of the very mechanism meant to protect it. Fire once.
123
+ sleep "${KIJITO_SEND_SETTLE:-1.2}"
124
+ if tmux send-keys -t "$TMUX_PANE" Enter 2>/dev/null; then
125
+ lc_log SELFCLEAR_DONE "cycle=$cyc"
126
+ else
127
+ lc_log SELFCLEAR_FAILED "cycle=$cyc Enter refused pane=$TMUX_PANE — /clear typed, NOT submitted"
128
+ fi
129
+ else
130
+ lc_log SELFCLEAR_FAILED "cycle=$cyc send-keys refused pane=$TMUX_PANE — NOT cleared"
131
+ fi
132
+ ) >/dev/null 2>&1 &
133
+ echo "self-clear scheduled (cycle $cyc, uncapped): /clear → $TMUX_PANE in ${delay}s. This MUST be your FINAL action — stop now; SessionStart re-catches-up and resumes the preloaded work."
134
+ exit 0
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # Delayed self-send of the catch-up prompt INTO a tmux pane → instigates the first turn.
3
+ # Called (detached, via nohup &) by the SessionStart hook ONLY when the pane is armed + in tmux.
4
+ # Args: $1 = target tmux pane (normally "$TMUX_PANE"). Fixed-delay approach — proven reliable.
5
+ # Needs Kijito: OPTIONAL — set KIJITO_AUTOCATCHUP_PROMPT for your own text, or KIJITO_MODE=off for a
6
+ # generic (non-Kijito) default prompt.
7
+ set -u
8
+ # Resolve the shared lib NEXT TO THIS SCRIPT so the repo copy is runnable/testable in place, and
9
+ # fall back to the installed location for a stray single-file copy. KIJITO_LC_LIB overrides both.
10
+ _kjt_lib="${KIJITO_LC_LIB:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lifecycle-lib.sh}"
11
+ [ -f "$_kjt_lib" ] || _kjt_lib="$HOME/.claude/lifecycle-lib.sh"
12
+ . "$_kjt_lib"
13
+ pane="${1:?target pane required}"
14
+ command -v tmux >/dev/null 2>&1 || exit 0
15
+ lc_stopped && { lc_log AUTOSEND_SKIP "kill switch"; exit 0; }
16
+
17
+ if [ -n "${KIJITO_AUTOCATCHUP_PROMPT:-}" ]; then
18
+ prompt="$KIJITO_AUTOCATCHUP_PROMPT"
19
+ elif [ "${KIJITO_MODE:-on}" = "off" ]; then
20
+ # standalone (no Kijito): catch up against whatever notes/handoff you keep
21
+ prompt="Catch up on this project's context and any handoff / next-steps notes from the prior session, then CONTINUE the active work to its DONE-WHEN without waiting for further instruction. If there's no active work, report ready."
22
+ else
23
+ # Invoke the packaged catch-up routine (~/.claude/skills/kijito-start) by name — prose-invoked
24
+ # rather than send-keys'ing a literal "/kijito-start" (the TUI slash-autocomplete menu is an
25
+ # extra failure mode this path doesn't need). The skill covers catch-up + inbox-arm + new-persona
26
+ # setup; the trailing directive carries the autonomous-resume mandate. (This prompt predated the
27
+ # skill — updated 2026-07-10 per Jason.)
28
+ prompt="Run the kijito-start skill (Skill: kijito-start) and follow it fully — catch up deeply on memory, arm the inbox, and if this is a brand-new project with no persona yet, set that up per CLAUDE.md. Then, if the current-state / next-steps pointer shows ACTIVE WORK in progress, CONTINUE it autonomously without waiting for further instruction — work to its DONE-WHEN criteria, stopping only for a genuine gate. If there is no active work to resume, report ready."
29
+ fi
30
+
31
+ delay="${KIJITO_AUTOCATCHUP_DELAY:-4.0}" # seconds for the TUI to become input-ready
32
+ sleep "$delay"
33
+ lc_stopped && { lc_log AUTOSEND_ABORT "stop appeared"; exit 0; }
34
+ lc_pane_alive "$pane" || { lc_log AUTOSEND_ABORT "pane gone"; exit 0; }
35
+ # NOTE: do NOT gate on pane_current_command — it's unreliable (reports "bash" for a wrapped
36
+ # claude, the version for an exec'd one). send-keys reaches the pane's TTY (claude) regardless.
37
+ tmux send-keys -t "$pane" -l -- "$prompt" 2>/dev/null
38
+
39
+ # ⛔ THE ENTER NEEDS A GAP AFTER THE TEXT, AND WITHOUT ONE THE WHOLE AUTONOMOUS LOOP SILENTLY DIES.
40
+ # Observed 2026-08-01 (Jason, live): "the injected start prompt was just entered into the input but
41
+ # remained unsent." The two send-keys calls used to be back-to-back. The TUI is an Ink app that
42
+ # buffers a fast burst of characters as a PASTE, and an Enter arriving inside that burst is taken as
43
+ # a NEWLINE IN THE BUFFER rather than as submit. The prompt then sits in the input box, complete and
44
+ # unsent, forever.
45
+ #
46
+ # ★ WHY THIS IS THE WORST POSSIBLE PLACE FOR A SILENT FAILURE: this send is the ONLY thing that
47
+ # restarts work after a /clear. A self-clear with a broken re-send does not degrade the loop, it
48
+ # ENDS it — and it ends it in the state that looks most like success, because /clear ran, the pane
49
+ # is alive, and the prompt is visibly right there on screen.
50
+ settle="${KIJITO_SEND_SETTLE:-1.2}" # let the TUI finish ingesting the paste
51
+ sleep "$settle"
52
+
53
+ # ⚠️ AND SENDING ENTER IS NOT THE SAME AS HAVING SENT THE PROMPT, so verify rather than hope.
54
+ # After a successful submit the input box is empty and the text has moved up into the transcript, so
55
+ # the prompt's TAIL disappears from the BOTTOM few lines. If it is still down there, the Enter did
56
+ # not take — retry a bounded number of times rather than leaving the loop dead.
57
+ #
58
+ # The probe is the prompt's LAST 40 characters: the tail is what remains visible in a wrapped input
59
+ # box, and matching a fixed string with -F avoids any regex metacharacter in the prompt.
60
+ probe=$(printf '%s' "$prompt" | tail -c 40)
61
+ sent=0
62
+ for _try in 1 2 3; do
63
+ tmux send-keys -t "$pane" Enter 2>/dev/null
64
+ sleep 1.5
65
+ if ! tmux capture-pane -p -t "$pane" 2>/dev/null | tail -6 | grep -qF -- "$probe"; then
66
+ sent=1; break
67
+ fi
68
+ lc_log AUTOSEND_RETRY "enter did not submit (attempt $_try)"
69
+ done
70
+
71
+ if [ "$sent" = 1 ]; then
72
+ lc_log AUTOSEND_FIRE "delay=$delay settle=$settle"
73
+ else
74
+ # ⚠️ UNCONFIRMED, NOT FAILED — and the distinction is the same one the gate runner enforces
75
+ # between BLOCKED and FAIL. The probe reads the BOTTOM of the pane, so it can only observe that
76
+ # the prompt's tail is still down there. That is strong evidence in the real TUI (a submitted
77
+ # message scrolls up and the input box empties) but it is not proof: any host whose display keeps
78
+ # the text visible at the bottom — a plain shell echoing input, a narrow pane, an unusual theme —
79
+ # produces a false negative on a delivery that actually worked.
80
+ # ⇒ Say what was observed, never more. Claiming "the loop is NOT running" when the loop may be
81
+ # perfectly fine is exactly the wrong-diagnosis-costs-more failure argus and I have both been
82
+ # chasing tonight; a confident wrong log entry sends the next reader hunting the wrong thing.
83
+ lc_log AUTOSEND_UNCONFIRMED "sent 3 Enters; prompt tail still visible at the bottom of the pane — delivery NOT confirmed (it may still have worked; check the pane before acting)"
84
+ fi
85
+ exit 0
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook → (1) ALWAYS print a passive catch-up reminder + an EXPLICIT, per-persona
3
+ # WAKE-CAPABLE inbox-arming instruction, (2) ARMED auto-send if this pane is armed.
4
+ #
5
+ # Why the explicit arming block (Jason fleet-directive, "an unmonitored mailbox is useless"):
6
+ # agents fail two ways — they forget to arm, or they arm WRONG. A bare background `tail -F` is
7
+ # CAPTURE-ONLY: it writes matching lines to a file and never exits, so the harness never
8
+ # re-invokes the agent and mail is silently missed (argus's exact failure, 2026-06-29). The
9
+ # wake-capable consumer in Claude Code is the Monitor TOOL (persistent), which streams each event
10
+ # as a live notification that interrupts the agent. This hook injects the EXACT Monitor call so
11
+ # there is one unambiguous first action.
12
+ #
13
+ # Resolve the shared lib and sibling scripts NEXT TO THIS SCRIPT so the repo copy is runnable and
14
+ # testable in place, falling back to the installed location for a stray single-file copy.
15
+ # KIJITO_LC_LIB overrides the lib path.
16
+ _kjt_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ _kjt_lib="${KIJITO_LC_LIB:-$_kjt_dir/lifecycle-lib.sh}"
18
+ [ -f "$_kjt_lib" ] || _kjt_lib="$HOME/.claude/lifecycle-lib.sh"
19
+ . "$_kjt_lib" 2>/dev/null
20
+
21
+ # Read the hook stdin ONCE (both .source and .cwd come from it).
22
+ _in=$(cat 2>/dev/null)
23
+ src=$(printf '%s' "$_in" | jq -r '.source // "startup"' 2>/dev/null); [ -z "$src" ] && src=startup
24
+ hook_cwd=$(printf '%s' "$_in" | jq -r '.cwd // empty' 2>/dev/null)
25
+
26
+ case "$src" in
27
+ clear) pre="You just /clear'd — context was intentionally reset to a clean slate." ;;
28
+ compact) pre="Context was just compacted — detail was summarized away; memory is now the source of truth." ;;
29
+ *) pre="New session." ;;
30
+ esac
31
+
32
+ # ── Resolve THIS project's persona from a .kijito_persona marker (self-describing, travels with
33
+ # the project, survives a rename — preferred over parsing CLAUDE.md prose or a central dir->persona
34
+ # map that rots). Search order: $CLAUDE_PROJECT_DIR, the hook-reported cwd, $PWD.
35
+ #
36
+ # ── ASK THE PRODUCER FOR THE FILENAME RULE; NEVER RE-IMPLEMENT IT (row M290) ──────────────────────
37
+ # This line used to read `sed 's/[^A-Za-z0-9._-]/_/g'`, described as matching the producer's rule. It
38
+ # did not, in two ways that matter: the producer CASEFOLDS (the local filesystem is case-insensitive,
39
+ # so it must) and it accepts any UNICODE alphanumeric. So a persona named `Loom` got `Loom.jsonl` here
40
+ # and `loom.jsonl` from the producer; `Ωmega` got `_mega` here and `ωmega` there. The hook then told
41
+ # the user their mail was "not being collected" and pointed a Monitor at a file that will never exist
42
+ # — silence forever, no error (beta feedback #14/#16).
43
+ # ⚠️ AND IT WAS INVISIBLE TO EVERYONE WHO TESTED IT ON A MAC: APFS is case-INSENSITIVE, so the `-e`
44
+ # probe below SUCCEEDS on the producer's differently-cased file. The bug only exists on Linux, which
45
+ # is why it reached a user rather than a reviewer.
46
+ # ⇒ The producer publishes the rule as `--safe-persona NAME` (a pure string transform: no token, no
47
+ # network, no state file). We ask it. If we CANNOT ask it — no producer installed, or one too old to
48
+ # answer — we do NOT fall back to guessing, because a guess is what produced this defect; we say so
49
+ # and name the fix. A wrong path here is unfalsifiable by construction: it fails as silence.
50
+ # The marker read lives in kijito-persona-lib.sh so the status line (row M309) and anything added
51
+ # later cannot drift from it the way the filename rule did. Falling back to the inline loop keeps a
52
+ # partially-installed ~/.claude working rather than silently resolving no persona at all.
53
+ _persona=""
54
+ _lib="$(dirname -- "${BASH_SOURCE[0]:-$0}")/kijito-persona-lib.sh"
55
+ if [ -r "$_lib" ]; then
56
+ # shellcheck source=/dev/null
57
+ . "$_lib"
58
+ _persona=$(kijito_persona_from_marker "${CLAUDE_PROJECT_DIR:-}" "$hook_cwd" "$PWD" || true)
59
+ fi
60
+ for d in "${CLAUDE_PROJECT_DIR:-}" "$hook_cwd" "$PWD"; do
61
+ if [ -z "$_persona" ] && [ -n "$d" ] && [ -f "$d/.kijito_persona" ]; then
62
+ # ⛔ TRIM THE ENDS, NEVER THE MIDDLE (row M290). This read was `tr -d '[:space:]'`, which deletes
63
+ # EVERY space in the name: a persona written `name (purpose)` in the marker became `name(purpose)`
64
+ # here and `name_purpose_` as a filename, while the producer — which receives the name with its
65
+ # space intact from the API — wrote `name__purpose_`. THAT is the exact pair of filenames beta
66
+ # feedback #14/#16 reported, and it is a different defect from the sanitizer mismatch beside it:
67
+ # the name was already corrupted BEFORE any sanitizer ran, so fixing only the sanitizer would have
68
+ # left this case broken while looking fixed. A marker file's payload is its first line with the
69
+ # ends trimmed; anything stricter silently renames the user's persona.
70
+ _persona=$(head -n1 "$d/.kijito_persona" | tr -d '\r\n')
71
+ _persona="${_persona#"${_persona%%[![:space:]]*}"}" # strip leading blanks
72
+ _persona="${_persona%"${_persona##*[![:space:]]}"}" # strip trailing blanks
73
+ [ -n "$_persona" ] && break
74
+ fi
75
+ done
76
+ _km_bin=""
77
+ for _c in "${KIJITOMON_BIN:-}" "$(command -v kijito-inbox-monitor 2>/dev/null)" \
78
+ "$HOME/.local/bin/kijito-inbox-monitor" "/usr/local/bin/kijito-inbox-monitor"; do
79
+ if [ -n "$_c" ] && [ -x "$_c" ]; then _km_bin=$_c; break; fi
80
+ done
81
+ _safe=""; _rule=no-producer
82
+ if [ -n "$_persona" ]; then
83
+ if [ -n "$_km_bin" ] && _safe=$("$_km_bin" --safe-persona "$_persona" 2>/dev/null) && [ -n "$_safe" ]; then
84
+ _rule=ok
85
+ else
86
+ # ── SECOND NON-GUESSING ROUTE: ASK THE STREAM WHO IT BELONGS TO ─────────────────────────────────
87
+ # No producer on PATH, or one from before --safe-persona existed. The tempting fallback is to
88
+ # re-implement the rule "just for this case" — that is precisely how the three drifted copies got
89
+ # written, so it is the one thing we will not do. Instead we read the producer's OWN OUTPUT: every
90
+ # event line it writes carries the persona it was written for, so the file itself can say whose
91
+ # mail it collects. That is evidence, not inference, and it stays correct no matter how the rule
92
+ # changes. (A brand-new persona with no stream yet simply has no answer here — correctly so: the
93
+ # honest report is then "nothing is collecting your mail", which is the truth.)
94
+ _safe=""; _rule=too-old
95
+ [ -z "$_km_bin" ] && _rule=no-producer
96
+ if command -v python3 >/dev/null 2>&1; then
97
+ _found=$(KJ_PERSONA="$_persona" python3 - <<'PYSCAN' 2>/dev/null
98
+ import glob, json, os, sys
99
+ want = os.environ["KJ_PERSONA"].casefold()
100
+ home = os.path.expanduser("~")
101
+ hits = []
102
+ for pat in (os.path.join(home, ".kijito-monitor", "*.jsonl"),
103
+ os.path.join(home, ".cache", "kijito-inbox-monitor", "events.*.ndjson")):
104
+ for path in glob.glob(pat):
105
+ try:
106
+ with open(path, "rb") as fh:
107
+ # the FIRST line is enough and is O(1): the producer stamps every event with its
108
+ # persona, and a stream never mixes personas (one owned sink per persona).
109
+ line = fh.readline(65536)
110
+ who = json.loads(line).get("persona")
111
+ except Exception:
112
+ continue
113
+ if isinstance(who, str) and who.casefold() == want:
114
+ hits.append(path)
115
+ if len(hits) == 1:
116
+ sys.stdout.write(hits[0])
117
+ PYSCAN
118
+ )
119
+ # ⛔ A FILE EXISTING IS NOT A PRODUCER RUNNING (assay cert finding F1, 2026-09-21). The first
120
+ # version of this route stopped here and reported "producer: UP for '<persona>'" on the
121
+ # strength of a glob hit. A STALE stream file — a persona whose producer died, or one that
122
+ # moved seats — then manufactured a confident UP, and because the path was found BY GLOBBING
123
+ # EXISTING FILES it could never reach the "a producer is running but NOT for you" branch
124
+ # below: that message was unreachable on this route by construction. The agent would arm a
125
+ # Monitor on a dead file AND be told everything was fine, so nothing would ever contradict it.
126
+ # That is worse than the silence this whole row is about, and it is reachable exactly in the
127
+ # population the row exists for (seats whose producer predates --safe-persona).
128
+ # ⇒ Require EVIDENCE THAT A LIVE PRODUCER COVERS THIS PERSONA, from a running process's own
129
+ # argv. Any one of three suffices, because the supervisors spell it differently:
130
+ # · the resolved path appears verbatim (systemd's --events-file <path>)
131
+ # · --persona <this persona> appears (a per-persona unit, whatever path spelling)
132
+ # · --all-personas appears (one producer covering every persona, incl. ours)
133
+ # A launchd producer passes --events-file-template, so its argv holds the TEMPLATE and not the
134
+ # resolved path — which is precisely why the second and third forms are needed and why
135
+ # matching the path alone would have been a new false-negative to replace the false positive.
136
+ if [ -n "${_found:-}" ]; then
137
+ _live=""
138
+ if command -v pgrep >/dev/null 2>&1; then
139
+ # ⛔ A PROCESS THAT MERELY MENTIONS THE PRODUCER IS NOT THE PRODUCER (assay observation, 2026-09-21:
140
+ # their verification SHELL matched this three times, because its command line contained both the
141
+ # product name and `--persona <p>` — and it then reported UP for a persona with no producer, which
142
+ # is F1's exact symptom arriving through the CHECKER instead of through a stale file). The
143
+ # sibling tool producer-health.sh already guards this by requiring the match to be a PYTHON
144
+ # process; the same rule belongs here, and a checker that can satisfy its own check is worth
145
+ # more caution than its low reachability suggests.
146
+ _live=$(pgrep -af "kijito[-_]inbox[-_]monitor" 2>/dev/null \
147
+ | grep -E "[Pp]ython|/kijito-inbox-monitor( |$)" \
148
+ | grep -v -e "[[:space:]]grep[[:space:]]" -e "session-catchup-hint" \
149
+ | grep -F -e "$_found" -e "--persona $_persona" -e "--all-personas" | head -n1)
150
+ fi
151
+ if [ -n "$_live" ]; then
152
+ _rule=by-content
153
+ else
154
+ _rule=stale-stream
155
+ fi
156
+ fi
157
+ fi
158
+ fi
159
+ fi
160
+
161
+ # ── Producer topology. THE PRODUCER WRITES A DIFFERENT PATH ON EACH SUPERVISOR, and this script
162
+ # used to hardcode the macOS one in all five places it appears. On a Linux seat that meant: a pgrep
163
+ # for "kijito_inbox_monitor.py" that can never match the `kijito-inbox-monitor` console script, a
164
+ # `launchctl` restart hint that means nothing under systemd, and Monitor templates pointing at
165
+ # ~/.cache/kijito-inbox-monitor/events.<p>.ndjson while the producer writes ~/.kijito-monitor/<p>.jsonl.
166
+ #
167
+ # ⚠️ EVERY ONE OF THOSE FAILS TOWARD FALSE CALM. An agent that obeys the hint tails a file that will
168
+ # never exist, and "no events" is indistinguishable from "no mail" — forever, with no error. Measured
169
+ # 2026-07-31: three personas hit this on one Linux seat in one evening; one hand-built a REST poller
170
+ # instead, and one was told "producer: DOWN" while the producer was up.
171
+ #
172
+ # DETECT, DON'T FORK ON `uname`. The question is not "what OS is this" but "where does the producer
173
+ # on THIS box actually write", so ask the filesystem: an events file that exists is proof, and a
174
+ # supervisor definition is the next-best evidence. uname is the last resort, not the first test.
175
+ _mac_events="$HOME/.cache/kijito-inbox-monitor/events.${_safe}.ndjson"
176
+ _lnx_events="$HOME/.kijito-monitor/${_safe}.jsonl"
177
+ if [ "$_rule" = by-content ] || [ "$_rule" = stale-stream ]; then
178
+ # The producer's own output named this file. It outranks every derivation below, because it is the
179
+ # only one of them that was written by the process we are asking about.
180
+ # ⚠️ A STALE stream still resolves to THIS path deliberately: it is genuinely this persona's file,
181
+ # it is simply not being written any more. Falling through to the derivations below would be worse
182
+ # than useless here — with no --safe-persona answer they would produce `~/.kijito-monitor/.jsonl`,
183
+ # an empty component that looks like a path and names nothing. The producer line says it is stale;
184
+ # the arming block should still point at the file that will come back when it restarts.
185
+ _events="$_found"
186
+ case "$_events" in *.jsonl) _sup="systemd" ;; *) _sup="launchd" ;; esac
187
+ elif [ -n "$_safe" ] && [ -e "$_lnx_events" ]; then _events="$_lnx_events"; _sup="systemd"
188
+ elif [ -n "$_safe" ] && [ -e "$_mac_events" ]; then _events="$_mac_events"; _sup="launchd"
189
+ elif [ -d "$HOME/.kijito-monitor" ]; then _events="$_lnx_events"; _sup="systemd"
190
+ elif [ -d "$HOME/.cache/kijito-inbox-monitor" ]; then _events="$_mac_events"; _sup="launchd"
191
+ elif [ -f "$HOME/Library/LaunchAgents/com.kijito.inbox-monitor.plist" ]; then _events="$_mac_events"; _sup="launchd"
192
+ elif [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then _events="$_mac_events"; _sup="launchd"
193
+ else _events="$_lnx_events"; _sup="systemd"; fi
194
+ # The generic (no-marker) branch cannot name a file, so it shows the directory shape instead.
195
+ case "$_sup" in
196
+ launchd) _events_tmpl="\$HOME/.cache/kijito-inbox-monitor/events.<persona>.ndjson" ;;
197
+ *) _events_tmpl="\$HOME/.kijito-monitor/<persona>.jsonl" ;;
198
+ esac
199
+
200
+ # Producer health, PER PERSONA — because "a producer is running" and "YOUR mail is being collected"
201
+ # are different facts, and on a multi-persona seat they come apart routinely. The old check asked
202
+ # the host-global question and printed a per-persona answer.
203
+ #
204
+ # ⚠️ The pgrep pattern must not match the CONSUMER. On the macOS layout the tail's own path contains
205
+ # the string "kijito-inbox-monitor", so a loose pattern reports the producer UP whenever any agent is
206
+ # merely tailing — a false green in the one direction that matters. Anchor on how the executable
207
+ # appears in a command line, never on the bare product name.
208
+ if pgrep -f "kijito_inbox_monitor\.py|bin/kijito-inbox-monitor" >/dev/null 2>&1 \
209
+ || pgrep -f "kijito-inbox-monitor .*--persona" >/dev/null 2>&1; then
210
+ if [ -z "$_persona" ]; then
211
+ _prod="inbox-monitor producer: a producer process is running (persona unknown here — no .kijito_persona marker, so this hook cannot tell whether it covers YOUR inbox)."
212
+ elif [ "$_rule" = by-content ]; then
213
+ _prod="inbox-monitor producer: UP for '$_persona' ($_sup; events → $_events — identified from the stream's own persona stamp and confirmed against a running producer's own arguments, because the installed producer could not be asked for the filename rule)."
214
+ elif [ "$_rule" = stale-stream ]; then
215
+ # The most diagnosable state of the lot, and it used to read as UP: the file is there, nothing is
216
+ # writing it. Say that, rather than the generic "not being collected" — a stale file and a missing
217
+ # file need different fixes and a reader cannot tell them apart from the generic wording.
218
+ _prod="inbox-monitor producer: NOT running for '$_persona' — a stream file exists ($_found) but NO running producer names that path, this persona, or --all-personas, so it is STALE and your mail is not being collected. Anything tailing it will wait forever without an error. Start one: $(
219
+ [ "$_sup" = launchd ] \
220
+ && printf 'launchctl kickstart -k gui/$(id -u)/com.kijito.inbox-monitor' \
221
+ || printf 'systemctl --user enable --now kijito-inbox-monitor@%s' "$_persona" )"
222
+ elif [ "$_rule" = too-old ]; then
223
+ # We know the persona and a producer is running, but the installed producer cannot tell us how it
224
+ # spells that persona as a filename. Naming a path here would be a guess, and a guessed path fails
225
+ # as SILENCE. Say what is unknown and how to make it knowable.
226
+ _prod="inbox-monitor producer: RUNNING, but this hook cannot name the event stream for '$_persona' — the installed kijito-inbox-monitor ($_km_bin) does not answer --safe-persona, so the persona→filename rule is unresolved and any path printed here would be a guess. Upgrade the producer (that flag is how the rule is published), then re-open this session."
227
+ elif [ "$_rule" = no-producer ]; then
228
+ _prod="inbox-monitor producer: a producer process is running, but no kijito-inbox-monitor executable is on this PATH, so this hook cannot resolve where '$_persona''s events are written (set \$KIJITOMON_BIN if it lives somewhere unusual)."
229
+ elif [ -e "$_events" ]; then
230
+ _prod="inbox-monitor producer: UP for '$_persona' ($_sup; events → $_events)."
231
+ else
232
+ # The case that actually bit river on 2026-07-31: assay's producer was up, river's was not, and
233
+ # a host-global check would have called that UP and sent the agent off to tail a missing file.
234
+ # The path below came from the PRODUCER's own rule, so "does not exist" now means the stream is
235
+ # genuinely absent rather than that we spelled the name differently than the writer did.
236
+ _prod="inbox-monitor producer: a producer is running but NOT for '$_persona' — $_events does not exist, so YOUR mail is not being collected. Start one: $(
237
+ [ "$_sup" = launchd ] \
238
+ && printf 'launchctl kickstart -k gui/$(id -u)/com.kijito.inbox-monitor' \
239
+ || printf 'systemctl --user enable --now kijito-inbox-monitor@%s' "$_persona" )"
240
+ fi
241
+ else
242
+ _prod="inbox-monitor producer: DOWN — no events will arrive until restarted: $(
243
+ [ "$_sup" = launchd ] \
244
+ && printf 'launchctl kickstart -k gui/$(id -u)/com.kijito.inbox-monitor' \
245
+ || printf 'systemctl --user enable --now kijito-inbox-monitor@%s' "${_persona:-<persona>}" )"
246
+ fi
247
+
248
+ # Catch-up reminder.
249
+ cat <<EOF
250
+ [SESSION CATCH-UP — do this BEFORE the user's task] $pre Start continuous, not cold:
251
+ 1) kijito_startup(persona, project) → read the current-state pointer it names (kijito_get) → skim recent lessons.
252
+ 2) ARM A WAKE-CAPABLE INBOX CONSUMER as your first action (see the INBOX WAKE block below) — do NOT skip it, do NOT use a bare tail.
253
+ 3) If this is a BRAND-NEW project with NO persona yet: read ./CLAUDE.md + ~/.claude/CLAUDE.md and set your persona/project before writing any memory.
254
+ Never pause on a *feeling* of full context — run ~/.claude/myctx.sh for hard data.
255
+ EOF
256
+
257
+ # Inbox-wake arming block — exact, per-persona when the marker resolves, generic otherwise.
258
+ # ── Idempotency (fixes the duplicate-monitor bug, river+argus 2026-07-02). The wake consumer is a
259
+ # real `tail -n 0 -F …events.<persona>.ndjson` process that SURVIVES /clear + /compact (the session
260
+ # continues), so a naive re-arm stacks duplicates that each fire every event. Detect an existing
261
+ # consumer and INFORM — the hook can't know ownership (own-pre-clear vs a concurrent same-persona
262
+ # sibling vs a leaked orphan; the stream is shared per-persona), so it defers the keep-vs-arm
263
+ # decision to the agent's own task list and NEVER recommends a pattern-kill (a broad pkill on the
264
+ # stream can kill a live sibling's or your own consumer — proven during argus's testing).
265
+ #
266
+ # ⚠️ The duplicate-detection pattern has to follow the LAYOUT too. Hardcoding `events\.<p>\.ndjson`
267
+ # made this branch dead on every Linux seat: it could never match, so the hook always took the
268
+ # "nothing is armed" path and told a returning session to arm again — re-introducing the very
269
+ # duplicate-monitor bug this block was written to fix, on exactly the hosts where nobody was
270
+ # looking for it. Match on the resolved events file's basename instead.
271
+ _armed=""
272
+ if [ -n "$_safe" ]; then
273
+ _evbase=$(basename "$_events")
274
+ # basename is a literal filename; escape the regex metacharacter it can contain (.) so a dot
275
+ # cannot match an arbitrary character and over-report.
276
+ _evpat=$(printf '%s' "$_evbase" | sed 's/\./\\./g')
277
+ _armed=$(pgrep -f "tail -n 0 -F.*${_evpat}" 2>/dev/null | tr '\n' ' ')
278
+ fi
279
+
280
+ if [ -n "$_safe" ] && [ -n "$_armed" ]; then
281
+ cat <<EOF
282
+
283
+ [INBOX WAKE — a consumer already tails your stream; do NOT blindly add another] One or more processes
284
+ are already tailing $_events (pid(s): $_armed). This is EXPECTED after a /clear or /compact
285
+ (your own persistent Monitor survives the reset). The hook can't tell whose it is, so decide from YOUR
286
+ task list:
287
+ • You ALREADY have a live persistent inbox Monitor in your task list → keep it, do NOT arm another (a
288
+ 2nd double-fires every event — the duplicate bug). If your task list shows MORE than one, TaskStop
289
+ all but one.
290
+ • You have NO inbox Monitor (brand-new session; the tail is a sibling's or a leaked orphan that will
291
+ NOT wake YOU) → arm exactly one for yourself:
292
+ Monitor(command="tail -n 0 -F $_events | grep --line-buffered -E '\"event\": ?\"(new|alert|recovered|state_corrupt|baseline_skipped|seed_ahead|replay_capped|persona_added)\"'", persistent=true)
293
+ Dedupe with TaskStop (agent-scoped), NOT \`pkill -f …events…\` (that can kill a sibling's or your own live consumer). $_prod
294
+ EOF
295
+ elif [ -n "$_safe" ]; then
296
+ cat <<EOF
297
+
298
+ [INBOX WAKE — ARM AS YOUR FIRST ACTION, do not skip] An unmonitored mailbox is useless, and a bare
299
+ background \`tail\` is CAPTURE-ONLY (it writes to a file and never re-invokes you, so you are NOT
300
+ woken). Arm the wake-capable consumer with the Monitor TOOL (persistent) — it streams each event
301
+ as a live notification. Your persona for this project is "$_persona":
302
+
303
+ Monitor(command="tail -n 0 -F $_events | grep --line-buffered -E '\"event\": ?\"(new|alert|recovered|state_corrupt|baseline_skipped|seed_ahead|replay_capped|persona_added)\"'", persistent=true)
304
+
305
+ First confirm nothing is already monitoring that stream this session (avoid double-arming). $_prod
306
+ EOF
307
+ else
308
+ cat <<EOF
309
+
310
+ [INBOX WAKE — ARM AS YOUR FIRST ACTION, do not skip] An unmonitored mailbox is useless, and a bare
311
+ background \`tail\` is CAPTURE-ONLY (it writes to a file and never re-invokes you, so you are NOT
312
+ woken). Arm the wake-capable consumer for YOUR persona with the Monitor TOOL (persistent) — it
313
+ streams each event as a live notification. Substitute your persona name for <persona>:
314
+
315
+ Monitor(command="tail -n 0 -F $_events_tmpl | grep --line-buffered -E '\"event\": ?\"(new|alert|recovered|state_corrupt|baseline_skipped|seed_ahead|replay_capped|persona_added)\"'", persistent=true)
316
+
317
+ (No .kijito_persona marker found in this project — add a one-line \`.kijito_persona\` file with your
318
+ persona name in the project root so this resolves automatically next session.) $_prod
319
+ EOF
320
+ fi
321
+
322
+ # Armed auto-send (detached so it never blocks startup or pollutes the additionalContext above).
323
+ if command -v lc_is_armed >/dev/null 2>&1 && [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ] && lc_is_armed "$TMUX_PANE"; then
324
+ lc_log HOOK "src=$src autosend=ARMED pane=$TMUX_PANE"
325
+ _autosend="$_kjt_dir/session-autosend.sh"
326
+ [ -f "$_autosend" ] || _autosend="$HOME/.claude/session-autosend.sh"
327
+ nohup bash "$_autosend" "$TMUX_PANE" >/dev/null 2>&1 &
328
+ else
329
+ command -v lc_log >/dev/null 2>&1 && lc_log HOOK "src=$src autosend=skip(not-armed-or-no-tmux) tmux=${TMUX:+y} pane=${TMUX_PANE:-none}"
330
+ fi
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code statusline — model name + accurate context-window usage.
3
+ # Source of truth, in order:
4
+ # 1) harness-provided context_window.* fields (if this CC version supplies them)
5
+ # 2) fallback: compute from the transcript's API usage ledger — the same data
6
+ # /context is built on (sum input+cache_read+cache_creation of last asst turn).
7
+ # Reads the statusline JSON on stdin; prints one line. ~tens of ms; never enters the model's context.
8
+ input=$(cat)
9
+
10
+ model=$(printf '%s' "$input" | jq -r '.model.display_name // "Claude"')
11
+
12
+ # ── WHICH PANE AM I? (row M309) ──────────────────────────────────────────────────────────────────
13
+ # On the first external onboarding call a test message "looked lost" for several minutes because two
14
+ # people were watching the wrong terminal — "Wait, no, it's on the other one … it would be here
15
+ # because this is the fable". A human running eight agent panes has NOTHING on screen saying which
16
+ # persona each one is; the status line was the call's instant hit, so it is where that belongs.
17
+ #
18
+ # ⛔ THE MARKER IS READ THROUGH THE SHARED HELPER, NEVER RE-PARSED HERE. Re-implementing it is exactly
19
+ # how row M290 happened: the hook's own copy deleted interior spaces, so `name (purpose)` silently
20
+ # became a different persona. A third copy in the status line would drift the same way, and here it
21
+ # would be worse — a status line that says the WRONG persona is a confident label on the wrong pane,
22
+ # which is the very confusion this row exists to end.
23
+ _sl_dir="$(dirname -- "${BASH_SOURCE[0]:-$0}")"
24
+ persona=""
25
+ if [ -r "$_sl_dir/kijito-persona-lib.sh" ]; then
26
+ # shellcheck source=/dev/null
27
+ . "$_sl_dir/kijito-persona-lib.sh"
28
+ # The statusline JSON carries the pane's cwd; prefer it over $PWD, which is this script's.
29
+ _sl_cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty')
30
+ persona=$(kijito_persona_from_marker "${CLAUDE_PROJECT_DIR:-}" "$_sl_cwd" "$PWD" || true)
31
+ fi
32
+
33
+ # 1) Prefer harness-computed context window if present
34
+ used=$(printf '%s' "$input" | jq -r '.context_window.used_tokens // empty')
35
+ win=$(printf '%s' "$input" | jq -r '.context_window.total_tokens // empty')
36
+
37
+ # 2) Fallback: ground-truth from the transcript usage ledger
38
+ if [ -z "$used" ]; then
39
+ tp=$(printf '%s' "$input" | jq -r '.transcript_path // empty')
40
+ if [ -n "$tp" ] && [ -f "$tp" ]; then
41
+ used=$(jq -s 'map(select(.type=="assistant" and .message.usage != null) | .message.usage) | last // {}
42
+ | (.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0)' \
43
+ "$tp" 2>/dev/null)
44
+ fi
45
+ fi
46
+ [ -z "$used" ] && used=0
47
+ [ -z "$win" ] && win=1000000
48
+
49
+ pct=$(awk -v u="$used" -v w="$win" 'BEGIN { if (w>0) printf "%.0f", (u/w)*100; else print 0 }')
50
+ uk=$(awk -v u="$used" 'BEGIN { if (u>=1000) printf "%.0fk", u/1000; else printf "%d", u }')
51
+ wk=$(awk -v w="$win" 'BEGIN { if (w>=1000000) printf "%gm", w/1000000; else printf "%.0fk", w/1000 }')
52
+
53
+ # refresh-before-70 discipline: green <60, yellow 60-79, red >=80
54
+ col=$(awk -v p="$pct" 'BEGIN { if (p>=80) printf "\033[31m"; else if (p>=60) printf "\033[33m"; else printf "\033[32m" }')
55
+ # ⚠️ THE PERSONA GOES FIRST AND IS TRUNCATED, NOT THE CONTEXT FIGURE. The figure is what people were
56
+ # already watching; a long persona must never push it off a narrow terminal. 18 chars fits
57
+ # "name (purpose)"-shaped names, which is what real users type.
58
+ if [ -n "$persona" ]; then
59
+ printf '\033[36m%s\033[0m · ' "$(kijito_truncate "$persona" 18)"
60
+ fi
61
+ # ROW M291: the heartbeat watchdog raises this flag when this pane's persona has unread wake events
62
+ # in its stream and NO consumer reading it (typically after a usage-limit outage ended the loop). It
63
+ # is the one place a human watching the pane will see it; the watchdog removes it on re-arm.
64
+ if [ -n "${TMUX_PANE:-}" ] && [ -f "${KIJITO_LC_DIR:-$HOME/.claude/.lifecycle}/unconsumed.$TMUX_PANE" ]; then
65
+ printf '\033[31m⚠ inbox deaf\033[0m · '
66
+ fi
67
+ printf '%s · ctx %b%s/%s (%s%%)\033[0m' "$model" "$col" "$uk" "$wk" "$pct"