@ngockhoale/ukit 3.4.67 → 3.4.68

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 (62) hide show
  1. package/CHANGELOG.md +12 -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/execution-ledger.mjs +20 -3
  42. package/template_project/.claude/ukit/runtime/handoff-task-scope.mjs +205 -0
  43. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  44. package/template_project/.claude/ukit/runtime/plan-dispatch.mjs +32 -5
  45. package/template_project/.claude/ukit/runtime/resumable-run.mjs +310 -21
  46. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +405 -105
  47. package/template_project/.claude/ukit/runtime/task-record-reconciliation.mjs +46 -0
  48. package/template_project/.claude/ukit/runtime/task-watchdog.mjs +28 -2
  49. package/template_project/.claude/ukit/runtime/worktree-allocation.mjs +75 -0
  50. package/template_project/.codex/README.md +25 -0
  51. package/template_project/.omp/RULES.md +22 -22
  52. package/template_project/.omp/agents/code-reviewer.md +22 -9
  53. package/template_project/.omp/agents/handoff-planner.md +102 -12
  54. package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
  55. package/template_project/AGENTS.md +5 -1
  56. package/template_project/CLAUDE.md +1 -1
  57. package/template_project/docs/AI_HANDOFF/RULES.md +8 -0
  58. package/template_project/docs/AI_HANDOFF/tasks/_TEMPLATE.md +8 -1
  59. package/template_project/instructions/core.md +5 -1
  60. package/template_project/instructions/layout.yaml +2 -0
  61. package/template_project/instructions/overlays/omp-rules.md +22 -22
  62. package/template_project/ukit/storage/config.json +1 -1
