hubkit-cli 0.1.0__tar.gz → 0.2.1__tar.gz

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 (61) hide show
  1. hubkit_cli-0.2.1/CHANGELOG.md +63 -0
  2. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/CONTRIBUTING.md +1 -1
  3. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/PKG-INFO +15 -20
  4. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/README.md +14 -19
  5. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/SKILL.md +2 -2
  6. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/architecture.md +12 -4
  7. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/commands.md +11 -6
  8. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/configuration.md +14 -0
  9. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/intents.example.toml +2 -1
  10. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/pyproject.toml +1 -1
  11. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/cli.py +56 -2
  12. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/config.py +2 -2
  13. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/gate.py +30 -2
  14. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/hooks.py +8 -4
  15. hubkit_cli-0.2.1/scripts/hubcore/inbound.py +43 -0
  16. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/install.py +1 -1
  17. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/learnings.py +40 -6
  18. hubkit_cli-0.2.1/scripts/hubcore/ledger.py +248 -0
  19. hubkit_cli-0.2.1/scripts/hubcore/lessons.py +568 -0
  20. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/orders.py +30 -3
  21. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/routed.py +78 -6
  22. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/slow.py +3 -3
  23. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/status.py +77 -5
  24. hubkit_cli-0.2.1/scripts/hubcore/trust.py +41 -0
  25. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/learn.py +2 -1
  26. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/skills/hubkit-setup/SKILL.md +1 -1
  27. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/run_reverts.py +73 -8
  28. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_blockers.py +19 -0
  29. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_compat.py +1 -1
  30. hubkit_cli-0.2.1/tests/test_guards2.py +45 -0
  31. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_hubkit.py +2 -2
  32. hubkit_cli-0.2.1/tests/test_inbound.py +41 -0
  33. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_install.py +18 -1
  34. hubkit_cli-0.2.1/tests/test_learning_rules.py +52 -0
  35. hubkit_cli-0.2.1/tests/test_ledger.py +61 -0
  36. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_lessons.py +19 -0
  37. hubkit_cli-0.2.1/tests/test_own_work.py +27 -0
  38. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_peer_reports.py +43 -10
  39. hubkit_cli-0.2.1/tests/test_review_chain.py +44 -0
  40. hubkit_cli-0.2.1/tests/test_state_rules.py +82 -0
  41. hubkit_cli-0.2.1/tests/test_tool_guards.py +160 -0
  42. hubkit_cli-0.1.0/CHANGELOG.md +0 -36
  43. hubkit_cli-0.1.0/scripts/hubcore/lessons.py +0 -168
  44. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/.gitignore +0 -0
  45. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/CLAUDE.md +0 -0
  46. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/LICENSE +0 -0
  47. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/SECURITY.md +0 -0
  48. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/config.example.toml +0 -0
  49. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/troubleshooting.md +0 -0
  50. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/references/design-sources.md +0 -0
  51. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/references/research.md +0 -0
  52. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/__init__.py +0 -0
  53. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/compat.py +0 -0
  54. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/redact.py +0 -0
  55. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/store.py +0 -0
  56. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/watcher.py +0 -0
  57. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubkit +0 -0
  58. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/skills/hub-mep/SKILL.md +0 -0
  59. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/conftest.py +0 -0
  60. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_slow.py +0 -0
  61. {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_watcher.py +0 -0
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.2.1] - 2026-11-06
8
+
9
+ ### Added
10
+ - `hubkit find WORD [--since]` and, in `digest` and `eod`, the PRs the user's account opened plus what each peer session was asked (`hubcore/ledger.py`; read only on peers, prompts redacted, `gh` failures shown).
11
+
12
+ ### Changed
13
+ - The unpushed-work Stop reason only counts files changed, and commits made, since the turn began; older dirt in the checkout no longer holds a report.
14
+
15
+ ## [0.2.0] - 2026-11-06
16
+
17
+ ### Added
18
+ - Commands: `hubkit digest [--since]`, `hubkit trust record|ls`, `hubkit inbound ingest`, `hubkit work verify`, `learn.py address --check CMD`.
19
+ - The warn-only PreToolUse hook (`hubkit hooks --merge-guard`, matcher `Bash|TaskStop|AskUserQuestion`) now also warns on posts without a typed yes, banned commands, repeated commands, a whole-suite `pytest`, `TaskStop` without a stop word, and a decision card whose first option is not marked (Recommended). The routed hook warns on a brief with no done-when, output or estimate line, on a brief that forbids the fix a blocker needs, and when more than `routed.max_agents` agents hold work.
20
+ - Stop gate reasons: a "tests pass" or "it works" claim in a turn that ran no command, a PR or issue number with no link, an approval while CI at the head is not green, built work not pushed, an ask asked 3 times with no answer, and an unanswered prompt not re-read before "done".
21
+ - Order close: evidence names in a kind's `sha_evidence` must hold a commit sha.
22
+ - Watcher: stale finished agents (`watcher.stale_agent_min`), replies owed to other people (`watcher.reply_owed_min`), and own work not verified for `watcher.verify_stale_min`.
23
+ - `doctor` warns on a hubkit hook registered twice in `settings.json` and on no scheduled `hubkit eod`.
24
+ - Learning loop: `address --check`, an overlay cap of 200 lines, expiry and prose-only lists in `review`, and an ineffective mark on a class that repeats after it was addressed.
25
+ - New tests (336 pass on Linux) and revert entries for each guard.
26
+
27
+ ### Changed
28
+ - Correction patterns in `intents.example.toml` match insults aimed at the hub and no longer fire on one aimed at a tool.
29
+ - A routed item no longer closes on the peer's own "done"; it closes on an artifact (merged or closed PR, or a moved head with CI not failing). A merged or closed PR closes a review item.
30
+ - A one-line resume command may be given in the message as typed (`SKILL.md`).
31
+
32
+ ## [0.1.0]
33
+
34
+ ### Added
35
+ - Install like miseenplace: `uv tool install hubkit-cli` (published on PyPI as 0.1.0), `hubkit skills --install DIR` (hub, hub-mep, hubkit-setup), `hubkit hooks [--skills-dir DIR] [--merge-guard]` prints the settings.json hooks block, and a `hubkit-setup` skill.
36
+ - macOS and Windows support through one platform module (`hubcore/compat.py`); only Linux has been run so far.
37
+ - Blockers (`order block|result`), watcher fixes, peer-report closing, `asks ls`, and warn-only merge guard.
38
+ - Apache-2.0 license, `README.md` rewrite, `docs/` (architecture, configuration, commands, troubleshooting), `CONTRIBUTING.md`, `SECURITY.md`.
39
+ - `pyproject.toml` with package metadata and ruff, mypy and pytest settings.
40
+ - Tests for `learn.py promote-candidates`, `review` and the `address` error path, the entry-point import-failure path, and `doctor --check-clean`.
41
+
42
+ ### Fixed
43
+ - Stop gate and review close: on a PR written by `user.github_login`, a COMMENT review at the head counts (verdict check, item close, queue advance). Other authors' PRs keep the non-author rule.
44
+ - A brief is a review ask only when its first action on the PR is a review; "fix the findings in his review of #N" is not. A head moved by an owner asked to fix the PR raises no re-check alert; a review item still does.
45
+ - PR numbers in examples, quotes and code spans of a brief are not tracked. Acknowledgements that name a PR are not tracked.
46
+ - "waiting on the verdict" and similar awaiting wording no longer trips the verdict check.
47
+ - `order open` never fires from a question, urgent word or not; the prompt hook says so and shows `hubkit order open` for doing it by hand.
48
+ - `order close` requires evidence for every PR named in the order and names the missing ones.
49
+ - A completed agent's task notification closes its routed item. Work ids are never reused after archive (`work.seq`).
50
+ - `doctor` flags a stale or missing `seen/cron` when the crontab runs hubkit (`watchdog.cron_stale_min`).
51
+
52
+ ### Changed
53
+ - `mep: UNVERIFIED` now says that the optional `mep` CLI was not found and how to turn the check off (`[mep] enabled = false`).
54
+ - `doctor --check-clean` no longer scans `.git/` and also scans `.yml` and `.yaml` files.
55
+ - Docstrings and type annotations on public functions; `SKILL.md` and `skills/` reworded for clarity.
56
+ - The learnings file preamble says promoted rules go to the overlay.
57
+
58
+ ### Removed
59
+ - The unused `tools.mep_cursor` config key.
60
+
61
+ ## [0.1.0] - 2026-11-04
62
+
63
+ Initial private version: order clock, routed-work tracking, watchdog, stop gate, status block, end-of-day review and the learn loop.
@@ -7,7 +7,7 @@ Thanks for looking. hubkit is small on purpose: standard library only, Python 3.
7
7
  ```bash
8
8
  git clone https://github.com/ankitkpandey1/hubkit && cd hubkit
9
9
  pip install pytest ruff mypy coverage
10
- python3 -m pytest tests -q # about 2 minutes: real subprocesses
10
+ python3 -m pytest tests -q # about 4 minutes: real subprocesses
11
11
  ruff check .
12
12
  mypy scripts/hubcore scripts/learn.py scripts/hubkit
13
13
  python3 tests/run_reverts.py # slow; run it when you touch a guarded behaviour
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hubkit-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Session supervisor for Claude Code: hooks-based multi-agent orchestration with order clocks, routed-work tracking, a watchdog and a stop gate, standard library only.
5
5
  Project-URL: Repository, https://github.com/ankitkpandey1/hubkit
6
6
  Project-URL: Documentation, https://github.com/ankitkpandey1/hubkit/tree/main/docs
@@ -45,14 +45,15 @@ In a live hub, the prompt hook opens that order itself from your sentence, and t
45
45
  - **Order clock.** Orders open from your own prompts (never from peer or harness text), each with a first-action limit, a stall limit and a definition of done checked by evidence on close.
46
46
  - **Routed-work tracking.** A hook on `SendMessage` and `Agent` records what the hub asked of whom, one item per (owner, PR). It follows last heard-from, moved heads and GitHub reviews at the PR head.
47
47
  - **Watchdog and wake-up.** `hubkit tick` (cron) and `hubkit watch` (a background process) raise alerts with back-off; a watch exit wakes an idle hub.
48
- - **Stop gate.** The Stop hook refuses to let the turn end while the hub owes something (no watch running, a stalled item, an unbacked verdict, a question to you that blocks nothing).
49
- - **Status, end-of-day review.** A status block at session start and on each prompt, a daily archive, and `hubkit eod`.
48
+ - **Stop gate.** The Stop hook refuses to let the turn end while the hub owes something (no watch running, a stalled item, an unbacked verdict, a question to you that blocks nothing, a "tests pass" claim with no command run, a PR number with no link, an approval on red CI, built work not pushed).
49
+ - **Tool guards.** An optional warn-only PreToolUse hook (`hubkit hooks --merge-guard`) warns about an unordered merge, a post without your typed yes, a banned command, stopping an agent and an unmarked decision card. It never denies.
50
+ - **Status, digest, end-of-day review.** A status block at session start and on each prompt, a daily archive, `hubkit digest` for when you come back (it also lists the PRs your account opened and what each peer session was asked), `hubkit find WORD` to search orders, work, asks, peer prompts and your PRs, `hubkit eod`, a per-class `hubkit trust` ledger, and `hubkit inbound ingest` to track asks from other people so none goes unanswered.
50
51
 
51
52
  `SKILL.md` is the behaviour contract the hub session reads. This file is the operator view.
52
53
 
53
54
  ## Requirements
54
55
 
55
- - Linux. hubkit has only been run on Linux; macOS and Windows code exists but has never been run (see [Platform support](#platform-support)).
56
+ - Linux. hubkit has only been run on Linux so far (see [Platform support](#platform-support)).
56
57
  - Python 3.14 or newer (the code uses `except A, B:` without parentheses, PEP 758). Standard library only.
57
58
  - [Claude Code](https://code.claude.com), which provides the hooks and the skills directory.
58
59
  - Optional: `gh` (GitHub CLI) on `PATH` for PR checks; without it items show "PR unknown". Optional: [`mep`](https://pypi.org/project/miseenplace-cli/) for a personal task list in the status block; without it the block shows `mep: UNVERIFIED (mep CLI not found ...)`, and `[mep] enabled = false` silences that.
@@ -64,7 +65,7 @@ Install hubkit with `uv tool install hubkit-cli`, run `hubkit skills --install D
64
65
  for your tool, then run the hubkit-setup skill and follow it. Ask me only for things you cannot find out yourself.
