@ngockhoale/ukit 3.4.65 → 3.4.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/manifests/documentation.yaml +11 -0
- package/package.json +2 -2
- package/scripts/release/verify-release.mjs +7 -0
- package/scripts/test/run-handoff-isolated.mjs +178 -0
- package/template_project/.claude/agents/code-reviewer.md +4 -1
- package/template_project/.claude/agents/handoff-planner.md +3 -0
- package/template_project/.claude/commands/ukit/handoff-clear.md +34 -35
- package/template_project/.claude/commands/ukit/handoff-create.md +40 -21
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +141 -66
- package/template_project/.claude/commands/ukit/handoff-implement.md +27 -12
- package/template_project/.claude/commands/ukit/handoff-review.md +18 -8
- package/template_project/.claude/hooks/handoff-resume.sh +8 -0
- package/template_project/.claude/skills/advisor-plan/REFERENCE.md +33 -2
- package/template_project/.claude/skills/advisor-plan/SKILL.md +6 -4
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +40 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +128 -44
- package/template_project/.claude/ukit/runtime/task-watchdog.mjs +1 -1
- package/template_project/.omp/agents/code-reviewer.md +4 -1
- package/template_project/.omp/agents/handoff-planner.md +3 -0
- package/template_project/AGENTS.md +2 -2
- package/template_project/CLAUDE.md +2 -2
- package/template_project/docs/AI_HANDOFF/RULES.md +24 -27
- package/template_project/docs/AI_HANDOFF/queued/README.md +52 -0
- package/template_project/instructions/core.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.66 - 2026-10-23
|
|
6
|
+
|
|
7
|
+
**Handoff hardening: scope-frozen runs, completed-only ARCHIVE, bounded repair, isolated verification.**
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- `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`.
|
|
12
|
+
|
|
5
13
|
## 3.4.65 - 2026-10-22
|
|
6
14
|
|
|
7
15
|
**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.
|
|
3
|
+
"version": "3.4.66",
|
|
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 ./
|
|
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();
|
|
@@ -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
|
|
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 —
|
|
40
|
+
## Step 2 — Preserve staged owner work
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
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 —
|
|
46
|
+
## Step 3 — Preserve open work; archive only verified completion
|
|
47
47
|
|
|
48
|
-
Read
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
the
|
|
60
|
-
|
|
61
|
-
|
|
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 —
|
|
60
|
+
## Step 4 — Pause, or reset state files, without losing the backlog
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
write once; on second failure STOP and
|
|
79
|
-
files behind
|
|
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
|
|
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
|
-
|
|
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
|
|
46
|
-
explicitly says to abandon/clear the current run, first run `/ukit:handoff-clear
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
106
|
-
`QueueIntent: deliberate-future`.
|
|
107
|
-
(
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
171
|
-
|
|
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) → **
|
|
177
|
-
- Any task is `pending_review`, `changes_requested`, `merge_conflict`, or `blocked` → **do not overwrite
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
-
|
|
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
|
|
255
|
-
|
|
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` (
|
|
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
|
|