@ngockhoale/ukit 2.2.1 → 2.2.3

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,53 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.2.3 - 2026-08-26
6
+
7
+ Sessions were stalling mid-task — the model would read source, then simply stop, with the work half done and nothing wrong upstream. This wave stops treating that as a prompting problem. Instructions asking a model to "continue until the edit is made" are advisory by nature: every hidden backend behind `unic-lite` / `unic-code` / `unic-smart` / `unic-vision` reads them slightly differently, and the ones that read them loosely stall. UKit now records what actually happened — real Edit/Write receipts, real verification exit codes — and enforces completion mechanically at the point of stopping, identically on Claude Code and omp. Nothing in the enforcement path branches on provider branding.
8
+
9
+ ### Fixed
10
+
11
+ - **Silent stalls were unenforceable, because nothing measured them.** A stop after a read-only pass was indistinguishable from a stop after finished work: both were just the model ending its turn. UKit now keeps a session-scoped execution ledger (`.ukit/storage/cache/exec-ledger/<session>.json`) recording source reads, write attempts and outcomes, and verification commands with their exit codes. A new `Stop` hook (`completion-gate.sh`, and omp's `session_stop`) reads it against the routed contract's `completionEvidence` and blocks a premature stop with the specific missing evidence and a recovery instruction matched to the actual state — no source yet, failed write, no write, failed verification, or no verification each get different guidance. Recovery is capped at 6 continuations, then degrades to a warning, so the gate can never loop.
12
+ - **The omp bridge could exceed omp's own handler budget and freeze the session.** A single `Edit` fired 7 hook scripts as 7 serial `pi.exec` subprocesses at up to 8s each — worst case well past omp's ~30s outer timeout, which surfaces to the user as the session simply hanging. They now run in one bounded `hook-chain-runner.mjs` process: 4s per script, 10s total budget, under a 12s outer timeout. Per-event latency is appended to `.ukit/storage/cache/hook-latency/<session>.jsonl` so hook cost stops being invisible.
13
+ - **omp writes bypassed the safety gates entirely.** omp's `normalizeToolEventInput` emits `path` / `paths`; every UKit hook script reads `file_path`. So `protect-files.sh` and `stale-spec-guard.sh` saw no target on an omp edit and waved it through. The bridge now normalizes `path` / `paths[0]` → `file_path` at the host boundary, without mutating the caller's event.
14
+ - **An unmapped mutation-capable tool failed open and silently.** `mapToolName` returned `null` for any tool not in its table, and the bridge ran an empty script chain — so a host tool that writes files but is not yet mapped skipped every guard with no signal. Such tools are now blocked with a message naming the tool; unmapped read-only tools still pass through.
15
+ - **The ledger CLI silently did nothing under a symlinked project root.** Its main-guard compared `fileURLToPath(import.meta.url)` against `path.resolve(process.argv[1])`. Under macOS `/tmp` → `/private/tmp`, or any symlinked work directory, those differ — so `--record` and `--evaluate-stop` returned exit 0 having done nothing: no receipts, no gate. The whole unit suite stayed green; only a scratch install under `/tmp` exposed it. Both paths are now compared by realpath, covered by `tests/hooks/executionLedgerCli.test.js`.
16
+ - **`.omp/config.yml` documented the model aliases backwards**, calling the UNIC names "LITERAL model names, not aliases". They are stable provider-neutral aliases whose hidden backends change during development — which is precisely why orchestration must not branch on vendor names.
17
+
18
+ ### Added
19
+
20
+ - **`tests/hooks/executionLedgerCli.test.js`** (4 tests) — drives the real CLI through a symlinked root: receipts get written, a premature stop is blocked, a stop with write + passing verification is allowed, and the continuation cap engages at exactly 6. Verified RED against the pre-fix guard (3 of 4 fail).
21
+ - **Manifest entries and dev-mirror parity for the four new files** — `record-execution.sh`, `completion-gate.sh`, `execution-ledger.mjs`, `hook-chain-runner.mjs`. `templates/.claude/` is gitignored and existing template files were force-added, so new ones are invisible to both git and `npm pack` unless added the same way; verified present in the packed tarball, with hook scripts executable after a real `ukit install`.
22
+
23
+ ## 2.2.2 - 2026-08-22
24
+
25
+ Running Claude Code and omp side by side exposed that they were not, in fact, running the same UKit. `CLAUDE.md` and `AGENTS.md` are hand-maintained forks of one instruction set read by different harnesses, and `AGENTS.md` had fallen three minor versions behind — so omp was never told the model-tier table existed. This wave re-unifies them and adds a test that makes the drift impossible to repeat.
26
+
27
+ It also settles the three `TODO(verify)` items 2.2.1 shipped with. Those were plan-time guesses about omp's API, unresolvable at the time because omp was not installed on any machine that could answer them. omp v17.4.2 is now installed and was inspected directly — and two of the three guesses were wrong, in the quiet way where nothing errors and the feature simply never engages.
28
+
29
+ ### Fixed
30
+
31
+ - **Auto-compact was never actually configured on omp.** `.omp/config.yml` set `compact.autoCompactWindow: 180000`, a key omp does not have — `omp config get compact.autoCompactWindow` answers "Unknown setting", so the value was inert and omp stayed on its own default. The real key is `compaction.thresholdTokens` ("Fixed token limit for context maintenance; overrides percentage if set", omp's default `-1`). Verified end-to-end against a scratch project: the nested form now reads back as `180000`, which is below `compact.hardCapTokens` (220000) as intended, so auto-compact fires before `context-hardcap-gate.sh` starts blocking tool calls.
32
+ - **Post-compact hooks never ran on omp.** The bridge assumed omp re-emitted `session_start` after a compaction, the way Claude Code's matcher-less `SessionStart` hook fires with `source: "compact"`. It does not — it has a dedicated event instead (`session_before_compact` → `session.compacting` → `session_compact`, the last firing once compaction settles). So on omp the entire post-compact chain was lost: the run cursor stayed un-surfaced, compact pressure was never reset, and a mid-cycle handoff silently failed to resume after a compaction. `ukit-bridge.js` now maps `session_compact` onto the same script chain, keeping `hook_event_name: 'SessionStart'` in the payload so the scripts stay shared with Claude Code rather than forked.
33
+ - **Two UNIC gateway probes could never have fired.** `unic-gateway.mjs` probed an `OMP_BASE_URL` env var and a `baseUrl` key in `.omp/config.yml`. Neither exists: omp's anthropic provider reads `ANTHROPIC_BASE_URL` (falling back to `https://api.anthropic.com`), and its settings schema declares no `baseUrl` at any level. Both probes are removed rather than left returning a permanent `false` — a probe that can never fire is worse than no probe, because it reads as coverage. Detection is unchanged in behaviour and now literally matches what the docs already claimed: routing is decided only by `ANTHROPIC_BASE_URL`, on omp for the same reason as on Claude Code.
34
+ - **omp behaved as if UKit only had one model, because it was never told otherwise.** `templates/AGENTS.md` — the context doc omp, Codex and OpenCode read — was missing the entire `3-Tier Model Routing` block, plus `Post-Edit Sidecar Review` and `Skill Quality`, and still announced itself as `UKit v1.5.1`. `.omp/config.yml` declared `modelRoles` for all four lanes, but nothing in omp's context ever said *when* to use a tier, so everything ran inline on the session model.
35
+ - **Five section pairs had been renamed apart** — `Core Rule`/`Core UKit Rule`, `Fast Classification`/`Fast Task Classification`, `Internal Helper Policy`/`Internal Helper Routing`, `Selective Subagent Policy`/`Subagent Policy`, `Project Snapshot`/`Environment Snapshot`. This is what let the drift hide: once the headings differ, no diff lines up and the gap is invisible. Headings are now shared, and the two docs carry byte-identical bodies for every shared section.
36
+ - **Every session opened by being told to resume a finished cycle.** `docs/AI_HANDOFF/RUN.md` was left at `Phase: R4` after 2.2.1 shipped, so `handoff-resume.sh` — which runs in the `session_start` chain of *both* Claude Code and the omp bridge — fired a "CONTINUATION — resume immediately" banner at the start of every new, unrelated session. The cursor now reads `Phase: done`.
37
+ - **`/ukit:*` commands re-pasted the user's whole problem statement up to seven times per file.** Only one `$ARGUMENTS` site per command is a real interpolation (the `## Problem / feature` body); the other six were *references* that expanded to the entire paragraph again mid-sentence. They now say "the **Problem / feature** section above".
38
+ - **Eight fallback lines told the model to do something only a human can do.** `> Other tools without subagent support: manually switch to the strong model` reads, to a running model, as an instruction to itself — which it cannot follow, so it improvises. Each is now explicitly addressed: `> **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:**`.
39
+
40
+ ### Added
41
+
42
+ - **`### How a tier is actually bound`** in both context docs. The main session model never changes mid-turn; a tier only takes effect when work is handed to an agent whose own definition binds that model. The section carries a harness table (Claude Code `.claude/agents/*.md` + `subagent_type`, omp `.omp/agents/*.md` + `task-agent`) and states the consequence plainly: doing everything inline is exactly what makes UKit behave as if it had one model.
43
+ - **`tests/consistency/contextDocsParity.test.js`** (29 tests) — every `## ` section must exist in both `templates/CLAUDE.md` and `templates/AGENTS.md` with byte-identical bodies, unless allowlisted with a stated reason (currently one entry: `Session Start — OpenCode`, which exists because OpenCode does not auto-load `.claude/skills/`). Also asserts neither doc hardcodes a version, and that both manifest items declare the same variable set.
44
+ - **`.omp/RULES.md` grew from 4 rules to 7**, targeting direct chat specifically — direct chat is steered only by static context, so what is missing there is simply never applied. New: `Classify first, then act` (Trivial / Simple / Non-trivial lanes, decided *before* the first tool call), `Auto-activate skills`, and `Delegate to bind a model tier`. It stays short on purpose: it is re-attached near every turn.
45
+ - **The three `/ukit:*` sites that named only Claude Code now name omp too** — `handoff-fullstack.md`'s Model Split note, and the small-task-maintainer lines in `handoff-clear.md` and `handoff-status.md`.
46
+
47
+ ### Changed
48
+
49
+ - **Four more expired one-shot guards re-anchored, same class as the two below.** `ompDocsSync` cases 3 and 6 validated "the top CHANGELOG entry" — true on the day 2.2.1 shipped, false at the next release; they now find the omp entry by version. `ompContextLayer` case 10 guarded the compact key with a `TODO(verify)` marker instead of a value, and now asserts the verified key plus its provenance. `tests/handoff/cycle4/unic-gateway.test.mjs` case 13b had never actually run (it took its skip branch because the file it checked did not exist yet) and was matching a key spelling — `@vision:` — that belongs to the agent side, not the config side.
50
+ - **`tests/consistency/ompCommandParity.test.js` cases 4 and 5 no longer diff against `git show HEAD:`.** They were scope guards for one in-flight cycle ("TASK-005 must not modify these"), and that premise expired when the cycle shipped: against a committed baseline they pass vacuously, and against a dirty tree they block every later legitimate edit instead — which is exactly what they did. Both are re-expressed as the durable properties they stood in for (no `subagent_type` site in the two non-delegation commands; pinned per-file `Do NOT` and tier-justification-clause counts). No protection was dropped, only re-anchored.
51
+
5
52
  ## 2.2.1 - 2026-08-22
6
53
 
7
54
  UKit gains a fourth runtime and loses one. omp (Oh My Pi) reaches full parity with the Claude Code workspace — same agents, same skills, same hooks, same `/ukit:*` commands — and the Antigravity adapter, unused and unmaintained, is gone. The parity is deliberate rather than incidental: the port reuses the existing `.claude/` assets wherever omp's discovery can already reach them, so there is one source of truth per capability, not two that drift.
@@ -110,13 +110,21 @@ items:
110
110
  - docs-project
111
111
  - docs-memory
112
112
  mergeStrategy: overwrite_with_backup
113
+ # Same set as root-claude-md: AGENTS.md is the same body with a different H1 plus one
114
+ # OpenCode-only section, so it interpolates the same variables. Kept in sync by
115
+ # tests/consistency/contextDocsParity.test.js.
113
116
  variables:
114
117
  - project.name
118
+ - project.root
115
119
  - project.stack
116
120
  - stack.frontend
117
121
  - stack.backendApi
118
122
  - stack.postgres
119
123
  - runtime.packageManager
124
+ - runtime.os
125
+ - runtime.nodeVersion
126
+ - providers.unic
127
+ - ukit.version
120
128
  enabledByDefault: true
121
129
  packs:
122
130
  - core
@@ -1029,6 +1037,30 @@ items:
1029
1037
  packs:
1030
1038
  - core
1031
1039
 
1040
+ - id: hook-record-execution
1041
+ type: hook
1042
+ sourceTemplate: .claude/hooks/record-execution.sh
1043
+ targetPath: .claude/hooks/record-execution.sh
1044
+ requires:
1045
+ - ukit-runtime-execution-ledger-script
1046
+ mergeStrategy: overwrite_with_backup
1047
+ variables: []
1048
+ enabledByDefault: true
1049
+ packs:
1050
+ - core
1051
+
1052
+ - id: hook-completion-gate
1053
+ type: hook
1054
+ sourceTemplate: .claude/hooks/completion-gate.sh
1055
+ targetPath: .claude/hooks/completion-gate.sh
1056
+ requires:
1057
+ - ukit-runtime-execution-ledger-script
1058
+ mergeStrategy: overwrite_with_backup
1059
+ variables: []
1060
+ enabledByDefault: true
1061
+ packs:
1062
+ - core
1063
+
1032
1064
  - id: hook-auto-allow-bash
1033
1065
  type: hook
1034
1066
  sourceTemplate: .claude/hooks/auto-allow-bash.sh
@@ -1215,6 +1247,28 @@ items:
1215
1247
  packs:
1216
1248
  - core
1217
1249
 
1250
+ - id: ukit-runtime-execution-ledger-script
1251
+ type: config
1252
+ sourceTemplate: .claude/ukit/runtime/execution-ledger.mjs
1253
+ targetPath: .claude/ukit/runtime/execution-ledger.mjs
1254
+ requires: []
1255
+ mergeStrategy: overwrite_with_backup
1256
+ variables: []
1257
+ enabledByDefault: true
1258
+ packs:
1259
+ - core
1260
+
1261
+ - id: ukit-runtime-hook-chain-runner-script
1262
+ type: config
1263
+ sourceTemplate: .claude/ukit/runtime/hook-chain-runner.mjs
1264
+ targetPath: .claude/ukit/runtime/hook-chain-runner.mjs
1265
+ requires: []
1266
+ mergeStrategy: overwrite_with_backup
1267
+ variables: []
1268
+ enabledByDefault: true
1269
+ packs:
1270
+ - core
1271
+
1218
1272
  - id: ukit-runtime-safe-patch-core-script
1219
1273
  type: config
1220
1274
  sourceTemplate: .claude/ukit/runtime/safe-patch-core.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,7 +57,7 @@ Cleared: X worktrees removed, Y branches deleted, staged changes unstaged.
57
57
  Handoff docs reset. Ready for new cycle.
58
58
  ```
59
59
 
60
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku).
60
+ > Claude Code: spawn `ukit-small-task-maintainer` (haiku). omp: the `task` tool with `agent: "ukit-small-task-maintainer"`.
61
61
  > Other tools: lite model, follow steps 1–5.
62
62
 
63
63
  **Next:** `/ukit:handoff-create <new problem description>`
@@ -22,7 +22,7 @@ Planning is the **only** place in the handoff pipeline where asking the human is
22
22
  it is where every ambiguity must be burned off. Whatever is left unresolved here becomes a
23
23
  guess during implementation, where nobody is available to correct it.
24
24
 
25
- **Ask once, up front, batched.** Before Step 1, if `$ARGUMENTS` leaves a choice that changes
25
+ **Ask once, up front, batched.** Before Step 1, if the **Problem / feature** section above leaves a choice that changes
26
26
  *what gets built* — not how — collect every such question into a **single** `AskUserQuestion`
27
27
  call (max 4 questions, each with a `(Recommended)` first option). Then close the window.
28
28
 
@@ -49,7 +49,7 @@ After the batched call, run Steps 1–2.5 to completion without further question
49
49
  4. Read `docs/AI_HANDOFF/tasks/_TEMPLATE.md` → task file structure
50
50
  5. Return a compact summary. Do NOT write anything yet.
51
51
 
52
- > Other tools without subagent support: manually switch to the lite model, run the steps above yourself, keep the summary in context.
52
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the lite model, run the steps above yourself, keep the summary in context.
53
53
 
54
54
  ---
55
55
 
@@ -106,7 +106,7 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
106
106
 
107
107
  7. Report: task IDs, dependency graph, any `needs_breakdown` + reason
108
108
 
109
- > Other tools without subagent support: manually switch to the strong model, execute steps 1–7 above yourself.
109
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the strong model, execute steps 1–7 above yourself.
110
110
 
111
111
  ---
112
112
 
@@ -127,7 +127,7 @@ intact. What it no longer does is hand a stalled plan back and wait.
127
127
  2. `Issues Found` → route back to Step 2: planner revises `PLAN.md` and the affected `TASK-xxx.md` files to address every finding, then re-submit for another Step 2.5 review (this becomes the next round). Do NOT commit or hand off to executor on `Issues Found`.
128
128
  3. `Approved` → append `PLAN_REVIEW: Approved by <reviewer model>` to PLAN.md's `## Planner Report` footer, then proceed.
129
129
 
130
- > Other tools without subagent support: manually switch to the strong model in a **separate** chat/session from Step 2, paste PLAN.md, review using the Spec/Plan Review checklist in `.claude/agents/code-reviewer.md`.
130
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the strong model in a **separate** chat/session from Step 2, paste PLAN.md, review using the Spec/Plan Review checklist in `.claude/agents/code-reviewer.md`.
131
131
 
132
132
  ---
133
133
 
@@ -12,6 +12,8 @@
12
12
  | Planning + Review (reasoning, design, verification) | opus · unic-smart · strongest available |
13
13
 
14
14
  > Claude Code: spawn `ukit-small-task-maintainer` (haiku) for light steps; spawn `handoff-planner` (opus) for planning; spawn `feature-implementer` (sonnet) agents for implementation; spawn `code-reviewer` (opus) per task for review.
15
+ >
16
+ > omp: same four agents, launched with the `task` tool (`agent: "<name>"`). They live in `.omp/agents/` and bind their tier through `model: "@lite"` / `"@code"` / `"@smart"`, resolved via `modelRoles` in `.omp/config.yml` — you do not pass a model name.
15
17
 
16
18
  > **omp model tiers:** the tiers above map to `.omp/config.yml`'s `modelRoles`, referenced from agent frontmatter as `@lite` / `@code` / `@smart`.
17
19
 
@@ -20,17 +22,17 @@
20
22
  ## Problem / feature
21
23
  $ARGUMENTS
22
24
 
23
- > **If `$ARGUMENTS` is empty:** Do NOT proceed. Ask the user: "What problem or feature would you like to tackle in this handoff cycle? Please describe it in one or more sentences." Wait for the answer before running any phase.
25
+ > **If the **Problem / feature** section above is empty:** Do NOT proceed. Ask the user: "What problem or feature would you like to tackle in this handoff cycle? Please describe it in one or more sentences." Wait for the answer before running any phase.
24
26
 
25
27
  ---
26
28
 
27
29
  ## Autonomy Contract — read before anything else
28
30
 
29
31
  This command is a **one-shot pipeline**. The human is not watching. Treat every phase below
30
- as unattended: they may walk away after submitting `$ARGUMENTS` and come back to a finished
32
+ as unattended: they may walk away after submitting the request and come back to a finished
31
33
  result.
32
34
 
33
- **The single question window is P0, before any file is written.** If `$ARGUMENTS` leaves a
35
+ **The single question window is P0, before any file is written.** If the **Problem / feature** section above leaves a
34
36
  choice that would change what gets built — not how it gets built — batch every such question
35
37
  into **one** `AskUserQuestion` call (max 4 questions, each with a `(Recommended)` first
36
38
  option) and resolve them all at once. Record the answers in `PLAN.md` §1.
@@ -62,7 +64,7 @@ numbered step completes:
62
64
 
63
65
  ```
64
66
  Command: handoff-fullstack
65
- Goal: <one sentence from $ARGUMENTS>
67
+ Goal: <one sentence from the **Problem / feature** section above>
66
68
  Base: <BASE branch>
67
69
  Phase: <P1|P2|P2.5|P3|I1|I2|I3|I4|R1|R2|R3|R4|R5|done>
68
70
  Cursor: wave <N> batch <M> — <what just finished>
@@ -80,7 +82,7 @@ continue. Read the cursor, then jump straight to `Next:` and carry on. Tasks alr
80
82
  or `pending_review` are skipped; only `ready`, `in_progress`, `changes_requested` and
81
83
  `blocked` tasks are picked up.
82
84
 
83
- Only when `RUN.md` is absent or `Phase: done` does `$ARGUMENTS` start a fresh cycle.
85
+ Only when `RUN.md` is absent or `Phase: done` does a new request start a fresh cycle.
84
86
 
85
87
  ---
86
88
 
@@ -97,11 +99,11 @@ Only when `RUN.md` is absent or `Phase: done` does `$ARGUMENTS` start a fresh cy
97
99
 
98
100
  Return a compact summary. Do NOT write any files yet.
99
101
 
100
- > Other tools without subagent support: manually switch to the lite model and run P1 yourself.
102
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the lite model and run P1 yourself.
101
103
 
102
104
  ### P2 — Write PLAN.md + task files (strong model)
103
105
 
104
- **Claude Code — MANDATORY, do this before anything else in P2:** call the Agent tool with `subagent_type: "handoff-planner"` (omp: the `task` tool with `agent: "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.
106
+ **Claude Code — MANDATORY, do this before anything else in P2:** call the Agent tool with `subagent_type: "handoff-planner"` (omp: the `task` tool with `agent: "handoff-planner"`), passing it the P1 summary and the **Problem / feature** section above. 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.
105
107
 
106
108
  The planner agent does the following (use P1 summary — do NOT re-read files):
107
109
 
@@ -168,7 +170,7 @@ is written. What it no longer does is hand a stalled plan back to a human who is
168
170
  2. `Issues Found` → route back to P2: `handoff-planner` revises `PLAN.md` and the affected `TASK-xxx.md` files to address every finding, then re-submit for one more P2.5 review round — subject to the loop cap above.
169
171
  3. `Approved` → append `PLAN_REVIEW: Approved by <reviewer model>` to PLAN.md's `## Planner Report` footer, then proceed to P3.
170
172
 
171
- > Other tools without subagent support: manually switch to the strong model in a **separate** chat/session from P2, paste PLAN.md, review using the Spec/Plan Review checklist in `.claude/agents/code-reviewer.md`.
173
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the strong model in a **separate** chat/session from P2, paste PLAN.md, review using the Spec/Plan Review checklist in `.claude/agents/code-reviewer.md`.
172
174
 
173
175
  ### P3 — Commit the plan (lite model)
174
176
 
@@ -179,7 +181,7 @@ git add docs/AI_HANDOFF/ && git commit -m "handoff: plan — <goal>"
179
181
 
180
182
  Replace `<goal>` with the one-sentence goal from ACTIVE.md. This locks the plan in git before any implementation begins.
181
183
 
182
- > Other tools without subagent support: manually switch to the lite model for P2/P3.
184
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the lite model for P2/P3.
183
185
 
184
186
  ---
185
187
 
@@ -58,7 +58,7 @@ If this file already exists with `Phase:` not `done` when the command starts, th
58
58
  ## Step 1 — Setup
59
59
 
60
60
  Read `docs/AI_HANDOFF/ACTIVE.md` → get `Base: <BASE>`.
61
- Read `docs/AI_HANDOFF/INDEX.md` → collect `ready` tasks (or specific task from $ARGUMENTS).
61
+ Read `docs/AI_HANDOFF/INDEX.md` → collect `ready` tasks (or the specific task named in the request above).
62
62
  If there are no `ready` tasks, check for `changes_requested`/`blocked` ones and run those
63
63
  instead. Only if nothing is actionable → report and stop.
64
64
 
@@ -163,7 +163,7 @@ disk in the task file, which is where the reviewer reads them from. Pasting them
163
163
  second time is what blows up the orchestrator's context window and kills the run mid-wave.
164
164
  ```
165
165
 
166
- > Other tools without subagent support: open each task in a separate session with the code model.
166
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** open each task in a separate session with the code model.
167
167
 
168
168
  ### 3c — Orchestrator: copy changes to main + IMMEDIATELY delete worktree
169
169
 
@@ -188,4 +188,4 @@ This is the only place this command may ask for a compaction. All state lives in
188
188
  `INDEX.md` and `RUN.md`, so a compacted or fresh session resumes with nothing lost.
189
189
 
190
190
  > Orchestrator (this session) handles Step 1, 2e, and 3 directly — those are not delegated.
191
- > Other tools without subagent support: manually switch to the strong model, run 2a–2d per task (one session at a time).
191
+ > **For the human operator, on a tool with no agent support (Codex, OpenCode) — not an instruction to the model:** manually switch to the strong model, run 2a–2d per task (one session at a time).
@@ -45,5 +45,5 @@ Next action:
45
45
  no active cycle → /ukit:handoff-create <description>
46
46
  ```
47
47
 
48
- > Claude Code: spawn `ukit-small-task-maintainer` (haiku) with the above prompt.
48
+ > Claude Code: spawn `ukit-small-task-maintainer` (haiku) with the above prompt. omp: the `task` tool with `agent: "ukit-small-task-maintainer"`.
49
49
  > Other tools: use lite model, run the reads above, format the report.
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ # Stop hook: block premature terminal stops while routed completion evidence is missing.
3
+
4
+ INPUT=$(cat)
5
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
6
+ SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/execution-ledger.mjs"
7
+
8
+ if [ ! -f "$SCRIPT" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ printf '%s' "$INPUT" | UKIT_HARNESS=claude-code node "$SCRIPT" --evaluate-stop
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: persist session-scoped source/write/verification receipts.
3
+
4
+ INPUT=$(cat)
5
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
6
+ SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/execution-ledger.mjs"
7
+
8
+ if [ ! -f "$SCRIPT" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ printf '%s' "$INPUT" | UKIT_HARNESS=claude-code node "$SCRIPT" --record
@@ -139,6 +139,16 @@
139
139
  }
140
140
  ],
141
141
  "PostToolUse": [
142
+ {
143
+ "matcher": "Read|Grep|Glob",
144
+ "hooks": [
145
+ {
146
+ "type": "command",
147
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.sh\"",
148
+ "timeout": 4
149
+ }
150
+ ]
151
+ },
142
152
  {
143
153
  "matcher": "Edit|Write",
144
154
  "hooks": [
@@ -146,6 +156,11 @@
146
156
  "type": "command",
147
157
  "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-verify.sh\"",
148
158
  "timeout": 8
159
+ },
160
+ {
161
+ "type": "command",
162
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.sh\"",
163
+ "timeout": 4
149
164
  }
150
165
  ]
151
166
  },
@@ -156,6 +171,11 @@
156
171
  "type": "command",
157
172
  "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/compress-output.sh\"",
158
173
  "timeout": 8
174
+ },
175
+ {
176
+ "type": "command",
177
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.sh\"",
178
+ "timeout": 4
159
179
  }
160
180
  ]
