@ngockhoale/ukit 3.4.67 → 3.4.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/manifests/documentation.yaml +33 -0
  3. package/manifests/instructionRules.yaml +10 -0
  4. package/manifests/platform.full.yaml +67 -0
  5. package/package.json +1 -1
  6. package/src/core/applyPlan.js +111 -2
  7. package/src/core/buildPlan.js +3 -1
  8. package/src/core/diffPlan.js +49 -1
  9. package/src/core/executionContracts.js +49 -8
  10. package/src/core/metadata.js +45 -0
  11. package/src/core/runInstallPipeline.js +30 -2
  12. package/src/core/uninstall.js +106 -2
  13. package/src/manifest/validateManifest.js +25 -1
  14. package/template_project/.claude/agents/code-reviewer.md +22 -9
  15. package/template_project/.claude/agents/handoff-planner.md +97 -8
  16. package/template_project/.claude/commands/ukit/handoff-create.md +114 -19
  17. package/template_project/.claude/commands/ukit/handoff-fullstack.md +247 -79
  18. package/template_project/.claude/commands/ukit/handoff-implement.md +75 -40
  19. package/template_project/.claude/commands/ukit/handoff-review.md +14 -10
  20. package/template_project/.claude/hooks/auto-allow-bash.sh +73 -12
  21. package/template_project/.claude/hooks/auto-prune-bash.sh +10 -0
  22. package/template_project/.claude/hooks/delegation-gate.mjs +21 -0
  23. package/template_project/.claude/hooks/handoff-model-guard.sh +22 -6
  24. package/template_project/.claude/hooks/handoff-resume.sh +141 -1
  25. package/template_project/.claude/hooks/record-execution.mjs +140 -9
  26. package/template_project/.claude/hooks/skill-router.sh +173 -8
  27. package/template_project/.claude/settings.json +2 -2
  28. package/template_project/.claude/skills/advisor-plan/REFERENCE.md +10 -0
  29. package/template_project/.claude/skills/advisor-plan/SKILL.md +2 -2
  30. package/template_project/.claude/ukit/index/allocate-task-worktree.mjs +15 -0
  31. package/template_project/.claude/ukit/index/amend-guard.mjs +116 -0
  32. package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
  33. package/template_project/.claude/ukit/index/land-worktree.mjs +243 -21
  34. package/template_project/.claude/ukit/index/plan-count.mjs +115 -0
  35. package/template_project/.claude/ukit/index/ready-gate.mjs +170 -0
  36. package/template_project/.claude/ukit/index/reset-auto-permissions.mjs +293 -4
  37. package/template_project/.claude/ukit/index/route-resolver.mjs +48 -8
  38. package/template_project/.claude/ukit/index/route-task.mjs +180 -11
  39. package/template_project/.claude/ukit/index/source-snapshot.mjs +172 -0
  40. package/template_project/.claude/ukit/index/task-budget-validator.mjs +109 -3
  41. package/template_project/.claude/ukit/runtime/async-lock.mjs +3 -3
  42. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +20 -3
  43. package/template_project/.claude/ukit/runtime/handoff-task-scope.mjs +205 -0
  44. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  45. package/template_project/.claude/ukit/runtime/plan-dispatch.mjs +32 -5
  46. package/template_project/.claude/ukit/runtime/resumable-run.mjs +310 -21
  47. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +405 -105
  48. package/template_project/.claude/ukit/runtime/task-record-reconciliation.mjs +46 -0
  49. package/template_project/.claude/ukit/runtime/task-watchdog.mjs +28 -2
  50. package/template_project/.claude/ukit/runtime/worktree-allocation.mjs +75 -0
  51. package/template_project/.codex/README.md +25 -0
  52. package/template_project/.omp/RULES.md +22 -22
  53. package/template_project/.omp/agents/code-reviewer.md +22 -9
  54. package/template_project/.omp/agents/handoff-planner.md +102 -12
  55. package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
  56. package/template_project/AGENTS.md +5 -1
  57. package/template_project/CLAUDE.md +1 -1
  58. package/template_project/docs/AI_HANDOFF/RULES.md +8 -0
  59. package/template_project/docs/AI_HANDOFF/tasks/_TEMPLATE.md +8 -1
  60. package/template_project/instructions/core.md +5 -1
  61. package/template_project/instructions/layout.yaml +2 -0
  62. package/template_project/instructions/overlays/omp-rules.md +22 -22
  63. package/template_project/ukit/storage/config.json +1 -1
