kijito-tools 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +1 -1
- package/providers/codex/release-manifest.json +1 -1
- package/providers/monitor/CHANGELOG.md +17 -0
- package/providers/monitor/UPSTREAM +9 -9
- package/providers/monitor/docs/DESIGN.md +3 -1
- package/providers/monitor/kijito_inbox_monitor.py +36 -2
- package/providers/monitor/package.json +1 -1
- package/providers/monitor/pyproject.toml +1 -1
- package/providers/monitor/test_kijito_monitor.py +80 -0
package/README.md
CHANGED
|
@@ -23,6 +23,17 @@ opt-in per pane, and the skills are convenience wrappers, not requirements.
|
|
|
23
23
|
| `kijito-start` skill | The active, thorough version of session catch-up: load memory, read the current-state pointer and recent lessons, arm the inbox, and resume active work — or, for a new persona, set up identity and the pointer. | Yes |
|
|
24
24
|
| `kijito-qa-memory` skill | Memory curation that requires writing the new memories (not only fixing existing ones), then uses a fresh subagent to confirm a cold start can reconstruct the work. | Yes |
|
|
25
25
|
|
|
26
|
+
### The status line
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
`statusline-context.sh` shows, left to right: the pane's persona (from the project's `.kijito_persona`
|
|
31
|
+
marker, so several agent panes can be told apart), the persona's unread-mail count when
|
|
32
|
+
[kijito-inbox-monitor](https://github.com/KijitoAI/kijito-inbox-monitor) 0.5.7 or later is running and
|
|
33
|
+
has recorded one in the last 10 minutes (shown only when it is above zero), the model, and the context
|
|
34
|
+
window in use. The picture is rendered from the script's real output by
|
|
35
|
+
`scripts/render-statusline-svg.py`, and a test fails if the two disagree.
|
|
36
|
+
|
|
26
37
|
The two skills are conveniences, not the only way in: an agent can run the same catch-up and
|
|
27
38
|
curation by hand from a few prompts. They are packaged as skills because that makes them simple to
|
|
28
39
|
drop into `~/.claude/skills/` and invoke the same way everywhere.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kijito-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Installer for kijito-tools: copies the context-tracking, session catch-up, and self-clear scripts plus the Kijito skills into ~/.claude.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"artifacts": {
|
|
16
16
|
"wakeCoreSha256": "f6ca4f88841b371409dfb4946e3b427e83150bd511f38068b364de1090c5cd35",
|
|
17
|
-
"workflowSha256": "
|
|
17
|
+
"workflowSha256": "4fcbb75ffc5b309a54fcc1492db983bce5f15353694b098859d048ed59197fb6",
|
|
18
18
|
"planSha256": "d9d666b86ac06414f6e6a13a3e3e2115983a1dad44fcfded7eccc51b191fb451",
|
|
19
19
|
"wakeHelperSha256": "838f6b4462ba037a23a14b6fdb8dc34ee8b8f4a7df258b428832bc092fac3ea4",
|
|
20
20
|
"wsUdsSha256": "4da13eef89b572fb735e756cb06846a021919dec7f91c6d07989377944cc776c",
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to kijito-inbox-monitor are documented in this file.
|
|
4
4
|
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
|
|
5
5
|
|
|
6
|
+
## [0.5.8] - 2026-09-26
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **Runs on native Windows** (reported from Windows 11, CPython 3.14). Four problems, each of which stopped
|
|
10
|
+
the producer or corrupted delivery there:
|
|
11
|
+
- Startup crashed with `AttributeError: module 'os' has no attribute 'geteuid'`. The private-file guard's
|
|
12
|
+
owner and 0600 checks are POSIX-only now; on Windows it still refuses anything that is not a regular
|
|
13
|
+
file.
|
|
14
|
+
- A new events file never became durable. Windows cannot open a directory to fsync it, so every sync
|
|
15
|
+
reported failure and the cursor was held forever, re-delivering the same mail. The directory fsync is
|
|
16
|
+
skipped on Windows (NTFS journals directory metadata).
|
|
17
|
+
- Every directory level printed a "writable by other local users" warning, because Windows reports
|
|
18
|
+
directories as mode 0777. That warning is POSIX-only now.
|
|
19
|
+
- On a cp1252 console or Git Bash pipe, `--help` crashed with `UnicodeEncodeError`, and so did any event
|
|
20
|
+
whose message body had a character outside cp1252 (an emoji, CJK) - mid-delivery. stdout is now
|
|
21
|
+
written as UTF-8, which JSON Lines requires anyway, and the help text is ASCII.
|
|
22
|
+
|
|
6
23
|
## [0.5.7] - 2026-09-25
|
|
7
24
|
|
|
8
25
|
### Added
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# The upstream release providers/monitor is a byte-exact copy of (row M314).
|
|
2
2
|
# Written by scripts/import-monitor.sh; verified by tests/vendored_monitor_test.sh. Never hand-edit.
|
|
3
3
|
repo=KijitoAI/kijito-inbox-monitor
|
|
4
|
-
sha=
|
|
5
|
-
tree=
|
|
6
|
-
tag=v0.5.
|
|
4
|
+
sha=e3c69db68451e3e55b5b3da9c98546706fff7bff
|
|
5
|
+
tree=70a8b4dfc04c7832aec37cb9124178e75f7248c9
|
|
6
|
+
tag=v0.5.8
|
|
7
7
|
file=100644 8d88ccb0fd09b9144505f985b4f882db278fac1f .github/workflows/publish-npm.yml
|
|
8
8
|
file=100644 19ce3471a2cffa689be9d261049c1e74e3e7a25e .github/workflows/publish-pypi.yml
|
|
9
9
|
file=100644 4534290a5dd36d18d07a782b784b6b0ad5ee38fc .gitignore
|
|
10
|
-
file=100644
|
|
10
|
+
file=100644 b5cd0382bcfa90cd71fa39577a41a47707ac2bac CHANGELOG.md
|
|
11
11
|
file=100644 d645695673349e3947e8e5ae42332d0ac3164cd7 LICENSE
|
|
12
12
|
file=100644 f6b0e5634f3fd4156c1c000efb31578bfb068cf3 NOTICE
|
|
13
13
|
file=100644 73d2bf5a5d5d6cb622db014285cd9ada4c705724 README.md
|
|
@@ -15,12 +15,12 @@ file=100644 72f0c148f1e721ac1bb4a38e5cae690355774314 RELEASING.md
|
|
|
15
15
|
file=100755 fca1203267cb726736b0c8f239e2d74c40a7a895 arm-hive-monitor.sh
|
|
16
16
|
file=100755 bbe3f07de13dab8a8a7a4798481156ae4a46ab8f bin/cli.js
|
|
17
17
|
file=100644 fffa8bd23749a321285bd942a3f4f4a98936129c com.kijito.inbox-monitor.plist.template
|
|
18
|
-
file=100644
|
|
18
|
+
file=100644 b0c07b55c75e6061e6c82d5e23c2b7ddad5131cf docs/DESIGN.md
|
|
19
19
|
file=100644 4f5e53a8633e60e6c57c451e28546f8af8783fe8 kijito-inbox-monitor@.service.template
|
|
20
|
-
file=100644
|
|
21
|
-
file=100644
|
|
22
|
-
file=100644
|
|
20
|
+
file=100644 692aadd5fe603556e0ba0e9358c1487022b2bc05 kijito_inbox_monitor.py
|
|
21
|
+
file=100644 53538cfa0dd2a25f68d54b2a62fb072d599cec83 package.json
|
|
22
|
+
file=100644 44b676233aecdc02dc462a4144dd29120ff05d73 pyproject.toml
|
|
23
23
|
file=100755 b34a5c775385ecfaefd4b374c1fc1aa082a85d14 scripts/migrate-systemd-unit.sh
|
|
24
24
|
file=100755 22acef6d6875395a81c701eb535030d0a78b0df1 scripts/mutation-check.py
|
|
25
25
|
file=100755 acba6196213736922fc4f7135749386d85d172a6 scripts/prepublish-gate.sh
|
|
26
|
-
file=100644
|
|
26
|
+
file=100644 affc54d7cf969631c3f2d9160995e933cde17a60 test_kijito_monitor.py
|
|
@@ -28,7 +28,9 @@ no pip installs) that polls the Kijito inbox and emits one event per new message
|
|
|
28
28
|
running, as NDJSON on stdout and/or exec-a-command-per-event. It is the client-side liveness watcher: it
|
|
29
29
|
keeps a running agent's inbox live by waking it between tool calls. It is not a server, and not a
|
|
30
30
|
notification service. POSIX target (Linux/macOS); Windows runs interval-only (no SIGUSR1 seam, no flock,
|
|
31
|
-
per §10/§7.3).
|
|
31
|
+
per §10/§7.3). On Windows the private-file guard checks only "regular file" (there is no POSIX owner or
|
|
32
|
+
mode; access is the profile's inherited ACL), a directory fsync is skipped (Windows cannot open a directory
|
|
33
|
+
to fsync it; NTFS journals the metadata), and stdout is written as UTF-8 whatever the console code page.
|
|
32
34
|
|
|
33
35
|
## 2. The problem
|
|
34
36
|
|
|
@@ -33,7 +33,7 @@ try:
|
|
|
33
33
|
except ImportError: # pragma: no cover - Windows
|
|
34
34
|
fcntl = None
|
|
35
35
|
|
|
36
|
-
__version__ = "0.5.
|
|
36
|
+
__version__ = "0.5.8"
|
|
37
37
|
SOURCE = "kijito-inbox"
|
|
38
38
|
# A named User-Agent is REQUIRED: api.kijito.ai is fronted by a WAF that 403s the default Python-urllib UA.
|
|
39
39
|
USER_AGENT = "kijito-inbox-monitor/%s" % __version__
|
|
@@ -1261,6 +1261,12 @@ def _assert_private_fd(fd, path):
|
|
|
1261
1261
|
st = os.fstat(fd)
|
|
1262
1262
|
if not stat.S_ISREG(st.st_mode):
|
|
1263
1263
|
raise InsecureFile("%s is not a regular file" % path)
|
|
1264
|
+
if not IS_POSIX:
|
|
1265
|
+
# WINDOWS HAS NO POSIX OWNER OR MODE BITS TO CHECK (praetor, Windows 11 native, 2026-09-26: os.geteuid
|
|
1266
|
+
# does not exist there, so this line crashed the producer at startup). Access there is an ACL, and a
|
|
1267
|
+
# file created under the user's profile inherits a user-only ACL; st_mode reports a synthetic
|
|
1268
|
+
# 0666/0444 that says nothing about who can read it. The regular-file check above still holds.
|
|
1269
|
+
return
|
|
1264
1270
|
if st.st_uid != os.geteuid():
|
|
1265
1271
|
raise InsecureFile("%s is owned by uid %d, not by us (uid %d)" % (path, st.st_uid, os.geteuid()))
|
|
1266
1272
|
cur = st.st_mode & 0o777
|
|
@@ -1364,6 +1370,10 @@ def _makedirs_private(path):
|
|
|
1364
1370
|
# directory is never validated, and an existing one is exactly where a hostile path would already be.
|
|
1365
1371
|
# A sticky directory (/tmp, mode 1777) is excluded: the sticky bit is precisely what makes a shared
|
|
1366
1372
|
# writable directory safe, and warning about it would train the reader to ignore this line.
|
|
1373
|
+
# POSIX only: Windows reports every directory as 0777 (access is an ACL), so the warning would fire on
|
|
1374
|
+
# every level of every path and say nothing true.
|
|
1375
|
+
if not IS_POSIX:
|
|
1376
|
+
return
|
|
1367
1377
|
seen = path
|
|
1368
1378
|
while True:
|
|
1369
1379
|
try:
|
|
@@ -1386,7 +1396,13 @@ def _fsync_dir(path):
|
|
|
1386
1396
|
os.replace is atomic for a concurrent READER, but atomicity is not durability: after a power loss
|
|
1387
1397
|
the new file's contents can be on disk while the directory entry still names the old inode - i.e. a
|
|
1388
1398
|
silently OLDER cursor. Syncing the file alone (which is all we did) does not cover the rename.
|
|
1399
|
+
|
|
1400
|
+
Windows cannot open a directory for fsync at all, so this always failed there and the events sink held
|
|
1401
|
+
its cursor forever on a new file. NTFS journals directory metadata itself, so there is nothing further
|
|
1402
|
+
to do: report success.
|
|
1389
1403
|
"""
|
|
1404
|
+
if not IS_POSIX:
|
|
1405
|
+
return True
|
|
1390
1406
|
try:
|
|
1391
1407
|
fd = os.open(path, os.O_RDONLY)
|
|
1392
1408
|
except OSError:
|
|
@@ -3703,7 +3719,7 @@ def build_parser():
|
|
|
3703
3719
|
p.add_argument("--wait", type=int, default=50,
|
|
3704
3720
|
help="Long-poll hold (s) requested from /api/notify/pending so new mail wakes the watcher "
|
|
3705
3721
|
"near-instantly at ~the same request rate (default 50; the server clamps to its own max). "
|
|
3706
|
-
"0 disables long-poll
|
|
3722
|
+
"0 disables long-poll -> plain interval polling at --poll-seconds. If the server doesn't "
|
|
3707
3723
|
"support long-poll, the client auto-falls back to interval polling (no redeploy needed). "
|
|
3708
3724
|
"Clean shutdown during a held poll can take up to --wait seconds (a supervisor's SIGKILL "
|
|
3709
3725
|
"mid-hold is safe - state is persisted every cycle).")
|
|
@@ -3840,7 +3856,25 @@ def validate_args(args):
|
|
|
3840
3856
|
"not multi-persona/all-personas - each persona has its own cursor")
|
|
3841
3857
|
|
|
3842
3858
|
|
|
3859
|
+
def _utf8_stdout():
|
|
3860
|
+
"""Write stdout as UTF-8 whatever the locale says (praetor, Windows 11 native, 2026-09-26).
|
|
3861
|
+
|
|
3862
|
+
A Windows console or a Git Bash pipe reports cp1252, so `--help` (a non-ASCII character) and, far worse,
|
|
3863
|
+
any EVENT carrying a message body outside cp1252 (an emoji, CJK) raised UnicodeEncodeError - a
|
|
3864
|
+
ValueError, which the stdout sink's OSError guard does not catch, so it killed the producer mid-delivery.
|
|
3865
|
+
JSON Lines is UTF-8 by definition, so that is the right encoding for every consumer. No-op where stdout
|
|
3866
|
+
is already UTF-8 or cannot be reconfigured.
|
|
3867
|
+
"""
|
|
3868
|
+
enc = (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "").replace("_", "")
|
|
3869
|
+
if enc != "utf8" and hasattr(sys.stdout, "reconfigure"):
|
|
3870
|
+
try:
|
|
3871
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
3872
|
+
except (ValueError, OSError):
|
|
3873
|
+
pass
|
|
3874
|
+
|
|
3875
|
+
|
|
3843
3876
|
def main(argv=None):
|
|
3877
|
+
_utf8_stdout()
|
|
3844
3878
|
args = build_parser().parse_args(argv)
|
|
3845
3879
|
# A pure read of an existing report: no token, no network, no state file, no watch loop. Placed
|
|
3846
3880
|
# before validate_args so a heartbeat can call it without satisfying the watcher's own config.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kijito-inbox-monitor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Pointer to the Python package kijito-inbox-monitor. A zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives. Install the real tool with pipx/uv/pip.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "kijito-inbox-monitor"
|
|
10
|
-
version = "0.5.
|
|
10
|
+
version = "0.5.8"
|
|
11
11
|
description = "Zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
@@ -6274,3 +6274,83 @@ class PersistedUnreadCountTest(unittest.TestCase):
|
|
|
6274
6274
|
|
|
6275
6275
|
def test_zero_loads(self):
|
|
6276
6276
|
self.assertEqual(self._load({"unread": 0})["unread"], 0)
|
|
6277
|
+
|
|
6278
|
+
|
|
6279
|
+
class WindowsNativeTest(unittest.TestCase):
|
|
6280
|
+
"""praetor's report (Windows 11 native, CPython 3.14, 2026-09-26): the producer crashed at startup on
|
|
6281
|
+
`os.geteuid` (POSIX-only) in _assert_private_fd, and `--help` crashed on a cp1252 console. The next two
|
|
6282
|
+
walls after that were `_fsync_dir` (Windows cannot open a directory to fsync it, so the events sink held
|
|
6283
|
+
its cursor forever on a new file) and a spurious "writable by other users" warning (Windows reports
|
|
6284
|
+
every directory as 0777). Windows is SIMULATED here: IS_POSIX False and os.geteuid removed."""
|
|
6285
|
+
|
|
6286
|
+
def setUp(self):
|
|
6287
|
+
self._d = tempfile.TemporaryDirectory()
|
|
6288
|
+
self.addCleanup(self._d.cleanup)
|
|
6289
|
+
self._posix, km.IS_POSIX = km.IS_POSIX, False
|
|
6290
|
+
self.addCleanup(setattr, km, "IS_POSIX", self._posix)
|
|
6291
|
+
self._geteuid = getattr(os, "geteuid", None)
|
|
6292
|
+
if self._geteuid is not None:
|
|
6293
|
+
del os.geteuid
|
|
6294
|
+
self.addCleanup(setattr, os, "geteuid", self._geteuid)
|
|
6295
|
+
|
|
6296
|
+
def test_a_regular_file_passes_without_geteuid_and_is_not_chmodded(self):
|
|
6297
|
+
p = os.path.join(self._d.name, "events.jsonl")
|
|
6298
|
+
with open(p, "w"):
|
|
6299
|
+
pass
|
|
6300
|
+
os.chmod(p, 0o644)
|
|
6301
|
+
fd = os.open(p, os.O_RDONLY)
|
|
6302
|
+
try:
|
|
6303
|
+
km._assert_private_fd(fd, p) # crashed with AttributeError before the fix
|
|
6304
|
+
finally:
|
|
6305
|
+
os.close(fd)
|
|
6306
|
+
self.assertEqual(os.stat(p).st_mode & 0o777, 0o644, "no POSIX mode repair is attempted off POSIX")
|
|
6307
|
+
|
|
6308
|
+
def test_a_non_regular_file_is_still_refused(self):
|
|
6309
|
+
fd = os.open(self._d.name, os.O_RDONLY)
|
|
6310
|
+
try:
|
|
6311
|
+
with self.assertRaises(km.InsecureFile):
|
|
6312
|
+
km._assert_private_fd(fd, self._d.name)
|
|
6313
|
+
finally:
|
|
6314
|
+
os.close(fd)
|
|
6315
|
+
|
|
6316
|
+
def test_fsync_dir_reports_success(self):
|
|
6317
|
+
self.assertTrue(km._fsync_dir(os.path.join(self._d.name, "no-such-dir")))
|
|
6318
|
+
|
|
6319
|
+
def test_no_writable_directory_warning_off_posix(self):
|
|
6320
|
+
d = os.path.join(self._d.name, "open")
|
|
6321
|
+
os.mkdir(d)
|
|
6322
|
+
os.chmod(d, 0o777)
|
|
6323
|
+
buf = _capture_stderr(self)
|
|
6324
|
+
km._makedirs_private(os.path.join(d, "sub"))
|
|
6325
|
+
self.assertNotIn("writable by other local users", buf.getvalue())
|
|
6326
|
+
|
|
6327
|
+
def test_the_events_file_sink_writes_and_syncs(self):
|
|
6328
|
+
# End to end: before the fix a new events file made sync() return False on every poll (the
|
|
6329
|
+
# directory fsync), so the cursor never advanced and mail was re-delivered forever.
|
|
6330
|
+
sink = km.RotatingFileSink(os.path.join(self._d.name, "argus.jsonl"), 0, 3)
|
|
6331
|
+
self.addCleanup(sink.close)
|
|
6332
|
+
self.assertTrue(sink.write('{"event": "new", "content": "caf\\u00e9 \\u2713"}\n'))
|
|
6333
|
+
self.assertTrue(sink.sync())
|
|
6334
|
+
|
|
6335
|
+
|
|
6336
|
+
class Utf8StdoutTest(unittest.TestCase):
|
|
6337
|
+
"""A cp1252 stdout (a Windows console, a Git Bash pipe) must neither crash `--help` nor an event whose
|
|
6338
|
+
message body is outside cp1252. Real subprocesses, because the defect is the interpreter's own encoding."""
|
|
6339
|
+
|
|
6340
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
6341
|
+
|
|
6342
|
+
def _run(self, code):
|
|
6343
|
+
env = dict(os.environ, PYTHONIOENCODING="cp1252")
|
|
6344
|
+
env.pop("PYTHONUTF8", None)
|
|
6345
|
+
return subprocess.run([sys.executable, "-c", code], cwd=self.HERE, env=env, capture_output=True, timeout=60)
|
|
6346
|
+
|
|
6347
|
+
def test_help_does_not_crash_on_cp1252(self):
|
|
6348
|
+
r = self._run("import kijito_inbox_monitor as km, sys\ntry:\n km.main(['--help'])\nexcept SystemExit as e:\n sys.exit(e.code)")
|
|
6349
|
+
self.assertEqual(r.returncode, 0, r.stderr.decode("utf-8", "replace")[-300:])
|
|
6350
|
+
|
|
6351
|
+
def test_an_event_outside_cp1252_is_delivered_as_utf8(self):
|
|
6352
|
+
r = self._run("import kijito_inbox_monitor as km\nkm._utf8_stdout()\n"
|
|
6353
|
+
"em = km.Emitter('stdout-jsonl', None, 220, False)\n"
|
|
6354
|
+
"assert em.lifecycle('armed', persona='\\u6f22\\U0001F600') is True")
|
|
6355
|
+
self.assertEqual(r.returncode, 0, r.stderr.decode("utf-8", "replace")[-300:])
|
|
6356
|
+
self.assertIn("\u6f22\U0001F600", r.stdout.decode("utf-8"))
|