@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.
- package/CHANGELOG.md +21 -0
- package/manifests/documentation.yaml +33 -0
- package/manifests/instructionRules.yaml +10 -0
- package/manifests/platform.full.yaml +67 -0
- package/package.json +1 -1
- package/src/core/applyPlan.js +111 -2
- package/src/core/buildPlan.js +3 -1
- package/src/core/diffPlan.js +49 -1
- package/src/core/executionContracts.js +49 -8
- package/src/core/metadata.js +45 -0
- package/src/core/runInstallPipeline.js +30 -2
- package/src/core/uninstall.js +106 -2
- package/src/manifest/validateManifest.js +25 -1
- package/template_project/.claude/agents/code-reviewer.md +22 -9
- package/template_project/.claude/agents/handoff-planner.md +97 -8
- package/template_project/.claude/commands/ukit/handoff-create.md +114 -19
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +247 -79
- package/template_project/.claude/commands/ukit/handoff-implement.md +75 -40
- package/template_project/.claude/commands/ukit/handoff-review.md +14 -10
- package/template_project/.claude/hooks/auto-allow-bash.sh +73 -12
- package/template_project/.claude/hooks/auto-prune-bash.sh +10 -0
- package/template_project/.claude/hooks/delegation-gate.mjs +21 -0
- package/template_project/.claude/hooks/handoff-model-guard.sh +22 -6
- package/template_project/.claude/hooks/handoff-resume.sh +141 -1
- package/template_project/.claude/hooks/record-execution.mjs +140 -9
- package/template_project/.claude/hooks/skill-router.sh +173 -8
- package/template_project/.claude/settings.json +2 -2
- package/template_project/.claude/skills/advisor-plan/REFERENCE.md +10 -0
- package/template_project/.claude/skills/advisor-plan/SKILL.md +2 -2
- package/template_project/.claude/ukit/index/allocate-task-worktree.mjs +15 -0
- package/template_project/.claude/ukit/index/amend-guard.mjs +116 -0
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- package/template_project/.claude/ukit/index/land-worktree.mjs +243 -21
- package/template_project/.claude/ukit/index/plan-count.mjs +115 -0
- package/template_project/.claude/ukit/index/ready-gate.mjs +170 -0
- package/template_project/.claude/ukit/index/reset-auto-permissions.mjs +293 -4
- package/template_project/.claude/ukit/index/route-resolver.mjs +48 -8
- package/template_project/.claude/ukit/index/route-task.mjs +180 -11
- package/template_project/.claude/ukit/index/source-snapshot.mjs +172 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +109 -3
- package/template_project/.claude/ukit/runtime/async-lock.mjs +3 -3
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +20 -3
- package/template_project/.claude/ukit/runtime/handoff-task-scope.mjs +205 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/plan-dispatch.mjs +32 -5
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +310 -21
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +405 -105
- package/template_project/.claude/ukit/runtime/task-record-reconciliation.mjs +46 -0
- package/template_project/.claude/ukit/runtime/task-watchdog.mjs +28 -2
- package/template_project/.claude/ukit/runtime/worktree-allocation.mjs +75 -0
- package/template_project/.codex/README.md +25 -0
- package/template_project/.omp/RULES.md +22 -22
- package/template_project/.omp/agents/code-reviewer.md +22 -9
- package/template_project/.omp/agents/handoff-planner.md +102 -12
- package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
- package/template_project/AGENTS.md +5 -1
- package/template_project/CLAUDE.md +1 -1
- package/template_project/docs/AI_HANDOFF/RULES.md +8 -0
- package/template_project/docs/AI_HANDOFF/tasks/_TEMPLATE.md +8 -1
- package/template_project/instructions/core.md +5 -1
- package/template_project/instructions/layout.yaml +2 -0
- package/template_project/instructions/overlays/omp-rules.md +22 -22
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
|
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
|
|
28
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
169
|
-
|
|
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
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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.
|
|
769
|
-
//
|
|
770
|
-
//
|
|
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
|
|
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')) {
|