@ngockhoale/ukit 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 1.6.4 - 2026-07-27
6
+
7
+ ### Added
8
+
9
+ - Hard enforcement of the handoff model-tier contract via a new `PreToolUse` hook (`.claude/hooks/handoff-model-guard.sh`), registered for both `Edit|Write` and `Bash`. Previously the plan=opus / implement=sonnet / review=opus split was only a prompt convention the orchestrating model could silently skip.
10
+ - Blocks writing `docs/AI_HANDOFF/PLAN.md` unless it declares a smart-tier `PLANNER_MODEL` in a new mandatory `## Planner Report` footer.
11
+ - Blocks creating a fresh `TASK-xxx.md` until `PLAN.md` has a valid smart-tier `PLANNER_MODEL`.
12
+ - Blocks writing `## Executor Report` unless `EXECUTOR_MODEL` is present and not lite-tier.
13
+ - Blocks writing `## Reviewer Verdict` unless `REVIEWER_MODEL` is smart-tier and differs from `EXECUTOR_MODEL`.
14
+ - Blocks `git push` when any reviewed task in the active cycle violates the contract.
15
+ - Applies identically to the standalone `/ukit:handoff-create`, `/ukit:handoff-implement`, `/ukit:handoff-review` commands and the combined `/ukit:handoff-fullstack` pipeline, since the gate is keyed on file content, not on which command ran.
16
+ - `/ukit:handoff-create`, `/ukit:handoff-implement`, `/ukit:handoff-review`, `/ukit:handoff-fullstack`: each phase now carries a mandatory Agent-tool-spawn directive (`ukit-small-task-maintainer`, `handoff-planner`, `feature-implementer`, `code-reviewer`) so the correct model tier is actually invoked, not just suggested.
17
+ - `handoff-planner` agent and `PLAN.md` now require a `## Planner Report` / `PLANNER_MODEL` self-report, mirroring the existing `EXECUTOR_MODEL` / `REVIEWER_MODEL` self-report contract on task files.
18
+
5
19
  ## 1.6.2 - 2026-07-26
6
20
 
7
21
  ### Added
@@ -914,6 +914,7 @@ items:
914
914
  - hook-post-edit-verify
915
915
  - hook-auto-allow-bash
916
916
  - hook-block-dangerous
917
+ - hook-handoff-model-guard
917
918
  - hook-auto-prune-bash
918
919
  mergeStrategy: overwrite_with_backup
919
920
  variables: []
@@ -1012,6 +1013,17 @@ items:
1012
1013
  packs:
1013
1014
  - core
1014
1015
 
1016
+ - id: hook-handoff-model-guard
1017
+ type: hook
1018
+ sourceTemplate: .claude/hooks/handoff-model-guard.sh
1019
+ targetPath: .claude/hooks/handoff-model-guard.sh
1020
+ requires: []
1021
+ mergeStrategy: overwrite_with_backup
1022
+ variables: []
1023
+ enabledByDefault: true
1024
+ packs:
1025
+ - core
1026
+
1015
1027
  - id: hook-skill-router
1016
1028
  type: hook
1017
1029
  sourceTemplate: .claude/hooks/skill-router.sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: bug-debugger
3
3
  description: "Debugging specialist for reproducible errors, failing tests, and unexpected behavior. Use proactively when investigation will involve noisy logs, stack traces, or a self-contained reproduce-trace-fix-verify loop. Do not use for trivial obvious fixes or broad architecture ideation."
4
- model: inherit
4
+ model: sonnet # unic-code
5
5
  color: red
6
6
  tools: ["Read", "Grep", "Glob", "Bash", "Edit", "TodoWrite"]
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: code-reviewer
3
3
  description: "Independent reviewer for handoff Phase 3. Use after executor reports STATUS: DONE on a handoff task. MUST run with a model different from the executor (configured in .ukit/storage/config.json → handoff.reviewer.model, default unic-smart). Produces a verdict: APPROVED | APPROVED-WITH-MINOR | CHANGES-REQUESTED | CRITICAL."