65
66
  ```
66
67
 
67
- hubkit runs on Claude Code hooks, so the skills directory is Claude Code's: `~/.claude/skills` (user level) or `.claude/skills` (project level). The PyPI name `hubkit-cli` is free as of 2026-11-06 and is not published yet; until it is, install straight from GitHub with `uv tool install git+https://github.com/ankitkpandey1/hubkit` (or from a clone with `uv tool install ./hubkit`).
68
+ hubkit runs on Claude Code hooks, so the skills directory is Claude Code's: `~/.claude/skills` (user level) or `.claude/skills` (project level). `hubkit-cli` is on [PyPI](https://pypi.org/project/hubkit-cli/). To install the latest commit instead: `uv tool install git+https://github.com/ankitkpandey1/hubkit`.
68
69
 
69
70
  `hubkit skills --install DIR` copies three skills into `DIR`: `hub` (the rules and the scripts the hooks run), `hub-mep` (optional) and `hubkit-setup` (finds your facts, writes the config, registers the hooks, binds, proves it with `hubkit doctor`). Run it again after an upgrade to refresh the copies. `hubkit hooks --skills-dir DIR` prints the hooks block for `~/.claude/settings.json`.
70
71
 
@@ -112,6 +113,7 @@ hubkit order open --kind hotfix --quote "hotfix the billing bug now" # or let
112
113
  hubkit order ls
113
114
  hubkit work add --kind active --title "triage the flaky test"
114
115
  hubkit status # the status block
116
+ hubkit find billing --since 2d # search orders, asks, peer prompts, your PRs
115
117
  python3 ~/.claude/skills/hub/scripts/learn.py add --class ask-before-merge --gate G5 \
116
118
  --quote "I told you to wait for the review" --situation "..." --wrong "..." --right "..." --evidence "..."
