@ngockhoale/ukit 3.4.65 → 3.4.67

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 (27) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/manifests/documentation.yaml +11 -0
  3. package/package.json +2 -2
  4. package/scripts/release/verify-release.mjs +7 -0
  5. package/scripts/test/run-handoff-isolated.mjs +178 -0
  6. package/src/core/executionContracts.js +2 -2
  7. package/template_project/.claude/agents/code-reviewer.md +4 -1
  8. package/template_project/.claude/agents/handoff-planner.md +3 -0
  9. package/template_project/.claude/commands/ukit/handoff-clear.md +34 -35
  10. package/template_project/.claude/commands/ukit/handoff-create.md +40 -21
  11. package/template_project/.claude/commands/ukit/handoff-fullstack.md +141 -66
  12. package/template_project/.claude/commands/ukit/handoff-implement.md +27 -12
  13. package/template_project/.claude/commands/ukit/handoff-review.md +18 -8
  14. package/template_project/.claude/hooks/handoff-resume.sh +8 -0
  15. package/template_project/.claude/skills/advisor-plan/REFERENCE.md +33 -2
  16. package/template_project/.claude/skills/advisor-plan/SKILL.md +6 -4
  17. package/template_project/.claude/ukit/index/route-resolver.mjs +2 -2
  18. package/template_project/.claude/ukit/runtime/resumable-run.mjs +40 -7
  19. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +128 -44
  20. package/template_project/.claude/ukit/runtime/task-watchdog.mjs +1 -1
  21. package/template_project/.omp/agents/code-reviewer.md +4 -1
  22. package/template_project/.omp/agents/handoff-planner.md +3 -0
  23. package/template_project/AGENTS.md +2 -2
  24. package/template_project/CLAUDE.md +2 -2
  25. package/template_project/docs/AI_HANDOFF/RULES.md +24 -27
  26. package/template_project/docs/AI_HANDOFF/queued/README.md +52 -0
  27. package/template_project/instructions/core.md +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.67 - 2026-10-23
6
+
7
+ - Fix: a wait/monitor/hold-only request ("keep waiting on the verifier job, read output to the footer, do not edit while the candidate is frozen") is now classified verification-only (`READ_ONLY`, `verification-evidence`) instead of the `CODE_IMPLEMENTATION` default that demanded `write-evidence` and forced an Edit/Write. A wait that also carries a real order ("wait, then fix src/app.js") stays an implementation. Vietnamese "rồi" now splits clauses for this check. Both classifier copies (`src/core/executionContracts.js`, installed `route-resolver.mjs`) changed together; regression in `tests/hooks/gateIntentClassification.test.js`.
8
+ - Test-only: `hookProcessTree` escapee regression budget no longer races a cold Node start under full-suite load.
9
+
10
+ ## 3.4.66 - 2026-10-23
11
+
12
+ **Handoff hardening: scope-frozen runs, completed-only ARCHIVE, bounded repair, isolated verification.**
13
+ - Stop coordinator: INDEX task rows parse positionally (empty cells keep columns; malformed rows fail closed); an unreadable or vanished archive snapshot no longer falls back to stale task headers; optional `ScopeQueue`/`Cycle` freeze a run to its assigned scope so unrelated queued or archived history cannot prove completion; `Paused: yes` releases continuation without minting completion.
14
+ - Archive writer: canonical `docs/AI_HANDOFF/ARCHIVE/cycle-NN/`; refuses (before any write) symlinked docs/AI_HANDOFF/archive/cycle/INDEX/TASK paths, malformed or unreadable prior snapshots and any task-enumeration error instead of snapshotting a partial cycle; legacy lowercase archives stay read-only.
15
+ - Commands: create queues independent plans instead of overwriting, preserves sources and amendments; fullstack/implement/review commit with `git commit --only` explicit paths (unrelated staged and unstaged owner work preserved, proven against a real Git fixture); failures route to bounded repair or replan instead of blocked; one persisted Recovery exception after escalation; clear pauses and preserves unfinished work and keeps read-back write verification; blocked is reserved for external blockers or genuine owner requirement conflicts.
16
+ - `yarn test:handoff` now runs `tests/handoff/**` in a disposable copy with throwaway HOME, excluded live handoff state (any-case ARCHIVE), vetted symlinks, checked git setup, bounded children and explicit path validation; wired into `release:verify`.
17
+
5
18
  ## 3.4.65 - 2026-10-22
6
19
 
7
20
  **Configuration capture: Vietnamese comparison is not a mutation order.**
@@ -616,6 +616,17 @@ entries:
616
616
  validation: [none]
617
617
  archive_policy: immutable
618
618
  notes: 2026-10-19 captured OMP lifecycle closure and offline verification boundaries