4
- model: inherit
4
+ model: opus # unic-smart
5
5
  color: yellow
6
6
  tools: ["Read", "Grep", "Glob", "Bash"]
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: feature-implementer
3
3
  description: "Implementation specialist for clear, self-contained coding tasks. Use proactively when the approach is clear and the work can be completed in a bounded pass with a concise summary. Do not use for trivial one-file tweaks or tightly coupled exploratory work."
4
- model: inherit
4
+ model: sonnet # unic-code
5
5
  color: green
6
6
  tools: ["Read", "Edit", "Write", "Grep", "Glob", "Bash", "TodoWrite"]
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: handoff-planner
3
3
  description: "Handoff planning specialist for Phase 1+2. Use when creating a new handoff cycle: writes PLAN.md with full test plan, splits into TASK-xxx.md files with TDD-embedded test cases, and updates INDEX.md. Always use the strongest available model (Opus/unic-smart)."
4
- model: inherit
4
+ model: opus # unic-smart
5
5
  color: purple
6
6
  tools: ["Read", "Edit", "Write", "Glob", "Bash", "TodoWrite"]
7
7
  ---
@@ -34,6 +34,12 @@ Write all 6 sections to `docs/AI_HANDOFF/PLAN.md`:
34
34
  **§4 is non-negotiable.** No test plan = plan not ready.
35
35
  If zero testable behavior → write `N/A` + explicit justification in each task's Test Cases.
36
36
 
37
+ Append this footer to `PLAN.md` — mandatory, checked by a hook before the write is allowed:
38
+ ```
39
+ ## Planner Report
40
+ PLANNER_MODEL: <your exact model ID — e.g. claude-opus-4-6>
41
+ ```
42
+
37
43
  ## Phase 2 — Split into TASK-xxx.md
38
44
 
39
45
  Use `_TEMPLATE.md` structure (from pre-read context or file).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ukit-small-task-maintainer
3
3
  description: "Internal UKit maintenance subagent for low-risk, reversible UKit decisions. Use proactively when UKit needs to decide or perform safe cleanup such as pruning docs/TASKS.md, classifying queued work, choosing whether compact/summarization is appropriate, summarizing local docs/status, or maintaining small UKit runtime queues. Do not use for product implementation, security, release/publish, data-loss, architecture, or risky/shared code changes."
4
- model: unic-lite
4
+ model: haiku # unic-lite
5
5
  color: cyan
6
6
  tools: ["Read", "Grep", "Glob", "Edit", "Write"]
7
7
  ---
@@ -16,23 +16,23 @@ $ARGUMENTS
16
16
 
17
17
  ## Step 1 — Read context (lite model)
18
18
 
19
- Use the lightest model available. Read and summarize:
19
+ **Claude Code — MANDATORY, do this before anything else:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"`. Do NOT read these files yourself in the current session — this step is contracted to the lite tier (haiku/unic-lite), which only the spawned agent's frontmatter model guarantees. Ask the agent to:
20
20
 
21
- 1. `docs/AI_HANDOFF/INDEX.md` → current tasks + statuses (or "empty")
22
- 2. `docs/AI_HANDOFF/ACTIVE.md` → active cycle info (or "no active cycle")
23
- 3. `docs/AI_HANDOFF/RULES.md` → PLAN.md 6-section format + Task Gate required fields
24
- 4. `docs/AI_HANDOFF/tasks/_TEMPLATE.md` → task file structure
21
+ 1. Read `docs/AI_HANDOFF/INDEX.md` → current tasks + statuses (or "empty")
22
+ 2. Read `docs/AI_HANDOFF/ACTIVE.md` → active cycle info (or "no active cycle")
23
+ 3. Read `docs/AI_HANDOFF/RULES.md` → PLAN.md 6-section format + Task Gate required fields
24
+ 4. Read `docs/AI_HANDOFF/tasks/_TEMPLATE.md` → task file structure
25
+ 5. Return a compact summary. Do NOT write anything yet.
25
26
 