117
119
  ```
@@ -140,12 +142,13 @@ Every key is documented in [docs/configuration.md](docs/configuration.md).
140
142
 
141
143
  | Behaviour | Strength |
142
144
  |---|---|
143
- | The Stop hook holds the turn while the hub owes something: no watch running, a stalled item, an unbacked verdict, an unsolved blocker, an item over its estimate, a "done" while your newest prompt is unread | Enforced (blocks the turn end) |
145
+ | The Stop hook holds the turn while the hub owes something: no watch running, a stalled item, an unbacked verdict, an unsolved blocker, an item over its estimate, a "done" while your newest prompt is unread, a claim of passing tests with no command run, an approval while CI is not green | Enforced (blocks the turn end) |
144
146
  | An order closes only with the evidence its kind requires | Enforced (the close is refused) |
145
- | A learned class counts as fixed only if a rule for it exists | Enforced (the address is refused) |
146
- | A second job sent to a busy owner while an order is open; a brief that bans the action a blocker needs | Warning only |
147
- | `gh pr merge` with no live order naming that PR (the merge guard, `hubkit hook pretool`) | Warning only, never denies |
148
- | `hubkit doctor` warning when your `intents.toml` lacks keys the example has | Warning only |
147
+ | A learned class counts as fixed only if a rule for it exists (and, with `--check`, its command passes) | Enforced (the address is refused) |
148
+ | A merge order closes only with the merge commit sha | Enforced (the close is refused) |
149
+ | A second job sent to a busy owner while an order is open; a brief that bans the action a blocker needs; a brief with no done-when, output or estimate line; more than 4 agents holding work | Warning only |
150
+ | `gh pr merge` with no live order naming that PR, a post without your typed yes, a banned command, `TaskStop`, a repeated command, a whole-suite `pytest`, an unmarked decision card (`hubkit hook pretool`) | Warning only, never denies |
151
+ | `hubkit doctor` warning when your `intents.toml` lacks keys the example has, a hook registered twice, or no scheduled `hubkit eod` | Warning only |
149
152
 
150
153
  Details and exact rules: [SKILL.md](SKILL.md) and [docs/architecture.md](docs/architecture.md#stop-gate-gatepy).
151
154
 
@@ -168,19 +171,11 @@ python3 tests/run_reverts.py # each guard reverted in a copy must fail it
168
171
 
169
172
  ## Platform support
170
173
 
171
- All OS-specific code is in one module, `scripts/hubcore/compat.py` (standard library only): process start time and parent (`/proc` on Linux, `ps` on macOS, kernel32 through `ctypes` on Windows), file locks (`flock`, or `msvcrt` on Windows), the scheduler check (`crontab -l`, or `schtasks /query` on Windows) and UTF-8 stdio.
172
-
173
- | OS | Status |
174
- |---|---|
175
- | Linux | Full test suite run and passing here (ruff, mypy, pytest). |
176
- | macOS | The `ps` code path is exercised on Linux against the real `ps` (`tests/test_compat.py`), and mypy checks it with `--platform darwin`. Never run on a Mac. Not claimed to work until someone runs it there. |
177
- | Windows | The `ctypes` and `msvcrt` code is type-checked with `--platform win32` only. Never run on Windows. Tests that need a POSIX `sh` shim for `gh`, `crontab` or a notifier are skipped there. Not claimed to work until someone runs it there. There is no default desktop notification on Windows: set `tools.notify_cmd`. |
178
-
179
- There is no CI. Update this table only from a real run on that OS. On Windows the schedule is a Task Scheduler task whose name or command holds `hubkit` (for example `schtasks /create /sc minute /mo 5 /tn hubkit-tick /tr "python %USERPROFILE%\.claude\skills\hub\scripts\hubkit tick --if-bound"`), and hooks in `settings.json` call `python ...\hubkit hook <event>`.
174
+ hubkit has only been run on Linux so far, and the full test suite, ruff and mypy pass there. The code has paths for macOS and Windows, all in `scripts/hubcore/compat.py` (process lookup, file locks, scheduler check, UTF-8 stdio), but they have never been run on those systems. Treat them as unsupported until someone does.
180
175
 
181
176
  ## Limits and known gaps
182
177
 
183
- - Python 3.14 only. macOS and Windows are written but unproven; see Platform support.
178
+ - Python 3.14 only, and Linux only so far; see Platform support.
184
179
  - It targets one hub per machine. A second live hub is refused at bind.
185
180
  - Order creation takes a blocking `flock` (`scripts/hubcore/orders.py`); a process that holds the lock forever stalls order opens.
186
181
  - A hook payload with no `origin` field is treated as human input.
@@ -31,14 +31,15 @@ In a live hub, the prompt hook opens that order itself from your sentence, and t
31
31
  - **Order clock.** Orders open from your own prompts (never from peer or harness text), each with a first-action limit, a stall limit and a definition of done checked by evidence on close.
32
32
  - **Routed-work tracking.** A hook on `SendMessage` and `Agent` records what the hub asked of whom, one item per (owner, PR). It follows last heard-from, moved heads and GitHub reviews at the PR head.
33
33
  - **Watchdog and wake-up.** `hubkit tick` (cron) and `hubkit watch` (a background process) raise alerts with back-off; a watch exit wakes an idle hub.
34
- - **Stop gate.** The Stop hook refuses to let the turn end while the hub owes something (no watch running, a stalled item, an unbacked verdict, a question to you that blocks nothing).
35
- - **Status, end-of-day review.** A status block at session start and on each prompt, a daily archive, and `hubkit eod`.
34
+ - **Stop gate.** The Stop hook refuses to let the turn end while the hub owes something (no watch running, a stalled item, an unbacked verdict, a question to you that blocks nothing, a "tests pass" claim with no command run, a PR number with no link, an approval on red CI, built work not pushed).
35
+ - **Tool guards.** An optional warn-only PreToolUse hook (`hubkit hooks --merge-guard`) warns about an unordered merge, a post without your typed yes, a banned command, stopping an agent and an unmarked decision card. It never denies.
36
+ - **Status, digest, end-of-day review.** A status block at session start and on each prompt, a daily archive, `hubkit digest` for when you come back (it also lists the PRs your account opened and what each peer session was asked), `hubkit find WORD` to search orders, work, asks, peer prompts and your PRs, `hubkit eod`, a per-class `hubkit trust` ledger, and `hubkit inbound ingest` to track asks from other people so none goes unanswered.
36
37
 
37
38
  `SKILL.md` is the behaviour contract the hub session reads. This file is the operator view.
38
39
 
39
40
  ## Requirements
40
41
 
41
- - Linux. hubkit has only been run on Linux; macOS and Windows code exists but has never been run (see [Platform support](#platform-support)).
42
+ - Linux. hubkit has only been run on Linux so far (see [Platform support](#platform-support)).
42
43
  - Python 3.14 or newer (the code uses `except A, B:` without parentheses, PEP 758). Standard library only.
43
44
  - [Claude Code](https://code.claude.com), which provides the hooks and the skills directory.
44
45
  - Optional: `gh` (GitHub CLI) on `PATH` for PR checks; without it items show "PR unknown". Optional: [`mep`](https://pypi.org/project/miseenplace-cli/) for a personal task list in the status block; without it the block shows `mep: UNVERIFIED (mep CLI not found ...)`, and `[mep] enabled = false` silences that.
@@ -50,7 +51,7 @@ Install hubkit with `uv tool install hubkit-cli`, run `hubkit skills --install D
50
51
  for your tool, then run the hubkit-setup skill and follow it. Ask me only for things you cannot find out yourself.
51
52
  ```
52
53
 
53
- hubkit runs on Claude Code hooks, so the skills directory is Claude Code's: `~/.claude/skills` (user level) or `.claude/skills` (project level). The PyPI name `hubkit-cli` is free as of 2026-11-06 and is not published yet; until it is, install straight from GitHub with `uv tool install git+https://github.com/ankitkpandey1/hubkit` (or from a clone with `uv tool install ./hubkit`).
54
+ hubkit runs on Claude Code hooks, so the skills directory is Claude Code's: `~/.claude/skills` (user level) or `.claude/skills` (project level). `hubkit-cli` is on [PyPI](https://pypi.org/project/hubkit-cli/). To install the latest commit instead: `uv tool install git+https://github.com/ankitkpandey1/hubkit`.
54
55
 
55
56
  `hubkit skills --install DIR` copies three skills into `DIR`: `hub` (the rules and the scripts the hooks run), `hub-mep` (optional) and `hubkit-setup` (finds your facts, writes the config, registers the hooks, binds, proves it with `hubkit doctor`). Run it again after an upgrade to refresh the copies. `hubkit hooks --skills-dir DIR` prints the hooks block for `~/.claude/settings.json`.
56
57
 
@@ -98,6 +99,7 @@ hubkit order open --kind hotfix --quote "hotfix the billing bug now" # or let
98
99
  hubkit order ls