161
181
  }
@@ -181,6 +201,17 @@
181
201
  ]
182
202
  }
183
203
  ],
204
+ "Stop": [
205
+ {
206
+ "hooks": [
207
+ {
208
+ "type": "command",
209
+ "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/completion-gate.sh\"",
210
+ "timeout": 4
211
+ }
212
+ ]
213
+ }
214
+ ],
184
215
  "PreCompact": [
185
216
  {
186
217
  "hooks": [
@@ -13,10 +13,13 @@
13
13
  * 1. process.env.ANTHROPIC_BASE_URL -> 'env'
14
14
  * 2. project .claude/settings.json -> env.ANTHROPIC_BASE_URL -> 'claude-settings'
15
15
  * 3. ~/.claude/settings.json -> env.ANTHROPIC_BASE_URL -> 'claude-settings'
16
- * 4. process.env.OMP_BASE_URL (TASK-004, name unverified) -> 'omp-env'
17
- * 5. project .omp/config.yml -> baseUrl (TASK-004, key unverified) -> 'omp-config'
18
16
  *
19
- * Every probe here (including the two omp probes) is scoped to what changes the CALLING
17
+ * omp needs no probe of its own: verified against omp v17.4.2, it reads `ANTHROPIC_BASE_URL`
18
+ * for the anthropic provider, so probe 1 covers an omp session for the same reason it covers a
19
+ * Claude Code one. See the note above the probe list below for the two guessed omp probes this
20
+ * replaced, and why they could never have fired.
21
+ *
22
+ * Every probe here is scoped to what changes the CALLING
20
23
  * runtime's OWN outbound endpoint — same rule that already governs probes 1-3. Cross-tool
21
24
  * configs are NOT probed. This module is only ever invoked from Claude Code / omp hooks
22
25
  * (vision-router.sh, vision-gate.sh, route-task.mjs, and the omp bridge) to gate what THIS
@@ -51,21 +54,23 @@ import { fileURLToPath } from 'node:url';
51
54
  const UNIC_TOKEN = 'unicjsc.com';
52
55
  const VISION_MODEL_ALIAS = 'unic-vision';
53
56
 
54
- // TODO(verify): omp's own base-URL env var name is a guess -- omp is not installed on this
55
- // machine and no source/schema is vendored, so this has never been confirmed against a real
56
- // omp runtime. Resolve via TASK-008's manual smoke test before treating this as confirmed.
57
- const OMP_BASE_URL_ENV_VAR = 'OMP_BASE_URL';
58
-
59
- // TODO(verify): the `.omp/config.yml` key path (`baseUrl`, top-level) is a guess for the same
60
- // reason as OMP_BASE_URL_ENV_VAR above. Deliberately matched with a narrow anchored regex
61
- // instead of importing the `yaml` package at runtime -- this file is installed into end-user
62
- // projects via `ukit install` and must not risk depending on `yaml` being resolvable from a
63
- // target project's own node_modules tree for a single-key check. This mirrors the codebase's
64
- // own established precedent for the analogous Codex `config.toml` probe (see git history:
65
- // CODEX_BASE_URL_UNIC_RE, pre-TASK-007), which rejected a TOML parser dependency for the same
66
- // reason and documented the trade-off: a malformed/unexpected file just degrades to no hit,
67
- // which fails safe rather than throwing.
68
- const OMP_CONFIG_BASE_URL_UNIC_RE = /^\s*baseUrl\s*:\s*["']?[^"'\r\n#]*unicjsc\.com[^"'\r\n#]*["']?\s*(?:#.*)?$/m;
57
+ // RESOLVED 2026-08-22 against a real omp v17.4.2 install -- both omp-specific probes that
58
+ // shipped in 2.2.1 were guesses, and both were wrong:
59
+ //
60
+ // * `OMP_BASE_URL` does not exist in omp. Its bundled provider resolver reads
61
+ // `ANTHROPIC_BASE_URL` for the anthropic provider, falling back to
62
+ // "https://api.anthropic.com" -- i.e. the exact variable probe 1 already reads.
63
+ // * `.omp/config.yml` has no `baseUrl` key at any level. `omp config get baseUrl` answers
64
+ // "Unknown setting", and omp's settings schema declares none.
65
+ //
66
+ // So omp needs no probe of its own: an omp session pointed at the UNIC gateway is detected by
67
+ // probe 1, on the same variable and for the same reason as a Claude Code session. That is what
68
+ // makes the rule stated in CLAUDE.md/AGENTS.md ("routing is decided ONLY by ANTHROPIC_BASE_URL")
69
+ // literally true on omp rather than an approximation of it. Both dead probes are removed rather
70
+ // than left in place returning a permanent false -- a probe that can never fire is worse than no
71
+ // probe, because it reads as coverage. Custom per-model `baseUrl` values do exist in omp, but
72
+ // they live in its models config, not in `.omp/config.yml`; if that ever needs probing it is a
73
+ // new probe against a verified path, not a revival of these.
69
74
 
70
75
  function safeReadFile(filePath) {
71
76
  try {
@@ -115,12 +120,6 @@ function probeClaudeSettings(filePath) {
115
120
 
116
121
  // Narrow anchored regex probe (no YAML parsing) for the omp config file's `baseUrl` key. A
117
122
  // missing file, unreadable path, or malformed YAML all degrade to "no hit" -- never throws.
118
- function probeOmpConfig(filePath) {
119
- const raw = safeReadFile(filePath);
120
- if (raw == null) return false;
121
- return OMP_CONFIG_BASE_URL_UNIC_RE.test(raw);
122
- }
123
-
124
123
  /**
125
124
  * Checks whether the Codex model catalog exists and, if so, whether it lists `unic-vision`.
126
125
  * A missing/unreadable/malformed catalog is never an error — it degrades to "unknown" and the
@@ -163,8 +162,6 @@ export function detectUnicGateway(options = {}) {
163
162
  () => recordHit(probeEnvVar('ANTHROPIC_BASE_URL'), 'env'),
164
163
  () => recordHit(probeClaudeSettings(path.join(rootDir, '.claude', 'settings.json')), 'claude-settings'),
165
164
  () => recordHit(probeClaudeSettings(path.join(homeDir, '.claude', 'settings.json')), 'claude-settings'),
166
- () => recordHit(probeEnvVar(OMP_BASE_URL_ENV_VAR), 'omp-env'),
167
- () => recordHit(probeOmpConfig(path.join(rootDir, '.omp', 'config.yml')), 'omp-config'),
168
165
  ];
169
166
  for (const probe of probes) {
170
167
  try {