@@ -442,13 +442,26 @@ export async function runInstallPipeline({
442
442
  const trackedSettings = (Array.isArray(installMetaData?.files) ? installMetaData.files : [])
443
443
  .some((e) => (typeof e === 'string' ? e : e?.p) === '.claude/settings.json');
444
444
  const settingsMigration = { pending: trackedSettings && !installMetaData?.settingsDefaultsMigration };
445
- const diffResults = await diffInstallPlan(plan, { ompOwnership, settingsMigration });
445
+ const recordedManagedHashes = new Map(
446
+ (Array.isArray(installMetaData?.files) ? installMetaData.files : [])
447
+ .filter((e) => e && typeof e === 'object' && typeof e.h === 'string')
448
+ .map((e) => [path.join(pathConfig.projectRoot, e.p), e.h]),
449
+ );
450
+ // CR-13 provenance: sha256 of each `.ukit-new` sidecar UKit itself wrote.
451
+ // Absent record = the file is not UKit's; never overwrite or remove it.
452
+ const recordedSidecarHashes = new Map(
453
+ Object.entries(installMetaData?.sidecars ?? {})
454
+ .filter(([p, h]) => typeof p === 'string' && typeof h === 'string')
455
+ .map(([p, h]) => [path.join(pathConfig.projectRoot, p), h]),
456
+ );
457
+ const diffResults = await diffInstallPlan(plan, { ompOwnership, settingsMigration, recordedManagedHashes });
446
458
  const plannedSummary = summarizeDiff(diffResults);
447
459
 
448
460
  if (!dryRun) {
449
- const { writes, skippedUpdates, skippedByAction } = await applyDiffResults(diffResults, {
461
+ const { writes, skippedUpdates, skippedByAction, sidecarHashes } = await applyDiffResults(diffResults, {
450
462
  backupRoot: pathConfig.backupRoot,
451
463
  projectRoot: pathConfig.projectRoot,
464
+ recordedSidecarHashes,
452
465
  });
453
466
 
454
467
  for (const entry of diffResults) {
@@ -550,8 +563,23 @@ export async function runInstallPipeline({
550
563
  // adjusted counts never go negative.
551
564
  const summary = adjustSummaryForSkips(plannedSummary, skippedByAction, skippedUpdates);
552
565
 
566
+ const managedHashes = Object.fromEntries(
567
+ diffResults
568
+ .filter((e) => e.managedHash && ['create', 'update', 'unchanged'].includes(e.action))
569
+ .map((e) => [path.relative(pathConfig.projectRoot, e.targetPath).split(path.sep).join('/'), e.managedHash]),
570
+ );
571
+ // Sidecar provenance map (relative path → sha256 UKit wrote this run). Written
572
+ // only after apply succeeded, so a failed create never claims ownership.
573
+ const sidecarRecords = Object.fromEntries(
574
+ Object.entries(sidecarHashes).map(([absPath, h]) => [
575
+ path.relative(pathConfig.projectRoot, absPath).split(path.sep).join('/'),
576
+ h,
577
+ ]),
578
+ );
553
579
  await writeInstallMetadata({
554
580
  installMetaPath: pathConfig.installMetaPath,
581
+ managedHashes,
582
+ sidecarHashes: sidecarRecords,
555
583
  packageVersion,
556
584
  manifest,
557
585
  stackContext,
@@ -1,5 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadManifest } from '../manifest/loadManifest.js';
3
6
  import {
4
7
  cleanupEmptyParents,
5
8
  copyFileRawExclusive,
@@ -243,6 +246,76 @@ async function hasSymlinkedParent(projectRoot, absolutePath) {
243
246
  return false;
244
247
  }
245
248
 
249
+ // CR-13: managed AI_HANDOFF templates are tracked with an optional sha256 (`h`). An
250
+ // owner-edited copy (hash differs, or no hash and bytes differ from the shipped template)
251
+ // must survive uninstall. The managed set is DERIVED from the manifest's
252
+ // managedChildStrategies, never hardcoded, so a new managed child is covered automatically.
253
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
254
+
255
+ async function managedTemplateMap() {
256
+ const manifest = await loadManifest(path.join(PACKAGE_ROOT, 'manifests', 'platform.full.yaml'));
257
+ const map = new Map(); // project-relative target path -> shipped template absolute path
258
+ for (const item of manifest.items ?? manifest.files ?? []) {
259
+ for (const [childKey, strategy] of Object.entries(item.managedChildStrategies ?? {})) {
260
+ if (strategy !== 'managed_template') continue;
261
+ map.set(
262
+ path.posix.join(item.targetPath, childKey),
263
+ path.join(PACKAGE_ROOT, 'template_project', item.sourceTemplate, childKey),
264
+ );
265
+ }
266
+ }
267
+ return map;
268
+ }
269
+
270
+ const sha256Hex = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
271
+
272
+ // lstat-first sidecar inspection shared by the tracked-entry loop and dry-run listing:
273
+ // a symlink/non-regular file is never read through or claimed as UKit's.
274
+ async function readSidecarState(sidecarPath) {
275
+ let stat;
276
+ try {
277
+ stat = await fs.lstat(sidecarPath);
278
+ } catch (error) {
279
+ return error?.code === 'ENOENT' ? { state: 'absent' } : { state: 'unreadable' };
280
+ }
281
+ if (stat.isSymbolicLink() || !stat.isFile()) {
282
+ return { state: 'non-regular' };
283
+ }
284
+ try {
285
+ const bytes = await fs.readFile(sidecarPath);
286
+ return { state: 'file', hash: sha256Hex(bytes) };
287
+ } catch {
288
+ return { state: 'unreadable' };
289
+ }
290
+ }
291
+
292
+ // true = owner-edited (keep it). Missing/non-regular entries are not "owner content"
293
+ // and fall through to the normal removal path (which refuses real directories).
294
+ async function isOwnerEditedManagedFile(absolutePath, relativePath, managedMap, recordedHash) {
295
+ const isManaged = managedMap.has(relativePath);
296
+ if (!isManaged && typeof recordedHash !== 'string') {
297
+ return false;
298
+ }
299
+ let stat;
300
+ try {
301
+ stat = await fs.lstat(absolutePath);
302
+ } catch {
303
+ return false;
304
+ }
305
+ if (!stat.isFile()) {
306
+ return false;
307
+ }
308
+ const current = await fs.readFile(absolutePath);
309
+ if (typeof recordedHash === 'string') {
310
+ return sha256Hex(current) !== recordedHash;
311
+ }
312
+ // Legacy managed entry with no recorded hash: compare against the shipped template bytes.
313
+ // If the template cannot be read, keep the file rather than risk deleting owner content.
314
+ const templateAbs = managedMap.get(relativePath);
315
+ const shipped = await fs.readFile(templateAbs).catch(() => null);
316
+ return shipped === null || !current.equals(shipped);
317
+ }
318
+
246
319
  // Hardcoded fallback for installs that predate file tracking (no 'files' field
247
320
  // in install.json). New installs always have a 'files' list, so this fallback
248
321
  // only applies when upgrading from a very old UKit version.
@@ -349,6 +422,8 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
349
422
  // actually created. Fall back to the hardcoded list for old installs.
350
423
  let regularPaths;
351
424
  let linkPaths;
425
+ const preservedOwnerEdited = [];
426
+ const preservedSidecars = [];
352
427
 
353
428
  const stateDir = path.join(projectRoot, '.claude', 'ukit', '.ukit');
354
429
 
@@ -383,8 +458,10 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
383
458
  continue;
384
459
  }
385
460
 
386
- trackedEntries.push({ absolutePath, normalizedRelative, type });
461
+ const recordedHash = typeof entry === 'object' && typeof entry?.h === 'string' ? entry.h : null;
462
+ trackedEntries.push({ absolutePath, normalizedRelative, type, recordedHash });
387
463
  }
464
+ const managedMap = await managedTemplateMap();
388
465
 
389
466
  const trackedLinkRelativePaths = [
390
467
  ...new Set(trackedEntries.filter((entry) => entry.type === 'link').map((entry) => entry.normalizedRelative)),
@@ -405,7 +482,30 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
405
482
  continue;
406
483
  }
407
484
 
408
- regularPaths.push({ abs: entry.absolutePath, recursive: false });
485
+ // CR-13: an owner-edited managed template is kept byte-identical and reported.
486
+ const ownerEdited = await isOwnerEditedManagedFile(
487
+ entry.absolutePath,
488
+ entry.normalizedRelative,
489
+ managedMap,
490
+ entry.recordedHash,
491
+ );
492
+ if (ownerEdited) {
493
+ preservedOwnerEdited.push(entry.absolutePath);
494
+ } else {
495
+ regularPaths.push({ abs: entry.absolutePath, recursive: false });
496
+ }
497
+ // CR-13: a `.ukit-new` sidecar is removed ONLY when install.json recorded the
498
+ // sha256 of exactly these bytes. The filename is not provenance: an owner-authored
499
+ // or owner-modified `.ukit-new` is preserved (and reported), never deleted.
500
+ const sidecarAbs = `${entry.absolutePath}.ukit-new`;
501
+ const sidecarRel = `${entry.normalizedRelative}.ukit-new`;
502
+ const recordedSidecarHash = installData.sidecars?.[sidecarRel];
503
+ const sidecarState = await readSidecarState(sidecarAbs);
504
+ if (sidecarState.state === 'file' && typeof recordedSidecarHash === 'string' && recordedSidecarHash === sidecarState.hash) {
505
+ regularPaths.push({ abs: sidecarAbs, recursive: false });
506
+ } else if (sidecarState.state !== 'absent') {
507
+ preservedSidecars.push(sidecarAbs);
508
+ }
409
509
  }
410
510
 
411
511
  if (invalidPaths.length > 0) {
@@ -492,6 +592,8 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
492
592
  preservedPaths: importantPlan.preservedPaths,
493
593
  backupWarnings: importantPlan.backupWarnings,
494
594
  skippedSymlinkParents,
595
+ preservedOwnerEdited,
596
+ preservedSidecars,
495
597
  };
496
598
  }
497
599
 
@@ -569,5 +671,7 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
569
671
  preservedPaths: importantPlan.preservedPaths,
570
672
  backupWarnings: importantPlan.backupWarnings,
571
673
  skippedSymlinkParents,
674
+ preservedOwnerEdited,
675
+ preservedSidecars,
572
676
  };
573
677
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  const VALID_ITEM_TYPES = new Set(['command', 'skill', 'agent', 'hook', 'config', 'link']);
4
- const VALID_MERGE_STRATEGIES = new Set(['overwrite_with_backup', 'skip', 'append', 'prepend', 'merge_env_overwrite_with_backup']);
4
+ const VALID_MERGE_STRATEGIES = new Set(['overwrite_with_backup', 'skip', 'append', 'prepend', 'merge_env_overwrite_with_backup', 'managed_template']);
5
5
  const VALID_ITEM_LEVELS = new Set(['project', 'user']);
6
6
 
7
7
  function isPlainObject(value) {
@@ -96,6 +96,30 @@ export function validateManifest(manifest) {
96
96
  );
97
97
  }
98
98
 
99
+ if (item.managedChildStrategies !== undefined) {
100
+ // CR-13: per-child strategies for a directory sourceTemplate. Keys are paths
101
+ // RELATIVE to targetPath (never absolute, never escaping it) and every value
102
+ // must be a real strategy — a typo like `managed_templet` must fail loudly
103
+ // instead of silently behaving like the default (owner-preserving) strategy.
104
+ if (!isPlainObject(item.managedChildStrategies)) {
105
+ errors.push(`${prefix}.managedChildStrategies must be an object of relative child path → strategy.`);
106
+ } else {
107
+ for (const [childKey, strategy] of Object.entries(item.managedChildStrategies)) {
108
+ const childPrefix = `${prefix}.managedChildStrategies["${childKey}"]`;
109
+ if (childKey.trim() === '' || path.isAbsolute(childKey) || /^[A-Za-z]:[\\/]/.test(childKey)) {
110
+ errors.push(`${childPrefix} key must be a non-empty relative path.`);
111
+ } else if (containsPathTraversal(childKey)) {
112
+ errors.push(`${childPrefix} key must not contain path traversal segments.`);
113
+ }
114
+ if (!VALID_MERGE_STRATEGIES.has(strategy)) {
115
+ errors.push(
116
+ `${childPrefix} has invalid strategy '${String(strategy)}' — must be one of: ${Array.from(VALID_MERGE_STRATEGIES).join(', ')}`,
117
+ );
118
+ }
119
+ }
120
+ }
121
+ }
122
+
99
123
  if (item.level !== undefined && !VALID_ITEM_LEVELS.has(item.level)) {
100
124
  errors.push(`${prefix}.level must be one of: ${Array.from(VALID_ITEM_LEVELS).join(', ')}`);
101
125
  }
@@ -102,7 +102,13 @@ decision owned by the local decision model when staged. Read
102
102
  Model isolation is preserved: the verdict pass runs on the local unic-decision
103
103
  model, so reviewer ≠ executor still stands.
104
104
 
105
- ### Output (append to task file as `## Reviewer Verdict`)
105
+ ### Output (return `## Reviewer Verdict` to the orchestrator)
106
+
107
+ You are read-only: return the complete verdict block below, never shell-append it (`cat >>`,
108
+ heredoc, `tee`, `sed -i`). The orchestrator appends your complete verdict block with `Edit`
109
+ to the task file, preserving prior rounds, then updates INDEX serially. Do not edit INDEX.
110
+ Use Read/Grep/Glob for inspection; Bash verification is one simple command per call with
111
+ absolute paths (no `cd … &&`/`;` prefix, loops or compound writes).
106
112
 
107
113
  ```
108
114
  ## Reviewer Verdict
@@ -128,9 +134,11 @@ NEXT_STATUS_FOR_INDEX: approved | approved_minor | changes_requested | critical_
128
134
  NOTES: [1-2 sentences for human reviewer if needed]
129
135
  ```
130
136
 
131
- 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.
137
+ The orchestrator persists this block before aggregation or status changes; it sets the INDEX
138
+ status from NEXT_STATUS_FOR_INDEX and Reviewer from your model name. No independent verdict
139
+ is on disk until that append succeeds.
132
140
 
133
- **Then return to the orchestrator at most 6 lines** — the full verdict is already on disk:
141
+ **Include this short routing header before your complete verdict block**:
134
142
 
135
143
  ```
136
144
  TASK: TASK-xxx
@@ -140,10 +148,9 @@ VERIFICATION_RERUN: PASS | FAIL | NOT_RUN | N/A (+ one-line reason for NOT_RUN/N
140
148
  BLOCKING: <one line per critical/important finding, or "none">
141
149
  ```
142
150
 
143
- Do not paste the diff, the findings prose, or verification output into the returned message.
144
- The orchestrator is driving a whole pipeline in one context window and re-reads what it needs
145
- from the task file; pasted reviewer logs are a common reason a run exhausts its context and
146
- dies before the cycle finishes.
151
+ Return the bounded verdict block (≤30 lines) with actionable findings, not the diff or raw
152
+ verification logs. The complete block is necessary for persistence by the read-only caller contract.
153
+ For panel lead bucketing, likewise return the Panel Consensus block for serial persistence.
147
154
 
148
155
  **You may be running unattended.** Findings do not end the run — the caller feeds them to an
149
156
  auto-fix round and re-review. So write findings that a fresh executor can act on with no human
@@ -211,7 +218,7 @@ verdict block — with these additions:
211
218
  ### Inputs you expect
212
219
 
213
220
  - Path to the spec/plan document (e.g. `docs/plans/*.md`). No diff and no executor report.
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`.
221
+ - 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`.
215
222
  - 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.
216
223
 
217
224
  ### Review order
@@ -236,9 +243,15 @@ verdict block — with these additions:
236
243
 
237
244
  Only flag issues that would cause real problems during implementation planning. Approve unless there are serious gaps that would lead to a flawed plan.
238
245
 
246
+ ### Inspection commands (plan/spec review)
247
+
248
+ Read documents with `Read`/`Grep`/`Glob`. When Bash is needed, issue one simple read-only command per call
249
+ using absolute paths: no `cd … ;`/`cd … &&` prefixes, `for` loops, `;` or `&&` sequences, or `{a,b}` globs.
250
+ The host permission layer asks approval for such compound commands and an unattended review cannot answer it.
251
+
239
252
  ### Output
240
253
 
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:
254
+ 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:
242
255
 
243
256
  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
257
 
@@ -31,6 +31,16 @@ what you must avoid, not the work itself. Write the remaining modules into `PLAN
31
31
  explicitly out-of-scope-for-this-cycle, and add one `queued` row per module to `INDEX.md` so
32
32
  the next cycle picks them up. Order modules so the one others depend on is planned first.
33
33
 
34
+ **Plan count is a deliverable.** When the owner source states a number of plans or separate
35
+ plans ("at least two plans", "plan A and plan B"), deliver exactly that many real plan units
36
+ in the Handoff-supported layout: the first plan canonical (SPEC.md + PLAN.md + tasks/ under
37
+ OUTPUT_ROOT), each further plan as its own `docs/AI_HANDOFF/queued/<slug>/` blueprint with
38
+ its own SPEC.md, PLAN.md, INDEX.md (rows `queued_blueprint`), HANDOFF.md and tasks/ — the
39
+ queued-README layout. Group labels ("group A/B") inside one PLAN.md never satisfy a stated
40
+ 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
41
+ `PLAN_COUNT: ok (<found>/<N>)`. 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
+
34
44
  The caller may be running unattended (`/ukit:handoff-fullstack`). Stopping to ask means the
35
45
  run dies and the human returns hours later to a question, which is the failure mode this
36
46
  whole pipeline exists to prevent.
@@ -43,7 +53,7 @@ resolved. Executors then burn a whole round discovering it.
43
53
 
44
54
  Before writing any path or command into `PLAN.md` or a task file, verify it:
45
55
 
46
- - **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
47
57
  directory exists and the file is genuinely new. Mark new files `(new)` explicitly.
48
58
  - **Verification Commands** — read `package.json` `scripts` (or the stack's equivalent) and
49
59
  use the script names that are actually defined. Do not write `npm test` when the repo uses
@@ -70,11 +80,29 @@ The plan owns ALL unfinished work, not only the new request. Scan and fold in:
70
80
  Every discovered item becomes either a task row in the new plan or an explicitly recorded
71
81
  out-of-scope line in PLAN.md §2. Silent omission is a plan defect.
72
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
+
73
89
  **Recovery:** a stuck task record that cannot be cleanly resumed (orphaned worktree,
74
90
  contradicting reports, invalid state) is marked `cancelled_superseded` and replaced by
75
91
  `TASK-xxx-R1` (`-R2`, …) carrying the same spec references, acceptance criteria and
76
92
  verification — link both files' `## Discussion` threads.
77
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
+
78
106
  ## Phase 0.5 — Write SPEC.md
79
107
 
80
108
  Write `docs/AI_HANDOFF/SPEC.md` from the template at `template_project/docs/AI_HANDOFF/SPEC.md`
@@ -121,6 +149,21 @@ file that already ends with the footer above; refine it with `Edit` afterwards.
121
149
  validates `PLANNER_MODEL` on that write, so a refusal surfaces before you spend a long drafting
122
150
  pass — never draft the whole plan in your head/scratch and attempt the first write last.
123
151
 
152
+ ## Create files with Write/Edit only — never shell writes
153
+
154
+ `handoff-create` is planning-only and the model-tier guard refuses shell writes (`mkdir` + heredoc,
155
+ `cat > file`, `tee`, `echo >`, `sed -i`, compound `&&` chains that write). Create every plan, task, SPEC,
156
+ LEDGER, NOTES, INDEX and ACTIVE file with the `Write` tool (it creates missing parent directories) and
157
+ change existing files with `Edit`. Use Bash only for read-only inspection and the literal-path helper
158
+ commands this document names. This is the guard's own documented path, not a workaround, so choosing it
159
+ up front is required; only a refusal of a `Write`/`Edit` call triggers the report-and-stop rule below.
160
+
161
+ The same applies to the reviewer's round block and to `docs/WORKLOG.md`: the `code-reviewer` returns its
162
+ `### Round <N>` block in its reply (it has no write tool), and you add it to `PLAN.md`'s `## Plan Review Log`
163
+ with `Edit`; append the WORKLOG entry with `Edit` too. For inspection use one simple read-only command per
164
+ call (or `Read`/`Grep`/`Glob`) — no `cd … &&` chains, `for` loops, `;` sequences or `{a,b}` globs, which the
165
+ host permission layer asks approval for and an unattended run cannot answer.
166
+
124
167
  ## Blocked write = report, never retry or respawn
125
168
 
126
169
  If a `Write`/`Edit`/`Bash` call is refused (`BLOCKED …`, `hook error`, "denied by your
@@ -153,7 +196,9 @@ Use `_TEMPLATE.md` structure (from pre-read context or file).
153
196
  | 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 |
154
197
  | Acceptance Criteria | Verifiable checklist |
155
198
 
156
- Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`.
199
+ Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`. Every task is
200
+ created `needs_breakdown` anyway — the two gates in "Task-budget validator" below (validator
201
+ verdict + review Approved) are what promote it to `ready`.
157
202
 
158
203
  **Wave logic (for your reference when splitting):**
159
204
  - Wave 1 = tasks with `Dependencies: none`
@@ -163,13 +208,37 @@ Missing any field → `needs_breakdown`. Never mark incomplete tasks `ready`.
163
208
 
164
209
  ### Task-budget validator — gate before any task becomes `ready`
165
210
 
166
- Before marking a task `ready`, run the task-budget validator on the file you just wrote:
211
+ Write every task file `Status: needs_breakdown` first — `ready` is a verdict, not a
212
+ default. A task becomes `ready` only when BOTH gates pass:
213
+
214
+ 1. the task-budget validator printed `VERDICT: ok` for that file (or a one-line dismissal
215
+ is recorded in its `## Discussion` thread), and
216
+ 2. the independent plan review (Step 2.5 of `/ukit:handoff-create`, P2.5 of fullstack)
217
+ ended `Approved`.
218
+
219
+ Run the validator per task file, right after writing it, as ONE simple command from the
220
+ project root — one command per file, each with its literal path:
167
221
 
168
222
  ```
169
- node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-xxx.md
223
+ node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-001.md
224
+ node .claude/ukit/index/task-budget-validator.mjs docs/AI_HANDOFF/tasks/TASK-002.md
170
225
  ```
171
226
 
172
- 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).
227
+ No `cd` prefix, no `for`/`while` loop, no shell variable, no glob, no pipe, and no `&&`/`;` compound —
228
+ host permission checks reject those SHAPES ("cat reads a file by a relative path after a
229
+ cd in a compound command", "A variable in this command can't be checked before it runs"),
230
+ so a loop-shaped batch burns the round and the gate never runs. Eight tasks = eight
231
+ separate calls. Read files with the Read/Glob/Grep tools, never `cat`/`head`/`sed`
232
+ compounds.
233
+
234
+ 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.
235
+
236
+ 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.
237
+
238
+ If the run is interrupted (budget exhausted, command denied, crash) before both gates
239
+ pass, the tasks STAY `needs_breakdown` — INDEX.md and ACTIVE.md must name which gate is
240
+ missing. A batch that never saw a `VERDICT: ok` is never `ready`, no matter how complete
241
+ the files look.
173
242
 
174
243
  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.
175
244
 
@@ -189,6 +258,11 @@ dependency means B imports a symbol A creates, or B tests behavior A implements.
189
258
  - "Both touch the same area of the codebase" — irrelevant unless they touch the same *file*.
190
259
  - "A should be reviewed before B starts" — that is what the review phase is for.
191
260
 
261
+ One counter-case cuts the other way: when the OWNER SOURCE states a delivery-order
262
+ qualifier — "after import works", "once search lands" — that is a real dependency, not an
263
+ ordering preference. Encode it in `Dependencies` and name the source line in the task's
264
+ Spec references. Dropping it silently ships B before its precondition exists.
265
+
192
266
  Same-file collisions are the one real constraint, and they are cheap to design around: split
193
267
  along file boundaries so each task owns its files outright. If two pieces of work truly must
194
268
  edit one file, prefer merging them into a single task over chaining two — one task with two
@@ -199,7 +273,16 @@ most chains are ordering preferences that a wide wave 1 would satisfy just as we
199
273
 
200
274
  ## Phase 3 — Update state files
201
275
 
202
- **INDEX.md** row per task: `| TASK-001 | <name> | ready | none | - |`
276
+ **INDEX.md** row per task — write `needs_breakdown` at creation, flip to `ready` only after
277
+ both gates pass (validator `VERDICT: ok` per task + plan review `Approved`):
278
+ `| TASK-001 | <name> | needs_breakdown | none | - |`
279
+ Flip by re-reading the task files from disk: only a task whose `## Discussion` records
280
+ `Validator: ok <date>` (or `Validator: dismissed <date> — <reason>`) flips to `ready`; a
281
+ task without the recorded `Validator:` line stays `needs_breakdown` — a verdict held only
282
+ in the session does not count. Run the mechanical gate once before flipping any row —
283
+ `node .claude/ukit/index/ready-gate.mjs docs/AI_HANDOFF` (the planning root when queued) —
284
+ and flip only on `READY_GATE: ok`; a `tasks not ready:` or `index/task mismatch` line
285
+ names the gap to fix first. Never flip while the gate fails.
203
286
 
204
287
  **ACTIVE.md** — store base branch + cycle info:
205
288
  ```
@@ -209,6 +292,10 @@ Spec: docs/AI_HANDOFF/SPEC.md
209
292
  Tasks: <N> total
210
293
  Status: planning_done — ready for executor
211
294
  ```
295
+ When any task is still non-ready (a gate missing), write the honest state instead:
296
+ `Status: planning_incomplete — tasks not ready: <validator | plan review>` (a
297
+ human-readable ACTIVE.md marker only — machine readers parse INDEX.md rows and
298
+ task-file `- Status:` lines, never write `planning_incomplete` into those).
212
299
  Wave structure is NOT stored here — inferred from task `Dependencies` fields at runtime.
213
300
 
214
301
  **RUN.md** — write the run's `ExitPredicate:` line before iteration 1 (the orchestrator
@@ -254,11 +341,13 @@ catches it:
254
341
  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.
255
342
  11. Bugfix task → is there a regression test that fails against today's code?
256
343
  12. Could every listed test pass against an empty implementation? If so, the test cases describe nothing and must be rewritten.
344
+ 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.
345
+ 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.
257
346
 
258
347
  Append the result to `PLAN.md`:
259
348
  ```
260
349
  ## Planner Self-Audit
261
- Checklist: 14/14 pass
350
+ Checklist: 16/16 pass
262
351
  Fixed during audit: <what you changed, or "nothing">
263
352
  Known gaps: <what you deliberately left out and why, or "none">
264
353
  ```
@@ -284,7 +373,7 @@ budget is the constraint on the whole run. Detail belongs in `PLAN.md`, not in t
284
373
 
285
374
  - Do NOT implement. Job ends when all tasks are `ready` or `needs_breakdown`.
286
375
  - 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.
376
+ - 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.
288
377
  - 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
378
  - 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.
290
379
  - 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.