99
100
  hubkit work add --kind active --title "triage the flaky test"
100
101
  hubkit status # the status block
102
+ hubkit find billing --since 2d # search orders, asks, peer prompts, your PRs
101
103
  python3 ~/.claude/skills/hub/scripts/learn.py add --class ask-before-merge --gate G5 \
102
104
  --quote "I told you to wait for the review" --situation "..." --wrong "..." --right "..." --evidence "..."
103
105
  ```
@@ -126,12 +128,13 @@ Every key is documented in [docs/configuration.md](docs/configuration.md).
126
128
 
127
129
  | Behaviour | Strength |
128
130
  |---|---|
129
- | The Stop hook holds the turn while the hub owes something: no watch running, a stalled item, an unbacked verdict, an unsolved blocker, an item over its estimate, a "done" while your newest prompt is unread | Enforced (blocks the turn end) |
131
+ | The Stop hook holds the turn while the hub owes something: no watch running, a stalled item, an unbacked verdict, an unsolved blocker, an item over its estimate, a "done" while your newest prompt is unread, a claim of passing tests with no command run, an approval while CI is not green | Enforced (blocks the turn end) |
130
132
  | An order closes only with the evidence its kind requires | Enforced (the close is refused) |
131
- | A learned class counts as fixed only if a rule for it exists | Enforced (the address is refused) |
132
- | A second job sent to a busy owner while an order is open; a brief that bans the action a blocker needs | Warning only |
133
- | `gh pr merge` with no live order naming that PR (the merge guard, `hubkit hook pretool`) | Warning only, never denies |
134
- | `hubkit doctor` warning when your `intents.toml` lacks keys the example has | Warning only |
133
+ | A learned class counts as fixed only if a rule for it exists (and, with `--check`, its command passes) | Enforced (the address is refused) |
134
+ | A merge order closes only with the merge commit sha | Enforced (the close is refused) |
135
+ | A second job sent to a busy owner while an order is open; a brief that bans the action a blocker needs; a brief with no done-when, output or estimate line; more than 4 agents holding work | Warning only |
136
+ | `gh pr merge` with no live order naming that PR, a post without your typed yes, a banned command, `TaskStop`, a repeated command, a whole-suite `pytest`, an unmarked decision card (`hubkit hook pretool`) | Warning only, never denies |
137
+ | `hubkit doctor` warning when your `intents.toml` lacks keys the example has, a hook registered twice, or no scheduled `hubkit eod` | Warning only |
135
138
 
136
139
  Details and exact rules: [SKILL.md](SKILL.md) and [docs/architecture.md](docs/architecture.md#stop-gate-gatepy).
137
140
 
@@ -154,19 +157,11 @@ python3 tests/run_reverts.py # each guard reverted in a copy must fail it
154
157
 
155
158
  ## Platform support
156
159
 
157
- All OS-specific code is in one module, `scripts/hubcore/compat.py` (standard library only): process start time and parent (`/proc` on Linux, `ps` on macOS, kernel32 through `ctypes` on Windows), file locks (`flock`, or `msvcrt` on Windows), the scheduler check (`crontab -l`, or `schtasks /query` on Windows) and UTF-8 stdio.
158
-
159
- | OS | Status |
160
- |---|---|
161
- | Linux | Full test suite run and passing here (ruff, mypy, pytest). |
162
- | macOS | The `ps` code path is exercised on Linux against the real `ps` (`tests/test_compat.py`), and mypy checks it with `--platform darwin`. Never run on a Mac. Not claimed to work until someone runs it there. |
163
- | Windows | The `ctypes` and `msvcrt` code is type-checked with `--platform win32` only. Never run on Windows. Tests that need a POSIX `sh` shim for `gh`, `crontab` or a notifier are skipped there. Not claimed to work until someone runs it there. There is no default desktop notification on Windows: set `tools.notify_cmd`. |
164
-
165
- There is no CI. Update this table only from a real run on that OS. On Windows the schedule is a Task Scheduler task whose name or command holds `hubkit` (for example `schtasks /create /sc minute /mo 5 /tn hubkit-tick /tr "python %USERPROFILE%\.claude\skills\hub\scripts\hubkit tick --if-bound"`), and hooks in `settings.json` call `python ...\hubkit hook <event>`.
160
+ hubkit has only been run on Linux so far, and the full test suite, ruff and mypy pass there. The code has paths for macOS and Windows, all in `scripts/hubcore/compat.py` (process lookup, file locks, scheduler check, UTF-8 stdio), but they have never been run on those systems. Treat them as unsupported until someone does.
166
161
 
167
162
  ## Limits and known gaps
168
163
 
169
- - Python 3.14 only. macOS and Windows are written but unproven; see Platform support.
164
+ - Python 3.14 only, and Linux only so far; see Platform support.
170
165
  - It targets one hub per machine. A second live hub is refused at bind.
171
166
  - Order creation takes a blocking `flock` (`scripts/hubcore/orders.py`); a process that holds the lock forever stalls order opens.
172
167
  - A hook payload with no `origin` field is treated as human input.
@@ -43,7 +43,7 @@ A human prompt containing an order word (words come from `intents.toml`; hotfix,
43
43
  - **(a2) Saying it is blocked is not solving it.** A final message that calls an open order blocked, down, failed or waiting on a host, tool or person is held until a block record with a fix exists (word list: `intents.toml` `[stop_gate]`). *Enforced in code.*
44
44
  - **(b) An urgent step never queues behind other work on a busy agent.** Send it to a free owner. The routed hook warns when a second job goes to an owner already holding a live job while an order is open. *Warning enforced in code; the choice of owner is prose.*
45
45
  - **(f) Re-read his words before saying done.** `hubkit asks ls --since 12h` prints his recent prompts one per line; a final message that says done while his newest prompt is newer than the last `work done` and the last `asks ls` is held. *Enforced in code.*
46
- - **(g) A merge is done on his order only.** `hubkit hook pretool` (register it as a PreToolUse hook on Bash) warns, never denies, on `gh pr merge` with no live order and none of his typed words naming that PR. *Warning only.*
46
+ - **(g) A merge is done on his order only.** `hubkit hook pretool` (register it with `hubkit hooks --merge-guard`) warns, never denies, on `gh pr merge` with no live order and none of his typed words naming that PR; the same hook also warns on posts without his typed yes, banned commands, stopping an agent and an unmarked decision card. *Warning only.*
47
47
  - **(c) An agent's "that is a person's call" is checked, not relayed.** Read what the person already decided (the record, the order's words) before asking or telling anyone it needs them. *Checked in code:* a final message that relays "a person's call" with no Read/Grep/Glob/Bash call in that turn is held at the Stop gate (words: `intents.toml` `[stop_gate] unchecked_words`).
48
48
  - **(d) Before saying something is gone or absent, read the code that writes it.** A missing file, row or log line is proof of nothing until you know what produces it and when. *Checked in code:* a final message that says something is gone, lost or absent, with no lookup in that turn, is held the same way.
49
49
  - **(e) A brief must not forbid the one action that clears the blocker.** Before sending a brief, check its banned-command list against the step the order needs. *Checked in code:* the routed hook warns when a brief's "do not / never" line shares a key word with an open order's blocker or fix.
@@ -80,7 +80,7 @@ Silence on routed work past `stall_min` (reviews: `review_stall_min`, hard `OVER
80
80
  - At most 15 lines. Lead with what changed or what the user must decide. Tag claims verified / read / inferred. Full URLs. Times in the user's zone.
81
81
  - One line names what was left out and where it is. Input needed: the question tool after the report, recommended option first.
82
82
  - Hand over all PRs of a wave in one list, only when reviewed and ready.