619
+ - id: docs-analysis-handoff-hardening
620
+ path: docs/analysis/HANDOFF-HARDENING.md
621
+ class: canonical
622
+ audience: [maintainer]
623
+ owner: product
624
+ source_of_truth: docs/analysis/HANDOFF-HARDENING.md
625
+ merge_strategy: none
626
+ load_policy: on-demand
627
+ validation: [manual]
628
+ archive_policy: never
629
+ notes: Current owner scope, hardening evidence and remaining release obligations; not runtime state
619
630
  - id: docs-analysis-orchestration-final-verification
620
631
  path: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
621
632
  class: archive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.65",
3
+ "version": "3.4.67",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,7 +74,7 @@
74
74
  "release:verify": "node ./scripts/release/verify-release.mjs",
75
75
  "test": "vitest run",
76
76
  "test:liveness": "vitest run tests/liveness/",
77
- "test:handoff": "node ./bin/ukit install >/dev/null && node --test --test-concurrency=1 'tests/handoff/**/*.test.mjs' 'tests/handoff/**/*.test.js'",
77
+ "test:handoff": "node ./scripts/test/run-handoff-isolated.mjs",
78
78
  "test:watch": "vitest"
79
79
  },
80
80
  "engines": {
@@ -93,6 +93,13 @@ const steps = [
93
93
  args: ['vitest', 'run', 'tests/consistency/docContracts.test.js'],
94
94
  env: process.env,
95
95
  },
96
+ {
97
+ // Bounded behavioral lane: runs the handoff suite in a disposable copy (never the checkout).
98
+ label: 'Handoff isolated lane',
99
+ command: 'yarn',
100
+ args: ['test:handoff'],
101
+ env: process.env,
102
+ },
96
103
  {
97
104
  label: 'Core test suite',
98
105
  command: 'yarn',
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ // Runs the handoff test lane in a disposable copy of the project so `ukit install`
3
+ // (which writes .claude/, .ukit/, .cache/) can never overwrite the source checkout.
4
+ // The child's native exit code is propagated; the copy is removed in every case.
5
+ import { spawnSync } from 'node:child_process';
6
+ import { constants, cpSync, existsSync, lstatSync, mkdtempSync, readdirSync, readlinkSync, realpathSync, rmSync } from 'node:fs';
7
+ import { buildIsolatedEnv } from '../../tests/helpers/testHomeIsolation.mjs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+
11
+ // Project root is the directory the runner was invoked from (package.json scripts run at the root).
12
+ const sourceRoot = path.resolve(process.cwd());
13
+ const SKIP_TOP_LEVEL = new Set(['.git', 'node_modules']);
14
+ // Live handoff run/backlog state and runtime caches must never leak into the fixture: a copied
15
+ // RUN.md or queue makes the handoff gates act on the real project's pending work.
16
+ const LIVE_STATE_PREFIXES = [
17
+ 'docs/AI_HANDOFF/RUN.md',
18
+ 'docs/AI_HANDOFF/INDEX.md',
19
+ 'docs/AI_HANDOFF/ACTIVE.md',
20
+ 'docs/AI_HANDOFF/HISTORY.md',
21
+ 'docs/AI_HANDOFF/tasks',
22
+ 'docs/AI_HANDOFF/queued',
23
+ 'docs/AI_HANDOFF/archive',
24
+ 'docs/AI_HANDOFF/ARCHIVE',
25
+ 'docs/AI_HANDOFF/blocked-tests',
26
+ 'docs/AI_HANDOFF/benchmark',
27
+ '.ukit/storage/cache',
28
+ '.ukit/storage/security',
29
+ '.ukit/storage/runs',
30
+ ];
31
+ const unsafeSymlinks = [];
32
+
33
+ function shouldCopy(src) {
34
+ const rel = path.relative(sourceRoot, src);
35
+ if (rel === '') return true;
36
+ const top = rel.split(path.sep)[0];
37
+ if (SKIP_TOP_LEVEL.has(top)) return false;
38
+ // Nested worktrees would recursively copy the whole repo again.
39
+ if (rel.split(path.sep).slice(0, 2).join('/') === '.claude/worktrees') return false;
40
+ const posixRel = rel.split(path.sep).join('/');
41
+ // Case-insensitive: ARCHIVE/archive are the same directory on macOS/Windows.
42
+ const lower = posixRel.toLowerCase();
43
+ if (LIVE_STATE_PREFIXES.some((p) => lower === p.toLowerCase() || lower.startsWith(`${p.toLowerCase()}/`))) return false;
44
+ // A symlink must stay inside the tree; otherwise the copy would write through to the owner.
45
+ if (lstatSync(src).isSymbolicLink()) {
46
+ const target = readlinkSync(src);
47
+ const resolved = path.resolve(path.dirname(src), target);
48
+ const inside = path.relative(sourceRoot, resolved);
49
+ if (path.isAbsolute(target) || inside.startsWith('..') || path.isAbsolute(inside)) {
50
+ unsafeSymlinks.push(posixRel);
51
+ return false;
52
+ }
53
+ }
54
+ if (LIVE_STATE_PREFIXES.some((p) => posixRel === p || posixRel.startsWith(`${p}/`))) return false;
55
+ return true;
56
+ }
57
+
58
+ let childEnv = process.env;
59
+ // Hard bound for every child (git, install, the node:test run) so a hung test cannot stall a release.
60
+ const CHILD_TIMEOUT_MS = 20 * 60 * 1000;
61
+
62
+ function run(cwd, command, args) {
63
+ const result = spawnSync(command, args, { cwd, stdio: 'inherit', env: childEnv, timeout: CHILD_TIMEOUT_MS });
64
+ if (result.error) {
65
+ console.error(`[handoff-isolated] failed to start ${command}: ${result.error.message}`);
66
+ return 1;
67
+ }
68
+ if (result.signal) {
69
+ console.error(`[handoff-isolated] ${command} killed by signal ${result.signal}`);
70
+ return 1;
71
+ }
72
+ return result.status ?? 1;
73
+ }
74
+
75
+ function main() {
76
+ const copyRoot = mkdtempSync(path.join(os.tmpdir(), 'ukit-handoff-isolated-'));
77
+ console.log(`[handoff-isolated] copy=${copyRoot}`);
78
+ // Throwaway HOME so install/tests never touch the caller's real user layer.
79
+ const homeRoot = mkdtempSync(path.join(os.tmpdir(), 'ukit-handoff-home-'));
80
+ childEnv = { ...process.env, ...buildIsolatedEnv(homeRoot) };
81
+ let exitCode = 1;
82
+ try {
83
+ cpSync(sourceRoot, copyRoot, { recursive: true, filter: shouldCopy, verbatimSymlinks: true });
84
+ if (unsafeSymlinks.length > 0) {
85
+ console.error(`[handoff-isolated] unsafe source symlink (resolves outside the project): ${unsafeSymlinks.join(', ')}`);
86
+ return;
87
+ }
88
+
89
+ // Several handoff tests call git (worktree add, ls-files, check-ignore). The copy has no
90
+ // .git, so give it an isolated throwaway repo with the copied tree committed.
91
+ for (const gitArgs of [
92
+ ['init', '-q'],
93
+ ['add', '-A'],
94
+ ['-c', 'user.email=isolated@ukit.test', '-c', 'user.name=isolated', 'commit', '-q', '-m', 'isolated snapshot'],
95
+ ]) {
96
+ const code = run(copyRoot, 'git', gitArgs);
97
+ if (code !== 0) {
98
+ console.error(`[handoff-isolated] git snapshot setup failed (git ${gitArgs.find((a) => !a.startsWith('-') && !a.includes('=')) ?? gitArgs[0]} exit ${code})`);
99
+ exitCode = code;
100
+ return;
101
+ }
102
+ }
103
+
104
+ // Dependencies are cloned (copy-on-write where supported) AFTER the snapshot commit so tool
105
+ // caches written under node_modules can never write through to the source checkout.
106
+ // UKIT_HANDOFF_DEPS_ROOT lets a worktree without node_modules borrow an installed checkout's.
107
+ const depsRoot = path.resolve(process.env.UKIT_HANDOFF_DEPS_ROOT || sourceRoot);
108
+ const sourceModules = path.join(depsRoot, 'node_modules');
109
+ const copyModules = path.join(copyRoot, 'node_modules');
110
+ if (existsSync(sourceModules) && !existsSync(copyModules)) {
111
+ // Vet dependency symlinks too: one that resolves outside node_modules (for example a link
112
+ // back to the source checkout) would let the copy write through to the owner's tree.
113
+ const realModules = realpathSync(sourceModules);
114
+ cpSync(sourceModules, copyModules, {
115
+ recursive: true,
116
+ verbatimSymlinks: true,
117
+ mode: constants.COPYFILE_FICLONE,
118
+ filter: (src) => {
119
+ if (!lstatSync(src).isSymbolicLink()) return true;
120
+ const resolved = path.resolve(path.dirname(src), readlinkSync(src));
121
+ const rel = path.relative(realModules, resolved);
122
+ return !path.isAbsolute(readlinkSync(src)) && !rel.startsWith('..') && !path.isAbsolute(rel);
123
+ },
124
+ });
125
+ }
126
+
127
+ const installCode = run(copyRoot, process.execPath, [path.join(copyRoot, 'bin', 'ukit'), 'install']);
128
+ if (installCode !== 0) {
129
+ console.error(`[handoff-isolated] install failed with exit ${installCode}`);
130
+ exitCode = installCode;
131
+ } else {
132
+ // Optional targeted mode: positional args are handoff test files relative to the project root.
133
+ const handoffRoot = path.join(copyRoot, 'tests', 'handoff');
134
+ const targeted = [];
135
+ for (const rel of process.argv.slice(2)) {
136
+ const full = path.resolve(copyRoot, rel);
137
+ const within = path.relative(handoffRoot, full);
138
+ if (within === '' || within.startsWith('..') || path.isAbsolute(within) || path.isAbsolute(rel)) {
139
+ console.error(`[handoff-isolated] invalid handoff test path: ${rel}`);
140
+ exitCode = 2;
141
+ return;
142
+ }
143
+ targeted.push(full);
144
+ }
145
+ const testFiles = [];
146
+ const testDir = path.join(copyRoot, 'tests', 'handoff');
147
+ if (targeted.length > 0) testFiles.push(...targeted);
148
+ else if (existsSync(testDir)) collectTests(testDir, testFiles);
149
+ if (testFiles.length === 0) {
150
+ console.error('[handoff-isolated] no handoff test files found');
151
+ exitCode = 1;
152
+ } else {
153
+ exitCode = run(copyRoot, process.execPath, ['--test', '--test-concurrency=1', ...testFiles]);
154
+ }
155
+ }
156
+ } finally {
157
+ // Diagnostic escape hatch: keep the copy for inspection when explicitly requested.
158
+ if (process.env.UKIT_HANDOFF_KEEP_COPY === '1') {
159
+ console.log(`[handoff-isolated] keeping copy for inspection: ${copyRoot}`);
160
+ } else try {
161
+ rmSync(copyRoot, { recursive: true, force: true });
162
+ } catch (error) {
163
+ console.error(`[handoff-isolated] cleanup failed for ${copyRoot}: ${error.message}`);
164
+ }
165
+ try { rmSync(homeRoot, { recursive: true, force: true }); } catch {}
166
+ process.exitCode = exitCode;
167
+ }
168
+ }
169
+
170
+ function collectTests(dir, out) {
171
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
172
+ const full = path.join(dir, entry.name);
173
+ if (entry.isDirectory()) collectTests(full, out);
174
+ else if (/\.test\.(mjs|js)$/.test(entry.name)) out.push(full);
175
+ }
176
+ }
177
+
178
+ main();
@@ -261,12 +261,12 @@ function declaredReadOnly(promptText, releasePolicy) {
261
261
  // "template/install", "source change") as mutation orders. Order is structural: a request whose
262
262
  // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
263
263
  // change-leading clause, orders no change - it owes verification evidence, not a write.
264
- const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
264
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:(?:keep|continue|tiep tuc)\s+)?(?:verify|re-?run|confirm|xac nhan|validate|double-?check|wait(?:ing)?|monitor|watch|cho(?=\s+(?:dung\s+)?(?:job|verifier|build|test|suite|gate|ci|run|task|ket qua|xong|output)\b))\b/;
265
265
  const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
266
266
  export function isVerificationOnlyRequest(promptText) {
267
267
  const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
268
268
  .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
269
- const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
269
+ const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b|\broi\b)\s*/)
270
270
  .map((clause) => clause.split(/\bwithout\b/)[0]);