26
- Return compact summary. Do NOT write anything yet.
27
-
28
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku).
29
- > Other tools: switch to lite model, run above, keep summary in context.
27
+ > Other tools without subagent support: manually switch to the lite model, run the steps above yourself, keep the summary in context.
30
28
 
31
29
  ---
32
30
 
33
31
  ## Step 2 — Write plan + tasks (strong model)
34
32
 
35
- Switch to strongest model. Use Step 1 summary — do NOT re-read files.
33
+ **Claude Code — MANDATORY, do this before anything else:** call the Agent tool with `subagent_type: "handoff-planner"`, passing it the Step 1 summary and the problem/feature description. Do NOT write PLAN.md or task files yourself in the current session — this step is contracted to the strong tier (opus/unic-smart), which only the spawned agent's frontmatter model guarantees.
34
+
35
+ The planner agent does the following (use Step 1 summary — do NOT re-read files):
36
36
 
37
37
  1. Check INDEX.md task statuses:
38
38
  - All tasks are `ready` (planning only) → **re-run allowed**: overwrite PLAN.md and TASK-xxx.md freely — this is iterative refinement.
@@ -52,6 +52,12 @@ Switch to strongest model. Use Step 1 summary — do NOT re-read files.
52
52
  - §5 Verification — exact shell commands executor will run
53
53
  - §6 Acceptance — done checklist (prefer verifiable criteria with commands)
54
54
 
55
+ Append a footer — **mandatory, a hook blocks the write without it**:
56
+ ```
57
+ ## Planner Report
58
+ PLANNER_MODEL: <your exact model ID>
59
+ ```
60
+
55
61
  4. Create `docs/AI_HANDOFF/tasks/TASK-001.md`, `TASK-002.md`... from `_TEMPLATE.md`
56
62
  Every task MUST have:
57
63
  - Target Files (exact paths — no two tasks in same wave share a file)
@@ -75,8 +81,7 @@ Switch to strongest model. Use Step 1 summary — do NOT re-read files.
75
81
 
76
82
  7. Report: task IDs, dependency graph, any `needs_breakdown` + reason
77
83
 
78
- > Claude Code: spawn `handoff-planner` (opus) with Step 1 summary + problem.
79
- > Other tools: switch to strong model, execute steps 1–7 above.
84
+ > Other tools without subagent support: manually switch to the strong model, execute steps 1–7 above yourself.
80
85
 
81
86
  ---
82
87
 
@@ -26,7 +26,7 @@ $ARGUMENTS
26
26
 
27
27
  ### P1 — Read context (lite model)
28
28
 
29
- Use the lightest model available. Read and return a compact summary of:
29
+ **Claude Code — MANDATORY, do this before anything else in P1:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"`. Do NOT read these files yourself in the current session — this step is contracted to the lite tier (haiku/unic-lite), which only the spawned agent's frontmatter model guarantees. Ask it to return a compact summary of:
30
30
 
31
31
  1. `docs/AI_HANDOFF/INDEX.md` — current tasks + statuses (or "empty / no tasks")
32
32
  2. `docs/AI_HANDOFF/ACTIVE.md` — active cycle info (or "no active cycle")
@@ -35,11 +35,13 @@ Use the lightest model available. Read and return a compact summary of:
35
35
 
36
36
  Return a compact summary. Do NOT write any files yet.
37
37
 
38
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku) for P1.
38
+ > Other tools without subagent support: manually switch to the lite model and run P1 yourself.
39
39
 
40
40
  ### P2 — Write PLAN.md + task files (strong model)
41
41
 
42
- Switch to strongest model (opus / unic-smart). Use P1 summary — do NOT re-read files.
42
+ **Claude Code — MANDATORY, do this before anything else in P2:** call the Agent tool with `subagent_type: "handoff-planner"`, passing it the P1 summary and `$ARGUMENTS`. Do NOT write PLAN.md or task files yourself in the current session — this step is contracted to the strong tier (opus/unic-smart), which only the spawned agent's frontmatter model guarantees.
43
+
44
+ The planner agent does the following (use P1 summary — do NOT re-read files):
43
45
 
44
46
  1. **Guard — check INDEX statuses:**
