kijito-tools 0.2.3 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kijito-tools",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",
@@ -3,6 +3,19 @@
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.6] - 2026-09-25
7
+
8
+ ### Fixed
9
+ - **The launchd template names the state file the producer actually writes (row M289).** It passed a base
10
+ (`--state-file <dir>/hive.json`) that the producer turned into `hive.<persona>.json`, so anyone reading the
11
+ template looked for a file that never existed. It now passes `--state-file-template <dir>/hive.{persona}.json`,
12
+ which resolves to the SAME file as before, so upgrading moves nothing. The README's multi-persona example
13
+ likewise names `state.{persona}.json`. A new `_state_path_from_args()` is the one place the producer
14
+ decides a persona's state file, and `StateFileNameAgreementTest` derives the name from the template's own
15
+ argv through it.
16
+ - **A negative persisted `consecutive_failures` makes the state file CORRUPT.** A count below zero was
17
+ resumed as-is and silently postponed the dead-man alert by that many polls.
18
+
6
19
  ## [0.5.5] - 2026-09-24
7
20
 
8
21
  ### Added
@@ -269,7 +269,7 @@ owned, rotated event file, and each agent session consumes only its own:
269
269
  ```sh
270
270
  kijito-inbox-monitor --all-personas \
271
271
  --events-file-template ~/.local/state/kijito-inbox-monitor/events.{persona}.ndjson \
272
- --state-file ~/.local/state/kijito-inbox-monitor/state.json
272
+ --state-file-template ~/.local/state/kijito-inbox-monitor/state.{persona}.json
273
273
  ```
274
274
 
275
275
  Each session then wakes on its own `events.<persona>.ndjson` using the recipe in
@@ -1,26 +1,26 @@
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=35b2fe6688b2f2f536a98952f40816c96057c69f
5
- tree=994bb014166bb65b57223a524bbb0673f57ce22d
6
- tag=v0.5.5
4
+ sha=04bb12ae11fc882d14c30ac36a9220591b5fab65
5
+ tree=101419def29ffc023991a6f5aace5933d042768a
6
+ tag=v0.5.6
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 9e7c054bccb95cbcb42ea8642eb1fab76e5863d5 CHANGELOG.md
10
+ file=100644 8d92a7f3d76e6802faed79328e349b6b7786b87e CHANGELOG.md
11
11
  file=100644 d645695673349e3947e8e5ae42332d0ac3164cd7 LICENSE
12
12
  file=100644 f6b0e5634f3fd4156c1c000efb31578bfb068cf3 NOTICE
13
- file=100644 fa81d2fc2645a5503117825b714200e0ae642cc4 README.md
13
+ file=100644 73d2bf5a5d5d6cb622db014285cd9ada4c705724 README.md
14
14
  file=100644 72f0c148f1e721ac1bb4a38e5cae690355774314 RELEASING.md
15
15
  file=100755 fca1203267cb726736b0c8f239e2d74c40a7a895 arm-hive-monitor.sh
16
16
  file=100755 bbe3f07de13dab8a8a7a4798481156ae4a46ab8f bin/cli.js
17
- file=100644 23e088613c314bf86b384640639b545de60ea9d0 com.kijito.inbox-monitor.plist.template
17
+ file=100644 fffa8bd23749a321285bd942a3f4f4a98936129c com.kijito.inbox-monitor.plist.template
18
18
  file=100644 30519b1ad6098541083648392f344da68e688246 docs/DESIGN.md
19
19
  file=100644 4f5e53a8633e60e6c57c451e28546f8af8783fe8 kijito-inbox-monitor@.service.template
20
- file=100644 a88a59eb52d45918794b46d54de826617d977ff2 kijito_inbox_monitor.py
21
- file=100644 8c2d292152fbe752d5ebf3445141a9d7df5576e5 package.json
22
- file=100644 199474330b420fa514378ab0c1211e21db9c6c41 pyproject.toml
20
+ file=100644 5038ff752d4b936e5b9bfab5642f38e7622fd4a4 kijito_inbox_monitor.py
21
+ file=100644 5e682088a3dafbe9526761c87f9a16573c3bbdc9 package.json
22
+ file=100644 6f90a103962a6ee2fa985f8356ff8c95cb93a6a7 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 8745e5bfe9573b9e85dd4e10f8904273d1dff864 test_kijito_monitor.py
26
+ file=100644 ea22077c22b25d53d6719af4b0e7d9e719aeea7d test_kijito_monitor.py
@@ -37,8 +37,8 @@
37
37
  <string>__PROGRAM__</string>