271
271
  return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
272
272
  && !clauses.some((clause) => CHANGE_LEAD.test(clause));
@@ -210,8 +210,9 @@ verdict block — with these additions:
210
210
 
211
211
  ### Inputs you expect
212
212
 
213
- - Path to the spec/plan document (e.g. `docs/plans/*.md`). No diff, no task file, no executor report — review the document itself.
213
+ - Path to the spec/plan document (e.g. `docs/plans/*.md`). No diff and no executor report.
214
214
  - 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`.
215
+ - 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.
215
216
 
216
217
  ### Review order
217
218
 
@@ -239,6 +240,8 @@ Only flag issues that would cause real problems during implementation planning.
239
240
 
240
241
  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:
241
242
 
243
+ 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.
244
+
242
245
  ```
243
246
  ### Round <N> — <YYYY-MM-DD> · <your model>
244
247
  Status: Approved | Issues Found
@@ -284,5 +284,8 @@ budget is the constraint on the whole run. Detail belongs in `PLAN.md`, not in t
284
284
 
285
285
  - Do NOT implement. Job ends when all tasks are `ready` or `needs_breakdown`.
286
286
  - 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.
287
+ - 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.
288
+ - 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.
289
+ - 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.
287
290
  - 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.
288
291
  - You may be invoked unattended. Ask nothing; where the caller allowed questions, that window closed before you were spawned. Resolve ambiguity by reading the codebase, choose the option you would recommend, and record the choice and its rationale in `PLAN.md §3`.
