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.
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -0
- package/bin/cli.js +34 -0
- package/install.sh +161 -0
- package/package.json +50 -0
- package/providers/_shared/wake-core.mjs +358 -0
- package/providers/claude/CLAUDE.md.snippet +31 -0
- package/providers/claude/install.sh +186 -0
- package/providers/claude/scripts/arm-session.sh +66 -0
- package/providers/claude/scripts/claude-armed.sh +101 -0
- package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
- package/providers/claude/scripts/inbox-selftest.sh +277 -0
- package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
- package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
- package/providers/claude/scripts/lifecycle-lib.sh +167 -0
- package/providers/claude/scripts/myctx.sh +46 -0
- package/providers/claude/scripts/self-clear.sh +134 -0
- package/providers/claude/scripts/session-autosend.sh +85 -0
- package/providers/claude/scripts/session-catchup-hint.sh +330 -0
- package/providers/claude/scripts/statusline-context.sh +67 -0
- package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
- package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
- package/providers/claude/skills/kijito-start/SKILL.md +122 -0
- package/providers/claude/wiring/README.md +93 -0
- package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
- package/providers/codex/README.md +75 -0
- package/providers/codex/codex-kijito-parity-plan.md +265 -0
- package/providers/codex/docs-codex-setup.md +64 -0
- package/providers/codex/install.mjs +184 -0
- package/providers/codex/n0-capability-probe-protocol.md +472 -0
- package/providers/codex/n0-harness/README.md +57 -0
- package/providers/codex/n0-harness/cli.mjs +58 -0
- package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
- package/providers/codex/n0-harness/fixture.mjs +279 -0
- package/providers/codex/n0-harness/lib.mjs +141 -0
- package/providers/codex/n0-harness/manifest.mjs +36 -0
- package/providers/codex/n0-harness/oracle.mjs +464 -0
- package/providers/codex/n0-harness/parser.mjs +200 -0
- package/providers/codex/n0-harness/prompt.mjs +43 -0
- package/providers/codex/n0-harness/snapshot.mjs +120 -0
- package/providers/codex/n0-harness/specimen.mjs +37 -0
- package/providers/codex/notify/kijito-notify-count.mjs +117 -0
- package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
- package/providers/codex/plans/gate4-battery-log.md +184 -0
- package/providers/codex/plans/gate4-battery-protocol.md +43 -0
- package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
- package/providers/codex/plans/hive-user-first-plan.md +247 -0
- package/providers/codex/release-manifest.json +53 -0
- package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
- package/providers/codex/same-chat-continuation-plan.md +458 -0
- package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
- package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
- package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
- package/providers/codex/skills/kijito-start/SKILL.md +173 -0
- package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
- package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
- package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
- package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
- package/providers/codex/test/n0-guard-census.json +35479 -0
- package/providers/codex/test/n0-guard-census.mjs +31 -0
- package/providers/codex/test/n0-guard-census.test.mjs +374 -0
- package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
- package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
- package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
- package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
- package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
- package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
- package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
- package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
- package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
- package/providers/codex/test/n0-harness.test.mjs +553 -0
- package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
- package/providers/codex/test/n0-subject-boundary.mjs +97 -0
- package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
- package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
- package/providers/codex/tools/refresh-manifest.mjs +81 -0
- package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
- package/providers/codex/wake-helper/integration.test.mjs +285 -0
- package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
- package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
- package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
- package/providers/codex/wake-helper/status-probe.mjs +45 -0
- package/providers/codex/wake-helper/ws-uds.mjs +153 -0
- package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
- package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
- package/providers/monitor/CHANGELOG.md +465 -0
- package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
- package/providers/monitor/LICENSE +202 -0
- package/providers/monitor/NOTICE +7 -0
- package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
- package/providers/monitor/README.md +571 -0
- package/providers/monitor/RELEASING.md +125 -0
- package/providers/monitor/arm-hive-monitor.sh +13 -0
- package/providers/monitor/bin/cli.js +43 -0
- package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
- package/providers/monitor/docs/DESIGN.md +984 -0
- package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
- package/providers/monitor/kijito_inbox_monitor.py +3666 -0
- package/providers/monitor/package.json +35 -0
- package/providers/monitor/pyproject.toml +42 -0
- package/providers/monitor/scripts/mutation-check.py +399 -0
- package/providers/monitor/scripts/prepublish-gate.sh +284 -0
- 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"
|