@@ -0,0 +1,46 @@
1
+ // Conservative task-record comparison only. Evidence must be independently loaded and
2
+ // authenticated by the caller; this function grants neither completion nor cleanup.
3
+ import crypto from 'node:crypto';
4
+
5
+ const sha = (text) => crypto.createHash('sha256').update(text).digest('hex');
6
+ const fail = (reason) => { throw new Error(reason); };
7
+ const statusLine = /^- Status: `([^`\r\n]+)`\r?$/gm;
8
+ const verdicts = (text) => [...text.matchAll(/^VERDICT:\s*([^\r\n]+)/gm)].map((m) => m[1].trim());
9
+
10
+ export function reconcileTaskRecord(source, destination, proof, evidence) {
11
+ if (typeof source !== 'string' || typeof destination !== 'string' || !proof || !(evidence instanceof Map)) fail('invalid-proof');
12
+ const srcStatus = [...source.matchAll(statusLine)];
13
+ const dstStatus = [...destination.matchAll(statusLine)];
14
+ if (srcStatus.length !== 1 || dstStatus.length !== 1) fail('status-ambiguous');
15
+ let preserved = source;
16
+ let status = null;
17
+ if (srcStatus[0][1] !== dstStatus[0][1]) {
18
+ const p = proof.status;
19
+ // This narrow repair may retain a blocked integration, never promote delivery.
20
+ if (!p || p.before !== srcStatus[0][1] || p.after !== dstStatus[0][1] || p.after !== 'blocked') fail('status-unsupported');
21
+ const ev = evidence.get(p.evidence);
22
+ if (typeof ev !== 'string' || !ev.includes(dstStatus[0][0])) fail('status-evidence-missing');
23
+ preserved = source.slice(0, srcStatus[0].index) + dstStatus[0][0] + source.slice(srcStatus[0].index + srcStatus[0][0].length);
24
+ status = { ...p };
25
+ }
26
+ // All source bytes other than the separately evidenced status MUST be preserved.
27
+ // That includes requirements, AC, dependencies, executor reports and old verdicts.
28
+ if (!destination.startsWith(preserved)) fail('protected-content-changed');
29
+ const additions = proof.additions;
30
+ if (!Array.isArray(additions) || additions.map((p) => p.text).join('') !== destination.slice(preserved.length)) fail('unexplained-addition');
31
+ const knownVerdicts = new Set(verdicts(source));
32
+ const checked = [];
33
+ for (const p of additions) {
34
+ if (!p || typeof p.text !== 'string' || !p.text.trim()) fail('invalid-addition');
35
+ const ev = evidence.get(p.evidence);
36
+ if (typeof ev !== 'string' || !ev.includes(p.text)) fail('addition-evidence-missing');
37
+ const headings = [...p.text.matchAll(/^#{1,6}\s+(.+)$/gm)].map((m) => m[1]);
38
+ if (!headings.length || headings.some((h) => !/^(?:Reviewer Verdict|Recovery (?:evidence|persistence|integration debt)|Owner-bound integration blocker)(?:\s|$)/.test(h))) fail('metadata-section-unsupported');
39
+ for (const v of verdicts(p.text)) {
40
+ if (knownVerdicts.size && !knownVerdicts.has(v)) fail('verdict-changed');
41
+ knownVerdicts.add(v);
42
+ }
43
+ checked.push({ sha256: sha(p.text), evidence: p.evidence });
44
+ }
45
+ return { kind: 'history-progression', sourceSha256: sha(source), destinationSha256: sha(destination), protectedSha256: sha(preserved), status, additions: checked, completion: false, cleanupEligible: false };
46
+ }
@@ -136,6 +136,9 @@ export async function readState(statePath) {
136
136
  return {
137
137
  firstSeen: isPlainObject(parsed?.firstSeen) ? { ...parsed.firstSeen } : {},
138
138
  hardBlocks: isPlainObject(parsed?.hardBlocks) ? { ...parsed.hardBlocks } : {},
139
+ // Cycle stamp (stale-state across cycles): surfaced so the evaluator can
140
+ // detect anchors left by a PREVIOUS cycle on a reused task id.
141
+ ...(typeof parsed?.cycle === 'string' ? { cycle: parsed.cycle } : {}),
139
142
  };
140
143
  } catch {
141
144
  return { firstSeen: {}, hardBlocks: {} };
@@ -393,18 +396,37 @@ export function pickLastGreen(task) {
393
396
  * listInProgressTasks(projectRoot))
394
397
  * @param {number} [options.lockBudgetMs] - per-acquisition lock budget; the
395
398
  * coordinator passes a short slice so two acquisitions fit the hook deadline
399
+ * @param {string} [options.cycle] - the run's current cycle (`Cycle:` from RUN.md,
400
+ * `cycle-NN`/`NN`). Task ids restart per cycle, so anchors recorded under a
401
+ * DIFFERENT cycle say nothing about this cycle's tasks: the state's
402
+ * firstSeen/hardBlocks are reset and re-stamped before evaluation. Absent or
403
+ * unparseable → legacy cycle-blind behavior (state kept as-is).
396
404
  */
397
- export async function evaluateStopWatchdog({ projectRoot, now = Date.now(), config = null, tasks = null, lockBudgetMs } = {}) {
405
+ export async function evaluateStopWatchdog({ projectRoot, now = Date.now(), config = null, tasks = null, lockBudgetMs, cycle } = {}) {
398
406
  if (!config) config = await loadConfig(path.join(projectRoot, '.ukit', 'storage', 'config.json'));
399
407
  if (!tasks) tasks = await listInProgressTasks(projectRoot);
400
408
 
401
409
  const statePath = path.join(projectRoot, '.ukit', 'storage', 'cache', 'task-watchdog', 'state.json');
402
410
  const state = await readState(statePath);
411
+ // Same identity rule as the claim stamps (resumable-run.mjs normalizeClaimCycle):
412
+ // `cycle-NN`/`NN` → `cycle-NN`, anything else → no scoping this pass.
413
+ const cycleMatch = String(cycle ?? '').trim().match(/^(?:cycle-)?(\d+)$/i);
414
+ const cycleTag = cycleMatch ? `cycle-${cycleMatch[1].padStart(2, '0')}` : null;
415
+ // A missing stamp (pre-upgrade state) resets too: the only legacy window is
416
+ // the upgrade transition, and a fresh clock under-enforces mildly where a
417
+ // stale one false-trips a brand-new task. Same-cycle stamps keep everything.
418
+ const staleCycle = cycleTag !== null && state.cycle !== cycleTag;
403
419
  const lockOptions = Number.isFinite(lockBudgetMs) ? { deadlineMs: lockBudgetMs } : {};
404
420
 
405
421
  // Anchor firstSeen via one lock-scoped read-modify-write (TASK-024 semantics).
406
422
  try {
407
423
  await mutateWatchdogState(statePath, (fresh) => {
424
+ if (staleCycle) {
425
+ // New cycle: the reused ids are NEW tasks — fresh clocks, fresh caps.
426
+ fresh.firstSeen = {};
427
+ fresh.hardBlocks = {};
428
+ }
429
+ if (cycleTag !== null) fresh.cycle = cycleTag;
408
430
  for (const task of tasks) {
409
431
  try {
410
432
  ensureFirstSeen(fresh, task.id, now);
@@ -413,7 +435,11 @@ export async function evaluateStopWatchdog({ projectRoot, now = Date.now(), conf
413
435
  }, lockOptions);
414
436
  } catch {}
415
437
 
416
- const evals = evaluateBudgets({ tasks, state, now, config });
438
+ // A successful reset above re-anchored everything at `now`; evaluate against
439
+ // the reset view even if the lock failed (empty view → `now` anchors, which
440
+ // can only under-report, never falsely trip a fresh cycle's task).
441
+ const effectiveState = staleCycle ? { firstSeen: {}, hardBlocks: {} } : state;
442
+ const evals = evaluateBudgets({ tasks, state: effectiveState, now, config });
417
443
  const hardResults = evals.filter((r) => r.phase === 'hard');
418
444
  const softResults = evals.filter((r) => r.phase === 'soft');
419
445
 
@@ -0,0 +1,75 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { withAsyncLock } from './async-lock.mjs';
6
+ import { readTaskClaim, taskClaimsPath } from './resumable-run.mjs';
7
+
8
+ const exec = promisify(execFile);
9
+
10
+ // Run-owned owner authorization, not a host permission override. Agent isolation
11
+ // remains forbidden: only the orchestrator's explicit allocation uses this path.
12
+ export function allocationPolicy(cursor, taskId) {
13
+ if (!/^TASK-\d+$/.test(taskId)) throw new Error('invalid task id');
14
+ if (!/^Worktree-Policy:[ \t]*existing-only\b/m.test(cursor)) throw new Error('existing-only policy required');
15
+ const cycles = [...cursor.matchAll(/^Cycle:[ \t]*(\d+)[ \t]*$/gm)];
16
+ if (cycles.length !== 1) throw new Error('ambiguous cycle');
17
+ const cycle = cycles[0][1];
18
+ const entries = [...cursor.matchAll(/^Worktree-Allow:[ \t]*(\S+)[ \t]+(\S+)[ \t]+(\S+)[ \t]+(\S+)[ \t]*$/gm)].filter(m => m[1] === taskId);
19
+ if (!entries.length) throw new Error('task allocation not authorized');
20
+ if (entries.length !== 1) throw new Error('ambiguous allocation authorization');
21
+ const [, , branch, relativePath, authorization] = entries[0];
22
+ const slug = taskId.toLowerCase();
23
+ if (branch !== `handoff/${slug}`) throw new Error('invalid task branch');
24
+ if (relativePath !== `.worktrees/${slug}`) throw new Error('invalid task path');
25
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,95}$/.test(authorization)) throw new Error('invalid authorization reference');
26
+ const scope = new RegExp(`^TaskScope:[ \\t]*cycle-${cycle}/${taskId}[ \\t]*=[ \\t]*(?:plan|queue):[a-zA-Z0-9_-]+[ \\t]*$`, 'm');
27
+ if (!scope.test(cursor)) throw new Error('task outside frozen scope');
28
+ return { taskId, branch, relativePath, authorization, cycle };
29
+ }
30
+
31
+ async function absent(file) {
32
+ try { await fs.lstat(file); return false; } catch (e) { if (e.code === 'ENOENT') return true; throw e; }
33
+ }
34
+
35
+ export async function allocateTaskWorktree(projectRoot, taskId, { git = null } = {}) {
36
+ const root = await fs.realpath(projectRoot);
37
+ const runGit = git ?? (async (args) => {
38
+ const result = await exec('git', ['-C', root, ...args], { timeout: 30_000, maxBuffer: 1024 * 1024 });
39
+ return result.stdout.trim();
40
+ });
41
+ const outcome = await withAsyncLock(taskClaimsPath(root), { deadlineMs: 3000 }, async () => {
42
+ const cursor = await fs.readFile(path.join(root, 'docs/AI_HANDOFF/RUN.md'), 'utf8');
43
+ const policy = allocationPolicy(cursor, taskId);
44
+ const claim = await readTaskClaim(root, taskId);
45
+ if (claim.unreadable || claim.claimed) throw new Error('task claim present or unreadable; allocation refused');
46
+ const parent = path.join(root, '.worktrees');
47
+ if (!(await absent(parent))) {
48
+ const stat = await fs.lstat(parent);
49
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('unsafe worktree parent');
50
+ }
51
+ const destination = path.join(root, policy.relativePath);
52
+ if (!(await absent(destination))) throw new Error('worktree path already exists');
53
+ const intentDir = path.join(root, '.ukit/storage/runs/worktree-allocations');
54
+ await fs.mkdir(intentDir, { recursive: true });
55
+ if (await fs.realpath(intentDir) !== intentDir) throw new Error('unsafe allocation intent path');
56
+ const intentPath = path.join(intentDir, `${policy.cycle}-${taskId}.json`);
57
+ if (!(await absent(intentPath))) throw new Error('allocation intent already exists; inspect history, never allocate again');
58
+ const base = await runGit(['rev-parse', '--verify', 'HEAD']);
59
+ if (!/^[a-f0-9]{40,64}$/.test(base)) throw new Error('invalid base commit');
60
+ let branchExists = false;
61
+ try { await runGit(['show-ref', '--verify', '--quiet', `refs/heads/${policy.branch}`]); branchExists = true; }
62
+ catch (e) { if (e.code !== 1) throw e; }
63
+ if (branchExists) throw new Error('task branch already exists');
64
+ const intent = { v: 1, ...policy, claim, base, status: 'intent', ts: Date.now() };
65
+ const handle = await fs.open(intentPath, 'wx');
66
+ try { await handle.writeFile(`${JSON.stringify(intent, null, 2)}\n`); await handle.sync(); }
67
+ finally { await handle.close(); }
68
+ // Keep the exclusive intent even on failure/timeout. An ambiguous git result
69
+ // is recovery debt, not permission to allocate another worktree.
70
+ await runGit(['worktree', 'add', '-b', policy.branch, destination, base]);
71
+ return { ...intent, path: destination, intentPath };
72
+ });
73
+ if (!outcome.ok) throw new Error('claims busy; no allocation');
74
+ return outcome.value;
75
+ }
@@ -177,3 +177,28 @@ Skills are shared from `.claude/skills/` via symlink at `.codex/skills/`.
177
177
  - No unrelated changes
178
178
  - Verification executed and reported
179
179
  - Docs updated when source truth changed
180
+
181
+ ## Handoff Planning Gates
182
+
183
+ When a handoff create/planner/fullstack session plans work under `docs/AI_HANDOFF/`, the
184
+ gates are mechanical and read from disk, never from session memory:
185
+
186
+ - Tasks are born `needs_breakdown`. A task flips to `ready` only after BOTH gates: the
187
+ task-budget validator's `VERDICT: ok` recorded on disk (the tool writes it —
188
+ `node .claude/ukit/index/task-budget-validator.mjs --record docs/AI_HANDOFF/tasks/TASK-001.md`)
189
+ AND the independent plan review's `PLAN_REVIEW: Approved` in PLAN.md.
190
+ - Before any flip, `node .claude/ukit/index/ready-gate.mjs docs/AI_HANDOFF` must print
191
+ `READY_GATE: ok` — it re-checks both gates plus INDEX/task agreement from disk.
192
+ - An owner document is persisted verbatim BEFORE planning reads it:
193
+ `node .claude/ukit/index/source-snapshot.mjs <original-path> docs/AI_HANDOFF/SOURCE.md`
194
+ (exclusive create, never overwrites; if it cannot run, state `source snapshot MISSING`).
195
+ - Implementation lands through the receipt-backed helper, never a manual copy:
196
+ `node .claude/ukit/index/land-worktree.mjs --scope <scope.json> --receipt <receipt.json>`.
197
+ - Overwriting PLAN/SPEC/LEDGER needs an `amendments/rev-<NN>/` copy first, verified with
198
+ `node .claude/ukit/index/amend-guard.mjs`.
199
+
200
+ Every one of these is a bare single `node` command — no `cd`, no `$()` substitution, no
201
+ compound — so the same instructions pass host permission checks on Claude Code, Codex and
202
+ omp alike. On Codex this whole gate set is instruction-mediated only: there is no pre-tool
203
+ hook surface, so nothing is hard-blocked — follow the verdicts recorded on disk instead of
204
+ trusting session memory.
@@ -12,8 +12,7 @@ nothing else is loaded. Keep this short: every line here is a per-turn tax.
12
12
  - **Non-trivial / risky** (auth, migration, shared runtime, data-loss, flaky) — index-first, then
13
13
  activate the matching skill, then verify.
14
14
 
15
- Pick the lane before the first tool call. Skipping this is what turns a direct chat into a
16
- scattershot one.
15
+ Pick the lane before the first tool call — skipping this is what turns a direct chat into a scattershot one.
17
16
 
18
17
  ## 2. Execution Contract
19
18
 
@@ -33,9 +32,9 @@ files before widening further. These node scripts work unchanged under omp.
33
32
 
34
33
  ## 5. Auto-activate skills
35
34
 
36
- Skills live in `.claude/skills/` and omp reads them through its `claude` discovery provider — no
37
- mirror under `.omp/`. On any non-trivial task, and again after the first tool calls reveal what the
38
- work really is, read the matching `SKILL.md` without being asked. Users never name skills.
35
+ Skills live in `.claude/skills/`, read by omp through its `claude` discovery provider — no mirror under
36
+ `.omp/`. On any non-trivial task — and again after the first tool calls reveal the real work — read the
37
+ matching `SKILL.md` without being asked. Users never name skills.
39
38
 
40
39
  ## 6. Delegate to bind a model tier
41
40
 
@@ -43,36 +42,37 @@ Your own model does not change mid-turn. A tier only applies when work is handed
43
42
  `.omp/agents/`, whose `model:` field (`@smol` / `@default` / `@slow` / `@vision`) resolves through
44
43
  `modelRoles` in `.omp/config.yml`.
45
44
 
46
- - `tiny-fix` → `@smol` · `local-fix`, `local-build`, `shared-edit`, `find-cause`, `map-impact` →
47
- `@default` · `review-release` → `@slow`
48
- - Images: `@vision` only. `@default`/`@slow` cannot see images on the UNIC gateway and must never
49
- guess at their contents — hand every image to `ukit-vision-analyst` first.
45
+ - `tiny-fix` → `@smol` · `local-fix`/`local-build`/`shared-edit`/`find-cause`/`map-impact` → `@default` · `review-release` → `@slow`
46
+ - Images: `@vision` only — `@default`/`@slow` cannot see images on the UNIC gateway; hand every image to `ukit-vision-analyst` first.
50
47
 
51
48
  Doing everything inline is what makes UKit look like it only has one model. Keep direct execution
52
49
  for trivial/simple work; delegate when the tier differs or a noisy lane would flood this context.
53
50
 
54
51
  ## 7. Safe Patch
55
52
 
56
- Prefer unique current-file anchors over line numbers or stale pasted blocks. Never silently merge a
57
- stale spec — re-read current source and confirm before applying. Preserve existing BOM and line
58
- endings.
53
+ Prefer unique current-file anchors over line numbers or stale pasted blocks; never silently merge a
54
+ stale spec — re-read current source and confirm before applying. Preserve existing BOM and line endings.
59
55
 
60
56
  ## 8. Handoff runs never park mid-cycle
61
57
 
62
58
  While `docs/AI_HANDOFF/RUN.md` `Phase:` is not `done`/`blocked`, a handoff-fullstack run is
63
59
  still in flight: a recap or checkpoint is progress output, never a stopping point. Continue
64
60
  the cursor's `Next:` step; only `HANDOFF FULLSTACK COMPLETE` or `HANDOFF FULLSTACK BLOCKED`
65
- ends the run. `handoff-clear` must close the cursor (`Phase: done` or delete) or the gate
66
- keeps the next session running too.
61
+ ends the run; `handoff-clear` must close the cursor or the gate keeps the next session running.
62
+ Before any stop, reconcile task/AC/findings/jobs/independent work/integration/release/consumer/
63
+ pause/blocker; the Stop gate is the backstop; a turn wait is not run done; a blocker releases only
64
+ with no READY independent work. Planning gates are mechanical, disk-read: `needs_breakdown`
65
+ tasks flip `ready` only on a recorded validator `VERDICT: ok` + `PLAN_REVIEW: Approved`, with
66
+ `node .claude/ukit/index/ready-gate.mjs docs/AI_HANDOFF` printing `READY_GATE: ok` before the
67
+ flip; owner docs persist via `source-snapshot.mjs`, work lands via `land-worktree.mjs` — all bare
68
+ single `node` commands.
67
69
 
68
70
  ## 9. Never end a turn silently
69
71
 
70
72
  Ending a turn with no output is a defect, not a pause. If you stop before the work is finished, the
71
- last thing you emit is one short line naming what is unfinished and what you need. Silence is never
72
- a status — the user cannot distinguish it from a crash.
73
-
74
- > Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
75
- > On a non-UNIC provider, edit only the cost tiers `smol`/`default`/`slow` — never `vision`
76
- > (`unic-vision`) or `decision` (`unic-decision`, the LOCAL JEV/Lava typed-decision model —
77
- > `docs/pstack/UNIC_DECISION_GUIDE.md`); both are capability lanes. Aliases `lite`/`code`/`smart`
78
- > map smol↔lite, default↔code, slow↔smart — edit each alongside its tier.
73
+ last line you emit names what is unfinished and what you need. Silence is never a status.
74
+
75
+ > Maintainer note, not a per-turn rule: `modelRoles` ships UNIC gateway names; on a non-UNIC
76
+ > provider edit only cost tiers `smol`/`default`/`slow` — never `vision` (`unic-vision`) or
77
+ > `decision` (`unic-decision`, LOCAL JEV/Lava typed-decision — `docs/pstack/UNIC_DECISION_GUIDE.md`);
78
+ > both capability lanes. Aliases lite/code/smart = smol/default/slow — edit alongside its tier.
@@ -101,7 +101,13 @@ decision owned by the local decision model when staged. Read
101
101
  Model isolation is preserved: the verdict pass runs on the local unic-decision
102
102
  model, so reviewer ≠ executor still stands.
103
103
 
104
- ### Output (append to task file as `## Reviewer Verdict`)
104
+ ### Output (return `## Reviewer Verdict` to the orchestrator)
105
+
106
+ You are read-only: return the complete verdict block below, never shell-append it (`cat >>`,
107
+ heredoc, `tee`, `sed -i`). The orchestrator appends your complete verdict block with `Edit`
108
+ to the task file, preserving prior rounds, then updates INDEX serially. Do not edit INDEX.
109
+ Use Read/Grep/Glob for inspection; Bash verification is one simple command per call with
110
+ absolute paths (no `cd … &&`/`;` prefix, loops or compound writes).
105
111
 