38
38
  <string>--token-file</string>
39
39
  <string>__HOME__/.config/kijito-inbox-monitor/token</string>
40
- <string>--state-file</string>
41
- <string>__HOME__/.cache/kijito-inbox-monitor/hive.json</string>
40
+ <string>--state-file-template</string>
41
+ <string>__HOME__/.cache/kijito-inbox-monitor/hive.{persona}.json</string>
42
42
  <string>--events-file-template</string>
43
43
  <string>__HOME__/.cache/kijito-inbox-monitor/events.{persona}.ndjson</string>
44
44
  <string>--heartbeat</string>
@@ -33,7 +33,7 @@ try:
33
33
  except ImportError: # pragma: no cover - Windows
34
34
  fcntl = None
35
35
 
36
- __version__ = "0.5.5"
36
+ __version__ = "0.5.6"
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__
@@ -1543,7 +1543,7 @@ class StateFile:
1543
1543
  "%s\n" % self.path)
1544
1544
  return CORRUPT_STATE
1545
1545
  if not ((cursor is None or _is_int(cursor)) and state in ("UP", "DOWN")
1546
- and _is_int(failures)):
1546
+ and _is_int(failures) and failures >= 0): # a failure COUNT is never negative
1547
1547
  sys.stderr.write("kijito-inbox-monitor: WARNING state-file has a valid envelope but invalid "
1548
1548
  "fields; refusing to baseline over it: %s\n" % self.path)
1549
1549
  return CORRUPT_STATE
@@ -1858,6 +1858,15 @@ def _persona_path(template, persona):
1858
1858
  return template.replace("{persona}", _state_safe_persona(persona))
1859
1859
 
1860
1860
 
1861
+ def _state_path_from_args(args, persona):
1862
+ """THE state file a persona's watch target opens (row M289): the `{persona}` template when given, else the
1863
+ per-persona derivation of the `--state-file` base. One function, so a supervisor template, the docs and a
1864
+ test can ask the producer which file it will write instead of re-deriving the name and drifting."""
1865
+ if getattr(args, "state_file_template", None):
1866
+ return _persona_path(args.state_file_template, persona)
1867
+ return _state_path_for_persona(getattr(args, "state_file", None), persona)
1868
+
1869
+
1861
1870
  def _created_epoch(created):
1862
1871
  """A message's `created` as epoch seconds, or None if it cannot be read. The server writes naive UTC
1863
1872
  ('2026-09-24 21:34:24.943580'); an explicit offset or 'Z' is honoured when present."""
@@ -2022,8 +2031,7 @@ class WatchTarget:
2022
2031
  cp = urllib.parse.urlsplit(url)
2023
2032
  self.unread_persona = dict(urllib.parse.parse_qsl(cp.query)).get("persona") or persona
2024
2033
 
2025
- state_path = (_persona_path(args.state_file_template, persona) if args.state_file_template
2026
- else _state_path_for_persona(args.state_file, persona))
2034
+ state_path = _state_path_from_args(args, persona)
2027
2035
  if state_path:
2028
2036
  self.state_file = StateFile(state_path, self.identity)
2029
2037
  if not args.self_test:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kijito-inbox-monitor",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
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.5"
10
+ version = "0.5.6"
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"
@@ -6114,3 +6114,82 @@ class StillUnreadBackstopTest(unittest.TestCase):
6114
6114
  self.assertIn(">= 0", err(["--persona", "a", "--still-unread-after", "-1"]))
6115
6115
  self.assertIn(">= 1", err(["--persona", "a", "--still-unread-max", "0"]))
6116
6116
  self.assertIsNone(err(["--persona", "a", "--still-unread-after", "0"]))
