@ngockhoale/ukit 3.4.67 → 3.4.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/manifests/documentation.yaml +33 -0
  3. package/manifests/instructionRules.yaml +10 -0
  4. package/manifests/platform.full.yaml +67 -0
  5. package/package.json +1 -1
  6. package/src/core/applyPlan.js +111 -2
  7. package/src/core/buildPlan.js +3 -1
  8. package/src/core/diffPlan.js +49 -1
  9. package/src/core/executionContracts.js +49 -8
  10. package/src/core/metadata.js +45 -0
  11. package/src/core/runInstallPipeline.js +30 -2
  12. package/src/core/uninstall.js +106 -2
  13. package/src/manifest/validateManifest.js +25 -1
  14. package/template_project/.claude/agents/code-reviewer.md +22 -9
  15. package/template_project/.claude/agents/handoff-planner.md +97 -8
  16. package/template_project/.claude/commands/ukit/handoff-create.md +114 -19
  17. package/template_project/.claude/commands/ukit/handoff-fullstack.md +247 -79
  18. package/template_project/.claude/commands/ukit/handoff-implement.md +75 -40
  19. package/template_project/.claude/commands/ukit/handoff-review.md +14 -10
  20. package/template_project/.claude/hooks/auto-allow-bash.sh +73 -12
  21. package/template_project/.claude/hooks/auto-prune-bash.sh +10 -0
  22. package/template_project/.claude/hooks/delegation-gate.mjs +21 -0
  23. package/template_project/.claude/hooks/handoff-model-guard.sh +22 -6
  24. package/template_project/.claude/hooks/handoff-resume.sh +141 -1
  25. package/template_project/.claude/hooks/record-execution.mjs +140 -9
  26. package/template_project/.claude/hooks/skill-router.sh +173 -8
  27. package/template_project/.claude/settings.json +2 -2
  28. package/template_project/.claude/skills/advisor-plan/REFERENCE.md +10 -0
  29. package/template_project/.claude/skills/advisor-plan/SKILL.md +2 -2
  30. package/template_project/.claude/ukit/index/allocate-task-worktree.mjs +15 -0
  31. package/template_project/.claude/ukit/index/amend-guard.mjs +116 -0
  32. package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
  33. package/template_project/.claude/ukit/index/land-worktree.mjs +243 -21
  34. package/template_project/.claude/ukit/index/plan-count.mjs +115 -0
  35. package/template_project/.claude/ukit/index/ready-gate.mjs +170 -0
  36. package/template_project/.claude/ukit/index/reset-auto-permissions.mjs +293 -4
  37. package/template_project/.claude/ukit/index/route-resolver.mjs +48 -8
  38. package/template_project/.claude/ukit/index/route-task.mjs +180 -11
  39. package/template_project/.claude/ukit/index/source-snapshot.mjs +172 -0
  40. package/template_project/.claude/ukit/index/task-budget-validator.mjs +109 -3
  41. package/template_project/.claude/ukit/runtime/async-lock.mjs +3 -3
  42. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +20 -3
  43. package/template_project/.claude/ukit/runtime/handoff-task-scope.mjs +205 -0
  44. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  45. package/template_project/.claude/ukit/runtime/plan-dispatch.mjs +32 -5
  46. package/template_project/.claude/ukit/runtime/resumable-run.mjs +310 -21
  47. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +405 -105
  48. package/template_project/.claude/ukit/runtime/task-record-reconciliation.mjs +46 -0
  49. package/template_project/.claude/ukit/runtime/task-watchdog.mjs +28 -2
  50. package/template_project/.claude/ukit/runtime/worktree-allocation.mjs +75 -0
  51. package/template_project/.codex/README.md +25 -0
  52. package/template_project/.omp/RULES.md +22 -22
  53. package/template_project/.omp/agents/code-reviewer.md +22 -9
  54. package/template_project/.omp/agents/handoff-planner.md +102 -12
  55. package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
  56. package/template_project/AGENTS.md +5 -1
  57. package/template_project/CLAUDE.md +1 -1
  58. package/template_project/docs/AI_HANDOFF/RULES.md +8 -0
  59. package/template_project/docs/AI_HANDOFF/tasks/_TEMPLATE.md +8 -1
  60. package/template_project/instructions/core.md +5 -1
  61. package/template_project/instructions/layout.yaml +2 -0
  62. package/template_project/instructions/overlays/omp-rules.md +22 -22
  63. package/template_project/ukit/storage/config.json +1 -1
@@ -64,7 +64,10 @@ ones and run those instead. A stuck record that cannot resume (orphaned worktree
64
64
  contradicting reports) gets the recovery treatment: mark it `cancelled_superseded`, create
65
65
  `TASK-xxx-R1` with the same spec references/acceptance/verification, and run the
66
66
  replacement — never leave stranded work just because a record was pending.
67
- Only if nothing is actionable → report and stop.
67
+ Only if nothing is actionable → report and stop, naming the gate: when INDEX has
68
+ `needs_breakdown` tasks while PLAN.md lacks `PLAN_REVIEW: Approved`, report exactly
69
+ `tasks not ready: <gate> -> run /ukit:handoff-create resume gates` — implementation does
70
+ not re-plan and does not run the gates itself; the create command owns that resume path.
68
71
 
