@ngockhoale/ukit 3.4.12 → 3.4.14
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 +64 -0
- package/manifests/documentation.yaml +10 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +1 -1
- package/src/index/routeCatalog.js +18 -0
- package/src/index/taskRouting.js +82 -1
- package/template_project/.claude/commands/ukit/handoff-create.md +131 -12
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +37 -2
- package/template_project/.claude/hooks/handoff-model-guard.sh +272 -7
- package/template_project/.claude/hooks/handoff-resume.sh +47 -0
- package/template_project/.claude/hooks/skill-router.sh +90 -0
- package/template_project/.claude/skills/advisor-plan/REFERENCE.md +87 -0
- package/template_project/.claude/skills/advisor-plan/SKILL.md +94 -0
- package/template_project/.claude/ukit/index/route-catalog.mjs +18 -0
- package/template_project/.claude/ukit/index/route-task.mjs +83 -1
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +293 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +33 -70
- package/template_project/docs/AI_HANDOFF/RULES.md +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.14 - 2026-10-04
|
|
6
|
+
|
|
7
|
+
**New `advisor-plan` skill, a create-session write-scope guard, and a deliberate-future queue contract.**
|
|
8
|
+
`advisor-plan` turns an advisor opinion, reference document, roadmap or replan request
|
|
9
|
+
into a plan and nothing else (planning-only; it never implements). It builds a requirement
|
|
10
|
+
LEDGER where every row is dispositioned (`feasible`/`adapted`/`deferred`/`blocker`),
|
|
11
|
+
backed by feasibility evidence read from the current source and `git log`/`diff`/`status`;
|
|
12
|
+
drafts 2-3 candidate plans and scores them before picking one; gives every task detailed
|
|
13
|
+
TDD test cases (happy, edge, error) instead of "tests pass"; and applies a queue
|
|
14
|
+
threshold — at most 10 active tasks, the rest become queued blueprints. Ledger coverage
|
|
15
|
+
means rows dispositioned, not implemented.
|
|
16
|
+
|
|
17
|
+
- **Create write-scope guard.** While a session's latest handoff intent is `create`
|
|
18
|
+
(`/ukit:handoff-create`), `handoff-model-guard.sh` (`createScopeVerdict`) allows docs
|
|
19
|
+
and test files only and blocks source/config/hook/agent/skill/command/`AGENTS.md`
|
|
20
|
+
writes. Claude Code is enforced through the PreToolUse `Edit|Write` and `Bash` chains;
|
|
21
|
+
omp is enforced for the tools its bridge maps (`apply_patch`/`ast_edit` → `Edit`);
|
|
22
|
+
Codex has no pre-tool hooks, so the scope is advisory only (instruction-mediated) and
|
|
23
|
+
is not enforced. Patch-body edits (`*** Update File:`, unified-diff headers) are parsed
|
|
24
|
+
for their targets. Known gaps: the shell check is a regex, not a parser, so writes via
|
|
25
|
+
`node -e`, `python -c`, `ruby -e`, `wget -O`, `tar -x`, `unzip`, `git pull|switch|worktree add`, git aliases, `eval`/`bash -c` indirection and similar slip through, and a `>` or
|
|
26
|
+
the word `install` in an argument can block a harmless command; a missing transcript
|
|
27
|
+
fails open silently and an inconclusive intent scan fails open with a systemMessage.
|
|
28
|
+
Kill switch: `handoff.createScopeGuard`.
|
|
29
|
+
- **Automatic routing.** A deterministic EN/VN classifier (`classifyAdvisorRequest`, three
|
|
30
|
+
identical copies) force-loads `advisor-plan` into the first active-skill slot for advisor /
|
|
31
|
+
reference / replan hand-offs and explicit "plan this task" requests; implementation orders
|
|
32
|
+
that merely mention a plan or advisor do not load it. Known gap: `unic-decision` is not
|
|
33
|
+
consulted for this recognition (its `route.intent-kind.v1` has no advisor value); wiring it
|
|
34
|
+
in is queued follow-up work.
|
|
35
|
+
- **Deliberate-future queue.** Queued blueprints under `docs/AI_HANDOFF/queued/<slug>/`
|
|
36
|
+
may carry `QueueIntent: deliberate-future`, `QueueOrder`, `QueueDrain` and
|
|
37
|
+
`QueueSource` in their `HANDOFF.md` header. `/ukit:handoff-fullstack` reports them as
|
|
38
|
+
`Backlog: <slug>` instead of folding them into the current cycle (an explicit slug
|
|
39
|
+
drains one); blueprints without the header keep the legacy fold-in behaviour. No new
|
|
40
|
+
INDEX status was added.
|
|
41
|
+
- **Route catalog.** `advisor-plan` (order 12.15) with a matching `advisor-plan-skill`
|
|
42
|
+
manifest item. `classifyAdvisorRequest` stamps `advisorIntent` (`advisor` | `reference` |
|
|
43
|
+
`replan`) on the route summary in English and Vietnamese (diacritics folded; "advisory
|
|
44
|
+
lock", "legal advisor" and bug-fix-verb prompts stay null); mirrored in `taskRouting.js`,
|
|
45
|
+
`route-task.mjs` and `skill-router.sh`.
|
|
46
|
+
- Docs: `docs/HOST_CAPABILITY_MATRIX.md` gains a per-host "Create write-scope guard
|
|
47
|
+
(C94)" note; `STATUS.md`, `CODE_MAP.md` and `skill-audits/advisor-plan/` updated.
|
|
48
|
+
Tests: `advisorPlanSkill`, `createScopeGuard`, `advisorReleaseDocs`, `advisorIntentRouting`,
|
|
49
|
+
`advisorStatusDocs`, `routeCatalog`.
|
|
50
|
+
|
|
51
|
+
## 3.4.13 - 2026-10-04
|
|
52
|
+
|
|
53
|
+
**`/ukit:handoff-create` is strictly planning-only; the Stop gate follows the session's CURRENT handoff intent.**
|
|
54
|
+
`handoff-create` writes SPEC/PLAN/task files under `docs/AI_HANDOFF/` and stops with tasks
|
|
55
|
+
`ready` (queued) — it never edits product code, launches executors, or escalates itself
|
|
56
|
+
into implementation, even when an unfinished `RUN.md` exists or after compact/resume.
|
|
57
|
+
New shared `handoff-intent.mjs` classifies a transcript by its MOST RECENT structured
|
|
58
|
+
intent (`/ukit:handoff-create` vs `/ukit:handoff-fullstack` command tag, `Skill` call,
|
|
59
|
+
SessionStart resume banner). The Stop gate and `handoff-resume.sh` both use it: a session
|
|
60
|
+
that planned after an earlier fullstack run is no longer bounced into the stale run,
|
|
61
|
+
while a session actually driving `handoff-fullstack` (including create → fullstack
|
|
62
|
+
re-acquire) is still held to completion — the completion gate and ExitPredicate are
|
|
63
|
+
unchanged. Unknown/unreadable/windowed-out transcripts stay fail-closed. Claude Code,
|
|
64
|
+
Codex and omp share the same runtime, with native transcript envelopes normalized.
|
|
65
|
+
Planning alongside a live run preserves its cursor, index and tasks and writes a
|
|
66
|
+
separate queued plan instead. Tests: `handoffIntentSeparation`,
|
|
67
|
+
`handoffResumeIntent`, `handoffCreateQueueing`.
|
|
68
|
+
|
|
5
69
|
## 3.4.12 - 2026-10-02
|
|
6
70
|
|
|
7
71
|
**Handoff Stop gate is session-scoped — a stale `RUN.md` no longer hijacks unrelated sessions.**
|
|
@@ -405,6 +405,16 @@ entries:
|
|
|
405
405
|
load_policy: never
|
|
406
406
|
validation: [none]
|
|
407
407
|
archive_policy: immutable
|
|
408
|
+
- id: docs-skill-audits
|
|
409
|
+
path: docs/skill-audits/
|
|
410
|
+
class: archive
|
|
411
|
+
audience: [maintainer]
|
|
412
|
+
owner: product
|
|
413
|
+
source_of_truth: docs/skill-audits/
|
|
414
|
+
merge_strategy: none
|
|
415
|
+
load_policy: never
|
|
416
|
+
validation: [none]
|
|
417
|
+
archive_policy: immutable
|
|
408
418
|
|
|
409
419
|
# ── template_project/ canonical authoring sources ───────────────────────────────
|
|
410
420
|
- id: tpl-claude-md
|
|
@@ -555,6 +555,19 @@ items:
|
|
|
555
555
|
packs:
|
|
556
556
|
- core
|
|
557
557
|
|
|
558
|
+
- id: advisor-plan-skill
|
|
559
|
+
type: skill
|
|
560
|
+
sourceTemplate: .claude/skills/advisor-plan
|
|
561
|
+
targetPath: .claude/skills/advisor-plan
|
|
562
|
+
requires:
|
|
563
|
+
- docs-quality-skill
|
|
564
|
+
- docs-ai-handoff
|
|
565
|
+
mergeStrategy: overwrite_with_backup
|
|
566
|
+
variables: []
|
|
567
|
+
enabledByDefault: true
|
|
568
|
+
packs:
|
|
569
|
+
- core
|
|
570
|
+
|
|
558
571
|
- id: docs-manager-skill
|
|
559
572
|
type: skill
|
|
560
573
|
sourceTemplate: .claude/skills/docs-manager
|
package/package.json
CHANGED
|
@@ -237,6 +237,24 @@ export const ROUTE_CATALOG = [
|
|
|
237
237
|
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)(?![A-Za-z0-9_])/i, score: 7 },
|
|
238
238
|
],
|
|
239
239
|
},
|
|
240
|
+
{
|
|
241
|
+
id: 'advisor-plan',
|
|
242
|
+
path: '.claude/skills/advisor-plan/SKILL.md',
|
|
243
|
+
order: 12.15,
|
|
244
|
+
contextMode: 'standalone',
|
|
245
|
+
signals: [
|
|
246
|
+
{ type: 'prompt', regex: /\b(?:advis[eo]r)(?:'s|s)?\s+(?:\w+\s+){0,3}(?:docs?|documents?|opinions?|feedback|notes?|plans?|roadmaps?|recommendations?|reports?)\b/i, score: 9 },
|
|
247
|
+
{ type: 'prompt', regex: /\b(?:turn|convert|translate|break(?: down)?|distill)\b.{0,48}\b(?:roadmap|reference|advis[eo]r|external plan)\b.{0,48}\b(?:into|to)\b.{0,16}\b(?:plan|tasks?|ledger)\b/i, score: 9 },
|
|
248
|
+
{ type: 'prompt', regex: /\b(?:roadmap|reference|external plan)\s+(?:material|docs?|documents?)\b.{0,48}\bplan\b/i, score: 9 },
|
|
249
|
+
{ type: 'prompt', regex: /\bplan(?:ning)?\s+(?:from|using|based on|per|according to)\b.{0,48}\b(?:reference|roadmap|advis[eo]r|external|document)\b/i, score: 9 },
|
|
250
|
+
{ type: 'prompt', regex: /\b(?:replan|re-plan|plan again)\b.{0,60}\b(?:advis[eo]r|feedback|documents?|reference|roadmap)\b/i, score: 7 },
|
|
251
|
+
{ type: 'prompt', regex: /\b(?:planning[- ]only|plan[- ]only|plan (?:any|this|the|a) (?:task|feature|work).{0,48}(?:do not|don't|without|never) implement\w*)\b/i, score: 7 },
|
|
252
|
+
{ type: 'prompt', regex: /\bplan\s+(?:this|these|the following)\s+(?:task|tasks|work|request|feature)\b/i, score: 7 },
|
|
253
|
+
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(?:lập kế hoạch|lap ke hoach|lên kế hoạch|len ke hoach)\s+cho\s+(?:task|việc|viec|tính năng|tinh nang|yêu cầu|yeu cau|feature)(?![A-Za-z0-9_])/i, score: 7 },
|
|
254
|
+
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(?:lập kế hoạch|lap ke hoach|lên lại kế hoạch|len lai ke hoach|lên kế hoạch lại|len ke hoach lai).{0,60}(?:tài liệu|tai lieu|tư vấn|tu van|advisor|tham khảo|tham khao|roadmap)/i, score: 9 },
|
|
255
|
+
{ type: 'prompt', regex: /(?<![A-Za-z0-9_])(?:tài liệu tư vấn|tai lieu tu van|tư vấn của advisor|tu van cua advisor|lên lại kế hoạch|len lai ke hoach|lập kế hoạch cho bất kỳ task nào|lap ke hoach cho bat ky task nao)/i, score: 7 },
|
|
256
|
+
],
|
|
257
|
+
},
|
|
240
258
|
{
|
|
241
259
|
id: 'update-status',
|
|
242
260
|
path: '.claude/skills/update-status/SKILL.md',
|
package/src/index/taskRouting.js
CHANGED
|
@@ -755,6 +755,10 @@ export function buildRouteSummary({
|
|
|
755
755
|
const modelRolesBlock = MODEL_ROLES_DELEGATING_MODES.has(executionMode)
|
|
756
756
|
? formatModelRolesBlock(modelRolesResolution.roles)
|
|
757
757
|
: null;
|
|
758
|
+
const advisorIntent = classifyAdvisorRequest({
|
|
759
|
+
promptText: routingContext.promptText,
|
|
760
|
+
commandText: routingContext.commandText,
|
|
761
|
+
});
|
|
758
762
|
const summaryLine = [
|
|
759
763
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
760
764
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
@@ -832,6 +836,9 @@ export function buildRouteSummary({
|
|
|
832
836
|
// only when subagentOrchestrator.roleAdvice.stage != 'off' (conditional
|
|
833
837
|
// spread keeps the stage-off route byte-identical).
|
|
834
838
|
...(delegationAdvice ? { delegationAdvice } : {}),
|
|
839
|
+
// FR-004 additive field — present only when the prompt is an advisor/reference/replan
|
|
840
|
+
// request, so every other route stays byte-identical.
|
|
841
|
+
...(advisorIntent ? { advisorIntent } : {}),
|
|
835
842
|
line: summaryLine || 'task=unknown',
|
|
836
843
|
};
|
|
837
844
|
}
|
|
@@ -1106,6 +1113,60 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
1106
1113
|
const mutationOrder = /(?<![A-Za-z0-9_])(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![A-Za-z0-9_])/.test(residue);
|
|
1107
1114
|
return !mutationOrder;
|
|
1108
1115
|
}
|
|
1116
|
+
// <advisor-intent:begin>
|
|
1117
|
+
// FR-004: an advisor/reference/replan request hands over a document or roadmap
|
|
1118
|
+
// and wants a plan back (the advisor-plan route). Deterministic, no I/O.
|
|
1119
|
+
// Every kind needs plan/document context, so "advisory lock", "hang-advisory",
|
|
1120
|
+
// a "legal advisor" label or "replan the SQL index" stay null. Three copies
|
|
1121
|
+
// (taskRouting.js, route-task.mjs, skill-router.sh) — keep them identical.
|
|
1122
|
+
export function classifyAdvisorRequest({ promptText = '', commandText = '' } = {}) {
|
|
1123
|
+
const folded = `${promptText ?? ''}\n${commandText ?? ''}`
|
|
1124
|
+
.toLowerCase()
|
|
1125
|
+
.normalize('NFD')
|
|
1126
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1127
|
+
.replace(/\u0111/g, 'd')
|
|
1128
|
+
.trim();
|
|
1129
|
+
if (!folded) return null;
|
|
1130
|
+
// A prompt that opens with a bug-fix/edit verb is a code change, not a plan request.
|
|
1131
|
+
if (/^(?:please\s+|pls\s+)?(?:fix|debug|rename|remove|delete|refactor|edit|sua\s+loi|xoa|doi\s+ten)(?![a-z0-9_])/.test(folded)) return null;
|
|
1132
|
+
const text = folded.replace(/\b(?:legal|financial|tax|investment|academic|career|mortgage)\s+advisors?\b/g, ' ');
|
|
1133
|
+
const PLAN = '(?:plans?|planning|ke\\s+hoach|phuong\\s+an|lo\\s+trinh|roadmaps?|blueprints?|specs?)';
|
|
1134
|
+
const hasPlan = new RegExp(`\\b${PLAN}\\b`).test(text);
|
|
1135
|
+
const hasDoc = /\b(?:tai\s+lieu|documents?|docs?|feedback|gop\s+y)\b|\.md\b/.test(text);
|
|
1136
|
+
const replanToken = /\bre-?plan(?:ning|ned)?\b/;
|
|
1137
|
+
if ((replanToken.test(text)
|
|
1138
|
+
&& new RegExp(`\\b(?:advisors?|feedback|gop\\s+y|reference|handoff(?:-create)?|backlog|${PLAN})\\b`).test(text.replace(replanToken, ' ')))
|
|
1139
|
+
|| /\b(?:lap|len|vach|xay\s+dung)\s+lai\s+(?:ke\s+hoach|plan|phuong\s+an|lo\s+trinh|roadmap)\b/.test(text)
|
|
1140
|
+
|| /\b(?:revise|rework|redo|rewrite)\s+(?:(?:the|this|my|our|that)\s+)?(?:plan|roadmap)\b/.test(text)) {
|
|
1141
|
+
return 'replan';
|
|
1142
|
+
}
|
|
1143
|
+
// An explicit "plan this task" order stands on its own; the document kinds below
|
|
1144
|
+
// are gated so an implementation order that merely mentions a plan, spec, doc or
|
|
1145
|
+
// advisor ("implement the plan from the spec") stays null. Plan-making verbs
|
|
1146
|
+
// ("create a plan", "handoff-create") are not implementation orders.
|
|
1147
|
+
if (/\bplan\s+(?:this|these|the\s+following)\s+(?:task|tasks|work|request|feature)\b/.test(text)
|
|
1148
|
+
|| /\b(?:lap|len)\s+ke\s+hoach\s+cho\s+(?:(?:task|viec|yeu\s+cau)\s+nay|task|viec|tinh\s+nang|yeu\s+cau|feature)\b/.test(text)) {
|
|
1149
|
+
return 'advisor';
|
|
1150
|
+
}
|
|
1151
|
+
const orderText = text
|
|
1152
|
+
.replace(/\bhandoff-create\b/g, ' handoff ')
|
|
1153
|
+
.replace(/\b(?:create|build|make|write|draft|produce|generate|tao|viet|soan|lap)\s+(?:(?:a|an|the|me|my|our|one|new|detailed|full|giup|cho|toi|ra)\s+){0,3}(?:plans?|roadmaps?|blueprints?|specs?|ke\s+hoach|phuong\s+an|lo\s+trinh)\b/g, ' ');
|
|
1154
|
+
if (hasMutationOrder({ promptText: orderText })) return null;
|
|
1155
|
+
if ((/\badvisors?\b/.test(text) && (hasPlan || hasDoc || /\bhandoff-create\b/.test(text)))
|
|
1156
|
+
|| (/\b(?:tu|co)\s+van\b(?!\s+de\b)/.test(text) && (hasPlan || /\btai\s+lieu\b|\bhandoff-create\b/.test(text)))
|
|
1157
|
+
|| (/\bhandoff-create\b/.test(text) && (hasPlan || hasDoc))) {
|
|
1158
|
+
return 'advisor';
|
|
1159
|
+
}
|
|
1160
|
+
if (/\breference\s+(?:docs?|documents?|roadmaps?|plans?|specs?|material|brief)\b/.test(text)
|
|
1161
|
+
|| /\b(?:roadmap|plan|spec|document|doc)\s+(?:as|for)\s+(?:a\s+|the\s+)?reference\b/.test(text)
|
|
1162
|
+
|| (/\btai\s+lieu\s+tham\s+khao\b/.test(text) && hasPlan)
|
|
1163
|
+
|| /\b(?:plan|ke\s+hoach)\b[^.\n]{0,40}\b(?:from|based\s+on|according\s+to|theo|dua\s+(?:tren|vao))\s+(?:(?:this|the|a|nay)\s+)?(?:roadmap|spec|brief|document|doc|tai\s+lieu)\b/.test(text)) {
|
|
1164
|
+
return 'reference';
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
// <advisor-intent:end>
|
|
1169
|
+
|
|
1109
1170
|
// C85-019: mutation vocabulary ≠ mutation order. The same residue logic the
|
|
1110
1171
|
// advisory gate uses — quotes, negated clauses, conditionals (nếu/để/cho/if),
|
|
1111
1172
|
// reported demands (yêu cầu/ép/requires), necessity phrases (cần được/must be),
|
|
@@ -1529,9 +1590,29 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
|
|
|
1529
1590
|
.filter((entry) => entry.score > 0)
|
|
1530
1591
|
.filter((entry) => shouldKeepRouteEntryForIntent(entry, intentMode))
|
|
1531
1592
|
.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
1593
|
+
// FR-004: a classified advisor/reference/replan hand-off always loads
|
|
1594
|
+
// advisor-plan, even when no catalog signal fired. It takes the FIRST of the
|
|
1595
|
+
// MAX_ACTIVE_ROUTE_SKILLS slots; the best-scoring other route fills the second.
|
|
1596
|
+
const advisorKind = classifyAdvisorRequest({ promptText, commandText });
|
|
1597
|
+
const advisorCatalogEntry = advisorKind
|
|
1598
|
+
? ROUTE_CATALOG.find((entry) => entry.id === 'advisor-plan')
|
|
1599
|
+
: null;
|
|
1600
|
+
// A catalog-only advisor-plan hit on an implementation order ("implement the
|
|
1601
|
+
// advisor plan in docs/plan.md") must not load a planning-only skill.
|
|
1602
|
+
const dropCatalogAdvisor = !advisorKind
|
|
1603
|
+
&& hasMutationOrder({ promptText, commandText })
|
|
1604
|
+
&& !/\b(?:planning[- ]only|plan[- ]only)\b|\b(?:turn|convert|translate|break(?: down)?|distill)\b.{0,48}\b(?:into|to)\b.{0,16}\b(?:plan|tasks?|ledger)\b/i.test(String(promptText ?? ''));
|
|
1605
|
+
const candidates = advisorCatalogEntry
|
|
1606
|
+
? [
|
|
1607
|
+
scoreSkillRouteEntry(advisorCatalogEntry, routeSignals),
|
|
1608
|
+
...scoredEntries.filter((entry) => entry.id !== 'advisor-plan'),
|
|
1609
|
+
]
|
|
1610
|
+
: dropCatalogAdvisor
|
|
1611
|
+
? scoredEntries.filter((entry) => entry.id !== 'advisor-plan')
|
|
1612
|
+
: scoredEntries;
|
|
1532
1613
|
const active = [];
|
|
1533
1614
|
|
|
1534
|
-
for (const entry of
|
|
1615
|
+
for (const entry of candidates) {
|
|
1535
1616
|
if (await pathExists(path.join(rootDir, entry.path))) {
|
|
1536
1617
|
active.push(entry);
|
|
1537
1618
|
if (active.length >= MAX_ACTIVE_ROUTE_SKILLS) {
|
|
@@ -14,6 +14,103 @@ $ARGUMENTS
|
|
|
14
14
|
> **Can be run multiple times.** If the plan or tasks aren't satisfactory after the first run, re-run `/ukit:handoff-create` to refine until the plan is good — as long as no task has been implemented yet.
|
|
15
15
|
> **NO git commit. NO git push.** This phase only writes docs files. No code changes.
|
|
16
16
|
|
|
17
|
+
## Planning-only contract — this command never implements
|
|
18
|
+
|
|
19
|
+
`/ukit:handoff-create` produces **SPEC.md, PLAN.md, and decomposed task files under
|
|
20
|
+
`docs/AI_HANDOFF/` — nothing else.** It never edits product code, never launches an
|
|
21
|
+
executor, and never escalates itself into implementation. Long plans are simply
|
|
22
|
+
**queued**: after Step 2.5 the command stops with the tasks in `status=ready`, and
|
|
23
|
+
implementation waits for an explicit human action.
|
|
24
|
+
|
|
25
|
+
This holds even when `docs/AI_HANDOFF/RUN.md` already carries an unfinished
|
|
26
|
+
handoff-fullstack cursor. Running this command **makes the session's handoff intent
|
|
27
|
+
planning**, so the Stop gate and the SessionStart compact/resume hook both keep quiet
|
|
28
|
+
until the human explicitly resumes.
|
|
29
|
+
|
|
30
|
+
### A live fullstack run is never overwritten — the new plan is QUEUED
|
|
31
|
+
|
|
32
|
+
Before writing anything, read `docs/AI_HANDOFF/RUN.md`. If it exists and its `Phase:`
|
|
33
|
+
is **not** `done`/`blocked`, a fullstack run is live and owns the canonical plan:
|
|
34
|
+
|
|
35
|
+
- **Never touch** `RUN.md`, `INDEX.md`, `PLAN.md`, `SPEC.md`, `ACTIVE.md`, or
|
|
36
|
+
`tasks/TASK-*.md`. Overwriting them — which the "all tasks are `ready`" rule below
|
|
37
|
+
would otherwise allow — destroys in-flight tasks, executor reports and verdicts.
|
|
38
|
+
- Write the whole new plan under the **queued path** instead:
|
|
39
|
+
`docs/AI_HANDOFF/queued/<slug>/` — `SPEC.md`, `PLAN.md`, `INDEX.md`, `HANDOFF.md`
|
|
40
|
+
(activation notes: what it depends on, when to start) and `tasks/`. This is a
|
|
41
|
+
blueprint, not a runnable cycle: no `tasks/TASK-*.md` row is created and nothing is
|
|
42
|
+
executed.
|
|
43
|
+
- Report the queued path; the live run continues untouched.
|
|
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.
|
|
50
|
+
|
|
51
|
+
### Planning-only output root (decided here, passed to the planner)
|
|
52
|
+
|
|
53
|
+
The planner writes to exactly ONE root, chosen above: the **canonical root**
|
|
54
|
+
`docs/AI_HANDOFF/` when no run is live, else the **queued root**
|
|
55
|
+
`docs/AI_HANDOFF/queued/<slug>/`. Pass it to the planner as its planning-only output
|
|
56
|
+
root; the planner writes nothing outside it and never launches an executor.
|
|
57
|
+
|
|
58
|
+
- To implement later: `/ukit:handoff-implement` (plan-only flow), or
|
|
59
|
+
`/ukit:handoff-fullstack` (one-shot pipeline) — that explicit invocation reacquires
|
|
60
|
+
ownership, resumes a live run, or sweeps a queued blueprint in once the run is closed.
|
|
61
|
+
- To abandon a paused run first: `/ukit:handoff-clear`.
|
|
62
|
+
|
|
63
|
+
### Write scope — documentation and tests only
|
|
64
|
+
|
|
65
|
+
This command may add or update **documentation** (`docs/**`, root `README*.md` /
|
|
66
|
+
`CHANGELOG.md`) and **tests**/testing files only. Inside `docs/` the guard still denies
|
|
67
|
+
script extensions (`docs/**/*.{sh,js,mjs,cjs,ts,py}`), any path with a hidden segment
|
|
68
|
+
(`docs/.x/...`) and extensions outside the doc allowlist (`.md .mdx .txt .json .yaml .yml
|
|
69
|
+
.png .svg`; e.g. `.html` is denied) — plan those as prose or a `.md` file instead. Under
|
|
70
|
+
`tests/` the guard likewise denies shell/runtime extensions (`.sh .ps1 .cmd ...`) and hidden segments. It must **never** edit source, config,
|
|
71
|
+
hooks, `package.json`, skills, commands, agents, or `AGENTS.md` / `CLAUDE.md` — those are
|
|
72
|
+
executable instructions for future sessions. There is no GREEN step and no escalation into
|
|
73
|
+
implementation: urgency, hook friction or "just one line" do not widen the scope; changing
|
|
74
|
+
source takes another command from the human.
|
|
75
|
+
|
|
76
|
+
Guard strength per host: **Claude Code** and **omp** enforce this through the PreToolUse
|
|
77
|
+
chain (omp via its bridge; unmapped shell paths are best effort). The guard checks the target
|
|
78
|
+
path of Write/Edit/MultiEdit/NotebookEdit and the file headers inside an `apply_patch` /
|
|
79
|
+
unified-diff body (`*** Add|Update|Delete File:`, `*** Move to:`, `diff --git`, `---`/`+++`);
|
|
80
|
+
a patch-like body with no parseable path is denied. Shell commands are a regex check, not a
|
|
81
|
+
parser: `node -e`, `python -c` and similar interpreter one-liners are an accepted gap. **Codex** has no pre-tool
|
|
82
|
+
hooks, so the rule is advisory there — instruction-mediated only. Codex also has no per-phase
|
|
83
|
+
model selection: never claim a guard blocked anything, or that a different model ran a phase.
|
|
84
|
+
|
|
85
|
+
### Advisor intake — large advisor / reference / replan documents
|
|
86
|
+
|
|
87
|
+
When the router flags advisor, reference-document or replan intent (or the human asks to plan
|
|
88
|
+
any task on request), load the `advisor-plan` skill; it adds no new slash command. It turns
|
|
89
|
+
the document into a LEDGER, a bounded task set and a queue, and never implements:
|
|
90
|
+
|
|
91
|
+
- **Phase A — gather (lite tier):** chunk the document, extract candidate requirements, and
|
|
92
|
+
collect feasibility evidence from the **current source** (index-first) plus `git log`,
|
|
93
|
+
`git diff` and `git status` of the touched areas. Gatherers decide nothing.
|
|
94
|
+
- **Phase B — decide (smart tier):** write the LEDGER at `${OUTPUT_ROOT}LEDGER.md` — every
|
|
95
|
+
requirement gets exactly one disposition (`feasible`/`adapted`/`deferred`/`blocker`); the
|
|
96
|
+
footer reads `Coverage: N/N rows dispositioned`; note that
|
|
97
|
+
disposition coverage is not implementation coverage. Draft 2-3 candidate plans, score them, pick the best and record
|
|
98
|
+
the rejected ones in `PLAN.md`. Every task carries detailed TDD test cases (happy, edge,
|
|
99
|
+
error with expected results) that prove it works.
|
|
100
|
+
- **Phase C — review:** the independent plan review in Step 2.5.
|
|
101
|
+
- **Phase D — implement (code tier, separate phase):** never in this session; it runs later
|
|
102
|
+
under `/ukit:handoff-fullstack` or `/ukit:handoff-implement`.
|
|
103
|
+
- **Queue threshold:** at most 10 tasks in total: all active (`ready`). More than 10: the
|
|
104
|
+
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).
|
|
110
|
+
|
|
111
|
+
Model split on Claude Code: haiku/`unic-lite` gathers, opus/`unic-smart` decides, and
|
|
112
|
+
sonnet/`unic-code` implements later; omp maps these to `@smol`/`@slow`/`@default`.
|
|
113
|
+
|
|
17
114
|
---
|
|
18
115
|
|
|
19
116
|
## Question Policy — this is the one phase that may ask
|
|
@@ -47,7 +144,8 @@ After the batched call, run Steps 1–2.5 to completion without further question
|
|
|
47
144
|
2. Read `docs/AI_HANDOFF/ACTIVE.md` → active cycle info (or "no active cycle")
|
|
48
145
|
3. Read `docs/AI_HANDOFF/RULES.md` → PLAN.md 6-section format + Task Gate required fields
|
|
49
146
|
4. Read `docs/AI_HANDOFF/tasks/_TEMPLATE.md` → task file structure
|
|
50
|
-
5.
|
|
147
|
+
5. Read `docs/AI_HANDOFF/RUN.md` → its `Phase:` line (or "no run"). This decides the output root.
|
|
148
|
+
6. Return a compact summary. Do NOT write anything yet.
|
|
51
149
|
|
|
52
150
|
> **For the human operator, on a tool with no agent support (Codex) — not an instruction to the model:** manually switch to the lite model, run the steps above yourself, keep the summary in context.
|
|
53
151
|
|
|
@@ -55,11 +153,19 @@ After the batched call, run Steps 1–2.5 to completion without further question
|
|
|
55
153
|
|
|
56
154
|
## Step 2 — Write plan + tasks (strong model)
|
|
57
155
|
|
|
58
|
-
**Claude Code — MANDATORY, do this before anything else:** call the Agent tool with `subagent_type: "handoff-planner"` (omp: the `task` tool with `agent: "handoff-planner"`), passing it the Step 1 summary
|
|
156
|
+
**Claude Code — MANDATORY, do this before anything else:** call the Agent tool with `subagent_type: "handoff-planner"` (omp: the `task` tool with `agent: "handoff-planner"`), passing it the Step 1 summary, the problem/feature description, and the **planning-only output root** (`docs/AI_HANDOFF/` when no run is live, else `docs/AI_HANDOFF/queued/<slug>/`) decided in the live-run guard below. Do NOT write PLAN.md or task files yourself in the current session — this step is contracted to the strong tier (opus/unic-smart), which only the spawned agent's frontmatter model guarantees. The planner never launches an executor.
|
|
59
157
|
|
|
60
158
|
The planner agent does the following (use Step 1 summary — do NOT re-read files):
|
|
61
159
|
|
|
62
|
-
|
|
160
|
+
0. **Live-run guard — decide OUTPUT_ROOT first (see "A live fullstack run is never
|
|
161
|
+
overwritten").** If Step 1 reported `RUN.md` with a `Phase:` that is not `done`/`blocked`,
|
|
162
|
+
a fullstack run is live: set `OUTPUT_ROOT = docs/AI_HANDOFF/queued/<slug>/` (`<slug>` =
|
|
163
|
+
a short kebab name for this work), write ONLY there, and skip steps 1, 6 and 7 — the
|
|
164
|
+
canonical `RUN.md`/`INDEX.md`/`PLAN.md`/`SPEC.md`/`ACTIVE.md`/`tasks/` stay untouched.
|
|
165
|
+
Otherwise `OUTPUT_ROOT = docs/AI_HANDOFF/`. Everything below writes under `OUTPUT_ROOT`.
|
|
166
|
+
|
|
167
|
+
1. Check INDEX.md task statuses (canonical root only — skip when the live-run guard routed
|
|
168
|
+
you to the queued root):
|
|
63
169
|
- All tasks are `ready` (planning only) → **re-run allowed**: overwrite PLAN.md and TASK-xxx.md freely — this is iterative refinement.
|
|
64
170
|
- 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.
|
|
65
171
|
- No tasks → fresh cycle, proceed normally.
|
|
@@ -69,7 +175,7 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
69
175
|
BASE=$(git symbolic-ref --short HEAD)
|
|
70
176
|
```
|
|
71
177
|
|
|
72
|
-
3. Write
|
|
178
|
+
3. Write `${OUTPUT_ROOT}SPEC.md` — the detailed implementation spec (BẮT BUỘC before tasks).
|
|
73
179
|
Follow `docs/AI_HANDOFF/SPEC.md`'s template sections (or `template_project/docs/AI_HANDOFF/SPEC.md`
|
|
74
180
|
on a fresh tree): problem/context, goals, non-goals, user journeys, functional requirements
|
|
75
181
|
with Given/When/Then + error cases, fullstack scope (backend, schema/migrations, API
|
|
@@ -83,7 +189,7 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
83
189
|
resolved to a chosen default recorded inline — the plan phase is the only question
|
|
84
190
|
window, so anything left "TBD" becomes a guess downstream.
|
|
85
191
|
|
|
86
|
-
4. Write
|
|
192
|
+
4. Write `${OUTPUT_ROOT}PLAN.md` — all 6 sections mandatory:
|
|
87
193
|
- §1 Intent — problem + success definition
|
|
88
194
|
- §2 Scope — in / out of scope. **Add a constraint**: same-wave tasks must not modify the same file (prevents merge conflicts). If two tasks need the same file, make one depend on the other.
|
|
89
195
|
- §3 Approach — solution, trade-offs, alternatives rejected
|
|
@@ -97,7 +203,7 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
97
203
|
PLANNER_MODEL: <your exact model ID>
|
|
98
204
|
```
|
|
99
205
|
|
|
100
|
-
5. Create
|
|
206
|
+
5. Create `${OUTPUT_ROOT}tasks/TASK-001.md`, `TASK-002.md`... from `_TEMPLATE.md`
|
|
101
207
|
Every task MUST have:
|
|
102
208
|
- Spec references (SPEC.md section IDs this task implements)
|
|
103
209
|
- Target Files (exact paths — no two tasks in same wave share a file)
|
|
@@ -108,9 +214,12 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
108
214
|
- Acceptance Criteria (verifiable checklist)
|
|
109
215
|
Missing any field → status: `needs_breakdown`, never `ready`
|
|
110
216
|
|
|
111
|
-
6. Update
|
|
217
|
+
6. Update `${OUTPUT_ROOT}INDEX.md` — one row per task, `status=ready`. **Skip entirely when
|
|
218
|
+
the live-run guard routed you to the queued root** — a blueprint carries no runnable
|
|
219
|
+
`ready` row; its `HANDOFF.md` activation notes are the record instead.
|
|
112
220
|
|
|
113
|
-
7. Update `ACTIVE.md
|
|
221
|
+
7. Update `ACTIVE.md` (canonical root only — **skip when queued**; the queued blueprint's
|
|
222
|
+
`HANDOFF.md` replaces it):
|
|
114
223
|
```
|
|
115
224
|
Cycle: <ID> Date: <YYYY-MM-DD> Base: <BASE>
|
|
116
225
|
Goal: <1 sentence>
|
|
@@ -120,9 +229,10 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
120
229
|
```
|
|
121
230
|
Note: wave structure is inferred from task Dependencies fields — not stored here.
|
|
122
231
|
|
|
123
|
-
8. Report: task IDs, dependency graph, any `needs_breakdown` + reason
|
|
232
|
+
8. Report: task IDs, dependency graph, any `needs_breakdown` + reason — and, when queued,
|
|
233
|
+
the `OUTPUT_ROOT` path plus why it was queued (the live run it must not disturb).
|
|
124
234
|
|
|
125
|
-
> **For the human operator, on a tool with no agent support (Codex) — not an instruction to the model:** manually switch to the strong model, execute steps
|
|
235
|
+
> **For the human operator, on a tool with no agent support (Codex) — not an instruction to the model:** manually switch to the strong model, execute steps 0–8 above yourself.
|
|
126
236
|
|
|
127
237
|
---
|
|
128
238
|
|
|
@@ -137,7 +247,7 @@ The planner agent does the following (use Step 1 summary — do NOT re-read file
|
|
|
137
247
|
Two independent strong-model passes shape the plan before any code is written — that gate is
|
|
138
248
|
intact. What it no longer does is hand a stalled plan back and wait.
|
|
139
249
|
|
|
140
|
-
**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
|
|
250
|
+
**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.
|
|
141
251
|
|
|
142
252
|
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).
|
|
143
253
|
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`.
|
|
@@ -147,4 +257,13 @@ intact. What it no longer does is hand a stalled plan back and wait.
|
|
|
147
257
|
|
|
148
258
|
---
|
|
149
259
|
|
|
150
|
-
**Next:**
|
|
260
|
+
**Next:** the plan is complete and every task is `ready` (queued — nothing has been
|
|
261
|
+
implemented). To start implementation, the human runs `/ukit:handoff-implement` for the
|
|
262
|
+
plan-only flow, or `/ukit:handoff-fullstack` to drive plan → implement → review as one
|
|
263
|
+
one-shot pipeline. This command does not do either on its own.
|
|
264
|
+
|
|
265
|
+
When a live fullstack run forced the plan into `docs/AI_HANDOFF/queued/<slug>/`, that
|
|
266
|
+
blueprint is dormant: `/ukit:handoff-fullstack` sweeps it into the canonical plan once the
|
|
267
|
+
current run is closed (or the human explicitly abandons it with `/ukit:handoff-clear`),
|
|
268
|
+
copying the queued tasks into `tasks/` at that point — exactly like any other queued
|
|
269
|
+
blueprint. Nothing is dropped, and nothing is auto-implemented before then.
|
|
@@ -111,6 +111,23 @@ all treats it as closed. It is *paused*, not abandoned: an explicit `/ukit:hando
|
|
|
111
111
|
re-invocation resumes it as a continuation; `ukit handoff-clear` abandons it. Only when
|
|
112
112
|
`RUN.md` is absent or `Phase: done` does a new request start a fresh cycle.
|
|
113
113
|
|
|
114
|
+
### Explicit invocation reacquires ownership
|
|
115
|
+
|
|
116
|
+
`/ukit:handoff-create` is planning-only and makes a session's handoff intent *planning* — so
|
|
117
|
+
while the human is planning, neither the Stop gate nor the SessionStart compact/resume hook
|
|
118
|
+
forces a paused run back into implementation. This command is the explicit counter-action:
|
|
119
|
+
|
|
120
|
+
- Invoking `/ukit:handoff-fullstack` (or running the `handoff-fullstack` skill) makes the
|
|
121
|
+
session's handoff intent *implementation* again — ownership is reacquired, the queued
|
|
122
|
+
`ready` tasks from the create phase are picked up, and a `Phase:` outside `done`/`blocked`
|
|
123
|
+
in `RUN.md` is once more held to completion by the Stop gate.
|
|
124
|
+
- Work queued by planning is **not lost**: it waits in `INDEX.md`/`PLAN.md` until this
|
|
125
|
+
explicit invocation (or `/ukit:handoff-implement`) claims it.
|
|
126
|
+
|
|
127
|
+
Enforcement is therefore never weakened — it is scoped to *which* command the session
|
|
128
|
+
actually invoked. A session driving this pipeline still cannot stop early; a session that
|
|
129
|
+
only planned never gets bounced into a run it did not ask for.
|
|
130
|
+
|
|
114
131
|
---
|
|
115
132
|
|
|
116
133
|
## §0 — Completion contract (terminal outputs)
|
|
@@ -153,11 +170,29 @@ work, not only the current plan:
|
|
|
153
170
|
3. Previous cycles — `docs/AI_HANDOFF/HISTORY.md` and `archive/` for cycles closed with
|
|
154
171
|
unfinished tasks; their leftovers join THIS cycle.
|
|
155
172
|
4. `docs/TASKS.md` — `Ready for AI` items are newly-assigned work; fold them into the plan.
|
|
156
|
-
5. `
|
|
173
|
+
5. `docs/AI_HANDOFF/queued/*/` — dormant blueprints. A planning run that could **not**
|
|
174
|
+
touch the canonical plan (a live run was in flight) queues its SPEC/PLAN/tasks under
|
|
175
|
+
`docs/AI_HANDOFF/queued/<slug>/` instead of overwriting (see `/ukit:handoff-create`).
|
|
176
|
+
When this run is planning a fresh cycle (P2), sweep those blueprints in: copy a
|
|
177
|
+
blueprint's tasks into `tasks/TASK-xxx.md` and schedule them like any other item. Never
|
|
178
|
+
overwrite a blueprint — a blueprint the run decides to defer stays queued.
|
|
179
|
+
Blueprints WITHOUT the `QueueIntent` metadata are still folded in as described above.
|
|
180
|
+
A blueprint whose header carries `QueueIntent: deliberate-future` is the exception: it is
|
|
181
|
+
**not folded** in; report it as `Backlog: <slug>` and leave it untouched.
|
|
182
|
+
Backlog items do not count toward `index:all-done` / `ExitPredicate`, so they never block completion. Only
|
|
183
|
+
an explicit slug in the invocation drains one (one blueprint per invocation). The
|
|
184
|
+
create phase of this command applies the same advisor intake as `/ukit:handoff-create`.
|
|
185
|
+
6. `git status` / `git diff` — uncommitted work-in-progress that must be finished or
|
|
157
186
|
checkpointed, never silently dropped.
|
|
158
187
|
|
|
159
188
|
The inventory feeds P2: the planner either schedules every discovered item or records why
|
|
160
|
-
it is out of scope (§1/§2 of PLAN.md). Silent omission is a plan defect
|
|
189
|
+
it is out of scope (§1/§2 of PLAN.md). Silent omission is a plan defect — a queued
|
|
190
|
+
blueprint dropped without either scheduling or a recorded reason is exactly that defect.
|
|
191
|
+
|
|
192
|
+
> **Never overwrite a live run to consume a queued blueprint.** A blueprint is folded in
|
|
193
|
+
> only through P2's normal plan-write path, and only when no run is mid-flight; on a
|
|
194
|
+
> Resume (RUN.md `Phase:` not `done`/`blocked`) the run continues from its own cursor and
|
|
195
|
+
> leaves the queued blueprint untouched for the next cycle.
|
|
161
196
|
|
|
162
197
|
**Recovery rule — stuck task records.** For every `pending`, stale `in_progress`, or
|
|
163
198
|
`blocked` task whose record cannot be cleanly resumed (invalid state, orphaned worktree
|