106
112
  ```
107
113
  ## Reviewer Verdict
@@ -127,9 +133,11 @@ NEXT_STATUS_FOR_INDEX: approved | approved_minor | changes_requested | critical_
127
133
  NOTES: [1-2 sentences for human reviewer if needed]
128
134
  ```
129
135
 
130
- After writing the verdict, update `docs/AI_HANDOFF/INDEX.md` row for this task: set Status = NEXT_STATUS_FOR_INDEX, set Reviewer = your model name.
136
+ The orchestrator persists this block before aggregation or status changes; it sets the INDEX
137
+ status from NEXT_STATUS_FOR_INDEX and Reviewer from your model name. No independent verdict
138
+ is on disk until that append succeeds.
131
139
 
132
- **Then return to the orchestrator at most 6 lines** — the full verdict is already on disk:
140
+ **Include this short routing header before your complete verdict block**:
133
141
 
134
142
  ```
135
143
  TASK: TASK-xxx
@@ -139,10 +147,9 @@ VERIFICATION_RERUN: PASS | FAIL | NOT_RUN | N/A (+ one-line reason for NOT_RUN/N
139
147
  BLOCKING: <one line per critical/important finding, or "none">
140
148
  ```
141
149
 
142
- Do not paste the diff, the findings prose, or verification output into the returned message.
143
- The orchestrator is driving a whole pipeline in one context window and re-reads what it needs
144
- from the task file; pasted reviewer logs are a common reason a run exhausts its context and
145
- dies before the cycle finishes.
150
+ Return the bounded verdict block (≤30 lines) with actionable findings, not the diff or raw
151
+ verification logs. The complete block is necessary for persistence by the read-only caller contract.
152
+ For panel lead bucketing, likewise return the Panel Consensus block for serial persistence.
146
153
 