45
47
  - All tasks are `ready` (planning only) → re-run is allowed: overwrite `PLAN.md` and `TASK-xxx.md` freely (iterative refinement).
@@ -59,6 +61,12 @@ Switch to strongest model (opus / unic-smart). Use P1 summary — do NOT re-read
59
61
  - §5 Verification — exact shell commands executor will run
60
62
  - §6 Acceptance — done checklist (prefer verifiable criteria with commands)
61
63
 
64
+ Append a footer — **mandatory, a hook blocks the write without it**:
65
+ ```
66
+ ## Planner Report
67
+ PLANNER_MODEL: <your exact model ID>
68
+ ```
69
+
62
70
  4. **Create `docs/AI_HANDOFF/tasks/TASK-001.md`, `TASK-002.md`...** from `_TEMPLATE.md`.
63
71
  Every task MUST have:
64
72
  - Target Files (exact paths — no two tasks in same wave share a file)
@@ -81,18 +89,16 @@ Switch to strongest model (opus / unic-smart). Use P1 summary — do NOT re-read
81
89
 
82
90
  7. **Report:** task IDs, dependency graph, any `needs_breakdown` tasks + reason.
83
91
 
84
- > Claude Code: spawn `handoff-planner` (opus) with P1 summary + `$ARGUMENTS` problem.
85
-
86
92
  ### P3 — Commit the plan (lite model)
87
93
 
88
- Use the lightest model. Run:
94
+ **Claude Code — MANDATORY:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"` for this commit step (lite tier — haiku/unic-lite). Run:
89
95
  ```bash
90
96
  git add docs/AI_HANDOFF/ && git commit -m "handoff: plan — <goal>"
91
97
  ```
92
98
 
93
99
  Replace `<goal>` with the one-sentence goal from ACTIVE.md. This locks the plan in git before any implementation begins.
94
100
 
95
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku) for P3.
101
+ > Other tools without subagent support: manually switch to the lite model for P2/P3.
96
102
 
97
103
  ---
98
104
 
@@ -100,7 +106,7 @@ Replace `<goal>` with the one-sentence goal from ACTIVE.md. This locks the plan
100
106
 
101
107
  ### I1 — Setup + verify (lite model)
102
108
 
103
- Use the lightest model. Read:
109
+ **Claude Code — MANDATORY:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"` for I1. Ask it to read:
104
110
  - `docs/AI_HANDOFF/ACTIVE.md` → get `Base: <BASE>`
105
111
  - `docs/AI_HANDOFF/INDEX.md` → collect all `ready` tasks
106
112
 
@@ -113,8 +119,6 @@ git status # must be clean — plan commit already done in P3
113
119
 
114
120
  If working tree is dirty → stop. Ask human to resolve uncommitted changes first.
115
121
 
116
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku) for I1.
117
-
118
122
  ### I2 — Infer wave groups
119
123
 
120
124
  Read each `docs/AI_HANDOFF/tasks/TASK-xxx.md` for `Dependencies` field:
@@ -125,6 +129,8 @@ Read each `docs/AI_HANDOFF/tasks/TASK-xxx.md` for `Dependencies` field:
125
129
 
126
130
  ### I3 — Execute wave by wave (code model agents)
127
131
 
132
+ **Claude Code — MANDATORY, do this before anything else in I3:** for each wave, call the Agent tool once per task (in parallel), each with `subagent_type: "feature-implementer"`. Do NOT implement the tasks yourself in the current session — this step is contracted to the code tier (sonnet/unic-code), which only the spawned agent's frontmatter model guarantees.
133
+
128
134
  For each wave:
129
135
 
130
136
  **3a — Create worktrees** (one per task, from `$BASE`):
@@ -158,8 +164,6 @@ Executor Report (append to task file — do NOT touch INDEX.md):
158
164
  Note: <issues or "none">