69
72
  Verify working tree state:
70
73
  ```bash
@@ -120,11 +123,20 @@ wave, never reorders it across waves.
120
123
 
121
124
  For each wave (and for each batch within it):
122
125
 
123
- ### 3a — Create worktrees (from `$BASE`, no extra branch)
126
+ ### 3a — Create worktrees (from the `Base:` branch recorded in ACTIVE.md, no extra branch)
127
+ For `Worktree-Policy: existing-only`, never use the raw command below or Agent isolation.
128
+ An explicit owner exception is one RUN.md line per frozen task:
129
+ `Worktree-Allow: TASK-003 handoff/task-003 .worktrees/task-003 owner-20261025`.
130
+ Allocate only through `node .claude/ukit/index/allocate-task-worktree.mjs <absolute-project-root> TASK-003`.
131
+ It records task/branch/path/current claim and immutable HEAD before allocation under the claims lock.
132
+ An existing intent/path/branch refuses another allocation; inspect and resume the exact retained task,
133
+ never repurpose or delete it. This exception does not authorize reviewer/investigator trees or cleanup.
134
+ Other runs use the normal command:
124
135
  ```bash
125
- git worktree add -b handoff/task-xxx .worktrees/task-xxx $BASE
126
- # (omit -b if branch already exists)
136
+ git worktree add -b handoff/task-xxx .worktrees/task-xxx <base-branch>
127
137
  ```
138
+ (Omit `-b` when the branch already exists. One simple command per worktree — no variable,
139
+ no compound; `<base-branch>` is the literal branch name read from ACTIVE.md.)
128
140
 
129
141
  ### 3b — Run tasks in parallel (one agent/session per task)
130
142
 
@@ -140,12 +152,12 @@ the task file is the slice. If they disagree, implement the spec and note it in
140
152
 
141
153
  TDD — mandatory:
142
154
  1. Write tests from §Test Cases
143
- cd .worktrees/task-xxx && <test command>
155
+ run <test command> from the worktree root (your cwd IS .worktrees/task-xxx — no cd)
144
156
  Confirm RED — paste the actual failing output, don't just assert it happened
145
157
  (immediately GREEN → test is wrong — flag this)
146
158
  2. Implement → run → confirm GREEN
147
159
  3. Run §Verification Commands inside the worktree:
148
- cd .worktrees/task-xxx && <each verification command>
160
+ run <each verification command> from the worktree root — no cd, no compound
149
161
  Paste full output.
150
162
 
151
163
  DO NOT run: git add, git commit, git push — leave files as-is in worktree.
@@ -180,7 +192,8 @@ second time is what blows up the orchestrator's context window and kills the run
180
192
  ### 3c — Orchestrator: land each task's work (task-scoped, receipt-backed)
181
193
 
182
194
  Landing uses the shared helper, never an ad-hoc cp loop. `land-worktree.mjs` snapshots
183
- the source worktree's FINAL content relative to the immutable `$BASE` — committed
195
+ the source worktree's FINAL content relative to the immutable `<base-branch>` head (the
196
+ `Base:` branch recorded in ACTIVE.md) — committed
184
197
  milestone, staged, unstaged and nonignored untracked changes, with deletions, rename
185
198
  origins and binary / executable / symlink semantics — and applies that exact delta to the
186
199
  destination. It refuses (exit 2, no mutation, no receipt) on a dirty destination, a
@@ -189,37 +202,59 @@ target-path untracked collision, an out-of-scope change or identity drift.
189
202
  For each task that reported back:
190
203
 
191
204
  **PASS + EXECUTOR_MODEL present:**
192
- ```bash
193
- TASK="TASK-xxx"
194
- WORKTREE=".worktrees/task-xxx"
195
-
196
- # Scope file: exact task-approved target paths (both rename endpoints). Written
197
- # outside the source worktree AND outside the target set.
198
- cat > ".worktrees/$TASK-scope.json" <<JSON
199
- {
200
- "schemaVersion": 1,
201
- "taskId": "$TASK",
202
- "baseCommit": "$BASE",
203
- "sourceRoot": "$(git -C "$WORKTREE" rev-parse --show-toplevel)",
204
- "destinationRoot": "$(git rev-parse --show-toplevel)",
205
- "sourceHead": "$(git -C "$WORKTREE" rev-parse HEAD)",
206
- "destinationHead": "$(git rev-parse HEAD)",
207
- "targetPaths": ["<each approved path>"]
208
- }
209
- JSON
210
-
211
- node .claude/ukit/index/land-worktree.mjs \
212
- --scope ".worktrees/$TASK-scope.json" \
213
- --receipt "docs/AI_HANDOFF/benchmark/$TASK-landing.json"
214
- # exit 0 → landed; exit 2 → refused (untouched); exit 1 → apply/receipt failure.
215
- # After each exit-0 landing, checkpoint-commit the landed paths (receipt stays outside the target set)
216
- # BEFORE landing the next task: the helper refuses a destination with tracked modifications
217
- # (destination-dirty), so uncommitted landed work would block every later task in the wave.
218
- git add -- <landed paths>
219
- git commit --only -m "checkpoint: $TASK landed" -- <landed paths>
220
-
221
- # update INDEX.md: status → pending_review
222
- ```
205
+
206
+ 1. Write the scope file with the **Write tool** (never a shell heredoc, no variable) to
207
+ `docs/AI_HANDOFF/benchmark/TASK-xxx-scope.json` — every field a literal. Get each value with ONE simple
208
+ command and paste the printed output: `git rev-parse <base-branch>` (baseCommit),
209
+ `git -C .worktrees/task-xxx rev-parse --show-toplevel` (sourceRoot),
210
+ `git rev-parse --show-toplevel` (destinationRoot),
211
+ `git -C .worktrees/task-xxx rev-parse HEAD` (sourceHead), `git rev-parse HEAD`
212
+ (destinationHead).
213
+ ```json
214
+ {
215
+ "schemaVersion": 1,
216
+ "taskId": "TASK-xxx",
217
+ "baseCommit": "<base-branch head sha>",
218
+ "sourceRoot": "<absolute worktree root>",
219
+ "destinationRoot": "<absolute project root>",
220
+ "sourceHead": "<worktree HEAD sha>",
221
+ "destinationHead": "<project HEAD sha>",
222
+ "targetPaths": ["<each approved path>"]
223
+ }
224
+ ```
225
+ Scope file: exact task-approved target paths (both rename endpoints); written outside
226
+ the source worktree AND outside the target set. This is coordinator metadata,
227
+ not task implementation: keep it in the docs benchmark directory so the main-session
228
+ ownership guard remains intact.
229
+ 2. Land with ONE simple command, literal paths:
230
+ `node .claude/ukit/index/land-worktree.mjs --scope docs/AI_HANDOFF/benchmark/TASK-xxx-scope.json --receipt docs/AI_HANDOFF/benchmark/TASK-xxx-landing.json`
231
+ — exit 0 → landed; exit 2 → refused (untouched); exit 1 → apply/receipt failure. Landing is self-fencing: it reads the destination claims store under the claims lock held through apply+receipt, and exits 2 `worker-not-terminated` while any delegated worker claim for the task remains (handback must release it; lease expiry does not), or `claims-unreadable`/`claims-busy` (fail closed). Never fabricate a claim or receipt to pass.
232
+ 3. After each exit-0 landing, checkpoint-commit the landed paths (receipt stays outside
233
+ the target set) BEFORE landing the next task: the helper refuses a destination with
234
+ tracked modifications (destination-dirty), so uncommitted landed work would block every
235
+ later task in the wave.
236
+ ```bash
237
+ git add -- <landed paths>
238
+ git commit --only -m "checkpoint: TASK-xxx landed" -- <landed paths>
239
+ ```
240
+ 4. Update INDEX.md: status → pending_review (the orchestrator writes it; agents never do).
241
+
242
+ **Already-applied / divergent-history recovery (separate from normal landing):**
243
+ Use `--adopt-applied` only when the complete scoped source snapshot equals destination
244
+ blob+mode. If preserved task-record history differs, do not repeat known-failing adoption,
245
+ copy metadata, omit scope, or fabricate a landed receipt. With explicit owner authorization,
246
+ use `--reconcile-metadata <proof-json> --preflight --scope <scope-json> --receipt <new-json>`
247
+ for every affected full scope before applying the same command without `--preflight`.
248
+ File evidence must be hash-pinned and colocated with the proof in its audit directory;
249
+ hashes establish bytes, not semantic provenance: independently review that provenance.
250
+ A receipt with `receiptKind: metadata-reconciliation`, `status: reconciled`,
251
+ `completion:false`, `cleanupEligible:false` resolves only its verified integration-history
252
+ debt. Verify its HEAD/snapshot/scope identities still match; retain blocked status until
253
+ current-source independent review and the missing verification gates justify progression.
254
+ Continue those gates on the destination source, not another byte-identical adoption attempt.
255
+ This receipt is NEVER a normal landing, automatic pending_review/done transition, acceptance,
256
+ cleanup authorization or archive/completion authority; those retain their separate gates.
257
+ Preserve source worktrees and both histories, and obey any owner prohibition on cleanup.
223
258
 
224
259
  **FAIL:**
225
260
  ```bash