147
154
  **You may be running unattended.** Findings do not end the run — the caller feeds them to an
148
155
  auto-fix round and re-review. So write findings that a fresh executor can act on with no human
@@ -212,7 +219,7 @@ additions:
212
219
  ### Inputs you expect
213
220
 
214
221
  - Path to the spec/plan document (e.g. `docs/plans/*.md`). No diff and no executor report.
215
- - When invoked from the handoff pipeline you get BOTH `docs/AI_HANDOFF/SPEC.md` and `docs/AI_HANDOFF/PLAN.md`. Review them as one unit: the spec is the contract, the plan is the decomposition. Verdicts still append to PLAN.md's `## Plan Review Log`.
222
+ - When invoked from the handoff pipeline you get BOTH `docs/AI_HANDOFF/SPEC.md` and `docs/AI_HANDOFF/PLAN.md`. Review them as one unit: the spec is the contract, the plan is the decomposition. Return the verdict for the planner to append to PLAN.md's `## Plan Review Log`.
216
223
  - A plan is not reviewable from SPEC+PLAN alone. Start from the original: `SOURCE.md` (verbatim original bytes; verify the recorded sha256 against them, and for chat input the durable verbatim message origin stands in for the file), then the classification in `LEDGER.md` (every row dispositioned, feasible/adapted rows carry evidence), `NOTES.md` when present (optional source-only notes; a missing note is caught because the chain starts at SOURCE, not at NOTES), the `tasks/TASK-*.md` files of that plan (spec references, acceptance and verification), and the current source the Target Files touch (index-first). Compare the chain SOURCE → classification (LEDGER) → tasks; check NOTES against the same chain. A `feasible`/`adapted` requirement that the ledger or source shows but no task covers is a Completeness finding, unless it is exempt when their queue entry, owner decision, or classification is recorded (deferred, blocker, rejected, proposal, and example rows are not tasks); a task whose verification does not prove its acceptance is a Clarity finding.