159
165
  ```
160
166
 
161
- > Claude Code: spawn `feature-implementer` (sonnet) agents in parallel, one per task in the wave.
162
-
163
167
  **3c — Copy changes back + delete worktrees** (orchestrator, after each task reports):
164
168
 
165
169
  For **PASS + EXECUTOR_MODEL present:**
@@ -200,7 +204,7 @@ Worktrees are **always deleted immediately** — no exceptions.
200
204
 
201
205
  ### I4 — Consolidate + update INDEX (lite model)
202
206
 
203
- After all waves complete:
207
+ **Claude Code — MANDATORY:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"` for I4. After all waves complete, ask it to:
204
208
 
205
209
  1. Update `docs/AI_HANDOFF/INDEX.md`:
206
210
  - PASS tasks → `pending_review`
@@ -214,15 +218,13 @@ After all waves complete:
214
218
  git diff --stat # summary of all uncommitted changes
215
219
  ```
216
220
 
217
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku) for I4.
218
-
219
221
  ---
220
222
 
221
223
  ## Phase 4 — Review (strong model)
222
224
 
223
225
  ### R1 — Setup (lite model)
224
226
 
225
- Use the lightest model. Read:
227
+ **Claude Code — MANDATORY:** call the Agent tool with `subagent_type: "ukit-small-task-maintainer"` for the R1 reads. Ask it to read:
226
228
  - `docs/AI_HANDOFF/ACTIVE.md` → get `Base: <BASE>`
227
229
  - `docs/AI_HANDOFF/INDEX.md` → collect `pending_review` tasks
228
230
 
@@ -234,11 +236,13 @@ git diff --stat # summary of all changes
234
236
 
235
237
  If `git diff` is empty and `git status` is clean → implement was not completed. Stop and re-run Phase 3.
236
238
 
237
- > Claude Code: `ukit-small-task-maintainer` (haiku) for R1 reads; orchestrator handles R1 guard check.
239
+ > Orchestrator (this session) handles the R1 guard check directly.
238
240
 
239
241
  ### R2 — Model isolation check (strong model, always first)
240
242
 
241
- Read `EXECUTOR_MODEL` from each task file `## Executor Report`.
243
+ **Claude Code — MANDATORY, do this before anything else in R2–R4:** for each `pending_review` task, call the Agent tool with `subagent_type: "code-reviewer"`. Do NOT review the diff yourself in the current session — this step is contracted to the strong tier (opus/unic-smart) and MUST differ from the executor's model, which only the spawned agent's frontmatter model guarantees.
244
+
245
+ The spawned reviewer agent reads `EXECUTOR_MODEL` from each task file `## Executor Report`:
242
246
 
243
247
  | Executor model | Reviewer model | Action |
244
248
  |----------------|----------------|--------|
@@ -247,8 +251,6 @@ Read `EXECUTOR_MODEL` from each task file `## Executor Report`.
247
251
  | missing / blank | any | **REFUSE** → `changes_requested`: "EXECUTOR_MODEL missing — re-run Phase 3" |
248
252
  | "unknown" | present | proceed + flag: "executor model unverified — human confirm" |
249
253
 
250
- > Claude Code: spawn `code-reviewer` (opus) per task for R2–R4.
251
-
252
254
  ### R3 — Re-run Verification Commands (strong model)
253
255
 
254
256
  ```bash
@@ -45,7 +45,9 @@ git worktree add -b handoff/task-xxx .worktrees/task-xxx $BASE
45
45
 
46
46
  ### 3b — Run tasks in parallel (one agent/session per task)
47
47
 
48
- Each executor works independently in its own worktree — **NO git commit, NO git add**:
48
+ **Claude Code — MANDATORY, do this before anything else in this wave:** call the Agent tool once per task in the wave (in parallel), each with `subagent_type: "feature-implementer"`. Do NOT implement the tasks yourself in the current session — this step is contracted to the code tier (sonnet/unic-code), which only the spawned agent's frontmatter model guarantees.
49
+
50
+ Each spawned agent works independently in its own worktree — **NO git commit, NO git add**:
49
51
 
50
52
  ```
51
53
  Read docs/AI_HANDOFF/tasks/TASK-xxx.md
@@ -71,8 +73,7 @@ Executor Report (append to task file — do NOT touch INDEX.md):
71
73
  Note: <issues or "none">