@@ -267,7 +302,7 @@ already on disk (task files hold the full logs, git holds the code). Do all four
267
302
  `/compact` here. If the session ends regardless, `RUN.md` plus the `SessionStart` resume
268
303
  hook bring the run back to exactly this point.
269
304
 
270
- Then create the next wave's worktrees from `$BASE` and repeat 3a–3c.
305
+ Then create the next wave's worktrees from the recorded `Base:` branch and repeat 3a–3c.
271
306
 
272
307
  ## Step 4 — Finalize: verify cleanup
273
308
 
@@ -286,7 +321,7 @@ snapshot, unmerged history. Unexplained leftovers fail the gate; nothing is remo
286
321
  An eligible task is retired only through the shared, receipt-gated helper
287
322
  (`node .claude/ukit/index/cleanup-task-worktree.mjs --receipt <json> --approval <json> --apply`).
288
323
 
289
- All changes are committed as **one commit per wave on `$BASE`**, nothing pushed. Worktrees and
324
+ All changes are committed as **one commit per wave on the recorded `Base:` branch**, nothing pushed. Worktrees and
290
325
  branches are retired only through the receipt-gated helper; any that remain are named in the
291
326
  retained-reason report. Set `Phase: done` in `docs/AI_HANDOFF/RUN.md` only when no task is