217
224
 
218
225
  ### Review order
@@ -237,9 +244,15 @@ additions:
237
244
 
238
245
  Only flag issues that would cause real problems during implementation planning. Approve unless there are serious gaps that would lead to a flawed plan.
239
246
 
247
+ ### Inspection commands (plan/spec review)
248
+
249
+ Read documents with `Read`/`Grep`/`Glob`. When Bash is needed, issue one simple read-only command per call
250
+ using absolute paths: no `cd … ;`/`cd … &&` prefixes, `for` loops, `;` or `&&` sequences, or `{a,b}` globs.
251
+ The host permission layer asks approval for such compound commands and an unattended review cannot answer it.
252
+
240
253
  ### Output
241
254
 
242
- Append (do NOT overwrite) this block to the end of the reviewed document under a `## Plan Review Log` section — create the section if it doesn't exist yet, keep all prior round entries:
255
+ Produce this block as the final part of your reply, verbatim. You have no `Write`/`Edit` tool and must not append it with a shell command (`cat >>`, heredoc, `tee`, `sed -i`): the planning-only guard refuses shell writes. The calling planner appends it (append, do NOT overwrite) to the end of the reviewed document under a `## Plan Review Log` section — creating the section if it doesn't exist yet, keeping all prior round entries:
243
256
 
244
257
  Every finding bullet is written as `<location> :: <claim>` (location = the SOURCE/LEDGER/NOTES/TASK/SPEC/PLAN line or section it cites; claim = the defect in one sentence). The planner derives the finding signature from this exact pair (lowercased, whitespace collapsed, claim truncated to 120 characters), so keep it stable across rounds for an unchanged defect.
245
258
 
@@ -30,6 +30,17 @@ what you must avoid, not the work itself. Write the remaining modules into `PLAN
30
30
  explicitly out-of-scope-for-this-cycle, and add one `queued` row per module to `INDEX.md` so
31
31
  the next cycle picks them up. Order modules so the one others depend on is planned first.
32
32
 
33
+ **Plan count is a deliverable.** When the owner source states a number of plans or separate
34
+ plans ("at least two plans", "plan A and plan B"), deliver exactly that many real plan units
35
+ in the Handoff-supported layout: the first plan canonical (SPEC.md + PLAN.md + tasks/ under
36
+ OUTPUT_ROOT), each further plan as its own `docs/AI_HANDOFF/queued/<slug>/` blueprint with
37
+ its own SPEC.md, PLAN.md, INDEX.md (rows `queued_blueprint`), HANDOFF.md and tasks/ — the
38
+ queued-README layout. Group labels ("group A/B") inside one PLAN.md never satisfy a stated
39
+ count. Verify the count mechanically before reporting: `node .claude/ukit/index/plan-count.mjs docs/AI_HANDOFF <N>` (the planning root, the stated count) must print
40
+ `PLAN_COUNT: ok (<found>/<N>)`.
41
+ count. If the layout genuinely cannot carry the count, record an explicit owner-decision
42
+ item (a NOTES.md `question` entry naming the decision) instead of silently collapsing it.
43
+
33
44
  The caller may be running unattended (`/ukit:handoff-fullstack`). Stopping to ask means the
34
45
  run dies and the human returns hours later to a question, which is the failure mode this
35
46
  whole pipeline exists to prevent.
@@ -42,7 +53,7 @@ resolved. Executors then burn a whole round discovering it.
42
53
 
43
54
  Before writing any path or command into `PLAN.md` or a task file, verify it:
44
55
 
45
- - **Target Files** — for each path, confirm it exists (`ls`/Glob), or that its parent
56
+ - **Target Files** — for each path, confirm it exists (Glob), or that its parent
46
57
  directory exists and the file is genuinely new. Mark new files `(new)` explicitly.
47
58
  - **Verification Commands** — read `package.json` `scripts` (or the stack's equivalent) and
48
59
  use the script names that are actually defined. Do not write `npm test` when the repo uses
@@ -69,11 +80,29 @@ The plan owns ALL unfinished work, not only the new request. Scan and fold in:
69
80
  Every discovered item becomes either a task row in the new plan or an explicitly recorded
70
81
  out-of-scope line in PLAN.md §2. Silent omission is a plan defect.
71
82
 
83
+ **Resume gates — `needs_breakdown` rows with no `PLAN_REVIEW: Approved` in PLAN.md:** a
84
+ previous planning run died before the gates. Do NOT re-intake or rewrite SPEC/PLAN/tasks on
85
+ this path; run only the missing gates — the per-task validator (`--record`, below) for
86
+ tasks lacking a recorded `Validator:` line — and report back for the review + flip. Never
87
+ overwrite the existing plan on this path.
88
+
72
89
  **Recovery:** a stuck task record that cannot be cleanly resumed (orphaned worktree,
73
90
  contradicting reports, invalid state) is marked `cancelled_superseded` and replaced by
74
91
  `TASK-xxx-R1` (`-R2`, …) carrying the same spec references, acceptance criteria and
75
92
  verification — link both files' `## Discussion` threads.
