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.
- hubkit_cli-0.2.1/CHANGELOG.md +63 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/CONTRIBUTING.md +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/PKG-INFO +15 -20
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/README.md +14 -19
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/SKILL.md +2 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/architecture.md +12 -4
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/commands.md +11 -6
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/configuration.md +14 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/intents.example.toml +2 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/pyproject.toml +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/cli.py +56 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/config.py +2 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/gate.py +30 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/hooks.py +8 -4
- hubkit_cli-0.2.1/scripts/hubcore/inbound.py +43 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/install.py +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/learnings.py +40 -6
- hubkit_cli-0.2.1/scripts/hubcore/ledger.py +248 -0
- hubkit_cli-0.2.1/scripts/hubcore/lessons.py +568 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/orders.py +30 -3
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/routed.py +78 -6
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/slow.py +3 -3
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/status.py +77 -5
- hubkit_cli-0.2.1/scripts/hubcore/trust.py +41 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/learn.py +2 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/skills/hubkit-setup/SKILL.md +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/run_reverts.py +73 -8
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_blockers.py +19 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_compat.py +1 -1
- hubkit_cli-0.2.1/tests/test_guards2.py +45 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_hubkit.py +2 -2
- hubkit_cli-0.2.1/tests/test_inbound.py +41 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_install.py +18 -1
- hubkit_cli-0.2.1/tests/test_learning_rules.py +52 -0
- hubkit_cli-0.2.1/tests/test_ledger.py +61 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_lessons.py +19 -0
- hubkit_cli-0.2.1/tests/test_own_work.py +27 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_peer_reports.py +43 -10
- hubkit_cli-0.2.1/tests/test_review_chain.py +44 -0
- hubkit_cli-0.2.1/tests/test_state_rules.py +82 -0
- hubkit_cli-0.2.1/tests/test_tool_guards.py +160 -0
- hubkit_cli-0.1.0/CHANGELOG.md +0 -36
- hubkit_cli-0.1.0/scripts/hubcore/lessons.py +0 -168
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/.gitignore +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/CLAUDE.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/LICENSE +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/SECURITY.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/config.example.toml +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/docs/troubleshooting.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/references/design-sources.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/references/research.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/__init__.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/compat.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/redact.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/store.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubcore/watcher.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/scripts/hubkit +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/skills/hub-mep/SKILL.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/conftest.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.1}/tests/test_slow.py +0 -0
- {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
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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).
|
|
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
|
|
147
|
-
|
|
|
148
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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).
|
|
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
|
|
133
|
-
|
|
|
134
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|