292
327
  still open.
@@ -24,8 +24,8 @@ The human is not watching.
24
24
  loop (Step 3). Escalate only for a blocker outside the repo, or a task still failing after
25
25
  both fix rounds, and even then finish everything else first.
26
26
 
27
- Never pause mid-review to ask for `/compact`. Reviewer agents return ≤6 lines and write the
28
- full verdict to disk; a compaction request belongs at the end of the command, never inside it.
27
+ Never pause mid-review to ask for `/compact`. Reviewer agents return bounded verdict blocks
28
+ for the orchestrator to persist with `Edit`; a compaction request belongs at the end, never inside.
29
29
 
30
30
  Maintain `docs/AI_HANDOFF/RUN.md` (`Command: handoff-review`, `Phase:`, `Cursor:`, `Next:`)
31
31
  after every step. If it already exists with `Phase:` not `done`, this is a **continuation**:
@@ -55,7 +55,7 @@ Classify by what the diff can show. An empty diff is NOT automatically "implemen
55
55
 
56
56
  **Batch the review set — mandatory.** Read `handoff.maxParallelAgents` from `.ukit/storage/config.json`. If more `pending_review` tasks exist than that, split into consecutive batches of at most that many; finish one batch's verdicts (2a–2d, appended to each task file) before starting the next. The cap counts child workers of ONE batch/review set, summed across roles (implementers and reviewers together); the main session is outside it, and it is not a session-wide limit. It is prompt-level guidance: no hook counts children, and the host's own concurrency limit is separate.
57
57
 
58
- **Spawn the review panel — MANDATORY, do this before anything else:** read `modelRoles['review-panel']` from `.ukit/storage/config.json` → `modelRoles` (default `['smart','code','lite']`; omp tier aliases `@smol`/`@default`/`@slow`). For each `pending_review` task in the batch, call the Agent tool **once per panel entry** (in parallel, bounded by `handoff.maxParallelAgents`), each with `subagent_type: "code-reviewer"` (omp: the `task` tool with `agent: "code-reviewer"`, likewise invoked once per panel entry per task) — each member bound to its panel tier and setting `PANEL_MEMBER: <tier>` in its verdict. Reviewer agents only read the diff and append a verdict to their own task file — no worktree, no shared write target — so running them in parallel carries none of Phase 3's file-conflict risk. Do NOT review the diff yourself in the current session — this step is contracted to the panel tiers 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.
58
+ **Spawn the review panel — MANDATORY, do this before anything else:** read `modelRoles['review-panel']` from `.ukit/storage/config.json` → `modelRoles` (default `['smart','code','lite']`; omp tier aliases `@smol`/`@default`/`@slow`). For each `pending_review` task in the batch, call the Agent tool **once per panel entry** (in parallel, bounded by `handoff.maxParallelAgents`), each with `subagent_type: "code-reviewer"` (omp: the `task` tool with `agent: "code-reviewer"`, likewise invoked once per panel entry per task) — each member bound to its panel tier and setting `PANEL_MEMBER: <tier>` in its verdict. Reviewers return the complete verdict block (≤30 lines), without shell writes; the orchestrator appends it with `Edit` to the task file, preserving prior rounds, before aggregation or status updates. No reviewer writes INDEX or task files; serial persistence avoids shared-write races. Do NOT review the diff yourself in the current session — this step is contracted to the panel tiers 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.
59
59
 
60
60
  Degenerate panel: if `modelRoles['review-panel']` is empty or has one entry, spawn a single reviewer and require `PANEL_MEMBER: solo` in its verdict.
61
61
 
@@ -65,10 +65,10 @@ Degenerate panel: if `modelRoles['review-panel']` is empty or has one entry, spa
65
65
  tasks:
66
66
  - agent: "code-reviewer"
67
67
  name: "TASK-001-smart"
68
- task: "Review docs/AI_HANDOFF/tasks/TASK-001.md per /ukit:handoff-review Step 2a-2d. PANEL_MEMBER: smart. Return at most 6 lines."
68
+ task: "Review docs/AI_HANDOFF/tasks/TASK-001.md per /ukit:handoff-review Step 2a-2d. PANEL_MEMBER: smart. Return the complete bounded verdict block; no file writes."
69
69
  - agent: "code-reviewer"
70
70
  name: "TASK-001-code"