76
93
 
94
+ **Source snapshot — when the request comes from an owner document.** Before chunking any
95
+ advisor / roadmap source, persist it verbatim with ONE simple command, literal paths:
96
+
97
+ ```
98
+ node .claude/ukit/index/source-snapshot.mjs <original-path> docs/AI_HANDOFF/SOURCE.md
99
+ ```
100
+
101
+ (under the queued root, pass that root's SOURCE.md path literally). It records the origin
102
+ path, the sha256 of the original bytes and a byte-identical copy, and refuses to overwrite
103
+ an existing snapshot. If it cannot run (denial, budget), PLAN.md and LEDGER.md must state
104
+ `source snapshot MISSING` explicitly — never cite only a mutable path as the "source".
105
+
77
106
  ## Phase 0.5 — Write SPEC.md
78
107
 
79
108
  Write `docs/AI_HANDOFF/SPEC.md` from the template at `template_project/docs/AI_HANDOFF/SPEC.md`
@@ -86,13 +115,16 @@ defect — fix it before writing tasks.
86
115
 
87
116
  ## Phase 1 — Write PLAN.md
88
117
 
89
- Write all 7 sections to `docs/AI_HANDOFF/PLAN.md`:
118
+ Write all 6 sections to `docs/AI_HANDOFF/PLAN.md`:
90
119
 
91
120
  ```
92
121
  §1 Intent — what problem, what success looks like
93
122
  §2 Scope — in-scope / out-of-scope
94
123
  CONSTRAINT: tasks in the same wave must not modify the same file.
95
124
  If two tasks need the same file → make one depend on the other.
125
+ Global constraints — version floors, dependency limits, naming/copy
126
+ rules, platform requirements as one-line constraints; every TASK-xxx.md
127
+ inherits §2 by reference — do not repeat them inside each task.
96
128
  §3 Approach — technical solution, trade-offs, alternatives rejected
97
129
  §4 Test Plan — happy path × N + ≥2 edge cases of DIFFERENT kinds (e.g. null/empty AND
98
130
  boundary/concurrent — two near-duplicate cases do not satisfy this) +
@@ -104,9 +136,6 @@ Write all 7 sections to `docs/AI_HANDOFF/PLAN.md`:
104
136
  command. If the project genuinely has none, state that explicitly —
105
137
  do not omit silently.
106
138
  §6 Acceptance — checklist of done criteria (prefer verifiable/command-based criteria)
107
- §7 Global Constraints — one line each: version floors, dependency limits, naming/copy
108
- rules, platform requirements. Every TASK-xxx.md inherits this section
109
- by reference — do not repeat these constraints inside each task.
110
139
  ```
111
140
 
112
141
  **§4 is non-negotiable.** No test plan = plan not ready.
@@ -123,6 +152,21 @@ file that already ends with the footer above; refine it with `Edit` afterwards.
123
152
  validates `PLANNER_MODEL` on that write, so a refusal surfaces before you spend a long drafting
124
153
  pass — never draft the whole plan in your head/scratch and attempt the first write last.
125
154
 
155
+ ## Create files with Write/Edit only — never shell writes
156
+
157
+ `handoff-create` is planning-only and the model-tier guard refuses shell writes (`mkdir` + heredoc,
158
+ `cat > file`, `tee`, `echo >`, `sed -i`, compound `&&` chains that write). Create every plan, task, SPEC,
159
+ LEDGER, NOTES, INDEX and ACTIVE file with the `Write` tool (it creates missing parent directories) and
160
+ change existing files with `Edit`. Use Bash only for read-only inspection and the literal-path helper
161
+ commands this document names. This is the guard's own documented path, not a workaround, so choosing it
162
+ up front is required; only a refusal of a `Write`/`Edit` call triggers the report-and-stop rule below.
163
+
164
+ The same applies to the reviewer's round block and to `docs/WORKLOG.md`: the `code-reviewer` returns its
165
+ `### Round <N>` block in its reply (it has no write tool), and you add it to `PLAN.md`'s `## Plan Review Log`
166
+ with `Edit`; append the WORKLOG entry with `Edit` too. For inspection use one simple read-only command per
167
+ call (or `Read`/`Grep`/`Glob`) — no `cd … &&` chains, `for` loops, `;` sequences or `{a,b}` globs, which the
168
+ host permission layer asks approval for and an unattended run cannot answer.
169
+
126
170
  ## Blocked write = report, never retry or respawn
127
171
 
128
172
  If a `Write`/`Edit`/`Bash` call is refused (`BLOCKED …`, `hook error`, "denied by your
@@ -155,7 +199,9 @@ Use `_TEMPLATE.md` structure (from pre-read context or file).
155
199
  | Verification Commands | Runnable shell commands — MUST include the project's lint/typecheck command if one exists (see §5 rule above). Apply the "Test selection" resolution order from docs/AI_HANDOFF/RULES.md: `src/`/`scripts/` targets → the `tests` array in `.cache/index/tests-map.json`; `template_project/.claude/**`/`.claude/**` targets → path convention (hooks → `tests/hooks/` + `tests/handoff/cycle*/`; manifest/settings → `tests/manifest/`; runtime `.mjs` mirrors → `tests/core/*Parity*` + `tests/index/`); if both resolve to fewer than one test file the task MUST fall back to `yarn test:release-core` — never the full suite by default, never an empty selection |
156
200
  | Acceptance Criteria | Verifiable checklist |
157
201
 
158
- Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`.
202
+ Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`. Every task is
203
+ created `needs_breakdown` anyway — the two gates in "Task-budget validator" below (validator
204
+ verdict + review Approved) are what promote it to `ready`.
159
205
 
160
206
  **Wave logic (for your reference when splitting):**
161
207
  - Wave 1 = tasks with `Dependencies: none`
@@ -165,13 +211,37 @@ Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`.
165
211
 
166
212
  ### Task-budget validator — gate before any task becomes `ready`
167
213
 
168
- Before marking a task `ready`, run the task-budget validator on the file you just wrote:
214
+ Write every task file `Status: needs_breakdown` first — `ready` is a verdict, not a
215
+ default. A task becomes `ready` only when BOTH gates pass:
216
+
217
+ 1. the task-budget validator printed `VERDICT: ok` for that file (or a one-line dismissal
218
+ is recorded in its `## Discussion` thread), and
219
+ 2. the independent plan review (Step 2.5 of `/ukit:handoff-create`, P2.5 of fullstack)
220
+ ended `Approved`.
221
+
222
+ Run the validator per task file, right after writing it, as ONE simple command from the
223
+ project root — one command per file, each with its literal path:
169
224
 
170
225
  ```
171
- node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-xxx.md
226
+ node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-001.md
227
+ node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-002.md
172
228
  ```
173
229
 
174
- The validator's first stdout line is `VERDICT: ok` or `VERDICT: needs_breakdown`, followed by `REASON:` lines. It always exits 0 (advisory). When the verdict is `needs_breakdown`, either split the task into smaller TASK files until each one passes, or record a one-line dismissal in that task's `## Discussion` thread explaining why the violation is acceptable. A `ready` task whose validator verdict is `needs_breakdown` is not actually ready — Phase 3 (state file write) must follow a `VERDICT: ok` (or a documented dismissal).
230
+ No `cd` prefix, no `for`/`while` loop, no shell variable, no glob, no pipe, and no `&&`/`;` compound —
231
+ host permission checks reject those SHAPES ("cat reads a file by a relative path after a
232
+ cd in a compound command", "A variable in this command can't be checked before it runs"),
233
+ so a loop-shaped batch burns the round and the gate never runs. Eight tasks = eight
234
+ separate calls. Read files with the Read/Glob/Grep tools, never `cat`/`head`/`sed`
235
+ compounds.
236
+
237
+ The validator's first stdout line is `VERDICT: ok` or `VERDICT: needs_breakdown`, followed by `REASON:` lines. It always exits 0 (advisory). When the verdict is `needs_breakdown`, either split the task into smaller TASK files until each one passes, or record a one-line dismissal in that task's `## Discussion` thread explaining why the violation is acceptable.
238
+
239
+ Persist every verdict the moment it exists: right after each `VERDICT: ok` — and right after recording a one-line dismissal — append the verdict to that task file's `## Discussion` thread as one line, `Validator: ok <YYYY-MM-DD>` or `Validator: dismissed <YYYY-MM-DD> — <reason>`. Record the ok verdict from the tool itself with ONE simple command per task — `node .claude/ukit/index/task-budget-validator.mjs --record docs/AI_HANDOFF/tasks/TASK-001.md` — which re-validates and appends `Validator: ok <YYYY-MM-DD> <tool-version>` to the thread (atomic temp-file + rename, existing BOM and line endings preserved, idempotent, and it refuses a non-ok verdict — a `needs_breakdown` file is never recorded); the dismissal line stays hand-written. Verdicts live on disk, not in session memory: compaction must not erase a gate, and the later flip-to-ready step reads the task files from disk, never your session memory.
240
+
241
+ If the run is interrupted (budget exhausted, command denied, crash) before both gates
242
+ pass, the tasks STAY `needs_breakdown` — INDEX.md and ACTIVE.md must name which gate is
243
+ missing. A batch that never saw a `VERDICT: ok` is never `ready`, no matter how complete
244
+ the files look.
175
245
 
176
246
  Numeric thresholds and the minute table live in `src/core/taskBudgetValidator.js` and the shipped CLI twin. Do not restate them here — `tests/consistency/configDocsSync.test.js` greps this file for stale numbers and will fail if a threshold is hard-coded. Reference the validator / PLAN §3 instead.
177
247
 
@@ -191,6 +261,11 @@ dependency means B imports a symbol A creates, or B tests behavior A implements.
191
261
  - "Both touch the same area of the codebase" — irrelevant unless they touch the same *file*.
192
262
  - "A should be reviewed before B starts" — that is what the review phase is for.
193
263
 
264
+ One counter-case cuts the other way: when the OWNER SOURCE states a delivery-order
265
+ qualifier — "after import works", "once search lands" — that is a real dependency, not an
266
+ ordering preference. Encode it in `Dependencies` and name the source line in the task's
267
+ Spec references. Dropping it silently ships B before its precondition exists.
268
+
194
269
  Same-file collisions are the one real constraint, and they are cheap to design around: split
195
270
  along file boundaries so each task owns its files outright. If two pieces of work truly must
196
271
  edit one file, prefer merging them into a single task over chaining two — one task with two
@@ -201,7 +276,16 @@ most chains are ordering preferences that a wide wave 1 would satisfy just as we
201
276
 
202
277
  ## Phase 3 — Update state files
203
278
 
204
- **INDEX.md** row per task: `| TASK-001 | <name> | ready | none | - |`
279
+ **INDEX.md** row per task — write `needs_breakdown` at creation, flip to `ready` only after
280
+ both gates pass (validator `VERDICT: ok` per task + plan review `Approved`):
281
+ `| TASK-001 | <name> | needs_breakdown | none | - |`
282
+ Flip by re-reading the task files from disk: only a task whose `## Discussion` records
283
+ `Validator: ok <date>` (or `Validator: dismissed <date> — <reason>`) flips to `ready`; a
284
+ task without the recorded `Validator:` line stays `needs_breakdown` — a verdict held only
285
+ in the session does not count. Run the mechanical gate once before flipping any row —
286
+ `node .claude/ukit/index/ready-gate.mjs docs/AI_HANDOFF` (the planning root when queued) —
287
+ and flip only on `READY_GATE: ok`; a `tasks not ready:` or `index/task mismatch` line
288
+ names the gap to fix first. Never flip while the gate fails.
205
289
 
206
290
  **ACTIVE.md** — store base branch + cycle info:
207
291
  ```
@@ -211,6 +295,10 @@ Spec: docs/AI_HANDOFF/SPEC.md
211
295
  Tasks: <N> total
212
296
  Status: planning_done — ready for executor
213
297
  ```
298
+ When any task is still non-ready (a gate missing), write the honest state instead:
299
+ `Status: planning_incomplete — tasks not ready: <validator | plan review>` (a
300
+ human-readable ACTIVE.md marker only — machine readers parse INDEX.md rows and
301
+ task-file `- Status:` lines, never write `planning_incomplete` into those).
214
302
  Wave structure is NOT stored here — inferred from task `Dependencies` fields at runtime.
215
303
 
216
304
  **RUN.md** — write the run's `ExitPredicate:` line before iteration 1 (the orchestrator
@@ -256,11 +344,13 @@ catches it:
256
344
  10. Does each test case state a concrete `Expected` value, not "works correctly" or "returns successfully"? A test whose expectation cannot fail is not a test.
257
345
  11. Bugfix task → is there a regression test that fails against today's code?
258
346
  12. Could every listed test pass against an empty implementation? If so, the test cases describe nothing and must be rewritten.
347
+ 13. Does every test case expect ONE deterministic outcome? "X or nothing" ("returns the rows or an empty list") is two expectations in one — pin the expected value or split the test; a disjunctive expectation cannot fail for the right reason.
348
+ 14. Is every Verification Command an executable proof that runs the tests? Grepping a doc for a literal line (e.g. `Coverage: 20/20`) proves only that the line exists — disposition bookkeeping is not implementation evidence and never substitutes for a test run.
259
349
 
260
350
  Append the result to `PLAN.md`:
261
351
  ```
262
352
  ## Planner Self-Audit
263
- Checklist: 14/14 pass
353
+ Checklist: 16/16 pass
264
354
  Fixed during audit: <what you changed, or "nothing">
265
355
  Known gaps: <what you deliberately left out and why, or "none">
266
356
  ```
@@ -286,7 +376,7 @@ budget is the constraint on the whole run. Detail belongs in `PLAN.md`, not in t
286
376
 
287
377
  - Do NOT implement. Job ends when all tasks are `ready` or `needs_breakdown`.
288
378
  - Undone tasks in INDEX.md → continue that cycle; plan only what is still missing. Do not ask whether to start fresh, and do not overwrite task files that already carry an executor report or reviewer verdict.
289
- - An independent new request is never an overwrite of a ready plan. Overwrite only when the human explicitly names the existing plan to amend. First copy every prior file (PLAN.md, SPEC.md, LEDGER.md, NOTES.md, SOURCE.md, tasks/TASK-*.md, when present) to the next unused `amendments/rev-<NN>/` (scan existing rev dirs, take max+1, never reuse one). Then compose the revised PLAN.md in full: carry the existing `## Amendment History` forward and append this amendment's entry (prior review rounds and verdicts; prior requirement, decision and task versions with what changed and why; each change's `invalidated` evidence: tasks, executor reports, verdicts that no longer hold). Write the revised PLAN.md only after that entry is in the composed text. Otherwise write to the queued root.
379
+ - An independent new request is never an overwrite of a ready plan. Overwrite only when the human explicitly names the existing plan to amend. First copy every prior file (PLAN.md, SPEC.md, LEDGER.md, NOTES.md, SOURCE.md, tasks/TASK-*.md, when present) to the next unused `amendments/rev-<NN>/` (scan existing rev dirs, take max+1, never reuse one). Then run `node .claude/ukit/index/amend-guard.mjs docs/AI_HANDOFF PLAN.md SPEC.md` (naming every file you will rewrite) and proceed only on `AMEND_GUARD: ok` — the checker is mechanical (the rev copy exists on disk); its invocation is prose. Then compose the revised PLAN.md in full: carry the existing `## Amendment History` forward and append this amendment's entry (prior review rounds and verdicts; prior requirement, decision and task versions with what changed and why; each change's `invalidated` evidence: tasks, executor reports, verdicts that no longer hold). Write the revised PLAN.md only after that entry is in the composed text. Otherwise write to the queued root.
290
380
  - A task that already carries an executor report is superseded, never overwritten: write the replacement as `TASK-<id>-R<n>.md` and keep the original file with its report.
291
381
  - Round cap: count only completed reviewer verdicts (`### Round` entries with a `PLAN_REVIEW:` or verdict line). Repair markers (`### Repair <N>`, `### Escalation <N>`) are never counted. When two verdicts exist and no escalation is recorded, record `### Escalation 1 — <reason>`, apply the findings with a changed strategy (`### Repair <N> — changed strategy`), and let the reviewer run once more. If the escalation is already spent, do not request another round: mark still-flagged tasks `needs_breakdown` and write `PLAN_REVIEW: Unresolved — …`, never `Approved`. Identical findings after a repair (the finding signature set is unchanged) are non-progress: do not run another identical loop; retain Unresolved with a precise diagnostic. **Recovery exception (explicit, at most once per plan):** it runs only when the escalation is spent, the latest verdict is still `Issues Found`, the latest repair changed the signature set, and the Plan Review Log has no `### Recovery 1 — consumed` marker. Then, in order: (1) append `### Recovery 1 — consumed <YYYY-MM-DD>` before any other work; (2) run a root-cause investigation and a different decomposition; (3) request one fresh independent review of the changed artifact and append its verdict as `### Round <N>`. The marker is never removed or re-created. If the marker already exists, or the signature set is unchanged, do not request another review: retain Unresolved with a precise diagnostic. An owner conflict routes an owner decision. Never mark a plan `Approved` by the cap alone. After every verdict, append `### Signatures Round <N>: <sig>; <sig>; …` where each `<sig>` is the reviewer's `<location> :: <claim>` pair normalized (lowercased, whitespace collapsed, claim first 120 characters). Mandatory overflow (feasible/adapted rows beyond the 10-task dispatch window) is grouped by dependency chain into `queued/<plan-slug>-overflow-<NN>/` (max+1 index; on collision append `-2`, `-3`, …), never a deliberate-future header.
292
382
  - Cannot determine test cases → `needs_breakdown` + Discussion thread note. Never mark a task `ready` with vague tests just to keep the pipeline moving — a task with fake tests passes review and ships a bug, which costs far more than one blocked task.
@@ -74,7 +74,7 @@ export const HOOK_EVENT_MAP = {
74
74
  // Delegation result: releases the per-task execution claim taken by the pre-call guard.
75
75
  'MultiEdit|NotebookEdit|Agent|Task': ['record-execution.mjs'],
76
76
  },
77
- before_agent_start: ['sensitive-data-guard.mjs', 'skill-router.sh', 'vision-router.sh', 'context-window-guard.sh'],
77
+ before_agent_start: ['sensitive-data-guard.mjs', 'record-execution.mjs', 'skill-router.sh', 'vision-router.sh', 'context-window-guard.sh'],
78
78
  'session.compacting': ['reinject-context.sh'],
79
79
  // TASK-003 (FR-003): the in-proc emit step runs LAST — project-important.sh
80
80
  // owns the mandate-first lead (TASK-042 contract); the emit is advisory,