83
- - Code or commands for the user go to a file, not a raw block (the overlay may name the clipboard step).
83
+ - Code or commands for the user go to a file, not a raw block (the overlay may name the clipboard step). A one-line resume command is the exception: put it in the message as typed.
84
84
 
85
85
  ## 8. End of day
86
86
 
@@ -46,7 +46,7 @@ State lives in `$HUBKIT_STATE`, else `$XDG_STATE_HOME/hubkit` (default `~/.local
46
46
  | PostToolUse `AskUserQuestion` | `answer` | A card answer goes through the same intake and human-only filter as a typed prompt, and closes `ask` work items. |
47
47
  | PostToolUse `SendMessage\|Agent` | `routed` | Records or refreshes a routed-work item (see Routed work). One `gh` baseline read per named PR, 3 s in total. |
48
48
  | Stop | `stop` | The gate. Prints JSON `{"decision": "block", "reason": ...}` while the hub owes something. |
49
- | PreToolUse `Bash` | `pretool` | Warn-only merge guard: `gh pr merge` with no live order and none of his typed words naming the PR adds a warning as additional context. Never denies. Register it yourself in `settings.json`. |
49
+ | PreToolUse `Bash\|TaskStop\|AskUserQuestion` | `pretool` | Warn-only guard; the warning goes in as additional context and nothing is ever denied. It warns on: `gh pr merge` with no live order and none of his typed words naming the PR; a post to a person or thread (`mep send|reply|post`, a chat send, `gh pr|issue comment|review`, a `gh api` comments or reviews call, or a Slack post or send) with no typed yes of his in the last 10 minutes; a banned command (`rm -rf`, `pkill`, `git reset --hard`, `git stash`, `git clean`, a forced push, `git checkout <ref> -- <path>`); the same command run 3 times in 10 minutes; `pytest` with no path, `-k` or `-m`; `TaskStop` without a stop word of his in the last 30 minutes; a decision card whose first option is not marked (Recommended). Register it with `hubkit hooks --merge-guard`, or add it to `settings.json` yourself. |
50
50
  | any | `record` | Schema canary. Every hub run writes the real payload's keys and value types (never values) to `payload-samples/`. |
51
51
 
52
52
  A hook failure is shown and logged (`hook-errors.jsonl`) only for the hub. The stop gate's own blocks are not counted as failures.
@@ -59,7 +59,7 @@ An order is a row family in `orders.jsonl` (`open`, `move`, `hold`, `resume`, `c
59
59
  - **Clock.** A first action is due within the kind's `first_action_min`; after that, movement (`order move`, or a touched `movement_files` glob) is due every `stall_min`. A deadline in the prompt ("in 30 min", "by 17:00") is parsed too.
60
60
  - **Holds.** Only explicit `hold_words` hold an order; `urgent_words` keep or restart the clock; `cancel_words` cancel. Long prompts (`verb_max_chars`) never hold or cancel.
61
61
  - **Chases.** A short prompt matching `intake.chase` while an order is open is recorded as a chase. Three chases print a stop-loss line.
62
- - **Close.** `order close` requires every evidence name the kind's `done_when` lists; `file:<path>` evidence must exist and be newer than the order. A closed order is archived under `archive/archive_<day>.jsonl`.
62
+ - **Close.** `order close` requires every evidence name the kind's `done_when` lists; `file:<path>` evidence must exist and be newer than the order. Evidence names in the kind's `sha_evidence` (the merge kind's `merged`) must hold a commit sha. A closed order is archived under `archive/archive_<day>.jsonl`.
63
63
 
64
64
  ## Routed work (`routed.py`)
65
65
 
@@ -98,6 +98,9 @@ Every tick (cron with `--if-bound`, or inside `hubkit watch`) also checks things
98
98
  - **Notification.** New alerts go to `tools.notify_cmd` when set. With none set, only `hub:*` and `peer:*` alerts use the platform default (`notify-send` on Linux, `osascript` on macOS, when installed). Nothing here ever posts to chat.
99
99
  - **False alarms.** A silent item is not a stall while it is `blocked_on` another live job (`hubkit work block`), or while its holder's session is `busy` and its transcript is still growing (for up to 3x the limit). A head moved by the owner's own newest commit is progress, not a moved head. Every stall alert names the holder's state (`holder: ...`).
100
100
  - **Short asks.** A short message to a peer is tracked when a request verb opens a sentence or it ends in a question; acknowledgements, status questions and thanks are not.
101
+ - **Own work.** `active` and `priority` items need an estimate within 5 minutes, say so out loud when over it, and raise a stale-verification alert when not stamped with `hubkit work verify` for `watcher.verify_stale_min`.
102
+ - **Finished agents.** A background agent that reported finished more than `watcher.stale_agent_min` ago while its item is still listed raises `agent:<owner>:stale`.
103
+ - **Inbound asks.** `hubkit inbound ingest` opens one `reply` item per message id from the inbox watcher's lines; one owed longer than `watcher.reply_owed_min` raises one alert.
101
104
  - **PRs are keyed by repo and number.** `owner/repo#N`, a full URL, or `<alias> N` for a `routed.repo_aliases` entry name their repo; only a bare `#N` falls back to `routed.repo`.
102
105
 
103
106
  ## Stop gate (`gate.py`)
@@ -110,7 +113,12 @@ The Stop hook blocks the end of the turn, with the exact next action, while any
110
113
  4. A job with an estimate on record is past it and not yet addressed (see the slowness check).
111
114
  5. A blocker is not being solved (no fix running, fix past its limit), a STOP-LOSS has no result since, or the final message calls an open order blocked/down/failed/waiting while no order has a fix on record.
112
115
  6. The final message says done while his newest prompt is newer than the last `work done` and the last `hubkit asks ls`; or it calls something gone/lost/absent, or "a person's call", with no Read/Grep/Glob/Bash call in the turn (transcript read).
113
- 7. The final message ends on a question (or holds a `stop_guard.phrases` phrase) and no `ask` work item says anything really waits on the user.
116
+ 7. The final message ends on a question (or holds a `stop_guard.phrases` phrase) and no `ask` work item says anything really waits on the user. A question that is really blocked on him must use the card tool, not prose. An `ask` item asked 3 times with no answer also holds the turn: take the safest default and record it.
117
+ 8. The final message says "tests pass", "it works" or "is deployed" (`[stop_gate] verified_words`) but the turn ran no Bash command; names a PR or issue number with no link (`[stop_gate] full_links`); approves a PR while CI at its head is not green; or reports built work while the repo at the hook's `cwd` has uncommitted changes or unpushed commits made since the turn began (older dirt in the checkout does not count).
118
+
119
+ ## Digest and trust
120
+
121
+ `hubkit digest [--since 12h]` prints what finished, which orders closed, the latest alerts, what waits on the user, the PRs his account opened and what each peer session was asked (`ledger.py`: peer transcripts are read only and prompts are redacted; a `gh` failure is shown, not hidden). `hubkit find WORD` searches the same record plus orders, work, the archive and his asks. `hubkit trust record --class C --outcome accept|edit|reject` appends to `trust.jsonl`; `trust ls` shows each class as `auto` (enough accepts and nothing else) or `ask` (propose first), and the session status block names the classes that earned `auto` and those he rejected.
114
122
 
115
123
  ## The learn loop
116
124
 
@@ -119,7 +127,7 @@ This is the adaptive part. The mechanism, in the order it runs (`hooks.py`, `lea
119
127
  1. **Detect.** The prompt hook injects a `CORRECTION` line, with the quote, when a typed prompt matches a `[corrections] patterns` regex or repeats a recent prompt (word-set similarity at least `repeat_similarity` within `repeat_window_min`).
120
128
  2. **Record** (`learn.py add`). The hub records a class of mistake: kebab-case class, the gate it broke (G1 to G8 in SKILL.md), the exact quote, situation, wrong, right, evidence. A repeat of an existing class appends a dated `seen` line. Each entry stores a `basis` hash of the rule texts (overlay plus SKILL.md) at the time. Every field is redacted; edits are locked and atomic.
121
129
  3. **Promote** (`learn.py promote-candidates`, then `learn.py promote <class> --gate G --rule "..."`). A class seen two or more times is a candidate. Promoting writes one short rule line tagged `[<class>]` into your overlay under its gate. Personal rules stay out of the shared `SKILL.md`.
122
- 4. **Address** (`learn.py address --class <c> --how "..."`). The class is marked fixed only if the rules changed since the entry was last seen **and** a rule line tagged `[<class>]` exists. Otherwise it is refused: a fix without a rule is not a fix.
130
+ 4. **Address** (`learn.py address --class <c> --how "..."`). The class is marked fixed only if the rules changed since the entry was last seen **and** a rule line tagged `[<class>]` exists. Otherwise it is refused: a fix without a rule is not a fix. With `--check CMD` the command must also exit 0, and every earlier check must still pass. `promote` is refused when the overlay would pass 200 lines. A class seen again after it was addressed is marked ineffective.
123
131
  5. **Probation.** The status block shows at most five learnings seen in the last 30 days; an addressed class stays visible for 14 days as "probation". If the class is seen again after it was addressed, `hubkit eod` and `learn.py review` list it as regressed (a fix that did not work), and it is to be fixed the same day.
124
132
 
125
133
  hubkit does the detecting, storing, refusing and reporting; the hub model does the recording and the rule writing, so the loop only adapts as far as the hub follows it.
@@ -7,7 +7,7 @@
7
7
  | Verb | What it does |
8
8
  |---|---|
9
9
  | `skills --install DIR` | Copies the hub, hub-mep and hubkit-setup skills into DIR (re-run refreshes; the running copy is left alone). |
10
- | `hooks [--skills-dir DIR] [--merge-guard]` | Prints the `hooks` JSON for settings.json. `--merge-guard` adds the warn-only PreToolUse merge guard. Never edits settings.json. |
10
+ | `hooks [--skills-dir DIR] [--merge-guard]` | Prints the `hooks` JSON for settings.json. `--merge-guard` adds the warn-only PreToolUse guard (matcher `Bash|TaskStop|AskUserQuestion`, see [architecture](architecture.md#hooks-scriptshubcorehookspy)). Never edits settings.json. |
11
11
  | `hubkit bind [--pid N]` | Bind this session (found by walking up the process tree, or the process `N`) as the hub. Interactive sessions only; refuses while another hub is live. |
12
12
  | `hubkit unbind` | Remove the bind. |
13
13
  | `hubkit status [--json]` | Rebuild `status.json` and print the status block (or the JSON). |
@@ -16,6 +16,11 @@
16
16
  | `hubkit order open --kind K --quote "..." [--unconfirmed]` | Open an order by hand. |
17
17
  | `hubkit order confirm ID` | Turn an unconfirmed order into an open one. |
18
18
  | `hubkit order move ID --note "..." [--estimate N]` | Record real movement. The first move also records the order's estimate (N minutes, else the usual time for its kind; printed when defaulted). |
19
+ | `hubkit digest [--since 12h]` | What finished, which orders closed, the latest alerts, what waits on the user, the PRs his account opened and what each peer session was asked, for a user coming back. |
20
+ | `hubkit inbound ingest [--file F]` | Turns the inbox watcher's lines into `reply` work items (one per asker, no duplicates). |
21
+ | `hubkit trust record --class C --outcome O` / `trust ls` | Per-class record of approved and corrected outcomes; a class with repeated corrections is shown as ask-first. |
22
+ | `hubkit work verify ID` | Stamp the item as checked against reality just now. An `active` or `priority` item not verified for `watcher.verify_stale_min` raises an alert. It runs no check itself. |
23
+ | `hubkit find WORD [--since 2d]` | Searches orders, work, the archive, his asks, peer session prompts and the PRs his account opened (title, body, changed file paths). Needs `gh` for the PR part; a `gh` failure is printed as UNKNOWN, never skipped. Read only on peer sessions; prompts are redacted. |
19
24
  | `hubkit asks ls [--since 12h]` | His recent prompts (from `recent-prompts.jsonl`), one line each, newest last. Marks the asks as re-read for the Stop gate. |
20
25
  | `hubkit order block ID --on "..." [--fix "..."] [--holder who]` | Record what blocks an order and the fix running now (a second call with the same `--on` adds the fix). No fix, or a fix older than `blocker_fix_min` with no result, alerts and holds the Stop gate. |
21
26
  | `hubkit order result ID --note "..."` | Record the result of an action: movement, clears the blocker and any STOP-LOSS. Notes from `order move` do neither. |
@@ -33,9 +38,9 @@
33
38
  | `hubkit work ls` | List live items. |
34
39
  | `hubkit tick [--if-bound] [--no-notify]` | One watchdog pass; prints `ALERT ...` lines. `--if-bound` is a no-op unless a hub is bound or an order is open (for cron). |
35
40
  | `hubkit watch [--interval S] [--max-cycles N]` | Loop `tick` (defaults 300 s, 12 cycles). Exits 0 printing `ALERT ...`, 3 after the cycles, 4 if another watch runs. |
36
- | `hubkit eod [--day D] [--note ".."]` | Write and print the end-of-day review. |
37
- | `hubkit doctor [--check-clean]` | Print config, state, bind, hook heartbeats, ledger health and a count of secret-shaped strings in state. `--check-clean` also lists checkout files containing a `private.terms` word. Exit 1 on a problem. |
38
- | `hubkit hook EVENT` | Run one hook (`session`, `prompt`, `answer`, `routed`, `stop`, `record`); reads the payload from stdin. For Claude Code, not for people. |
41
+ | `hubkit eod [--day D] [--note ".."]` | Write and print the end-of-day review, including the day's PRs and what each peer session was asked. |
42
+ | `hubkit doctor [--check-clean]` | Print config, state, bind, hook heartbeats, ledger health and a count of secret-shaped strings in state. Warns when your `intents.toml` lacks keys the example has, when a hubkit hook is registered twice in `settings.json` (read only), and when no scheduler entry runs `hubkit eod`. `--check-clean` also lists checkout files containing a `private.terms` word. Exit 1 on a problem. |
43
+ | `hubkit hook EVENT` | Run one hook (`session`, `prompt`, `answer`, `routed`, `stop`, `record`, `pretool`); reads the payload from stdin. For Claude Code, not for people. |
39
44
 
40
45
  ## learn.py
41
46
 
@@ -47,8 +52,8 @@
47
52
  | `list [--last N]` | The N most recently seen entries (default 30). |
48
53
  | `promote-candidates [--min N]` | Classes seen at least N times (default 2). |
49
54
  | `promote CLASS --gate G --rule "..."` | Write a rule line tagged `[CLASS]` into the overlay under its gate. |
50
- | `address --class C --how "..."` | Mark a class fixed. Refused unless the overlay or SKILL.md changed since it was last seen and holds a `[C]` rule line. |
51
- | `review [--day D]` | New, repeated, regressed, holding and unaddressed classes for a day. |
55
+ | `address --class C --how "..." [--check CMD]` | Mark a class fixed. Refused unless the overlay or SKILL.md changed since it was last seen and holds a `[C]` rule line. `--check` also runs a command that must exit 0 first, and every earlier `--check` must still pass. Promote is refused past the overlay's 200-line cap. |
56
+ | `review [--day D]` | New, repeated, regressed, holding and unaddressed classes for a day, plus rules to expire (addressed and not seen since) and rules that exist only as prose. A class seen again after it was addressed is marked ineffective. |
52
57
 
53
58
  ## Example
54
59
 
@@ -81,6 +81,9 @@ Estimates and the slowness check (hub-internal, see architecture).
81
81
  | `waiting_min` | `3` | A peer waiting on a prompt this long raises `peer:<name>:waiting`. |
82
82
  | `active_s` | `120` | A busy holder whose transcript grew this recently is working, not stalled. |
83
83
  | `watch_grace_min` | `5` | Live work with no running `hubkit watch` this long raises `watch:dead`. |
84
+ | `stale_agent_min` | `30` | A background agent that reported finished this long ago while its item is still listed raises `agent:<owner>:stale`. |
85
+ | `reply_owed_min` | `60` | A `reply` item (from `hubkit inbound ingest`) open this long raises one alert. |
86
+ | `verify_stale_min` | `180` | An `active` or `priority` item not stamped with `hubkit work verify` for this long raises an alert. |
84
87
  | `notify_user` | `true` | Use the platform default desktop notification for hub and peer alerts when `tools.notify_cmd` is empty. |
85
88
 
86
89
  ### `[stop_guard]`
@@ -101,6 +104,7 @@ Estimates and the slowness check (hub-internal, see architecture).
101
104
  | `review_stall_min` | `5` | Reviews: chase after this. |
102
105
  | `review_overdue_min` | `10` | Reviews: hard OVERDUE this long after routing, whatever the owner said. |
103
106
  | `skip_owners` | `["inbox"]` | Owners that are never tracked. |
107
+ | `max_agents` | `4` | More distinct agents than this holding live routed work makes the routed hook warn. |
104
108
  | `min_chars` | `200` | A message without a PR is work only from this length. |
105
109
  | `gh_timeout_s` | `10` | Timeout for one `gh` read. |
106
110
  | `queued_wait_min` | `60` | Work queued "after X" gets no review clock until this long after the ask (or until a review shows). |
@@ -126,11 +130,20 @@ The first kind whose words match wins, so put the narrower kind first (`hotfix`
126
130
  | `not_order_phrases` | Phrases that contain an order word but are not orders ("release notes"). |
127
131
  | `question_start` | Regex; an order word in a question is never an order, urgent words or not (the hook then says so and shows `hubkit order open` for opening one by hand). |
128
132
  | `unconfirmed_ttl_min` | `tick` expires unconfirmed orders after this long. |
133
+ | `blocker_fix_min` | A blocker with no fix, or a fix with no result after this many minutes (default `3`), alerts and holds the Stop gate. |
129
134
  | `held_review_min` | A hold this long raises an alert. |
130
135
  | `cancel_words`, `resume_words` | Words for the cancel and resume verbs. |
131
136
  | `verb_max_chars` | Prompts longer than this never hold, cancel, resume or count as chases. |
132
137
  | `chase` | Regex; a short prompt matching it while an order is open is a chase. |
133
138
 
139
+ ### `[stop_gate]`
140
+ | Key | Meaning |
141
+ |---|---|
142
+ | `blocked_words` | A final message with one of these, while an order is open and none has a fix on record, cannot end the turn. |
143
+ | `unchecked_words` | "A person's call" style claims held when the turn made no lookup. |
144
+ | `verified_words` | "Tests pass", "it works", "is deployed" style claims held when the turn ran no Bash command. |
145
+ | `full_links` | Default `true`. Set `false` to stop holding messages that name a PR or issue number with no link. |
146
+
134
147
  ### `[corrections]` (the learn loop)
135
148
  | Key | Meaning |
136
149
  |---|---|
@@ -146,6 +159,7 @@ The first kind whose words match wins, so put the narrower kind first (`hotfix`
146
159
  | `stall_min` | No movement for this long raises an alert. |
147
160
  | `dispatch` | Text printed when the order opens. |
148
161
  | `done_when` | Evidence names `order close` requires (`--evidence name=value`). |
162
+ | `sha_evidence` | Evidence names that must hold a commit sha (the example merge kind uses `["merged"]`). |
149
163
  | `movement_files` | Globs; a file touched after the order opened counts as movement. |
150
164
 
151
165
  Add your own kinds the same way; the words are matched as whole words, case-insensitively.
@@ -37,7 +37,7 @@ blocked_words = ["blocked", "is down", "are down", "failed", "failing", "no host
37
37
  [corrections]
38
38
  # A human prompt matching one of these regexes (or repeating the previous prompt) makes the prompt
39
39
  # hook inject a reminder to record it: learn.py add with the quote.
40
- patterns = ["(?i)\\b(useless|stupid|dumb|idiot|wtf|r u (dumb|kidding|sleeping))\\b", "(?i)^\\W*(wrong|no+|nope)[!. ]*$", "(?i)\\bwhy (did|didn't|do|are) (you|u)\\b", "(?i)\\bi (told|asked|said)\\b", "(?i)\\balready (told|said|asked|replied)\\b", "(?i)\\bnot what i (asked|said|meant)\\b"]
40
+ patterns = ["(?i)\\b(you|u|ur|you're|youre|r u)\\s+(are\\s+|r\\s+)?(so\\s+|really\\s+)?(useless|stupid|dumb|idiot|kidding|sleeping)\\b", "(?i)^\\W*(useless|stupid|dumb|idiot|wtf)\\W*$", "(?i)\\bwtf\\b", "(?i)^\\W*(wrong|no+|nope)[!. ]*$", "(?i)\\bwhy (did|didn't|do|are) (you|u)\\b", "(?i)\\bi (told|asked|said)\\b", "(?i)\\balready (told|said|asked|replied)\\b", "(?i)\\bnot what i (asked|said|meant)\\b"]
41
41
  repeat_similarity = 0.6
42
42
  repeat_window_min = 30
43
43
 
@@ -63,6 +63,7 @@ first_action_min = 5
63
63
  stall_min = 30
64
64
  dispatch = "Merge exactly the named set."
65
65
  done_when = ["merged"]
66
+ sha_evidence = ["merged"] # evidence names that must hold the merge commit sha
66
67
  movement_files = []
67
68
 
68
69
  [kinds.code_red]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hubkit-cli"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "Session supervisor for Claude Code: hooks-based multi-agent orchestration with order clocks, routed-work tracking, a watchdog and a stop gate, standard library only."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -4,12 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import json
7
+ import os
8
+ import re
7
9
  import sys
8
10
  import time
9
11
  import tomllib
10
12
  from pathlib import Path
11
13
 
12
- from . import compat, config, hooks, install, lessons, routed, status, store, watcher
14
+ from . import compat, config, hooks, inbound, install, ledger, lessons, routed, status, store, trust, watcher
13
15
  from .orders import Orders
14
16
  from .redact import count_hits_text
15
17
 
@@ -81,6 +83,8 @@ def cmd_work(a, cfg: dict) -> int:
81
83
  from . import slow
82
84
 
83
85
  print(slow.re_estimate(cfg, a.id, a.minutes or 0, a.why))
86
+ elif a.action == "verify":
87
+ print(status.work_verify(cfg, a.id))
84
88
  elif a.action == "done":
85
89
  print(status.work_done(cfg, a.id, a.note))
86
90
  else:
@@ -122,6 +126,29 @@ def intents_gaps(cfg: dict) -> list[str]:
122
126
  return gaps
123
127
 
124
128
 
129
+ def duplicate_hooks(settings: Path) -> list[str]:
130
+ """Events for which the user's settings.json runs `hubkit hook <event>` more than once (each run double-counts). Read only."""
131
+ if not settings.exists():
132
+ return []
133
+ try:
134
+ hooks_cfg = json.loads(settings.read_text(encoding="utf-8")).get("hooks", {})
135
+ except ValueError, AttributeError:
136
+ return []
137
+ seen: dict[str, int] = {}
138
+ for blocks in hooks_cfg.values():
139
+ for block in blocks if isinstance(blocks, list) else []:
140
+ for h in block.get("hooks", []):
141
+ m = re.search(r"hubkit\S*\s+hook\s+(\w+)", str(h.get("command", "")))
142
+ if m:
143
+ seen[m.group(1)] = seen.get(m.group(1), 0) + 1
144
+ return sorted(ev for ev, n in seen.items() if n > 1)
145
+
146
+
147
+ def eod_scheduled() -> bool:
148
+ """True when a scheduler entry runs `hubkit ... eod`."""
149
+ return any("hubkit" in x and re.search(r"\beod\b", x) for x in compat.scheduler_lines())
150
+
151
+
125
152
  def cmd_doctor(a, cfg: dict) -> int:
126
153
  """Print config, bind, hook heartbeat and state health; exit 1 on secret-shaped strings or private terms."""
127
154
  sd = store.state(cfg)
@@ -138,6 +165,11 @@ def cmd_doctor(a, cfg: dict) -> int:
138
165
  print(f"orders.jsonl: {len(rows)} rows, {bad} bad lines")
139
166
  errs = store.read_rows(sd / "hook-errors.jsonl")[0]
140
167
  print(f"hook-errors.jsonl: {len(errs)} rows")
168
+ if not eod_scheduled():
169
+ print(f"WARNING no {compat.scheduler_name()} entry runs `hubkit eod`: the end-of-day review and its learning pass only happen when someone asks for them.")
170
+ dup = duplicate_hooks(Path(os.environ.get("HUBKIT_SETTINGS") or Path.home() / ".claude" / "settings.json"))
171
+ if dup:
172
+ print(f"WARNING settings.json runs hubkit hook more than once for: {', '.join(dup)}. Each run double-counts prompts and answers; remove the duplicate entries.")
141
173
  gaps = intents_gaps(cfg)
142
174
  if gaps:
143
175
  print(f"WARNING intents.toml lacks what intents.example.toml has: {', '.join(gaps)}. Copy those lines from the example; the checks they configure use built-in defaults or stay off.")
@@ -183,6 +215,15 @@ def main(argv: list[str] | None = None) -> int:
183
215
  hk.add_argument("--skills-dir", default="$HOME/.claude/skills")
184
216
  hk.add_argument("--merge-guard", action="store_true", help="include the warn-only PreToolUse merge check")
185
217
 
218
+ tr = sub.add_parser("trust")
219
+ tr.add_argument("action", choices=["record", "ls"])
220
+ tr.add_argument("--class", dest="cls", default="")
221
+ tr.add_argument("--outcome", default="")
222
+
223
+ ib = sub.add_parser("inbound")
224
+ ib.add_argument("action", choices=["ingest"])
225
+ ib.add_argument("--file", help="the inbox watcher's output (default: stdin)")
226
+
186
227
  k = sub.add_parser("asks")
187
228
  k.add_argument("action", choices=["ls"])
188
229
  k.add_argument("--since", default="12h", help="how far back, for example 30m, 12h, 2d")
@@ -202,7 +243,7 @@ def main(argv: list[str] | None = None) -> int:
202
243
  o.add_argument("--estimate", type=int, help="move: minutes this order should take (default: the usual time)")
203
244
 
204
245
  w = sub.add_parser("work")
205
- w.add_argument("action", choices=["add", "done", "ls", "estimate", "block"])
246
+ w.add_argument("action", choices=["add", "done", "ls", "estimate", "block", "verify"])
206
247
  w.add_argument("id", nargs="?")
207
248
  w.add_argument("--kind", default="active")
208
249
  w.add_argument("--title", default="")
@@ -224,6 +265,11 @@ def main(argv: list[str] | None = None) -> int:
224
265
 
225
266
  st = sub.add_parser("status")
226
267
  st.add_argument("--json", action="store_true")
268
+ fd = sub.add_parser("find")
269
+ fd.add_argument("word")
270
+ fd.add_argument("--since", default="2d", help="how far back for asks, peer prompts and PRs, for example 12h, 2d")
271
+ dg = sub.add_parser("digest")
272
+ dg.add_argument("--since", default="12h", help="how far back, for example 30m, 12h, 2d")
227
273
  e = sub.add_parser("eod")
228
274
  e.add_argument("--day")
229
275
  e.add_argument("--note", default="")
@@ -254,6 +300,10 @@ def main(argv: list[str] | None = None) -> int:
254
300
  print("\n".join(install.install_skills(Path(a.install).expanduser())))
255
301
  elif a.cmd == "hooks":
256
302
  print(install.hooks_block(a.skills_dir, a.merge_guard))
303
+ elif a.cmd == "trust":
304
+ print(trust.record(cfg, a.cls, a.outcome) if a.action == "record" else "\n".join(f"{c}: {m}" for c, m in sorted(trust.mode(cfg).items())) or "no outcomes recorded")
305
+ elif a.cmd == "inbound":
306
+ print("\n".join(inbound.ingest(cfg, Path(a.file).read_text(encoding="utf-8") if a.file else sys.stdin.read())) or "nothing new")
257
307
  elif a.cmd == "asks":
258
308
  print("\n".join(lessons.asks_lines(cfg, a.since)))
259
309
  elif a.cmd == "watch":
@@ -261,6 +311,10 @@ def main(argv: list[str] | None = None) -> int:
261
311
  elif a.cmd == "status":
262
312
  st_ = status.write_status(cfg)
263
313
  print(json.dumps(st_, indent=1) if a.json else status.render_block(cfg, Orders(cfg), st_))
314
+ elif a.cmd == "find":
315
+ print("\n".join(ledger.find(cfg, a.word, a.since)))
316
+ elif a.cmd == "digest":
317
+ print("\n".join(status.digest(cfg, a.since)))
264
318
  elif a.cmd == "eod":
265
319
  day = a.day or store.day_of(cfg, time.time())
266
320
  out = status.eod(cfg, day, a.note)
@@ -28,14 +28,14 @@ DEFAULTS: dict = {
28
28
  "extensions_dir": "", # default: <config dir>/ext
29
29
  },
30
30
  "tools": {"mep": "mep", "notify_cmd": "", "gh": "gh"},
31
- "routed": {"stall_min": 15, "min_chars": 200, "skip_owners": ["inbox"], "gh_timeout_s": 10,
31
+ "routed": {"stall_min": 15, "min_chars": 200, "max_agents": 4, "skip_owners": ["inbox"], "gh_timeout_s": 10,
32
32
  "repo": "", "reply_window_min": 60, "queued_wait_min": 60, "owner_logins": {}, "skip_briefs": ["mep watch", "inbox watch"],
33
33
  "activity_globs": [
34
34
  "~/.claude/projects/*/*/subagents/agent-{id}.jsonl",
35
35
  "/tmp/claude-*/*/*/tasks/{id}.output",
36
36
  ], "reviewer_logins": [], "review_words": ["review"], "review_stall_min": 5, "review_overdue_min": 10},
37
37
  "slow": {"min_samples": 3, "order_default_min": 30, "floor_min": 2, "tool_min": 2, "scan_kb": 2048},
38
- "watcher": {"stale_agent_min": 30, "stuck_min": 30, "unhandled_min": 10, "waiting_min": 3, "active_s": 120, "watch_grace_min": 5, "notify_user": True},
38
+ "watcher": {"stale_agent_min": 30, "reply_owed_min": 60, "verify_stale_min": 180, "stuck_min": 30, "unhandled_min": 10, "waiting_min": 3, "active_s": 120, "watch_grace_min": 5, "notify_user": True},
39
39
  "mep": {"enabled": True, "timeout_s": 30},
40
40
  "watchdog": {"transcript_scan_hours": 24, "cron_stale_min": 15},
41
41
  "think": {"decisions": ""}, # path of the active slug's decisions.md; its last lines go in the status block