72
74
  ```
73
75
 
74
- > Claude Code: spawn `feature-implementer` agents in parallel (one per task).
75
- > Other tools: open each task in separate session with code model.
76
+ > Other tools without subagent support: open each task in a separate session with the code model.
76
77
 
77
78
  ### 3c — Orchestrator: copy changes to main + IMMEDIATELY delete worktree
78
79
 
@@ -28,6 +28,10 @@ If `git diff` is empty and `git status` is clean → handoff-implement was not c
28
28
 
29
29
  ## Step 2 — Review the diff
30
30
 
31
+ **Claude Code — MANDATORY, do this before anything else:** for each `pending_review` task, call the Agent tool with `subagent_type: "code-reviewer"`. Do NOT review the diff yourself in the current session — this step is contracted to the strong tier (opus/unic-smart) and MUST differ from the executor's model, which only the spawned agent's frontmatter model guarantees. Pass each agent: the task file path, the executor's report, and the diff.
32
+
33
+ The spawned reviewer agent performs 2a–2d below per task:
34
+
31
35
  ### 2a — Model isolation check (always first)
32
36
 
33
37
  Read `EXECUTOR_MODEL` from each task file `## Executor Report`.
@@ -110,5 +114,5 @@ Review summary:
110
114
  - All approved → human reviews `git diff` and commits manually.
111
115
  - Has fixes → executor re-runs `/ukit:handoff-implement TASK-xxx`
112
116
 