@@ -37,54 +37,53 @@ git branch | grep handoff # every handoff/* branch must be named in the retaine
37
37
  An unmerged `handoff/integration` branch is retained the same way: report it with its reason
38
38
  (`unmerged history`), never delete it with `-D`.
39
39
 
40
- ## Step 2 — Unstage any pending changes
40
+ ## Step 2 — Preserve staged owner work
41
41
 
42
- ```bash
43
- git restore --staged . 2>/dev/null || true
44
- ```
42
+ Inspect `git diff --cached --name-only`. Unstage only explicitly identified run-owned
43
+ paths when necessary; leave unrelated staged changes untouched. Clearing does not authorize
44
+ reverting source, discarding dirty files, or changing host permissions.
45
45
 
46
- ## Step 3 — Archive whatever is done (best-effort)
46
+ ## Step 3 — Preserve open work; archive only verified completion
47
47
 
48
- Read `docs/AI_HANDOFF/INDEX.md` for any tasks that were completed.
49
- Write `docs/AI_HANDOFF/archive/cycle-NNN.md` (if there is anything worth archiving):
50
- ```
51
- # Cycle NNN — <YYYY-MM-DD> — ABORTED
52
- ## Summary: cycle was cleared before completion
53
- ## Spec: <copy SPEC.md — or note its path if archived separately>
54
- ## Tasks: <copy INDEX.md table as-is>
55
- ```
56
- If `archive/` has > 3 files → delete oldest, append 1-line summary to `HISTORY.md`.
48
+ Clearing is cancellation or pause, **not completion**. Read INDEX, PLAN, review reports and
49
+ verification/release evidence together. Pending, blocked, pending-review, changes-requested,
50
+ pending-release and failed integration obligations remain open under `docs/AI_HANDOFF/`.
51
+ A `done` summary alone is not proof. Do not move an incomplete cycle into completed history.
57
52
 
