hubkit-cli 0.1.0__tar.gz → 0.2.0__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.1.0 → hubkit_cli-0.2.0}/CHANGELOG.md +20 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/CONTRIBUTING.md +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/PKG-INFO +14 -20
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/README.md +13 -19
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/SKILL.md +2 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/docs/architecture.md +12 -4
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/docs/commands.md +9 -5
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/docs/configuration.md +14 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/intents.example.toml +2 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/pyproject.toml +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/cli.py +51 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/config.py +2 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/gate.py +30 -2
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/hooks.py +8 -4
- hubkit_cli-0.2.0/scripts/hubcore/inbound.py +43 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/install.py +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/learnings.py +40 -6
- hubkit_cli-0.2.0/scripts/hubcore/lessons.py +341 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/orders.py +30 -3
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/routed.py +78 -6
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/slow.py +3 -3
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/status.py +72 -4
- hubkit_cli-0.2.0/scripts/hubcore/trust.py +41 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/learn.py +2 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/skills/hubkit-setup/SKILL.md +1 -1
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/run_reverts.py +66 -8
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_blockers.py +19 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_compat.py +1 -1
- hubkit_cli-0.2.0/tests/test_guards2.py +45 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_hubkit.py +2 -2
- hubkit_cli-0.2.0/tests/test_inbound.py +41 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_install.py +18 -1
- hubkit_cli-0.2.0/tests/test_learning_rules.py +52 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_lessons.py +19 -0
- hubkit_cli-0.2.0/tests/test_own_work.py +27 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_peer_reports.py +43 -10
- hubkit_cli-0.2.0/tests/test_review_chain.py +44 -0
- hubkit_cli-0.2.0/tests/test_state_rules.py +82 -0
- hubkit_cli-0.2.0/tests/test_tool_guards.py +135 -0
- hubkit_cli-0.1.0/scripts/hubcore/lessons.py +0 -168
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/.gitignore +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/CLAUDE.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/LICENSE +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/SECURITY.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/config.example.toml +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/docs/troubleshooting.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/references/design-sources.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/references/research.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/__init__.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/compat.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/redact.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/store.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubcore/watcher.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/scripts/hubkit +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/skills/hub-mep/SKILL.md +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/conftest.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_slow.py +0 -0
- {hubkit_cli-0.1.0 → hubkit_cli-0.2.0}/tests/test_watcher.py +0 -0
|
@@ -4,8 +4,27 @@ All notable changes to this project are recorded here. The format follows [Keep
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-11-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Commands: `hubkit digest [--since]`, `hubkit trust record|ls`, `hubkit inbound ingest`, `hubkit work verify`, `learn.py address --check CMD`.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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".
|
|
13
|
+
- Order close: evidence names in a kind's `sha_evidence` must hold a commit sha.
|
|
14
|
+
- 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`.
|
|
15
|
+
- `doctor` warns on a hubkit hook registered twice in `settings.json` and on no scheduled `hubkit eod`.
|
|
16
|
+
- 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.
|
|
17
|
+
- New tests (336 pass on Linux) and revert entries for each guard.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Correction patterns in `intents.example.toml` match insults aimed at the hub and no longer fire on one aimed at a tool.
|
|
21
|
+
- 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.
|
|
22
|
+
- A one-line resume command may be given in the message as typed (`SKILL.md`).
|
|
23
|
+
|
|
24
|
+
## [0.1.0]
|
|
25
|
+
|
|
7
26
|
### Added
|
|
8
|
-
- Install like miseenplace: `uv tool install hubkit-cli` (
|
|
27
|
+
- 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.
|
|
9
28
|
- macOS and Windows support through one platform module (`hubcore/compat.py`); only Linux has been run so far.
|
|
10
29
|
- Blockers (`order block|result`), watcher fixes, peer-report closing, `asks ls`, and warn-only merge guard.
|
|
11
30
|
- Apache-2.0 license, `README.md` rewrite, `docs/` (architecture, configuration, commands, troubleshooting), `CONTRIBUTING.md`, `SECURITY.md`.
|
|
@@ -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.
|
|
3
|
+
Version: 0.2.0
|
|
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, `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
|
|
|
@@ -140,12 +141,13 @@ Every key is documented in [docs/configuration.md](docs/configuration.md).
|
|
|
140
141
|
|
|
141
142
|
| Behaviour | Strength |
|
|
142
143
|
|---|---|
|
|
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) |
|
|
144
|
+
| 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
145
|
| 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
|
-
| `
|
|
146
|
+
| A learned class counts as fixed only if a rule for it exists (and, with `--check`, its command passes) | Enforced (the address is refused) |
|
|
147
|
+
| A merge order closes only with the merge commit sha | Enforced (the close is refused) |
|
|
148
|
+
| 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 |
|
|
149
|
+
| `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 |
|
|
150
|
+
| `hubkit doctor` warning when your `intents.toml` lacks keys the example has, a hook registered twice, or no scheduled `hubkit eod` | Warning only |
|
|
149
151
|
|
|
150
152
|
Details and exact rules: [SKILL.md](SKILL.md) and [docs/architecture.md](docs/architecture.md#stop-gate-gatepy).
|
|
151
153
|
|
|
@@ -168,19 +170,11 @@ python3 tests/run_reverts.py # each guard reverted in a copy must fail it
|
|
|
168
170
|
|
|
169
171
|
## Platform support
|
|
170
172
|
|
|
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>`.
|
|
173
|
+
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
174
|
|
|
181
175
|
## Limits and known gaps
|
|
182
176
|
|
|
183
|
-
- Python 3.14 only
|
|
177
|
+
- Python 3.14 only, and Linux only so far; see Platform support.
|
|
184
178
|
- It targets one hub per machine. A second live hub is refused at bind.
|
|
185
179
|
- Order creation takes a blocking `flock` (`scripts/hubcore/orders.py`); a process that holds the lock forever stalls order opens.
|
|
186
180
|
- 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, `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
|
|
|
@@ -126,12 +127,13 @@ Every key is documented in [docs/configuration.md](docs/configuration.md).
|
|
|
126
127
|
|
|
127
128
|
| Behaviour | Strength |
|
|
128
129
|
|---|---|
|
|
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) |
|
|
130
|
+
| 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
131
|
| 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
|
-
| `
|
|
132
|
+
| A learned class counts as fixed only if a rule for it exists (and, with `--check`, its command passes) | Enforced (the address is refused) |
|
|
133
|
+
| A merge order closes only with the merge commit sha | Enforced (the close is refused) |
|
|
134
|
+
| 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 |
|
|
135
|
+
| `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 |
|
|
136
|
+
| `hubkit doctor` warning when your `intents.toml` lacks keys the example has, a hook registered twice, or no scheduled `hubkit eod` | Warning only |
|
|
135
137
|
|
|
136
138
|
Details and exact rules: [SKILL.md](SKILL.md) and [docs/architecture.md](docs/architecture.md#stop-gate-gatepy).
|
|
137
139
|
|
|
@@ -154,19 +156,11 @@ python3 tests/run_reverts.py # each guard reverted in a copy must fail it
|
|
|
154
156
|
|
|
155
157
|
## Platform support
|
|
156
158
|
|
|
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>`.
|
|
159
|
+
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
160
|
|
|
167
161
|
## Limits and known gaps
|
|
168
162
|
|
|
169
|
-
- Python 3.14 only
|
|
163
|
+
- Python 3.14 only, and Linux only so far; see Platform support.
|
|
170
164
|
- It targets one hub per machine. A second live hub is refused at bind.
|
|
171
165
|
- Order creation takes a blocking `flock` (`scripts/hubcore/orders.py`); a process that holds the lock forever stalls order opens.
|
|
172
166
|
- 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.
|
|
118
|
+
|
|
119
|
+
## Digest and trust
|
|
120
|
+
|
|
121
|
+
`hubkit digest [--since 12h]` prints what finished, which orders closed, the latest alerts and what waits on the user. `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,10 @@
|
|
|
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 and what waits on the user, 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. |
|
|
19
23
|
| `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
24
|
| `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
25
|
| `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. |
|
|
@@ -34,8 +38,8 @@
|
|
|
34
38
|
| `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
39
|
| `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
40
|
| `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 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. |
|
|
42
|
+
| `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
43
|
|
|
40
44
|
## learn.py
|
|
41
45
|
|
|
@@ -47,8 +51,8 @@
|
|
|
47
51
|
| `list [--last N]` | The N most recently seen entries (default 30). |
|
|
48
52
|
| `promote-candidates [--min N]` | Classes seen at least N times (default 2). |
|
|
49
53
|
| `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. |
|
|
54
|
+
| `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. |
|
|
55
|
+
| `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
56
|
|
|
53
57
|
## Example
|
|
54
58
|
|
|
@@ -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.
|
|
7
|
+
version = "0.2.0"
|
|
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, 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,8 @@ 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
|
+
dg = sub.add_parser("digest")
|
|
269
|
+
dg.add_argument("--since", default="12h", help="how far back, for example 30m, 12h, 2d")
|
|
227
270
|
e = sub.add_parser("eod")
|
|
228
271
|
e.add_argument("--day")
|
|
229
272
|
e.add_argument("--note", default="")
|
|
@@ -254,6 +297,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
254
297
|
print("\n".join(install.install_skills(Path(a.install).expanduser())))
|
|
255
298
|
elif a.cmd == "hooks":
|
|
256
299
|
print(install.hooks_block(a.skills_dir, a.merge_guard))
|
|
300
|
+
elif a.cmd == "trust":
|
|
301
|
+
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")
|
|
302
|
+
elif a.cmd == "inbound":
|
|
303
|
+
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
304
|
elif a.cmd == "asks":
|
|
258
305
|
print("\n".join(lessons.asks_lines(cfg, a.since)))
|
|
259
306
|
elif a.cmd == "watch":
|
|
@@ -261,6 +308,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
261
308
|
elif a.cmd == "status":
|
|
262
309
|
st_ = status.write_status(cfg)
|
|
263
310
|
print(json.dumps(st_, indent=1) if a.json else status.render_block(cfg, Orders(cfg), st_))
|
|
311
|
+
elif a.cmd == "digest":
|
|
312
|
+
print("\n".join(status.digest(cfg, a.since)))
|
|
264
313
|
elif a.cmd == "eod":
|
|
265
314
|
day = a.day or store.day_of(cfg, time.time())
|
|
266
315
|
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
|
|
@@ -65,6 +65,23 @@ def unverified_verdict(cfg: dict, last: str) -> list[str]:
|
|
|
65
65
|
return sorted(routed.pr_num(u) for u in urls.values() if not _reviewed_at_head(cfg, u, timeout))
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def ci_not_green(cfg: dict, last: str) -> list[str]:
|
|
69
|
+
"""PRs the message gives an approving verdict on while CI at the head is failing or pending. A PR is resolved from
|
|
70
|
+
its URL, a repo-qualified ref or `routed.repo`; a bare number is matched to a tracked item's PR."""
|
|
71
|
+
out: list[str] = []
|
|
72
|
+
timeout = float(routed.opts(cfg)["gh_timeout_s"])
|
|
73
|
+
tracked = [u for i in routed.items(cfg) for u in [*i["prs"], *i.get("queue", [])]]
|
|
74
|
+
for sentence in re.split(r"(?<=[.!?])\s+|\n", last):
|
|
75
|
+
if not (re.search(r"(?i)\bapprov", sentence) and (PR_REF.search(sentence) or REPO_PR.search(sentence)) and not AWAITING.search(sentence)):
|
|
76
|
+
continue
|
|
77
|
+
for ref in routed.refs(cfg, sentence):
|
|
78
|
+
for u in [ref] if ref.startswith("http") else [t for t in tracked if routed.same_pr(t, ref)]:
|
|
79
|
+
info = routed.pr_info(cfg, u, timeout)
|
|
80
|
+
if info and info.get("ci") in ("failing", "pending"):
|
|
81
|
+
out.append(f"#{routed.pr_num(u)} ({info['ci']})")
|
|
82
|
+
return sorted(set(out))
|
|
83
|
+
|
|
84
|
+
|
|
68
85
|
def _reviewed_at_head(cfg: dict, url: str, timeout: float) -> bool:
|
|
69
86
|
"""True when GitHub shows a non-author review at the head, or cannot be read (an unreadable PR proves nothing)."""
|
|
70
87
|
info = routed.pr_info(cfg, url, timeout)
|
|
@@ -124,21 +141,32 @@ def blocker_reasons(cfg: dict, last: str, now: float) -> list[str]:
|
|
|
124
141
|
return out
|
|
125
142
|
|
|
126
143
|
|
|
127
|
-
def stop_reasons(cfg: dict, last: str, now: float | None = None, transcript: str | None = None) -> list[str]:
|
|
144
|
+
def stop_reasons(cfg: dict, last: str, now: float | None = None, transcript: str | None = None, cwd: str | None = None) -> list[str]:
|
|
128
145
|
"""Every reason the turn cannot end yet, each with its exact action. Empty = the turn may end."""
|
|
129
146
|
now = now or time.time()
|
|
147
|
+
ci_bad = ci_not_green(cfg, last) # before prompt_lines: a reviewed item closes there
|
|
130
148
|
reasons = routed.prompt_lines(cfg, now)
|
|
131
149
|
reasons += blocker_reasons(cfg, last, now)
|
|
132
|
-
|
|
150
|
+
if r := lessons.unpushed_work(last, cwd):
|
|
151
|
+
reasons.append(r)
|
|
152
|
+
reasons += [r for r in (lessons.unswept_asks(cfg, last, now), lessons.unchecked_claim(cfg, last, transcript), lessons.unrun_claim(cfg, last, transcript)) if r]
|
|
153
|
+
if refs := lessons.bare_refs(cfg, last):
|
|
154
|
+
reasons.append(f"hubkit: your message names {', '.join(refs[:4])} with no link. NEXT: give the full URL of each PR or issue so he can click it")
|
|
133
155
|
try:
|
|
134
156
|
reasons += slow.gate_lines(cfg, now)
|
|
135
157
|
except Exception as e: # a failing estimate check must not wedge the hub's turn end
|
|
136
158
|
store.append_row(store.state(cfg) / "hook-errors.jsonl", {"where": "slow.gate", "error": repr(e)[:200]})
|
|
137
159
|
if open_work(cfg) and not watch_live(cfg):
|
|
138
160
|
reasons.append(f"hubkit: nothing will wake you when routed work stalls. NEXT: start in the background: {WATCH_CMD}")
|
|
161
|
+
reasons += [f"hubkit: the ask {i['id']} ({i['title'][:50]}) was asked {i['asked']} times with no answer. Stop asking. NEXT: take the safest default, record it with the reason, "
|
|
162
|
+
f"and go on: hubkit work done {i['id']} --note \"defaulted to ...\"" for i in work_load(store.state(cfg)) if i.get("kind") == "ask" and int(i.get("asked", 1)) >= 3]
|
|
139
163
|
for n in unverified_verdict(cfg, last):
|
|
140
164
|
reasons.append(f"hubkit: your message gives a verdict on #{n}, but GitHub shows no review at its current head "
|
|
141
165
|
"(agent words are not evidence). NEXT: correct the message; relay a verdict only after the item closes itself")
|
|
166
|
+
if ci_bad:
|
|
167
|
+
reasons.append(f"hubkit: your message approves {', '.join(ci_bad)} but CI at the head is not green. NEXT: say CI is not green, or wait for it, before relaying an approval")
|
|
168
|
+
if (r := lessons.prose_ask(last, transcript, is_question(cfg, last) and blocked_on_user(cfg))):
|
|
169
|
+
reasons.append(r)
|
|
142
170
|
if is_question(cfg, last) and not blocked_on_user(cfg):
|
|
143
171
|
reasons.append("hubkit: you ended on a question to the user but nothing is blocked on him. NEXT: settle it from the record "
|
|
144
172
|
"or route it; only if it truly needs him run: hubkit work add --kind ask --title \"<the one question>\" --note \"<ordered work it blocks>\"")
|