71
- task: "Review docs/AI_HANDOFF/tasks/TASK-001.md per /ukit:handoff-review Step 2a-2d. PANEL_MEMBER: code. Return at most 6 lines."
71
+ task: "Review docs/AI_HANDOFF/tasks/TASK-001.md per /ukit:handoff-review Step 2a-2d. PANEL_MEMBER: code. Return the complete bounded verdict block; no file writes."
72
72
  ```
73
73
 
74
74
  Never add fields ordinary dispatch does not support: `sourceSession` and a per-task `model` are not supported there — omp 18.12.5 batch validation rejects them deterministically (confirmed in the installed binary's validator; not advertised as a live-probe pass) — and model binding comes from agent frontmatter via `modelRoles`, never from a task field. There is no seance workaround for reviewer dispatch. A deterministic validation rejection is never retried with identical args. A retry is allowed only after a relevant argument changes — `agent`, or the `tasks[]` entry's unsupported-field set (removing `sourceSession`/`model`). Prompt reword, `read`, and `status` text are NOT relevant changes: they never make a retry legitimate and never reset the failure budget, which stays the existing `maxAttemptsPerFailure: 2`. When that budget is spent, preserve across resume — in the task's `## Discussion` or the `RUN.md` cursor — a compact normalized error (prose stripped) plus the relevant key fingerprint and the attempt count, mark that task's verification `NOT_RUN` (never `PASS` — unverified work is not verified), and continue with independent tasks. Host pre-hook rejection behavior is unknown: never assume a hook will or will not catch the same call.
@@ -118,6 +118,10 @@ EXECUTOR_MODEL: <from Executor Report>
118
118
  REVIEW_KIND: product | integration-evidence | status-only
119
119
  PANEL_MEMBER: <panel tier | solo | ->
120
120
  VERIFICATION_RERUN: PASS | FAIL | NOT_RUN | N/A
121
+ command: <exact command, or ->
122
+ result: <actual pass/fail counts or NOT_RUN/N/A with reason>
123
+ TEST_PLAN_COVERAGE: all-followed | partial — <gaps> | missing — <gaps>
124
+ NOTES: <acceptance limitations, or none>
121
125
  FINDINGS:
122
126
  critical: <file:line — what fails> | none
123
127
  important: <file:line — risk> | none
@@ -127,7 +131,7 @@ NEXT_STATUS_FOR_INDEX: approved | approved_minor | changes_requested | critical_
127
131
 
128
132
  `VERDICT:` uses the aggregator's canonical hyphen vocabulary — underscore spellings (`changes_requested`, `critical_block`, `approved_with_minor`) parse to the same verdicts, so a blocking reviewer is never dropped as unparsed. `NOT_RUN` = the required re-run never executed in your session; `FAIL` = it executed and failed; either blocks approval. `N/A` is legitimate only when this review has no executable required checks (`command: -` or none) — an `N/A` beside a declared executable `command:` counts as `NOT_RUN`. `REVIEW_KIND` comes from the Step 1 classification; it scopes only the implementation-TDD evidence (new tests + RED_OUTPUT) — required executable checks and the task's original acceptance criteria are never waived, and approving an evidence review never marks product acceptance met.
129
133
 
130
- Then return to the orchestrator **at most 6 lines** — the full verdict is already on disk:
134
+ Return this routing header followed by the complete verdict block above; the orchestrator persists the block with `Edit` before reading it back and advancing 2e:
131
135
  ```
132
136
  TASK: TASK-xxx
133
137
  VERDICT: approved | approved_minor | changes_requested | critical_block
@@ -136,11 +140,11 @@ VERIFICATION_RERUN: PASS | FAIL | NOT_RUN | N/A (+ one-line reason)
136
140
  BLOCKING: <one line per critical/important finding, or "none">
137
141
  ```
138
142
  The return `VERDICT:` carries the NEXT_STATUS_FOR_INDEX vocabulary; the on-disk block's `VERDICT:` field uses the canonical hyphen vocabulary above.
139
- Do not paste the diff, the findings prose, or verification logs back into the orchestrator.
143
+ Return actionable findings inside the bounded block, not the diff or raw verification logs.
140
144
 
141
145
  ### 2e — Aggregate the panel + lead bucketing
142
146
 
143
- Once every panel member for a task has appended its `## Reviewer Verdict`, run:
147
+ Once the orchestrator has serially appended and read back every panel member's returned `## Reviewer Verdict`, run:
144
148
 
145
149
  ```bash
146
150
  node .claude/ukit/index/review-panel-aggregate.mjs docs/AI_HANDOFF/tasks/TASK-xxx.md
@@ -165,8 +169,8 @@ parseable blocks fail closed to `changes_requested`.
165
169
 
166
170
  Then the **lead member** (first entry in `modelRoles['review-panel']` = the judgment
167
171
  tier) applies the lead-judgment buckets to every finding — **Act on** / **Consider** /
168
- **Noted** / **Dismissed** — and appends a `## Panel Consensus` block to the task file
169
- carrying the Agreement Map and the bucketed findings. `Act on` findings feed Step 3's
172
+ **Noted** / **Dismissed** — and returns a `## Panel Consensus` block carrying the
173
+ Agreement Map and bucketed findings; the orchestrator appends it with `Edit` to the task file. `Act on` findings feed Step 3's
170
174
  auto-fix loop; `Consider`/`Noted`/`Dismissed` are advisory. A high-signal finding must
171
175
  not be bucketed below `Consider` without a stated reason.
172
176
 