58
- After writing the archive file, **verify the write** — read it back and confirm
59
- the `ABORTED` heading is present; on mismatch or read failure retry once, on
60
- second failure stop and report (same rule as handoff-fullstack's "State-file
61
- write verification").
53
+ Only when every assigned obligation has independently verified terminal-success evidence,
54
+ use the fullstack F3 procedure: move the completed task records to
55
+ `docs/AI_HANDOFF/ARCHIVE/cycle-NN/`, run `archive-cycle` before resetting INDEX, preserve
56
+ SPEC/PLAN/LEDGER/source/review/evidence, and verify the resulting snapshot. Never prune prior
57
+ history. Legacy lowercase `archive/` and single-file histories are read-only provenance,
58
+ not authorization to resume old work or proof that the current cycle completed.
62
59
 
63
- ## Step 4 — Reset state files
60
+ ## Step 4 — Pause, or reset state files, without losing the backlog
64
61
 
65
- ```
66
- PLAN.md → "# PLAN\n_(empty)_"
67
- SPEC.md → restore the untouched template (keep section headings, clear content)
68
- INDEX.md → empty table header only
69
- ACTIVE.md → "# ACTIVE\n_(no active cycle)_"
70
- RUN.md → set `Phase: done` (or delete) — a live cursor that is neither `done` nor `blocked`
71
- makes the Stop gate refuse the next session's stops; clearing a cycle MUST close the cursor.
72
- tasks/TASK-*.md → delete all (keep _TEMPLATE.md)
73
- ```
62
+ If unfinished work remains open, keep SPEC, PLAN, LEDGER, INDEX and task reports intact.
63
+ Set `Paused: yes` in RUN.md, keep its actual phase and scope, and record the cancellation
64
+ reason plus the next recovery action. Update ACTIVE.md to say paused, not completed.
65
+ The existing pause marker releases continuation safely; do not manufacture `Phase: done`
66
+ or erase RUN.md to evade a completion predicate.
67
+
68
+ Only after the completed-cycle archive and evidence are verified may the active planning
69
+ files be reset to their templates and INDEX to its empty header. Preserve `_TEMPLATE.md`,
70
+ queued plans, amendments and every record not included in that completed archive.
74
71
 
75
- Every file rewritten above is a state file — **verify the write** on each:
76
- read it back and confirm the marker you just wrote (empty INDEX header,
77
- `_(no active cycle)_`, `Phase: done`). On mismatch or read failure retry the
78
- write once; on second failure STOP and report — never leave unverified state
79
- files behind, or the next cycle resumes from stale bytes.
72
+ Every file rewritten above is a state file — **verify the write** on each: read it back and
73
+ confirm the marker you just wrote (`Paused: yes`, the paused ACTIVE.md line, or, after a
74
+ verified completed archive, the empty INDEX header). On mismatch or read failure retry the
75
+ write once; on second failure STOP, report the actual error and retain all recovery records —
76
+ never leave unverified state files behind (same rule as handoff-fullstack's "State-file write
77
+ verification").
80
78
 
81
79
  ## Step 5 — Report
82
80
 
83
81
  ```
84
- Cleared: X receipt-gated tasks removed, staged changes unstaged.
82
+ Cleared: X receipt-gated worktrees removed; unrelated staged changes preserved.
85
83
  Retained with reason: <list each surviving worktree/branch and why — receipt missing,
86
84
  not approved, FAIL, missing model, dirty vs snapshot, unmerged history>.
87
- Handoff docs reset. Ready for new cycle.
85
+ Open obligations: <IDs and recovery action; paused, not completed>.
86
+ Completed archive: <verified path, or none>.
88
87
  ```
89
88
 
90
89
  > Claude Code: spawn `ukit-small-task-maintainer` (haiku). omp: the `task` tool with `agent: "ukit-small-task-maintainer"`.
@@ -42,11 +42,14 @@ is **not** `done`/`blocked`, a fullstack run is live and owns the canonical plan
42
42
  executed.
43
43
  - Report the queued path; the live run continues untouched.
44
44
 
45
- **Explicit abandonment is the only way to replace a live run.** Only when the human
46
- explicitly says to abandon/clear the current run, first run `/ukit:handoff-clear`
47
- (sets `Phase: done` / removes `RUN.md`), then plan against the canonical files. A mere
48
- mention of the new work is not abandonment — absent that explicit instruction, always
49
- take the queued path.
45
+ **Explicit abandonment never overwrites an unfinished canonical run.** Only when the human
46
+ explicitly says to abandon/clear the current run, first run `/ukit:handoff-clear`, which
47
+ sets `Paused: yes` in `RUN.md` and keeps its actual phase and scope, recording the
48
+ cancellation reason and next recovery action (it never sets `Phase: done` and never erases
49
+ `RUN.md`). The paused canonical files are still unfinished: the new request is written under
50
+ the queued root `docs/AI_HANDOFF/queued/<slug>/` and the canonical task files, INDEX, and
51
+ SPEC/PLAN are not overwritten. A mere mention of the new work is not abandonment — absent
52
+ that explicit instruction, always take the queued path.
50
53
 
51
54
  ### Planning-only output root (decided here, passed to the planner)
52
55
 
@@ -102,11 +105,16 @@ the document into a LEDGER, a bounded task set and a queue, and never implements
102
105
  under `/ukit:handoff-fullstack` or `/ukit:handoff-implement`.
103
106
  - **Queue threshold:** at most 10 tasks in total: all active (`ready`). More than 10: the
104
107
  first 10 by dependency order are active and the rest become queued blueprints under
105
- `docs/AI_HANDOFF/queued/<slug>/` whose `HANDOFF.md` header carries
106
- `QueueIntent: deliberate-future`. Queue contract: the header also carries `QueueOrder: <int>`
107
- (drain order, lowest first), `QueueDrain: separate-backlog` and `QueueSource: <R-IDs/epics>`;
108
- `/ukit:handoff-fullstack` reports such a blueprint as `Backlog: <slug>`, never folds it into
109
- the current cycle, and drains it only when the human names the slug (one blueprint per invocation).
108
+ `docs/AI_HANDOFF/queued/<slug>/`. Only rows the LEDGER dispositions `deferred` (intentionally
109
+ later) get a `HANDOFF.md` header carrying `QueueIntent: deliberate-future`. Overflow of
110
+ `feasible`/`adapted` (mandatory) rows is queued **without** `QueueIntent: deliberate-future`,
111
+ so the fullstack sweep still folds it in — mandatory work is never silently excluded as
112
+ deliberate-future. Mandatory overflow carries none of the `Queue*` header lines; it is a legacy
113
+ blueprint and is consumed like any other queued plan. Queue contract for the deliberate-future kind
114
+ (LEDGER `deferred` rows only): the header also carries `QueueOrder: <int>` (drain order, lowest
115
+ first), `QueueDrain: separate-backlog` and `QueueSource: <R-IDs/epics>`; `/ukit:handoff-fullstack`
116
+ reports such a blueprint as `Backlog: <slug>` and never folds it into the current cycle; it drains
117
+ only when the human names the slug (one blueprint per invocation).
110
118
 
111
119
  Model split on Claude Code: haiku/`unic-lite` gathers, opus/`unic-smart` decides, and
112
120
  sonnet/`unic-code` implements later; omp maps these to `@smol`/`@slow`/`@default`.
@@ -167,14 +175,16 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
167
175
  0. **Live-run guard — decide OUTPUT_ROOT first (see "A live fullstack run is never
168
176
  overwritten").** If Step 1 reported `RUN.md` with a `Phase:` that is not `done`/`blocked`,
169
177
  a fullstack run is live: set `OUTPUT_ROOT = docs/AI_HANDOFF/queued/<slug>/` (`<slug>` =
170
- a short kebab name for this work), write ONLY there, and skip steps 1, 6 and 7 — the
171
- canonical `RUN.md`/`INDEX.md`/`PLAN.md`/`SPEC.md`/`ACTIVE.md`/`tasks/` stay untouched.
178
+ a short kebab name for this work; if that directory already exists, append `-2`, `-3`, … —
179
+ never overwrite an existing blueprint), write ONLY there, and skip steps 1 and 7. Step 6
180
+ writes the queued root's own draft `INDEX.md`, whose rows are `queued_blueprint`, never
181
+ `ready`. The canonical `RUN.md`/`INDEX.md`/`PLAN.md`/`SPEC.md`/`ACTIVE.md`/`tasks/` stay untouched.
172
182
  Otherwise `OUTPUT_ROOT = docs/AI_HANDOFF/`. Everything below writes under `OUTPUT_ROOT`.
173
183
 
174
184
  1. Check INDEX.md task statuses (canonical root only — skip when the live-run guard routed
175
185
  you to the queued root):
176
- - All tasks are `ready` (planning only) → **re-run allowed**: overwrite PLAN.md and TASK-xxx.md freely — this is iterative refinement.
177
- - Any task is `pending_review`, `changes_requested`, `merge_conflict`, or `blocked` → **do not overwrite**: that cycle is mid-flight and its task files carry executor reports and verdicts. Report which tasks are in flight and point the human at `/ukit:handoff-review` (to finish the cycle) or `/ukit:handoff-clear` (to abandon it). Planning is the one phase where stopping is correct — there is nothing safe to do automatically here.
186
+ - All tasks are `ready` (planning only) → **only an explicit amendment may overwrite**: when the human names the existing plan to refine: 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>/` under the same root (scan existing rev dirs and take max+1; never reuse or overwrite an existing rev dir). 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, so the overwrite can never drop the history. An **independent new request** (a different goal, even when the old tasks are all `ready`) is NOT an amendment: write it under the queued root `docs/AI_HANDOFF/queued/<slug>/` and never overwrite the existing canonical plan.
187
+ - Any task is `pending_review`, `changes_requested`, `merge_conflict`, or `blocked` → **do not overwrite** the in-flight cycle: its task files carry executor reports and verdicts. An **independent new request** is still planned and written under the queued root `docs/AI_HANDOFF/queued/<slug>/` (it is never refused for this reason); report which tasks are in flight and point the human at `/ukit:handoff-review` (to finish the cycle) or `/ukit:handoff-clear` (to abandon it). Only an explicit amendment of the in-flight plan is refused, with that pointer.
178
188
  - No tasks → fresh cycle, proceed normally.
179
189
 
180
190
  2. Resolve base branch:
@@ -221,9 +231,12 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
221
231
  - Acceptance Criteria (verifiable checklist)
222
232
  Missing any field → status: `needs_breakdown`, never `ready`
223
233
 
224
- 6. Update `${OUTPUT_ROOT}INDEX.md` — one row per task, `status=ready`. **Skip entirely when
225
- the live-run guard routed you to the queued root** — a blueprint carries no runnable
226
- `ready` row; its `HANDOFF.md` activation notes are the record instead.
234
+ 6. Update `${OUTPUT_ROOT}INDEX.md` — one row per task. Canonical root: `status=ready`.
235
+ **Queued root: a draft index with `status=queued_blueprint` rows, never `ready`.** The queued
236
+ blueprint's own `INDEX.md` and `tasks/` are a draft only: they are not canonical runnable
237
+ admission. The "first 10 active" rule applies to the canonical root alone; a blueprint's tasks
238
+ become runnable only when `/ukit:handoff-fullstack` folds them into `docs/AI_HANDOFF/tasks/`
239
+ and the canonical `INDEX.md`. Its `HANDOFF.md` activation notes are the record of intent.
227
240
 
228
241
  7. Update `ACTIVE.md` (canonical root only — **skip when queued**; the queued blueprint's
229
242
  `HANDOFF.md` replaces it):
@@ -249,14 +262,20 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
249
262
 
250
263
  - count = 0 → run the review below.
251
264
  - count = 1 and the round returned `Issues Found` → planner revises once, then run **one** more review round.
252
- - count ≥ 2 → do NOT invoke the reviewer again, and do not stop. Have the planner apply every outstanding finding directly to `PLAN.md` and the affected `TASK-xxx.md` files, append `### Round <N> — findings applied without re-review` listing what changed, then finish.
265
+ - Count completed reviewer verdicts = `### Round` entries that carry a `PLAN_REVIEW:` or verdict line (repair-only `### Round <N> — findings applied …` markers are NOT counted).
266
+ - count ≥ 2 and no escalation recorded yet → **bounded escalation, once, in this run.** Record the escalation in the log as `### Escalation 1 — <reason>` before the reviewer runs. The planner applies the outstanding findings with a **changed strategy** (different decomposition or task split, not a re-run of the same edit) and appends `### Repair <N> — changed strategy`. Then invoke the reviewer as a fresh independent smart-tier review (new agent, no prior context) and append its verdict as `### Round <N>`.
267
+ - count ≥ 2 and an escalation is already recorded → the escalation is spent. No further reviewer round runs except the **Recovery exception** below. Do not wait: continue the independent work (the other tasks, the queued requests), and mark each still-flagged task `needs_breakdown` with its open finding named unless the Recovery exception runs.
268
+ - Verdict `Approved` (in the escalation round or earlier) → append `PLAN_REVIEW: Approved by <reviewer model>` as in step 3 below.
269
+ - Verdict `Issues Found` after the escalation, or an escalation that cannot run → the planner applies the findings to the affected tasks only. Any task still carrying an open finding is `needs_breakdown` (never `ready`), and the footer reads `PLAN_REVIEW: Unresolved — round cap reached, findings applied without independent verification`, never `Approved`. The run then ends the planning step with that explicit state in the report; the `Plan Review Log` and the `needs_breakdown` marks name what is open. Nothing is left silently pending, and no later invocation is required to make progress.
270
+ - Finding signature: each open finding is identified by `<location>` (the file and section or line the reviewer cites) plus its normalized claim (lowercased, whitespace collapsed, first 120 characters). The reviewer's verdict lists each finding with that location and claim, and after every verdict the planner appends `### Signatures Round <N>: <sig>; <sig>; …` to the Plan Review Log, so the set is persisted per plan.
271
+ - Non-progress rule: a repair whose open-signature set equals the previous round's `### Signatures` set is non-progress and triggers the Unresolved path immediately; it is never retried in a loop and never marked DONE. **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 (at most one further recovery round); (3) run 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, no further review runs: retain Unresolved with a precise diagnostic and stop the loop. An owner conflict routes an owner decision.
253
272
 
254
- Two independent strong-model passes shape the plan before any code is written — that gate is
255
- intact. What it no longer does is hand a stalled plan back and wait.
273
+ Two independent strong-model passes shape the plan before any code is written, and the escalation
274
+ round gives one more fresh pass on anything still open. A stalled plan is never approved by the cap.
256
275
 
257
276
  **Claude Code — MANDATORY, do this before anything else:** call the Agent tool with `subagent_type: "code-reviewer"` (omp: the `task` tool with `agent: "code-reviewer"`), passing `REVIEW_TARGET_TYPE=plan` and the paths to `${OUTPUT_ROOT}PLAN.md` AND `${OUTPUT_ROOT}SPEC.md` (canonical root, or the queued root when one was chosen). This MUST be a separate agent invocation from Step 2's `handoff-planner` call (fresh context) — same-session self-review defeats the purpose of an independent gate.
258
277
 
259
- 1. Reviewer reads `SPEC.md` + `PLAN.md` (no diff, no task files, no executor report), checks Completeness / Consistency / Clarity / Scope / YAGNI plus the spec quality gate — every requirement testable, every fullstack layer covered, dependencies explicit, no vague instruction left — see `.claude/agents/code-reviewer.md` → Spec/Plan Review — and appends its verdict to PLAN.md's `## Plan Review Log` (new round entry, prior rounds kept).
278
+ 1. Reviewer reads `SPEC.md` + `PLAN.md` plus the plan's sources, starting from the original: `SOURCE.md` (the verbatim original bytes; verify the recorded sha256 against them; for chat input, the durable verbatim message origin stands in for the file), then `LEDGER.md` (classification of every requirement), `NOTES.md` (optional source-only notes, when present; a missing note is caught because the chain starts at SOURCE, not at NOTES), the `tasks/TASK-*.md` files of the plan, and the current source the plan's Target Files touch (no executor report, no diff). It compares the chain SOURCE → classification (LEDGER) → tasks (acceptance and verification), and NOTES are checked against the same chain. Each `feasible`/`adapted` row maps to a task with acceptance and a verification command. It checks that every `feasible`/`adapted` row and every requirement not explicitly deferred, blocked, or classified as rejected/proposal/example is covered by a task with acceptance and verification, that no feasible/adapted row lacks evidence, and Completeness / Consistency / Clarity / Scope / YAGNI plus the spec quality gate — every requirement testable, every fullstack layer covered, dependencies explicit, no vague instruction left — see `.claude/agents/code-reviewer.md` → Spec/Plan Review — and appends its verdict to PLAN.md's `## Plan Review Log` (new round entry, prior rounds kept).
260
279
  2. `Issues Found` → route back to Step 2: planner revises `PLAN.md` and the affected `TASK-xxx.md` files to address every finding, then re-submit for another Step 2.5 review (this becomes the next round). Do NOT commit or hand off to executor on `Issues Found`.
261
280
  3. `Approved` → append `PLAN_REVIEW: Approved by <reviewer model>` to PLAN.md's `## Planner Report` footer, then proceed.
262
281