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,465 @@
1
+ # Changelog
2
+
3
+ All notable changes to kijito-inbox-monitor are documented in this file.
4
+ The format is based on Keep a Changelog, and this project follows Semantic Versioning.
5
+
6
+ ## [0.5.0] - 2026-08-15
7
+
8
+ ### Fixed
9
+ - **Darwin `boottime` re-sourced: `CLOCK_MONOTONIC_RAW`, not `CLOCK_MONOTONIC`.** Darwin's `CLOCK_MONOTONIC`
10
+ does include sleep, but it is calendar-derived - measured 2026-08-15 it read *exactly*
11
+ `wall − kern.boottime` to three decimals - so it absorbs NTP adjustments to the wall clock. On a
12
+ fresh-uptime Mac whose wall clock stepped back ~8.3 s after boot, it emitted
13
+ `boottime 4957.865 < monotonic 4966.194`, violating the definitional invariant `boottime ≥ monotonic`
14
+ that consumers difference against. Both semantics now come from raw clocks sharing one tick source
15
+ (`CLOCK_MONOTONIC_RAW` / `CLOCK_UPTIME_RAW`), so the invariant holds by construction.
16
+ - **Inverted-pair quarantine at the emit chokepoint.** If a platform ever hands the producer
17
+ `boottime < monotonic`, the `boottime` reading is omitted (never faked) and preserved under
18
+ `emitted.clock_defect` with the rejected value and its source constant, so a broken mapping reports
19
+ itself on the row instead of poisoning dwell measurements downstream. A positive-control test proves
20
+ the quarantine fires on a deliberately inverted pair - the prior invariant test passed vacuously on any
21
+ zero-sleep host, where an inverted mapping reads as an equal pair.
22
+
23
+ ⚠️ **WHAT THE REVIEW OF THIS SET DOES AND DOES NOT COVER.** The seven alarm/liveness changes were reviewed
24
+ and approved by an independent engine-literate reviewer who re-ran the suite and the mutation harness rather
25
+ than accepting them. Two limits were disclosed rather than discovered: the reviewer did **not** run the
26
+ producer against a live account, so "verified live" claims here rest on the author's word; and the docs/gate
27
+ commit was out of that review's scope.
28
+
29
+ ### Added
30
+ - **A wake `nonce` on every event** - 11 base62 chars, `base62(sha256(event_id))[:11]`. It lets a consumer join
31
+ a delivered wake back to the queue entry that carried it, and it is recomputable from the `event_id` in the
32
+ same row. **Derived rather than random on purpose:** this producer already defines identity (a `new` event
33
+ keeps the same id across a restart, a state-loss re-delivery, and two watchers of one inbox), and a
34
+ per-emission random nonce would have contradicted that - calling one re-delivered message two different wakes,
35
+ so a consumer finds no queue entry for the second, scores it lost, and alarms **on the recovery path the
36
+ producer exists to survive**. Deriving inherits the existing semantics for both identity families and changes
37
+ neither. Note it is a label and not a secret (deterministic, hence guessable from the `event_id`), and that it
38
+ identifies a *wake* rather than a *delivery* - two panes handed the same message share a nonce, so consumer
39
+ records must key on `(nonce, session_id)`.
40
+ - **Emission timestamps (`emitted`)** - `wall`, `monotonic`, `boottime` and `src`, all read at one instant.
41
+ Differencing wall against monotonic across two events measures the time the machine **was not executing**,
42
+ which is what separates "this sat in a queue for hours" from "the host was suspended" - indistinguishable in
43
+ wall time and completely different problems. Measured on a Parallels guest: 72.79 h of hypervisor freeze
44
+ looked like ordinary elapsed wall time, while `BOOTTIME - MONOTONIC` read `0.00 s` throughout, because a
45
+ hypervisor pause stops the guest's clocks together and the guest is not running to notice.
46
+ **The keys name SEMANTICS, not OS constants, because on macOS the two are inverted:** `monotonic` means "does
47
+ not tick while the machine is not executing" (Linux `CLOCK_MONOTONIC`, Darwin `CLOCK_UPTIME_RAW`) and
48
+ `boottime` means "does tick" (Linux `CLOCK_BOOTTIME`, Darwin `CLOCK_MONOTONIC`). macOS `CLOCK_MONOTONIC`
49
+ *includes* sleep and macOS has no `CLOCK_BOOTTIME`, so reading constants by name there yields the wrong
50
+ quantity under the right label with nothing complaining - measured on a real Mac at 408.19 h vs 389.99 h,
51
+ an 18.20 h gap that is the accumulated sleep. `src` names the constant behind each value so consumers can
52
+ audit the mapping from the row instead of assuming it. A key is **omitted rather than faked** where its
53
+ *semantic* is unavailable: a fabricated value is indistinguishable from a real zero-freeze reading, which is
54
+ the failure the field exists to prevent. `ts` is unchanged; `emitted.wall` is the coherent wall reading.
55
+ - **An alarm for escalated mail nobody is answering.** Fires only when a member holds mail a sender marked
56
+ URGENT *and* no activity from that member has been observed - both halves positive. Silence alone is never
57
+ the trigger, because idle-by-design and wedged are indistinguishable from outside; the urgent flag is a
58
+ sender declaring an expectation, which is what makes the silence mean something. Clears when either half
59
+ clears, with no ack.
60
+ - **An alarm for unread mail the inbox window did not show** (`unread_not_shown` above zero) - the case where
61
+ the endpoint tells you it is holding mail this response did not hand you.
62
+ - **An attributable liveness signal** (`--activity-file`) and a one-shot evaluation of it
63
+ (`--check-activity`), so an external supervisor can assert the producer is not merely running but working.
64
+ - **A producer-owned event id on every event**, so a consumer can deduplicate across restarts and rotations
65
+ without inferring identity from content.
66
+ - **`--no-urgent-alerts`** - see Changed.
67
+
68
+ ### Changed
69
+ - **`--no-stranded-alerts` no longer silences the urgent-unanswered alarm.** It gated both, while saying so
70
+ in only one of the three places an operator reads - and its own documented advice ("set this if you keep
71
+ deliberate test inboxes") therefore turned off a higher-severity alarm about real members as a side effect.
72
+ The two alarms now have separate flags; pass both to disable both. The coupling is pinned by tests through
73
+ the real run loop and by its own mutation, because nothing tested the mapping either way before.
74
+ (Found in review of the urgent-unanswered alarm, which is unreleased - so no released behaviour changes.)
75
+ - **Account-level alarms are routed by evidence of a consumer**, instead of to every directory persona -
76
+ long-dead test personas were receiving alerts into streams nobody reads.
77
+
78
+ ### Fixed
79
+ - **One persona's hostile or un-tightenable `.lock` sidecar killed the whole producer.** `InsecureFile` is an
80
+ `OSError` by design, but every containment arm caught `FatalConfig` only, so the throw went past all of
81
+ them on the startup path and both late-add paths. Containment is now per-persona at four sites, and fails
82
+ **closed**: if every persona fails to initialise, the producer raises rather than staying up watching
83
+ nothing. The lesson worth keeping is that the earlier fix checked that the catch *existed* and never that
84
+ its type *covered the throw*.
85
+ - **`RELEASING.md` prescribed a path the package does not contain** - a clone could not run the gate the
86
+ document mandates. Restated as the property plus a self-contained check, with a third prepublish gate
87
+ (`path-escapes`) so the class cannot return.
88
+
89
+ ## [0.4.0] - 2026-07-28
90
+
91
+ ⚠️ **WHAT THIS RELEASE DOES AND DOES NOT ESTABLISH.** Two audit rounds swept a defect CLASS rather than
92
+ patching instances: *a safety check whose result nobody consumes, a safety state nothing clears, and - added
93
+ in the second round - a state committed as if an action succeeded before it did.* **It establishes that the
94
+ KNOWN INSTANCES ARE FIXED. It does NOT establish that no further shapes exist.** That distinction is not
95
+ boilerplate: widening the class immediately surfaced two more sites AND a blind spot in the detector itself
96
+ (bool-return detection was not transitive, so one function was invisible and another was caught only by the
97
+ accident of an unrelated `return True`). A class derived from N instances cannot be validated by
98
+ rediscovering those N.
99
+ ⚠️ **PROVENANCE OF THE REVIEW.** The final verdict came from the author of the acceptance criteria, who was
100
+ also a party to the technical dispute it adjudicated. She disclosed both conflicts and held the
101
+ do-not-ship outcome as genuinely reachable - and returned it once before this release. The independent
102
+ reviewer originally assigned never read the request.
103
+
104
+ ### Fixed (re-audit 11 - the alarm path)
105
+ - **An alarm could be recorded as raised when it was never delivered.** Every alarm committed its
106
+ "already alarmed" state BEFORE emitting and discarded the emit's answer, and three of the four had no
107
+ second channel. An undelivered alarm was never re-raised - not when the channel recovered, and not after
108
+ a restart, because `gap_alerted` is persisted. **Mail was never at risk** (the cursor holds correctly
109
+ throughout); it was the ALARMS that vanished, which matters because the headline promise is that a walk
110
+ which cannot complete pins *loudly* rather than in silence. `WatchTarget.lifecycle` now returns delivery,
111
+ and alarms go through `_alarm()`, which falls back to **stderr** - never a retry down the channel that
112
+ just failed. A pure announcement latch (`gap_alerted`) commits only on delivery; behavioural state
113
+ (`fsm_state`, `pin_evidence_intact`) commits regardless, because refusing to record evidence loss would
114
+ trade a lost alarm for a lost invariant. Lifecycle events remain unacknowledged and ungated (DESIGN.md
115
+ §170 unchanged); the guaranteed/informational split is now documented as §14.9.
116
+ - **The recovery edge failed the same way, facing the other way** - both `recovered` sites committed
117
+ `fsm_state = "UP"` and discarded the emit, leaving a consumer that saw the DOWN alert holding an alarm it
118
+ could never clear.
119
+ - **The dead-man's switch had no test at all.** Deleting the liveness DOWN alert outright left the entire
120
+ suite green - the one event the README sells as the headline feature was undefended. It now has tests and
121
+ its own mutation.
122
+ - **Two case-variant personas could refuse to start the whole producer.** `requested_personas()` deduped
123
+ exactly while `new_personas()` casefolds, so `--persona Loom --persona loom` resolved to one state path;
124
+ the second `flock` raised out of an uncaught list comprehension and killed startup for *every* persona,
125
+ blaming "another watcher" for a collision with itself.
126
+
127
+ ### Fixed (documentation)
128
+ - `RELEASING.md` claimed the producer "does not run this package (yet)" and "executes the WORKING TREE
129
+ directly", four lines after the preceding section said the opposite. It had been stale since the producer
130
+ was pinned to a read-only artifact, and the stale half was the dangerous one: a reader who trusted it
131
+ would edit the tree, restart, and **deploy nothing while believing they deployed**.
132
+
133
+
134
+ ### Security
135
+ - **The first fix for the permissions bug was itself a worse bug** (Loom re-audit 9, HIGH 1). The repair
136
+ introduced in the previous entry followed symlinks, validated neither owner nor file type, and - because
137
+ it was deliberately best-effort so that "a file we do not own cannot kill the watcher" - wrote the mail
138
+ anyway when the `chmod` failed. Measured: a pre-existing 0666 file stayed 0666 **and received mail**; a
139
+ symlink's target was chmod'ed and appended to; a *dangling* symlink caused its target to be created in
140
+ another directory. A passive disclosure had been turned into an active write primitive. Opens now use
141
+ `O_NOFOLLOW` and `O_NONBLOCK` (a FIFO at the path would otherwise block the writer forever - a hang,
142
+ which is worse than a crash because nothing reports it) and validate on the already-open descriptor that
143
+ the file is regular, owned by this user, and exactly 0600. Anything else is refused, and a refusal is a
144
+ **failed delivery**: the cursor holds and the mail is retried rather than written somewhere unsafe.
145
+ - **Only the file being opened was repaired** (Loom re-audit 9, HIGH 2). Pre-existing rotated archives and
146
+ an existing state file kept their old modes, and a 0700 file was left alone because the check tested
147
+ `mode & 0o077` rather than requiring exactly 0600. All persisted artifacts are now repaired, and
148
+ directories are 0700 at **every** level (`os.makedirs(mode=)` applies the mode to the leaf only, so
149
+ nested paths left their parents 0755). A directory writable by other users is reported.
150
+ - **The event stream was world-readable and it carries message bodies** (Loom re-audit 8, HIGH 1). Event
151
+ files and the state-file lock sidecar were created with a plain `open()`, which takes the process umask -
152
+ 022 by default - so every `events.<persona>.ndjson` was mode 0644 and readable by any other local user,
153
+ with message content in it unless `--no-content` was set. The auth token (0600) and the state file (0600
154
+ via `mkstemp`) were already correct, which is what made the gap easy to miss: the one file nobody had
155
+ thought about is the one holding the plaintext. Both are now created 0600, directories this tool creates
156
+ are 0700, and an **existing** file that is more permissive is tightened on open and the change reported -
157
+ because the creation mode does nothing for files that already leaked. Rotated archives inherit 0600 from
158
+ the live file. If you have been running an earlier version, check the modes on your events files.
159
+
160
+ ### Changed
161
+ - **Delivery is now ACKNOWLEDGED rather than assumed, and the delivery guarantee is stated honestly as
162
+ at-least-once, in order** (Loom re-audit 7, HIGH 1). The cursor *is* the acknowledgement: once it moves
163
+ past an id that message is never fetched again. It previously advanced on selection, so an `--exec` that
164
+ exited non-zero, timed out, or failed to spawn had its result discarded and the message was silently
165
+ dropped - on the one path whose entire purpose is waking an agent. It now advances only over messages the
166
+ emitter reports as delivered (`exec` exit 0, or a successful write), stopping at the first failure so a
167
+ consumer never sees message N+1 ahead of a retried N. **Make your consumer idempotent**; `KIJITOMON_ID` is
168
+ stable across re-deliveries. This also resolves a contradiction that already existed between the README
169
+ ("exactly once across restarts") and DESIGN.md ("best-effort/at-most-once"); the docs now agree.
170
+
171
+ ### Fixed
172
+ - **A window that withheld nothing while pointing at older mail was believed** (Loom re-audit 7, HIGH 4).
173
+ The server sets `next_before_id` *exactly* when rows were withheld, so "I hid nothing" and "there is more"
174
+ cannot both be true - and the gap check never looked at the continuation at all, so it took the first half
175
+ at its word and advanced over whatever the second half pointed at. Both directions of the contradiction now
176
+ pin. Verified against the live API across 14 pages, including the exactly-at-limit edge that could have made
177
+ the rule fire on healthy traffic (it does not).
178
+ - **A malformed pin field in the state file failed open** (Loom re-audit 7, HIGH 2). `pin_forced` was read
179
+ as `value is True`, so a JSON `1` normalised to false and silently *unpinned* the watermark, letting the
180
+ replay cap cross the very span the pin was protecting; `pin_evidence_intact: 0` had the mirror bug, and
181
+ booleans were accepted as message ids. Every persisted field is now read strictly, and anything
182
+ unrecognised is treated as a corrupt state file rather than a permissive default.
183
+ - **A persona respelled in a different case destroyed its own cursor** (Loom re-audit 7, HIGH 3). The state
184
+ *path* casefolds while the stored *identity* keeps the directory's spelling, so a file written as
185
+ `persona=Loom` was reloaded by a run that discovered `loom`, judged a mismatch, and re-baselined - skipping
186
+ every message since. A case-only difference now migrates the file and keeps the cursor. Deliberately
187
+ narrow: only the query *value* is compared case-insensitively.
188
+ - **The corruption-recovery pin could never clear** (Loom re-audit 7, HIGH 5). It parks the watermark one
189
+ below the window it re-emits, which made the ordinary release test unsatisfiable by that same window - so
190
+ the pin held forever, the cursor froze, and because delivered ids were recorded only while a *gap* was
191
+ pinned, the identical window was re-emitted on every poll and across every restart. The pin now carries a
192
+ persisted release floor, and every delivered id the watermark does not cover is remembered whatever left it
193
+ uncovered. (Repairing a fail-open into a permanent fail-closed is not a repair.)
194
+ - **A cursor write whose durability was unproven was reported to nobody** (Loom re-audit 9, MEDIUM). The
195
+ previous entry made `save()` *return* a durability status; the call site then discarded it - the same
196
+ defect one layer out. The watcher now consumes that answer and reports an unproven cursor once, clearing
197
+ when persistence recovers.
198
+ - **A sink that could not be opened safely crashed the poll loop or fell through to stdout** (Loom
199
+ re-audit 9, MEDIUM). A failed reopen after rotation raised out of `write()`, which under a supervisor is
200
+ a crash loop; and a refused per-persona sink returned `None`, which means "no sink configured, write to
201
+ stdout" - printing the very mail that had just been declined. Both are now failed deliveries, contained
202
+ to the affected persona, and a broken sink retries and recovers on its own.
203
+ - **The events file's DIRECTORY ENTRY was never made durable** (Loom re-audit 8, HIGH 2). `fsync` on the
204
+ file descriptor makes the *bytes* durable; the *name* lives in the directory. On create and on rotation
205
+ the directory was left unsynced, so the state directory could persist an advanced cursor while the event
206
+ pathname or a rotated archive was lost - and `--state-file` and `--events-file-template` may be in
207
+ *different* directories, so syncing one proves nothing about the other. The events directory is now
208
+ synced before the cursor that acknowledges those events is persisted, and a failure holds the cursor.
209
+ - **A failed state-directory `fsync` was reported as success** (Loom re-audit 8, HIGH 3). `save()` called
210
+ the sync and discarded its answer, so the cursor was written and its durability merely assumed, with no
211
+ diagnostic. `save()` now returns whether the write is durable and says so loudly when it is not. (The
212
+ failure direction is re-delivery rather than loss - a reverted state file replays mail - but a watcher
213
+ that cannot tell you it failed to persist will keep not telling you.)
214
+ - **A cursor could outlive the event it acknowledged** (Loom re-audit 7, MEDIUM). The state file's temp was
215
+ fsynced but the directory holding the rename was not, and the event sink was flushed but never fsynced.
216
+ Events are now fsynced *before* the cursor that acknowledges them is persisted, and the state directory is
217
+ fsynced after `os.replace`; a sink that cannot be synced retracts that poll's acknowledgements entirely.
218
+ - The single-writer lock file descriptor was never released - leaked on every refused lock, and the source of
219
+ the suite's two `ResourceWarning`s. `StateFile.unlock()` now exists and is called on shutdown.
220
+
221
+ ### Added
222
+ - **Alarm routing now requires evidence of a consumer.** Account-level alarms went to every directory
223
+ persona, which meant long-dead test personas kept receiving alerts into streams nobody reads. Eligibility
224
+ is now positive - observed authorship, or memories the directory says they own - and it mirrors the
225
+ stranded-mail ownership test on purpose, since two predicates for one question drift apart and then
226
+ disagree. Authorship alone suffices so first contact is not broken, and an unreported memory count leaves
227
+ a persona eligible because no data is not evidence of absence.
228
+
229
+ It fails open: if the predicate would leave nobody, every directory watcher is used instead. An alarm
230
+ delivered to a dead stream costs a line; one delivered to nobody is the silent failure the tool exists to
231
+ prevent. Measured live, recipients fell from 25 to 18.
232
+
233
+ - **Urgent-unanswered alarm.** The watcher reports members holding mail a sender marked **urgent** while no
234
+ activity from them has been observed.
235
+
236
+ "Is this member stuck?" is normally unanswerable from outside: idle-by-design and wedged look identical,
237
+ so the obvious alarm fires on every quiet persona and rots into noise. The urgent flag breaks the tie
238
+ because it is a *sender* declaring an expectation - not the recipient declaring liveness - and silence
239
+ only means something once something was expected. A quiet member with no urgent mail never trips it, so
240
+ the alarm fires exactly where somebody escalated and nothing happened.
241
+
242
+ Both halves of the predicate must be positive: `unread_urgent > 0` and an explicit "no activity in a span
243
+ we covered". A NOT-OBSERVABLE answer means the watcher was not running then, and reporting that as silence
244
+ would be a fabrication. It costs no request - `unread_urgent` arrives on the same row as the unread count
245
+ the fast path already fetches every tick, and was previously parsed and discarded.
246
+
247
+ Kept disjoint from the stranded-mail alarm on purpose - that one is for inboxes nobody owns, this one for
248
+ real members who are not responding - because two alarms covering one inbox drift apart and then disagree.
249
+ Same honesty rules as the rest: an `alert` rather than a new event name, one summarising event per
250
+ watcher, the observation and never the diagnosis, self-clearing when either half clears, and no ack.
251
+
252
+ - **`--activity-file PATH`: publish who has been observed AUTHORING mail.** Refreshed each tick, it lets a
253
+ harness answer "has X been active since my message?" from data the watcher already collects.
254
+
255
+ Authorship was chosen over the two signals that look better and are both forgeable by accident. Inbox
256
+ read-state can be produced for any persona by any agent calling the inbox with the default
257
+ `mark_read=true`, and it fails the other way too, since a member consuming its own event stream reads its
258
+ mail without touching read-state. And a GET on the presence endpoint carrying a persona parameter beats
259
+ that persona into the active roster, so merely probing someone makes them look alive. Only B produces B's
260
+ outbound, and nobody else can manufacture or erase it.
261
+
262
+ It costs no request: all-personas mode already fetches every inbox each tick with `mark_read=false`, and
263
+ every row already carries its author. That matters, because the alternative - a client polling every
264
+ inbox on a timer to reconstruct this - puts a loop that reads everyone's mail on a schedule, where one
265
+ missing `mark_read=false` destroys read-state fleet-wide.
266
+
267
+ Both coverage limits are published, because a claim of silence is only as good as the watching.
268
+ `observed_since` bounds the process; `observation_floor_id` is the MAXIMUM of the per-inbox window floors,
269
+ deliberately not the minimum - a persona's mail lands in whichever inbox they wrote to, so a claim that
270
+ they authored nothing is only as strong as the worst-covered inbox. Queries below the floor answer NOT
271
+ OBSERVABLE rather than "silent". The rendered observation carries the wait count and last-evidence stamp
272
+ and is asserted by test to state no cause, since deadlocked, unreachable and still-working are
273
+ indistinguishable from this data and need opposite responses.
274
+
275
+ `--check-activity PERSONA --since-id N` evaluates a published report in one shot, with no token, network
276
+ or watch loop, so a shell heartbeat can call it. It exits 0 on evidence of activity, 1 on a silence in a
277
+ span the report actually covered (printing the observation), and 2 on NOT OBSERVABLE or an unreadable
278
+ report. 1 and 2 are distinct deliberately: collapsing them turns "I was not watching" into "they were
279
+ silent". The watcher and the one-shot share one implementation of the tri-state.
280
+
281
+ - **Every event now carries a producer-owned `event_id`**, so a consumer can dedupe without hashing our NDJSON
282
+ bytes. Byte-hashing works until it doesn't: it couples the consumer to our serialisation, so a change to key
283
+ order, spacing or `--content-chars` silently changes the dedupe key and re-delivers old events. Prompted by a
284
+ real consumer deduping ID-less events on `event+ts` - unique only while two events never land inside one clock
285
+ tick, and the timestamp is stamped at emit time.
286
+
287
+ Two identities, because messages and signals need opposite guarantees. `new` events carry the message's
288
+ identity (`<persona>:new:<message id>`), so the same message always yields the same id - across a restart, a
289
+ re-delivery after state loss, and two watchers of one inbox; dedupe on it for exactly-once processing. Every
290
+ other event is a signal and gets an id unique to that emission (`<persona>:<event>:<run>-<n>`), because a
291
+ recurrence is a genuinely different event and a second outage is a second thing worth seeing. Repeated
292
+ announcements of an unchanged condition are suppressed at the source instead, where suppression belongs.
293
+
294
+ `<run>` is random per process. A bare in-process counter is specifically ruled out: it restarts at 1 and hands
295
+ ids a consumer has already seen to brand-new events, so a correct consumer drops live mail - a worse failure
296
+ than the duplicate the id was introduced to prevent. Ids are stamped at the single emit chokepoint, so a future
297
+ event kind cannot forget one, and are exported to `--exec` consumers as `$KIJITOMON_EVENT_ID`.
298
+
299
+ - **Unread-mail-outside-the-window alarm.** The inbox endpoint reports `unread_not_shown` - unread messages
300
+ it holds that this response did not return. Above zero, the watcher raises an `alert` carrying the count,
301
+ the window floor, the cursor, and `above_watermark`. It is deliberately a cheap signal rather than a
302
+ coverage mechanism: the count has no cursor of its own, so it can say THAT mail is out of view but never
303
+ WHICH rows, and coverage still comes from the backward walk that terminates.
304
+
305
+ The event states an observation, not a diagnosis. The count includes unread mail anywhere in the inbox -
306
+ among it messages this watcher already delivered that the agent simply has not read - so it is not on its
307
+ own evidence that anything was missed. `above_watermark` is the fact that separates the two cases, and is
308
+ reported rather than resolved.
309
+
310
+ A zero is not self-justifying, and this is the trap the implementation is built around: the server computes
311
+ the field only when it withheld something, so it is `0` **by construction** on a page with nothing older.
312
+ The negative answer therefore requires positive evidence - the zero was genuinely computed, or the window
313
+ is structurally complete - and a field the server never sent is a third state that asserts nothing either
314
+ way. For the same reason the check runs on the newest-page poll only. Measured against a live inbox holding
315
+ four unread messages: the newest page reported `0` (correctly), a mid-walk page reported `4` (the whole
316
+ inbox's unread, not that window's), and the terminal page of the walk reported `0` while all four sat above
317
+ it - so feeding walk pages to the check would both invent alarms and clear real ones.
318
+
319
+ Routed like the stranded-mail alarm (an `alert` rather than a new event name, no ack, self-clearing) but
320
+ failing the opposite way when the persona directory is unknown: that alarm withholds because it would
321
+ otherwise flag every persona, while this one concerns the target's own inbox, where firing needlessly costs
322
+ a line in a stream nobody reads and withholding costs the silent wake gap this tool exists to prevent.
323
+
324
+ - **Stranded-mail alarm.** The watcher reports mail sitting in an inbox that nothing consumes. Such mail
325
+ is undeliverable and nothing else reports it: the sender gets a success and a message id, the recipient
326
+ gets no signal, and there is no bounce. Two real cases prompted it - a case-variant of a live persona,
327
+ whose reply sat unread for 14 days, and a group-looking name (`all`) with no broadcast semantics behind
328
+ it, which swallowed a fleet-wide announcement for 4 days.
329
+
330
+ An inbox is flagged when it holds mail and **either** the persona directory does not list it **or** it
331
+ owns zero memories - nothing has ever written as that persona, so nobody is working under it. The second
332
+ test matters because a directory built as a union of registered *recipients* lists every typo the moment
333
+ someone sends to it, which would leave the first test unable to fire. Ownership reads the top-level
334
+ `memory_count`, deliberately not a sum of `projects[].count`: project counts exclude global-scoped
335
+ memories, so a persona whose memories are all global sums to zero and looks unowned - measured against a
336
+ live account, that mistake would have flagged eight of nine active personas. Where a server reports no
337
+ memory counts the signal stays quiet rather than guessing.
338
+
339
+ Both signals come from endpoints already fetched, so the check costs no extra request. Reported once per
340
+ inbox per process, to stderr and as one summarising event per watcher; a case-variant is diagnosed as
341
+ such, naming its twin. Disable with `--no-stranded-alerts`.
342
+
343
+ Two routing rules are load-bearing and easy to get wrong: the alarm is an `alert` rather than a new event
344
+ name, so consumers already filtering `alert` surface it without being rearmed - a fresh name would have gone
345
+ unwatched on every armed consumer, because a running `grep` never re-reads its argv; and it is
346
+ routed only to watchers backed by a real directory persona, because a stranded inbox has mail and
347
+ therefore acquires a watch target and stream of its own - alerting every target would write the alarm
348
+ into the very stream nobody reads. Producing an event is not delivering it.
349
+ - `$KIJITOMON_STRANDED` exposes the affected inboxes to `exec-per-event` consumers, comma-separated.
350
+
351
+ ### Fixed
352
+ - **A bounded inbox window could permanently skip mail** (reported by Loom). The inbox endpoint returns
353
+ the **newest** messages that fit a count limit *and* an aggregate content budget, and declares what it
354
+ left out via `truncated` / `size_truncated` / `size_dropped`. The watcher parsed only `result`, discarded
355
+ those fields, and advanced its cursor to the highest id it had seen - so any message the server omitted
356
+ while it sat *above* the cursor was never emitted and was stepped over permanently. The truncation was
357
+ never silent in the data, only in the handling of it.
358
+
359
+ The cursor is now a **confirmed-contiguous watermark**. When the window reaches back past it, every
360
+ omitted message is older than anything still owed and nothing changes - the steady state, since
361
+ long-polling keeps the backlog small. When the window starts *above* the watermark while the server
362
+ admits it withheld rows, the watcher **walks the span backward** with `before_id`, paging until it
363
+ reaches the watermark or the chain ends, and advances only then. A walk that fails, stalls, or exhausts
364
+ its page budget proves nothing, so the watermark **pins** and an `alert` names the cursor, the window
365
+ floor and the shortfall.
366
+
367
+ Coverage is established by **exhausting the chain, not by counting rows**. That distinction is what
368
+ makes an unquantified truncation resolvable at all: `truncated` states that rows were withheld without
369
+ saying how many, so no arithmetic can ever prove the span empty. It also reaches messages someone has
370
+ already **read** - precisely the rows most likely to be hiding in an old span, and the ones an
371
+ unread-only reconcile structurally cannot see.
372
+
373
+ Pinning is the point: advancing past an unresolved span makes the next poll see the window reaching back
374
+ past the cursor, declare itself safe, and bury the omission forever. The pin is persisted, so a restart
375
+ neither re-emits what was already delivered nor forgets the gap, and visible mail is still delivered
376
+ while pinned - failing closed costs no liveness. Pin tracking is bounded; on overflow the watcher says
377
+ plainly that it can no longer reason about the span rather than quietly dropping ids, and only an
378
+ authoritative walk can restore that ground truth.
379
+
380
+ Two accounting rules keep it honest. Mail arriving *between* the two reads is delivered but never counted
381
+ as recovery - new arrivals prove nothing about old omissions. And a lone oversized message
382
+ (`size_truncated` with `size_dropped: 0`) had its body clipped rather than being withheld, so it is not
383
+ an omission; count-limit truncation, size-budget drops and body clipping are accounted separately.
384
+
385
+ Corrupt pin state fails **closed**: a malformed record holds the pin with no tracking rather than
386
+ silently unpinning, because loading it as "nothing outstanding" would let the replay cap jump the cursor
387
+ over the very span the pin was protecting.
388
+
389
+ No mail was lost in practice before this: polling cadence kept every observed window reaching back past
390
+ the cursor. That was luck, not correctness - roughly eight typical messages in one gap exhausts the budget.
391
+ - **Case-variant personas no longer self-deadlock the watcher (silent wake gap).** A persona name was
392
+ mapped to its state file verbatim, but macOS (APFS) and Windows are case-**insensitive**, so
393
+ `Claude-chat` and `claude-chat` name the *same* file. Discovering a case-variant of an already-watched
394
+ persona made the watcher try to lock a state file it already held, so the variant was never adopted and
395
+ got **no event stream at all** - mail addressed to it woke nobody, and the failed adoption logged on
396
+ every tick (one observed 3-day run: 20,079 of 20,129 stderr lines from that single warning).
397
+ Persona matching is now case-insensitive throughout, and the persona's original case is preserved for the
398
+ API - case-insensitive match, case-preserving display. Note the deliberate asymmetry with the
399
+ stranded-mail check, which compares names **exactly**, because the server's inbox namespace *is*
400
+ case-sensitive and casefolding there would hide the very defect it detects.
401
+ - **Per-persona warnings are emitted once per process** instead of once per tick, so a condition that
402
+ cannot resolve itself can no longer grow stderr without bound.
403
+
404
+ ## [0.3.0] - 2026-06-29
405
+
406
+ Near-instant wake via long-polling, with full self-heal.
407
+
408
+ ### Added
409
+ - **Long-poll wake** (`--wait`, default 50s): the watcher holds a `/api/notify/pending?wait=&cursor=`
410
+ request that the server releases the instant new mail arrives, cutting wake latency from up to
411
+ `--poll-seconds` to near-instant **without raising the request rate** (one held connection per
412
+ account). Forward/backward compatible: against a server that doesn't support long-poll it
413
+ transparently falls back to interval polling and auto-upgrades once the server returns a cursor -
414
+ no redeploy. `--wait 0` disables it.
415
+ - **Instant new-persona pickup**: a newly created persona that receives mail is added as a watch
416
+ target within one tick (from the notify counts already fetched), instead of waiting for the
417
+ periodic `/api/personas` rescan.
418
+
419
+ ### Reliability
420
+ - **Self-heal on connection loss** (wifi/NAT/Cloudflare/server-restart): a dropped or half-open hold
421
+ is detected by a client timeout above the server hold, then reconnected with exponential backoff,
422
+ resuming from the last opaque cursor so no wake is missed across the gap (lossless). The periodic
423
+ full per-persona inbox poll remains the by-message-id correctness backstop.
424
+
425
+ ## [0.2.0] - 2026-06-29
426
+
427
+ Remote-only release. The monitor now watches your Kijito inbox at `api.kijito.ai` exclusively.
428
+
429
+ ### Changed
430
+ - **Breaking:** the monitor targets the Kijito API at `https://api.kijito.ai` only. The `--url`
431
+ destination override and the `--allow-loopback` / `--allow-private` flags are removed.
432
+ - **Breaking:** a Kijito API token is now required. Provide it via `$KIJITOMON_TOKEN` or
433
+ `--token-file`; the process exits with a clear error if no token is set.
434
+
435
+ ### Added
436
+ - A named `User-Agent` header on every request (required: the API is fronted by a WAF that
437
+ rejects the default Python-urllib agent).
438
+
439
+ ### Fixed
440
+ - Persona discovery (`/api/personas`) now correctly targets the configured API host.
441
+
442
+ ## [0.1.0] - 2026-06-24
443
+
444
+ First public release.
445
+
446
+ ### Added
447
+ - Single, zero-dependency Python stdlib watcher for the Kijito inbox. It polls the inbox
448
+ and emits one event per new message, either as NDJSON on stdout or by running a command
449
+ per event, to keep a running agent's inbox live between tool calls.
450
+ - Multi-persona mode: one process watches every persona in the account via `/api/personas`, with
451
+ one `/api/notify/pending` fetch per tick fanned out in-process, per-persona cursors, and periodic
452
+ rediscovery of new personas.
453
+ - Per-persona owned, self-rotating event logs via `--events-file-template`, so each session
454
+ tails only its own `events.<persona>.ndjson`.
455
+ - Liveness alert state machine (`alert` after N consecutive failures, `recovered`, optional
456
+ `heartbeat`) for use as a dead-man's switch.
457
+ - SSRF-guarded `--url` override, peek-only inbox reads, monotonic-id cursor dedup, and
458
+ single-writer state files that resume cleanly under a supervisor.
459
+ - Console command `kijito-inbox-monitor`, installable with pipx, uv, or pip.
460
+ - An npm package that acts as a signpost to the PyPI tool (it delegates to `uvx`/`pipx`, or
461
+ prints install guidance), so the name is reserved on npm without a fragile Node installer.
462
+
463
+ [0.3.0]: https://github.com/KijitoAI/kijito-inbox-monitor/releases/tag/v0.3.0
464
+ [0.2.0]: https://github.com/KijitoAI/kijito-inbox-monitor/releases/tag/v0.2.0
465
+ [0.1.0]: https://github.com/KijitoAI/kijito-inbox-monitor/releases/tag/v0.1.0
@@ -0,0 +1,68 @@
1
+ # providers/monitor — import provenance (P0-F29/A29 monitor-foundation)
2
+
3
+ **Imported by:** argus (monitor steward) · **Date:** 2026-08-17
4
+ **Source:** `KijitoAI/kijito-inbox-monitor` → **Destination:** `KijitoAI/kijito-tools` `providers/monitor/`
5
+ **Authorization:** steward prep `P0B-MONITOR-FOUNDATION-PREP.md` §4.2 · river CONCUR (hive msg 7986,
6
+ 2026-08-17: F29 foundation/release/host acceptance) · loom informed (P0 context). Resolver of record:
7
+ Kijito memory 28999 + the committed sole-author commit maps.
8
+
9
+ This manifest is the audit trail because the pre-rewrite lineage was scrubbed by the 2026-08-15
10
+ sole-author window and now lives only in the external remap overlay. Where a fact below could not be
11
+ re-derived from public history, it is recorded here so tree-equality is auditable post-hoc rather than
12
+ asserted.
13
+
14
+ ## Selection (P0-F29 "exact selected source")
15
+
16
+ | role | SHA | tree |
17
+ |---|---|---|
18
+ | Frozen pin (pre-rewrite, contract of record) | `bc9110807dc274da8bb4f84e08c111bc33d055dd` | `135417e5d3d821bf29884e4f2ac9f4e19211c8b0` |
19
+ | Overlay-resolved (imported selection) | `01a70bc6b26a7450cf38b051a4e6aa32992b1763` | `135417e5d3d821bf29884e4f2ac9f4e19211c8b0` |
20
+
21
+ **Tree-equality proof (measured independently by argus, 2026-08-17):** the frozen pin and the
22
+ overlay-resolved SHA share the **identical tree object** `135417e5d3d821bf29884e4f2ac9f4e19211c8b0`;
23
+ `git diff bc9110807dc274da8bb4f84e08c111bc33d055dd 01a70bc6b26a7450cf38b051a4e6aa32992b1763` is empty.
24
+ (bc91108 was still resolvable in the local `kijito-inbox-monitor` object store, so this diff was
25
+ re-run first-hand rather than cited from the 2026-08-15 measurement.) Importing the literal frozen
26
+ SHA was rejected — it would resurrect the authorship the sole-author window exists to have scrubbed.
27
+
28
+ The contract's "public `3d595914…` strict ancestor" clause refers to the **pre-rewrite** public
29
+ lineage; the rewrite destroyed that ancestry in the current object graph (its post-rewrite form is
30
+ `d24c8c8ced9be7e2817bcdf52bbbb1377a16d4e9` per the commit map), so it is recorded here, not asserted
31
+ as git-verifiable against `01a70bc`.
32
+
33
+ ## Post-import delta (journaled fast-forward, no selection change)
34
+
35
+ `01a70bc` → `902c21dbd2e93e91b7a6bd801104394577681b5e` (d1_clocks fix, PR #1, assay zero-findings
36
+ certification, hive 7715) → `bd04b28616d9a9558a4a6fa3bb5004db08fbf249` (`release: v0.5.0`, npm + PyPI
37
+ independently confirmed, fleet deployed).
38
+
39
+ | role | SHA | tree |
40
+ |---|---|---|
41
+ | FF target (imported HEAD content) | `bd04b28616d9a9558a4a6fa3bb5004db08fbf249` | `e46c189b0a1192406dc12c2039ef1dbe6500a96a` |
42
+
43
+ ## Landed in kijito-tools (byte-exact, verified via content-addressed subtree hash)
44
+
45
+ | import step | commit | `HEAD:providers/monitor` tree | matches source tree |
46
+ |---|---|---|---|
47
+ | import overlay-resolved 01a70bc | `d9be542` | `135417e5d3d821bf29884e4f2ac9f4e19211c8b0` | ✅ = tree(01a70bc) |
48
+ | ff to v0.5.0 bd04b28 | `e4a87da` | `e46c189b0a1192406dc12c2039ef1dbe6500a96a` | ✅ = tree(bd04b28) |
49
+
50
+ Re-verify at any time: `git -C <kijito-tools> rev-parse d9be542:providers/monitor` = the 01a70bc tree,
51
+ `git -C <kijito-tools> rev-parse e4a87da:providers/monitor` = the bd04b28 tree. This manifest commit
52
+ adds IMPORT-PROVENANCE.md, so the current `providers/monitor` tree differs from `e46c189b…` by exactly
53
+ this file.
54
+
55
+ ## Referenced artifacts (hashes)
56
+
57
+ - Commit map: `evidence/sole-author-remap-20260814/commit-map.inbox-monitor.txt`
58
+ — sha256 `0b1fb8a67165f3bb1146e80e4cc2eeabcf648649cd5fef15fafeacf9edcefc12`
59
+ (contains the line `bc91108… 01a70bc…`).
60
+ - Sole-author remap overlay: `Agents/OpenCode/SurfaceArea/SOLE-AUTHOR-SHA-REMAP-OVERLAY.md`
61
+ — sha256 `028199f2d81f14c7e145e744bba21cf593eb3b52819c289156f2b511837deb20`.
62
+
63
+ ## Not done by this import (later P0B sequence)
64
+
65
+ Old-source transition (kijito-inbox-monitor stays the publish/pin home until fleet producers re-pin —
66
+ P0-C7 terminal), `_shared` doorbell + authenticated consumer lease (§4.4), and F29/A29 certify+enforce
67
+ of opaque `--no-content` output in both service templates plus the live Mac-producer gap (§4.6) are
68
+ tracked separately and not claimed here.