@@ -111,12 +111,49 @@ if [[ "$BIN_BASENAME" =~ ^(rm|sudo|dd|mkfs|shutdown|reboot|halt|poweroff|chown|k
111
111
  exit 0
112
112
  fi
113
113
 
114
- # W2-D02: wrapper executables launch an arbitrary operand (a command string or
115
- # another binary), so minting Bash(<wrapper>:*) allows anything they can run.
116
- # They are NOT in the token-skip list above: skipping would resolve BIN to the
117
- # operand token (`timeout 30 ls` → BIN=30, `ssh host ls` → BIN=host) and mint a
118
- # nonsense wildcard rule. Deny outright and fall back to the permission prompt.
119
- if [[ "$BIN_BASENAME" =~ ^(eval|nice|timeout|xargs|ssh|su|chroot|docker|sudo|doas|watch|stdbuf|noglob)$ ]]; then
114
+ # UKIT:AUTO-ALLOW:DENYLIST:BEGIN v1
115
+ # Single source of truth for basenames that must never be auto-minted as
116
+ # Bash(<token>:*) allow rules. reset-auto-permissions.mjs --sanitize parses
117
+ # these three assignments out of this block (between the BEGIN/END markers),
118
+ # so the hook and the sanitizer share ONE definition and cannot drift. Tokens
119
+ # are matched as whole words against the BASENAME, so `./cd` / `/bin/cd` are
120
+ # denied exactly like `cd`.
121
+ #
122
+ # UKIT_DENY_SHELL_CONSTRUCT — shell reserved words, control constructs and
123
+ # compound openers (`{`, `}`, `[[`, `]]`, `!` are
124
+ # already rejected by the BIN charset check
125
+ # above; they are listed so the contract
126
+ # survives a charset widening), plus the
127
+ # `builtin` executor, which wraps ANY builtin
128
+ # (eval/source included).
129
+ # UKIT_DENY_STATE_BUILTIN — builtins that only mutate shell state or the
130
+ # shell's own process/context (cd included —
131
+ # it resolves nothing runnable). Ordinary
132
+ # builtin NAME twins that are real binaries
133
+ # (/usr/bin/test, /bin/echo, /usr/bin/printf,
134
+ # /bin/pwd) are deliberately NOT listed — they
135
+ # keep minting.
136
+ # UKIT_DENY_WRAPPER — executables that launch an arbitrary operand
137
+ # (W2-D02). They are NOT in the token-skip list
138
+ # above: skipping would resolve BIN to the
139
+ # operand token (`timeout 30 ls` → BIN=30,
140
+ # `ssh host ls` → BIN=host) and mint a nonsense
141
+ # wildcard rule.
142
+ #
143
+ # `time` is deliberately NOT denylisted: it stays in the token-skip list
144
+ # above so `time ls` keeps resolving to the real operand binary (`ls`).
145
+ # Denial is fail-open: the command falls back to the normal permission
146
+ # prompt (exit 0, nothing minted).
147
+ UKIT_DENY_SHELL_CONSTRUCT='for while until if then else elif fi do done case esac select function coproc { } [[ ]] ! builtin'
148
+ UKIT_DENY_STATE_BUILTIN='cd pushd popd source . exec export set unset alias trap declare typeset local readonly let shopt umask ulimit wait return exit shift break continue'
149
+ UKIT_DENY_WRAPPER='eval nice timeout xargs ssh su chroot docker sudo doas watch stdbuf noglob'
150
+ # UKIT:AUTO-ALLOW:DENYLIST:END
151
+
152
+ # Exact whole-word match on the basename (quoted token, space-padded on both
153
+ # sides) — deny outright, nothing is minted.
154
+ if [[ " $UKIT_DENY_SHELL_CONSTRUCT " == *" "$BIN_BASENAME" "* ]] \
155
+ || [[ " $UKIT_DENY_STATE_BUILTIN " == *" "$BIN_BASENAME" "* ]] \
156
+ || [[ " $UKIT_DENY_WRAPPER " == *" "$BIN_BASENAME" "* ]]; then
120
157
  exit 0
121
158
  fi
122
159
 
@@ -228,18 +265,39 @@ async function applyMutation() {
228
265
 
229
266
  const allow = settings.permissions.allow;
230
267
  let settingsChanged = false;
268
+ let usageChanged = false;
231
269
 
270
+ let added = false;
232
271
  if (!allow.includes(rule)) {
233
272
  allow.push(rule);
234
273
  settingsChanged = true;
274
+ added = true;
235
275
  await appendAudit("auto_add", { rule });
236
276
  }
237
277
 
238
- const prevMeta = usage.managedRules[rule] || {};
239
- usage.managedRules[rule] = {
240
- firstSeen: prevMeta.firstSeen || nowUtc,
241
- lastSeen: nowUtc,
242
- };
278
+ // LA-02: provenance is recorded ONLY when the hook itself added the rule. A
279
+ // rule that already existed in settings.local.json is user-authored —
280
+ // writing a managedRules record for it falsely attributed an owner rule to
281
+ // UKit (reset/sanitizer would then delete it). Already-managed rules keep
282
+ // their lastSeen fresh for TTL pruning, and legacy records (no `added`
283
+ // flag) are never laundered into proven ones by mere usage.
284
+ const prevMeta = usage.managedRules[rule];
285
+ if (added) {
286
+ usage.managedRules[rule] = {
287
+ firstSeen:
288
+ prevMeta && typeof prevMeta === "object" && prevMeta.firstSeen
289
+ ? prevMeta.firstSeen
290
+ : nowUtc,
291
+ lastSeen: nowUtc,
292
+ added: true,
293
+ addedBy: "auto-allow-bash/prov2",
294
+ };
295
+ usageChanged = true;
296
+ } else if (prevMeta && typeof prevMeta === "object" && prevMeta.lastSeen !== nowUtc) {
297
+ usage.managedRules[rule] = { ...prevMeta, lastSeen: nowUtc };
298
+ usageChanged = true;
299
+ }
300
+ // else: the rule pre-existed with no managed record — leave it unrecorded.
243
301
 
244
302
  const managedSorted = Object.entries(usage.managedRules).sort((a, b) => {
245
303
  const ta = Date.parse(a[1]?.lastSeen || "") || 0;
@@ -251,6 +309,7 @@ async function applyMutation() {
251
309
  const evict = managedSorted.slice(maxRules);
252
310
  for (const [evictRule] of evict) {
253
311
  delete usage.managedRules[evictRule];
312
+ usageChanged = true;
254
313
  const idx = allow.indexOf(evictRule);
255
314
  if (idx !== -1) {
256
315
  allow.splice(idx, 1);
@@ -263,7 +322,9 @@ async function applyMutation() {
263
322
  if (settingsChanged) {
264
323
  await writeJsonAtomic(settingsPath, settings);
265
324
  }
266
- await writeJsonAtomic(usagePath, usage);
325
+ if (usageChanged) {
326
+ await writeJsonAtomic(usagePath, usage);
327
+ }
267
328
  }
268
329
 
269
330
  (async () => {
@@ -227,4 +227,14 @@ async function pruneMutation() {
227
227
  });
228
228
  ' "$SETTINGS_LOCAL" "$USAGE_FILE" "$AUDIT_FILE" "$TTL_DAYS" "$NOW_UTC" 2>/dev/null || true
229
229
 
230
+ # LA-02: remove denylisted auto-minted rules (cd/for/while/...) that earlier UKit
231
+ # versions wrote. The sanitizer removes ONLY rules with proven UKit provenance
232
+ # (managedRules added:true or an audit auto_add event) AND a token in the shared
233
+ # auto-allow denylist; owner-authored and unproven rules stay. It takes the same
234
+ # usage lock, fails closed, and never blocks the session (exit 0 regardless).
235
+ SANITIZER="$SCRIPT_DIR/../ukit/index/reset-auto-permissions.mjs"
236
+ if [ -f "$SANITIZER" ] && command -v node >/dev/null 2>&1; then
237
+ (cd "$PROJECT_ROOT" && node "$SANITIZER" --sanitize --quiet) >/dev/null 2>&1 || true
238
+ fi
239
+
230
240
  exit 0
@@ -287,6 +287,27 @@ async function evaluate(ctx) {
287
287
  const env = ctx.env || {};
288
288
  const payload = ctx.payload || {};
289
289
  const toolName = payload.tool_name;
290
+ // An explicit run-owned existing-only policy applies to ALL dispatches, including
291
+ // read-only investigators, before role/tier exemptions or host allocation can run.
292
+ if (toolName === 'Agent' || toolName === 'Task' || toolName === 'Bash') {
293
+ const root = env.CLAUDE_PROJECT_DIR || ctx.projectRoot || process.cwd();
294
+ let cursor = '';
295
+ try { cursor = fs.readFileSync(path.join(root, 'docs/AI_HANDOFF/RUN.md'), 'utf8'); } catch (e) {
296
+ if (e.code !== 'ENOENT') return { code: 2, stdout: '', stderr: 'Worktree dispatch policy unreadable; refusing all Agent/Task dispatches.\n' };
297
+ }
298
+ // A runner may only add this restriction, never override a run-owned one.
299
+ // Planning-only sessions need enforcement before a RUN cursor exists.
300
+ if (env.UKIT_WORKTREE_POLICY === 'existing-only' || /^Worktree-Policy:[ \t]*existing-only\b/m.test(cursor)) {
301
+ const input = payload.tool_input || {};
302
+ if (toolName === 'Bash' && /\bworktree\s+(?:add|remove|prune|move)\b/i.test(String(input.command || ''))) {
303
+ return { code: 2, stdout: '', stderr: 'Worktree-Policy: existing-only — direct worktree mutation refused; task exceptions require allocate-task-worktree.mjs with a run-owned authorization and exclusive intent. Removal is not authorized.\n' };
304
+ }
305
+ const branches = [input, ...(Array.isArray(input.tasks) ? input.tasks : [])];
306
+ if (branches.some((entry) => entry && Object.hasOwn(entry, 'isolation'))) {
307
+ return { code: 2, stdout: '', stderr: 'Worktree-Policy: existing-only — isolation is forbidden before dispatch; omit isolation and use retained worktrees only.\n' };
308
+ }
309
+ }
310
+ }
290
311
  if (toolName !== 'Bash' && !MUTATING_TOOLS.has(toolName)) return PASS;
291
312
  if (env.UKIT_HARNESS && env.UKIT_HARNESS !== 'claude-code') return PASS;
292
313
  const projectRoot = env.CLAUDE_PROJECT_DIR || ctx.projectRoot || process.cwd();
@@ -765,9 +765,9 @@ async function createScopeGate() {
765
765
  }
766
766
 
767
767
  // Per-task execution claim (goal loop): no two executor agents run the same INDEX task, and a
768
- // finished task is never re-dispatched. Released by record-execution.mjs on the tool result;
769
- // an abandoned claim expires by TTL (stale takeover). Only lock/IO trouble degrades open —
770
- // an active foreign claim or a completed task is a hard, explained deny.
768
+ // finished task is never re-dispatched. record-execution.mjs releases on synchronous handback
769
+ // or host-authenticated async completion; lease expiry alone cannot authorize a successor.
770
+ // A failed acquire denies dispatch without modifying the unavailable claim evidence.
771
771
  async function taskClaimGate() {
772
772
  if (process.env.TASK_CLAIM_RUN !== '1' || (toolName !== 'Agent' && toolName !== 'Task')) return;
773
773
  try {
@@ -780,6 +780,13 @@ async function taskClaimGate() {
780
780
  } catch { return; }
781
781
  const indexText = await readTextSafe(path.join(projectRoot, 'docs/AI_HANDOFF/INDEX.md'));
782
782
  if (!indexText) return; // no handoff cycle here
783
+ // Cycle identity for the claim (stale-claim across cycles): task ids restart per cycle, so
784
+ // the claim is stamped with the run's `Cycle:` from RUN.md — a PREVIOUS cycle's ghost claim
785
+ // on a reused id is then reconciled (takeover) instead of denying this cycle's dispatch,
786
+ // while a live same-cycle worker stays protected. No/blank Cycle → legacy cycle-blind deny.
787
+ const runCycle = mod.normalizeClaimCycle
788
+ ? mod.normalizeClaimCycle(((await readTextSafe(path.join(projectRoot, 'docs/AI_HANDOFF/RUN.md')) || '').match(/^Cycle:\s*(.+)$/m) || [])[1] || '')
789
+ : null;
783
790
  const rowFor = (id) => indexText.split('\n').find((line) => new RegExp(`^\\|\\s*${id}\\s*\\|`, 'i').test(line));
784
791
  const targets = mod.delegatedClaimTargets(input, { isKnownTask: (id) => Boolean(rowFor(id)) });
785
792
  const owner = mod.delegatedClaimOwner(payload);
@@ -791,9 +798,18 @@ async function taskClaimGate() {
791
798
  if (status === 'done' || status === 'cancelled' || status === 'cancelled_superseded' || status === 'archived') {
792
799
  block(`${taskId} is already ${status} in docs/AI_HANDOFF/INDEX.md — it will not be dispatched again. Pick the next open task or reopen it explicitly in INDEX.md first.`);
793
800
  }
794
- const claim = await mod.acquireTaskClaim(projectRoot, taskId, { owner, sessionId: payload?.session_id || null, ttlMs: mod.DELEGATED_CLAIM_TTL_MS });
801
+ const claim = await mod.acquireTaskClaim(projectRoot, taskId, { owner, sessionId: payload?.session_id || null, ttlMs: mod.DELEGATED_CLAIM_TTL_MS, cycle: runCycle, requireTermination: true });
795
802
  if (!claim.ok && claim.reason === 'active-claim') {
796
- block(`${taskId} is already being executed by another agent (active claim). Wait for it to finish, or pick another task; a stale claim expires on its own.`);
803
+ block(`${taskId} is already being executed by another agent (active claim). Wait for its verified terminal handback, or pick another task; lease expiry alone does not prove worker termination.`);
804
+ }
805
+ if (!claim.ok) {
806
+ block(`${taskId} claim unavailable (${claim.reason || 'acquire-failed'}) — executor not dispatched. Preserve the claim store and reconcile its availability before retrying.`);
807
+ }
808
+ if (claim.completionRecovery === 'lease-only') {
809
+ process.on('exit', (code) => {
810
+ if (code !== 0) return;
811
+ try { fs.writeSync(1, `${JSON.stringify({ systemMessage: `UKit: ${taskId} delegation registry full — terminal release authority unavailable; TTL alone cannot authorize a successor. Preserve evidence and reconcile worker termination.` })}\n`); } catch {}
812
+ });
797
813
  }
798
814
  }
799
815
  }
@@ -808,7 +824,7 @@ async function subtaskGate() {
808
824
  const dispatch = await import(require('url').pathToFileURL(path.resolve(process.env.HOOK_DIR || '.', '../ukit/runtime/plan-dispatch.mjs')).href);
809
825
  const subtaskId = dispatch.subtaskIdFromInput(input);
810
826
  if (!subtaskId) return;
811
- const verdict = await dispatch.dispatchSubtask(projectRoot, subtaskId, input);
827
+ const verdict = await dispatch.dispatchSubtask(projectRoot, subtaskId, input, { toolUseId: payload.tool_use_id, sessionId: payload.session_id });
812
828
  if (verdict.ok) {
813
829
  const b = verdict.budget;
814
830
  if (b && (b.status === 'over-soft-budget' || b.status === 'over-cost-boundary')) {