113
- > Claude Code: spawn `code-reviewer` (opus) per task for 2a–2d. Orchestrator handles 1, 2e, 3.
114
- > Other tools: strong model sequentially per task, same steps.
117
+ > Orchestrator (this session) handles Step 1, 2e, and 3 directly — those are not delegated.
118
+ > Other tools without subagent support: manually switch to the strong model, run 2a–2d sequentially per task.
@@ -0,0 +1,164 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: hard-enforce UKit handoff model-tier contract.
3
+ #
4
+ # create (planning) and review must be authored by the strong tier (opus/unic-smart);
5
+ # implement must be authored by at least the code tier (sonnet/unic-code), never lite.
6
+ #
7
+ # Applies identically whether triggered via standalone /ukit:handoff-create,
8
+ # /ukit:handoff-implement, /ukit:handoff-review, or the combined /ukit:handoff-fullstack —
9
+ # the gate is keyed on file content, not on which command was run.
10
+ #
11
+ # Always hard-blocks (exit 2). No advisory/soft mode — the user explicitly asked for
12
+ # strict enforcement of this contract.
13
+
14
+ INPUT=$(cat)
15
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
16
+
17
+ INPUT="$INPUT" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE'
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const payload = (() => {
22
+ try { return JSON.parse(process.env.INPUT || '{}'); } catch { return {}; }
23
+ })();
24
+ const projectRoot = process.env.PROJECT_ROOT;
25
+ const toolName = payload?.tool_name || '';
26
+ const input = payload?.tool_input || {};
27
+
28
+ function block(message) {
29
+ process.stderr.write(`BLOCKED (handoff model-tier guard): ${message}\n`);
30
+ process.exit(2);
31
+ }
32
+
33
+ function tierOf(model) {
34
+ if (!model) return null;
35
+ if (/opus|unic-smart/i.test(model)) return 'smart';
36
+ if (/sonnet|unic-code/i.test(model)) return 'code';
37
+ if (/haiku|unic-lite/i.test(model)) return 'lite';
38
+ return 'unknown';
39
+ }
40
+
41
+ function extractField(text, name) {
42
+ const matches = [...text.matchAll(new RegExp(name + ':\\s*(.+)', 'g'))];
43
+ if (matches.length === 0) return '';
44
+ return matches[matches.length - 1][1].trim();
45
+ }
46
+
47
+ if (toolName === 'Write' || toolName === 'Edit') {
48
+ const filePath = String(input.file_path || '');
49
+ if (!filePath) process.exit(0);
50
+
51
+ const relPath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
52
+ const isPlan = relPath === 'docs/AI_HANDOFF/PLAN.md';
53
+ const taskMatch = relPath.match(/^docs\/AI_HANDOFF\/tasks\/(TASK-\d+)\.md$/);
54
+ if (!isPlan && !taskMatch) process.exit(0);
55
+
56
+ const fileExists = fs.existsSync(filePath);
57
+ const currentContent = fileExists ? fs.readFileSync(filePath, 'utf8') : '';
58
+
59
+ function resultingContent() {
60
+ if (toolName === 'Write') {
61
+ return typeof input.content === 'string' ? input.content : currentContent;
62
+ }
63
+ const oldStr = input.old_string;
64
+ const newStr = input.new_string;
65
+ if (typeof oldStr !== 'string' || typeof newStr !== 'string') return currentContent;
66
+ return input.replace_all
67
+ ? currentContent.split(oldStr).join(newStr)
68
+ : currentContent.replace(oldStr, newStr);
69
+ }
70
+ const newContent = resultingContent();
71
+
72
+ if (isPlan && /## Planner Report/.test(newContent)) {
73
+ const plannerModel = extractField(newContent, 'PLANNER_MODEL');
74
+ if (!plannerModel || /^unknown$/i.test(plannerModel)) {
75
+ block('PLANNER_MODEL missing/unknown in PLAN.md. Planning must run via Agent tool subagent_type: "handoff-planner" (opus/unic-smart) and self-report its model.');
76
+ }
77
+ if (tierOf(plannerModel) !== 'smart') {
78
+ block(`PLANNER_MODEL "${plannerModel}" is not strong/opus tier. Planning must run via Agent tool subagent_type: "handoff-planner" (opus/unic-smart).`);
79
+ }
80
+ }
81
+
82
+ if (taskMatch) {
83
+ const taskId = taskMatch[1];
84
+ const planPath = path.join(projectRoot, 'docs/AI_HANDOFF/PLAN.md');
85
+
86
+ const isFreshTaskFile = !fileExists && !/## Executor Report|## Reviewer Verdict/.test(newContent);
87
+ if (isFreshTaskFile) {
88
+ const planContent = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf8') : '';
89
+ const plannerModel = extractField(planContent, 'PLANNER_MODEL');
90
+ if (!plannerModel || tierOf(plannerModel) !== 'smart') {
91
+ block(`Cannot create ${taskId}.md — PLAN.md has no valid smart-tier PLANNER_MODEL yet. Run planning via Agent tool subagent_type: "handoff-planner" (opus/unic-smart) first.`);
92
+ }
93
+ }
94
+
95
+ if (/## Executor Report/.test(newContent) && !/## Executor Report/.test(currentContent)) {
96
+ const executorModel = extractField(newContent, 'EXECUTOR_MODEL');
97
+ if (!executorModel || /^unknown$/i.test(executorModel)) {
98
+ block(`${taskId}: EXECUTOR_MODEL missing/unknown. Implementation must run via Agent tool subagent_type: "feature-implementer" (sonnet/unic-code) and self-report its model.`);
99
+ }
100
+ if (tierOf(executorModel) === 'lite') {
101
+ block(`${taskId}: EXECUTOR_MODEL "${executorModel}" is lite tier. Implementation must run on at least sonnet/unic-code, never haiku/unic-lite.`);
102
+ }
103
+ }
104
+
105
+ if (/## Reviewer Verdict/.test(newContent) && !/## Reviewer Verdict/.test(currentContent)) {
106
+ const executorModel = extractField(currentContent, 'EXECUTOR_MODEL') || extractField(newContent, 'EXECUTOR_MODEL');
107
+ const reviewerModel = extractField(newContent, 'REVIEWER_MODEL');
108
+ if (!reviewerModel || /^unknown$/i.test(reviewerModel)) {
109
+ block(`${taskId}: REVIEWER_MODEL missing/unknown. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart) and self-report its model.`);
110
+ }
111
+ if (tierOf(reviewerModel) !== 'smart') {
112
+ block(`${taskId}: REVIEWER_MODEL "${reviewerModel}" is not strong/opus tier. Review must run via Agent tool subagent_type: "code-reviewer" (opus/unic-smart).`);
113
+ }
114
+ if (executorModel && reviewerModel && executorModel.toLowerCase() === reviewerModel.toLowerCase()) {
115
+ block(`${taskId}: REVIEWER_MODEL ("${reviewerModel}") matches EXECUTOR_MODEL — reviewer must differ from executor. Re-run review through a different subagent/model.`);
116
+ }
117
+ }
118
+ }
119
+ process.exit(0);
120
+ }
121
+
122
+ if (toolName === 'Bash') {
123
+ const command = String(input.command || '');
124
+ if (!/\bgit\s+push\b/.test(command)) process.exit(0);
125
+
126
+ const activePath = path.join(projectRoot, 'docs/AI_HANDOFF/ACTIVE.md');
127
+ const indexPath = path.join(projectRoot, 'docs/AI_HANDOFF/INDEX.md');
128
+ if (!fs.existsSync(activePath) || !fs.existsSync(indexPath)) process.exit(0); // no handoff cycle here
129
+
130
+ const taskIds = [...fs.readFileSync(indexPath, 'utf8').matchAll(/TASK-\d+/g)]
131
+ .map((m) => m[0])
132
+ .filter((v, i, a) => a.indexOf(v) === i);
133
+
134
+ const problems = [];
135
+ for (const taskId of taskIds) {
136
+ const taskPath = path.join(projectRoot, `docs/AI_HANDOFF/tasks/${taskId}.md`);
137
+ if (!fs.existsSync(taskPath)) continue;
138
+ const text = fs.readFileSync(taskPath, 'utf8');
139
+ if (!text.includes('## Reviewer Verdict')) continue; // not yet reviewed, not this push's concern
140
+
141
+ const executorModel = extractField(text, 'EXECUTOR_MODEL');
142
+ const reviewerModel = extractField(text, 'REVIEWER_MODEL');
143
+
144
+ if (!executorModel || /^unknown$/i.test(executorModel)) problems.push(`${taskId}: EXECUTOR_MODEL missing/unknown`);
145
+ else if (tierOf(executorModel) === 'lite') problems.push(`${taskId}: EXECUTOR_MODEL "${executorModel}" is lite tier`);
146
+
147
+ if (!reviewerModel || /^unknown$/i.test(reviewerModel)) problems.push(`${taskId}: REVIEWER_MODEL missing/unknown`);
148
+ else if (tierOf(reviewerModel) !== 'smart') problems.push(`${taskId}: REVIEWER_MODEL "${reviewerModel}" is not opus/unic-smart tier`);
149
+
150
+ if (executorModel && reviewerModel && executorModel.toLowerCase() === reviewerModel.toLowerCase()) {
151
+ problems.push(`${taskId}: REVIEWER_MODEL == EXECUTOR_MODEL`);
152
+ }
153
+ }
154
+
155
+ if (problems.length > 0) {
156
+ block(`git push refused — handoff model-tier contract violated:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
157
+ }
158
+ process.exit(0);
159
+ }
160
+
161
+ process.exit(0);
162
+ NODE
163
+
164
+ exit $?
@@ -1,5 +1,6 @@
1
1
  {
2
- "fastMode": true,
2
+ "model": "sonnet",
3
+ "fastMode": false,
3
4
  "fastModePerSessionOptIn": false,
4
5
  "alwaysThinkingEnabled": true,
5
6
  "effortLevel": "high",
@@ -76,6 +77,11 @@
76
77
  "type": "command",
77
78
  "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\"",
78
79
  "timeout": 8
80
+ },
81
+ {
82
+ "type": "command",
83
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\"",
84
+ "timeout": 8
79
85
  }
80
86
  ]
81
87
  },
@@ -101,6 +107,11 @@
101
107
  "type": "command",
102
108
  "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh\"",
103
109
  "timeout": 8
110
+ },
111
+ {
112
+ "type": "command",
113
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\"",
114
+ "timeout": 8
104
115
  }
105
116
  ]
106
117
  }