6117
+
6118
+
6119
+ class StateFileNameAgreementTest(unittest.TestCase):
6120
+ """Row M289 (second half): the launchd template, the docs and the producer must agree on the state file's
6121
+ NAME. The template used to pass a base (`--state-file <dir>/hive.json`) that the producer silently turned
6122
+ into `hive.<persona>.json`, so a reader of the template looked for a file that never existed. Now the
6123
+ template names the file with a `{persona}` template and this test derives the name from the template's
6124
+ OWN argv, through the producer's OWN parser and `_state_path_from_args` - one constant, no second copy."""
6125
+
6126
+ NAMES = ["argus", "Loom", "name (purpose)", "Ωmega", "two words", "Claude-chat"]
6127
+ HOME = "/HOMEDIR"
6128
+ _HERE = os.path.dirname(os.path.abspath(__file__))
6129
+
6130
+ def _launchd_argv(self):
6131
+ with open(os.path.join(self._HERE, "com.kijito.inbox-monitor.plist.template"), encoding="utf-8") as fh:
6132
+ text = fh.read()
6133
+ block = re.search(r"<key>ProgramArguments</key>\s*<array>(.*?)</array>", text, re.S).group(1)
6134
+ words = [w.replace("__HOME__", self.HOME) for w in re.findall(r"<string>([^<]*)</string>", block)]
6135
+ return words[words.index("__PROGRAM__") + 1:]
6136
+
6137
+ def _args(self):
6138
+ args = km.build_parser().parse_args(self._launchd_argv())
6139
+ km.validate_args(args)
6140
+ return args
6141
+
6142
+ def test_the_launchd_template_names_exactly_the_state_file_the_producer_writes(self):
6143
+ args = self._args()
6144
+ self.assertIsNotNone(args.state_file_template, "the template must NAME the per-persona file, not a base")
6145
+ for name in self.NAMES:
6146
+ with self.subTest(persona=name):
6147
+ path = km._state_path_from_args(args, name)
6148
+ self.assertEqual(os.path.basename(path), "hive.%s.json" % km._state_safe_persona(name))
6149
+
6150
+ def test_existing_launchd_installs_keep_their_state_file(self):
6151
+ # The old template's base `hive.json` was derived to `hive.<safe>.json`; the new template must land on
6152
+ # the SAME file, or an upgrade would baseline over every persona's cursor (skipping its backlog).
6153
+ args = self._args()
6154
+ legacy = self.HOME + "/.cache/kijito-inbox-monitor/hive.json"
6155
+ for name in self.NAMES:
6156
+ with self.subTest(persona=name):
6157
+ self.assertEqual(km._state_path_from_args(args, name), km._state_path_for_persona(legacy, name))
6158
+
6159
+ def test_the_producer_opens_the_path_state_path_from_args_returns(self):
6160
+ # Pins that the watch target uses THIS function, so the test above speaks for the running producer.
6161
+ with open(os.path.join(self._HERE, "kijito_inbox_monitor.py"), encoding="utf-8") as fh:
6162
+ src = fh.read()
6163
+ self.assertIn("state_path = _state_path_from_args(args, persona)", src)
6164
+
6165
+ def test_the_readme_multi_persona_example_writes_the_file_it_documents(self):
6166
+ with open(os.path.join(self._HERE, "README.md"), encoding="utf-8") as fh:
6167
+ readme = fh.read()
6168
+ m = re.search(r"--all-personas \\\n(?:.*\\\n)*?\s*--state-file-template (\S+)", readme)
6169
+ self.assertIsNotNone(m, "the multi-persona example must name its state file with a {persona} template")
6170
+ args = km.build_parser().parse_args(["--all-personas", "--state-file-template", m.group(1)])
6171
+ self.assertEqual(os.path.basename(km._state_path_from_args(args, "argus")), "state.argus.json")
6172
+ self.assertIn("state.<persona>.json", readme)
6173
+
6174
+
6175
+ class NegativeFailureCountStateTest(unittest.TestCase):
6176
+ """A persisted `consecutive_failures` below zero is not a count: resumed as-is it would postpone the
6177
+ dead-man edge by that many polls, silently. `_is_int` alone accepted it (a negative is a real integer)."""
6178
+
6179
+ def _load(self, failures):
6180
+ with tempfile.TemporaryDirectory() as d:
6181
+ p = os.path.join(d, "s.json")
6182
+ with open(p, "w") as f:
6183
+ json.dump({"identity": "idx", "cursor": 100, "state": "UP", "consecutive_failures": failures}, f)
6184
+ err, sys.stderr = sys.stderr, io.StringIO()
6185
+ try:
6186
+ return km.StateFile(p, "idx").load()
6187
+ finally:
6188
+ sys.stderr = err
6189
+
6190
+ def test_a_negative_count_is_CORRUPT(self):
6191
+ for bad in (-1, -1000):
6192
+ self.assertIs(self._load(bad), km.CORRUPT_STATE, "consecutive_failures=%r must not be resumed" % bad)
6193
+
6194
+ def test_zero_still_loads(self):
6195
+ self.assertIsNot(self._load(0), km.CORRUPT_STATE)