@ngockhoale/ukit 1.5.1 → 1.5.4

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 CHANGED
@@ -2,6 +2,55 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 1.5.4 - 2026-05-28
6
+
7
+ ### Fixed
8
+
9
+ - Fixed stale `docs/AI_HANDOFF.md` references in next-step skill, INSTALL.md, UKIT_USAGE_GUIDE.md, PROJECT.md (template + local).
10
+ - Fixed mirror `route-task.mjs` not showing `handoff=` in summary — wrong intent order (docsSpecific before handoff) + missing handoffFile field + missing display segment.
11
+ - Added `WORKLOG_ARCHIVE.md` to `.gitignore`.
12
+
13
+ ### Changed
14
+
15
+ - Compacted `docs/MEMORY.md` from 118KB/298 lines to 7.8KB/78 lines. Old entries archived to `docs/MEMORY_ARCHIVE.md`.
16
+ - Cleared handoff task queue after all cycle 002 tasks resolved.
17
+
18
+ ## 1.5.3 - 2026-05-28
19
+
20
+ ### Changed
21
+
22
+ - Restructured `docs/AI_HANDOFF.md` into `docs/AI_HANDOFF/` folder: ACTIVE.md (snapshot), RULES.md (flow + token budget), PLAN.md (brainstorm), INDEX.md (task index), tasks/ (per-task files), archive/ (completed cycles, max 3).
23
+ - Each task is now an isolated file in `tasks/TASK-xxx.md` — AI reads only the task it implements, not the entire backlog.
24
+ - Added token budget rule: combined handoff reads must stay under 200 lines per request.
25
+ - Added auto-compact rule: if any handoff file exceeds 80 lines, trigger `clear handoff`.
26
+ - Updated template mirror `templates/docs/AI_HANDOFF/` to match new folder structure.
27
+ - Updated `taskRouting.js` handoffFile target from `docs/AI_HANDOFF.md` to `docs/AI_HANDOFF/ACTIVE.md`.
28
+ - Updated `install.js`, `doctor.js`, `uninstall.js` to check for `docs/AI_HANDOFF/ACTIVE.md`.
29
+
30
+ ## 1.5.2 - 2026-05-28
31
+
32
+ ### Added
33
+
34
+ - Added first-class `ukit:handoff` prompt detection and routing for explicit handoff/brainstorm-to-task flows.
35
+ - Added `intentMode: handoff` to route summaries so hooks and helpers can prioritize handoff file automatically.
36
+ - Added a reusable generic handoff template for installed projects.
37
+
38
+ ### Changed
39
+
40
+ - Handoff prompts now route through `docs-quality` skill instead of generic `update-status`.
41
+ - Updated installed `next-step` skill guidance to read handoff file first for explicit handoff prompts.
42
+ - Updated `route-task` mirror and hook behavior in both source and installed artifact so `ukit install` users get consistent handoff routing.
43
+
44
+ ### Fixed
45
+
46
+ - Handoff authoring is now advisory (exit 0) in Safe Patch, so large handoff batches no longer block the handoff workflow.
47
+ - Hard runtime/shared-risk file broad rewrites still block by default unless `advisoryOnly=true` or `UKIT_SAFE_PATCH_ADVISORY=1` is set.
48
+
49
+ ### Tests
50
+
51
+ - Added explicit handoff routing coverage in `tests/index/taskRouting.test.js`.
52
+ - Verified Safe Patch protocol still passes 12 tests including the new handoff advisory case.
53
+
5
54
  ## 1.5.0 - 2026-05-24
6
55
 
7
56
  ### Fixed
package/README.md CHANGED
@@ -32,8 +32,7 @@ ukit install
32
32
  4. Fill in the generated docs baseline:
33
33
  - `docs/PROJECT.md`
34
34
  - `docs/MEMORY.md`
35
- - `docs/STATUS.md`
36
- - `docs/TASKS.md`
35
+ - `docs/AI_HANDOFF/`
37
36
  - `docs/WORKLOG.md`
38
37
  5. Open your AI tool and work in natural language.
39
38
 
@@ -66,7 +65,7 @@ If maintainers roll out a newer CLI build, the in-project workflow still stays t
66
65
  **Project support files**
67
66
  - `.claude/ukit/.ukit/` — installer manifests, metadata, backups
68
67
  - `.ukit/` — hidden shared runtime storage for config, cache, and cross-agent memory
69
- - `docs/` — PROJECT / MEMORY / STATUS / TASKS / WORKLOG baseline
68
+ - `docs/` — PROJECT / MEMORY / AI_HANDOFF / WORKLOG baseline
70
69
 
71
70
  ## UKit v1.3.1 Runtime
72
71
 
@@ -91,8 +90,7 @@ UKit v1.3.1 keeps the same shared runtime contract while adding Safe Patch Proto
91
90
  - install globally with `npm install -g @ngockhoale/ukit`
92
91
  - keep using the exact same human workflow inside projects: `ukit install`
93
92
  - preserve the same `ukit` binary, hooks, and install-first orchestration while standardizing the runtime root as hidden `.ukit/`
94
- - install `docs/STATUS.md` as a compact living state file for active work, debug threads, blockers, verification, and next candidates
95
- - install gitignored `docs/TASKS.md` as a local AI task queue for deferred/ready work, with safe AI cleanup rules that remove duplicates and prune completed history without deleting unfinished human intent
93
+ - install `docs/AI_HANDOFF/` as the cross-AI handoff folder with per-task isolation: ACTIVE.md (snapshot), INDEX.md (task index), tasks/ (one file per task) so each AI reads only the task it needs, with token budget rules in RULES.md
96
94
  - auto-route open-ended “what next?” / “continue” prompts to the `next-step` skill with a visible freshness cue when status may be stale
97
95
  - auto-route explicit handoff/wrap-up requests to the `update-status` skill while skipping trivial/no-state-change tasks
98
96
  - keep concrete debug/implementation/review prompts primary, so project status never replaces source/index-first task work
@@ -143,10 +143,10 @@ items:
143
143
  packs:
144
144
  - core
145
145
 
146
- - id: docs-status
146
+ - id: docs-ai-handoff
147
147
  type: config
148
- sourceTemplate: docs/STATUS.md
149
- targetPath: docs/STATUS.md
148
+ sourceTemplate: docs/AI_HANDOFF
149
+ targetPath: docs/AI_HANDOFF
150
150
  requires:
151
151
  - docs-project
152
152
  - docs-memory
@@ -159,19 +159,6 @@ items:
159
159
  packs:
160
160
  - core
161
161
 
162
- - id: docs-tasks
163
- type: config
164
- sourceTemplate: docs/TASKS.md
165
- targetPath: docs/TASKS.md
166
- requires:
167
- - docs-status
168
- mergeStrategy: skip
169
- variables:
170
- - project.name
171
- enabledByDefault: true
172
- packs:
173
- - core
174
-
175
162
  - id: docs-bugfix
176
163
  type: config
177
164
  sourceTemplate: docs/BUGFIX.md
@@ -495,8 +482,7 @@ items:
495
482
  targetPath: .claude/skills/next-step/SKILL.md
496
483
  requires:
497
484
  - docs-quality-skill
498
- - docs-status
499
- - docs-tasks
485
+ - docs-ai-handoff
500
486
  mergeStrategy: overwrite_with_backup
501
487
  variables: []
502
488
  enabledByDefault: true
@@ -509,8 +495,7 @@ items:
509
495
  targetPath: .claude/skills/update-status/SKILL.md
510
496
  requires:
511
497
  - docs-quality-skill
512
- - docs-status
513
- - docs-tasks
498
+ - docs-ai-handoff
514
499
  mergeStrategy: overwrite_with_backup
515
500
  variables: []
516
501
  enabledByDefault: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.5.1",
3
+ "version": "1.5.4",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import fs from 'node:fs/promises';
2
3
  import { pathExists, readJsonIfExists } from '../../core/fileOps.js';
3
4
  import { buildPathConfig } from '../../core/paths.js';
4
5
  import { buildRuntimePaths } from '../../core/runtimePaths.js';
@@ -45,6 +46,15 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
45
46
  : [];
46
47
  const codexAdapterTracked = trackedPaths.some((entry) => entry.startsWith('.codex/'));
47
48
 
49
+ const WORKLOG_MAX_LINES = 600;
50
+ let worklogLineCount = 0;
51
+ try {
52
+ const content = await fs.readFile(path.join(projectRoot, 'docs', 'WORKLOG.md'), 'utf8');
53
+ worklogLineCount = content.split('\n').length;
54
+ } catch {
55
+ // file may not exist
56
+ }
57
+
48
58
  const checks = {
49
59
  manifestLoaded: Boolean(manifest?.name),
50
60
  templatesDirExists: await pathExists(pathConfig.templatesRoot),
@@ -62,9 +72,9 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
62
72
  sessionMemoryDirExists: await pathExists(runtimePaths.sessionsDir),
63
73
  docsProjectExists: await pathExists(path.join(projectRoot, 'docs', 'PROJECT.md')),
64
74
  docsMemoryExists: await pathExists(path.join(projectRoot, 'docs', 'MEMORY.md')),
65
- docsStatusExists: await pathExists(path.join(projectRoot, 'docs', 'STATUS.md')),
66
- docsTasksExists: await pathExists(path.join(projectRoot, 'docs', 'TASKS.md')),
75
+ docsAiHandoffExists: await pathExists(path.join(projectRoot, 'docs', 'AI_HANDOFF', 'ACTIVE.md')),
67
76
  docsWorklogExists: await pathExists(path.join(projectRoot, 'docs', 'WORKLOG.md')),
77
+ docsWorklogUnderBudget: worklogLineCount <= WORKLOG_MAX_LINES,
68
78
  allProvidersConfigured: providers.allSupported,
69
79
  ...(codexAdapterTracked
70
80
  ? {
@@ -105,9 +115,9 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
105
115
  console.log(`[UKit] ${ok(checks.sessionMemoryDirExists)} .ukit/storage/memory/sessions/`);
106
116
  console.log(`[UKit] ${ok(checks.docsProjectExists)} docs/PROJECT.md`);
107
117
  console.log(`[UKit] ${ok(checks.docsMemoryExists)} docs/MEMORY.md`);
108
- console.log(`[UKit] ${ok(checks.docsStatusExists)} docs/STATUS.md`);
109
- console.log(`[UKit] ${ok(checks.docsTasksExists)} docs/TASKS.md`);
118
+ console.log(`[UKit] ${ok(checks.docsAiHandoffExists)} docs/AI_HANDOFF/`);
110
119
  console.log(`[UKit] ${ok(checks.docsWorklogExists)} docs/WORKLOG.md`);
120
+ console.log(`[UKit] ${ok(checks.docsWorklogUnderBudget)} docs/WORKLOG.md under budget (${worklogLineCount}/${WORKLOG_MAX_LINES} lines)`);
111
121
  if (codexAdapterTracked) {
112
122
  console.log(`[UKit] ${ok(checks.codexReadmeExists)} .codex/README.md`);
113
123
  console.log(`[UKit] ${ok(checks.codexSettingsExists)} .codex/settings.json`);
@@ -239,10 +239,10 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
239
239
  const docsLabels = [
240
240
  'docs/PROJECT.md',
241
241
  'docs/MEMORY.md',
242
- 'docs/STATUS.md',
243
- 'docs/TASKS.md',
242
+ 'docs/AI_HANDOFF/ACTIVE.md',
244
243
  'docs/WORKLOG.md',
245
244
  ];
245
+
246
246
  const docsPaths = docsLabels.map((label) => path.join(projectRoot, ...label.split('/')));
247
247
  const missingDocs = [];
248
248
  for (let i = 0; i < docsPaths.length; i++) {
@@ -254,7 +254,7 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
254
254
  if (missingDocs.length > 0) {
255
255
  console.log(`[UKit] Missing docs — fill these in before first use: ${missingDocs.join(', ')}`);
256
256
  } else {
257
- console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/TASKS.md, docs/WORKLOG.md');
257
+ console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/AI_HANDOFF/, docs/WORKLOG.md');
258
258
  console.log('[UKit] Fill them once with real project context for the best results.');
259
259
  }
260
260
 
@@ -47,5 +47,5 @@ export async function runUninstall({ projectRoot, argv = [] }) {
47
47
  }
48
48
 
49
49
  console.log(`[UKit] Uninstall complete. Removed ${result.removed}/${result.attempted} managed paths.`);
50
- console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/TASKS.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
50
+ console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/AI_HANDOFF/, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
51
51
  }
@@ -11,8 +11,6 @@ const UKIT_ENTRIES = [
11
11
  'opencode.json',
12
12
  'AGENTS.md',
13
13
  'CLAUDE.md',
14
- 'docs/STATUS.md',
15
- 'docs/TASKS.md',
16
14
  '.claude/ukit/.ukit/',
17
15
  '.claude/ukit/permission-usage.json',
18
16
  '.claude/ukit/permission-audit.log',
@@ -49,7 +49,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
49
49
  const safeOverrides = isPlainObject(overrides) ? overrides : {};
50
50
 
51
51
  return mergeObjects({
52
- version: '1.5.1',
52
+ version: '1.5.2',
53
53
  agent: 'claude-code',
54
54
  autonomy: {
55
55
  level: 'balanced',
@@ -139,6 +139,10 @@ export async function deriveTaskRoute({
139
139
  contextRecommendation,
140
140
  verificationRecommendation,
141
141
  });
142
+ const handoffBudget = intentMode === 'handoff'
143
+ ? await checkHandoffBudget(absoluteRoot)
144
+ : null;
145
+ const worklogBudget = await checkWorklogBudget(absoluteRoot);
142
146
  const routeSummary = buildRouteSummary({
143
147
  activeSkills,
144
148
  routingContext: {
@@ -157,6 +161,8 @@ export async function deriveTaskRoute({
157
161
  contextRecommendation,
158
162
  verificationRecommendation,
159
163
  nextAction,
164
+ handoffBudget,
165
+ worklogBudget,
160
166
  });
161
167
  const approachSelector = routeSummary?.approachSelector ?? null;
162
168
 
@@ -180,6 +186,8 @@ export async function deriveTaskRoute({
180
186
  verificationRecommendation,
181
187
  nextAction,
182
188
  routeSummary,
189
+ handoffBudget,
190
+ worklogBudget,
183
191
  ...(degradedWarnings.length > 0 ? { degradedWarnings } : {}),
184
192
  };
185
193
  }
@@ -190,6 +198,8 @@ export function buildRouteSummary({
190
198
  contextRecommendation = null,
191
199
  verificationRecommendation = null,
192
200
  nextAction = null,
201
+ handoffBudget = null,
202
+ worklogBudget = null,
193
203
  } = {}) {
194
204
  const autonomyLevel = routingContext.autonomyLevel ?? 'balanced';
195
205
  const delegationRecommendation = deriveDelegationRecommendation({
@@ -243,14 +253,18 @@ export function buildRouteSummary({
243
253
  ),
244
254
  );
245
255
  const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
256
+ const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
246
257
  const summaryLine = [
247
258
  routingContext.taskType ? `task=${routingContext.taskType}` : null,
259
+ handoffFile ? `handoff=${handoffFile}` : null,
248
260
  formatCompactSegment('targets', primaryTargets),
249
261
  formatCompactSegment('tests', relatedTests),
250
262
  formatCompactSegment('styles', styleFiles),
251
263
  editGuardHint ? `editGuard=${editGuardHint}` : null,
252
264
  delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
253
265
  policyMode ? `policy=${policyMode}` : null,
266
+ handoffBudget?.warning ? `budget=${handoffBudget.warning}` : null,
267
+ worklogBudget?.warning ? `budget=${worklogBudget.warning}` : null,
254
268
  ].filter(Boolean).join(' | ');
255
269
 
256
270
  return {
@@ -267,6 +281,9 @@ export function buildRouteSummary({
267
281
  completionState,
268
282
  continuationState,
269
283
  intentMode: routingContext.intentMode ?? null,
284
+ handoffFile,
285
+ handoffBudget,
286
+ worklogBudget,
270
287
  delegateHint: delegationRecommendation?.hint ?? null,
271
288
  nextActionType: nextAction?.type ?? null,
272
289
  nextActionCommand,
@@ -783,11 +800,11 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
783
800
  }
784
801
 
785
802
  function shouldKeepRouteEntryForIntent(entry, intentMode) {
786
- if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific'].includes(intentMode)) {
803
+ if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific', 'handoff'].includes(intentMode)) {
787
804
  return false;
788
805
  }
789
806
 
790
- if (entry.id === 'update-status' && intentMode === 'docs-specific') {
807
+ if (entry.id === 'update-status' && ['docs-specific', 'handoff'].includes(intentMode)) {
791
808
  return false;
792
809
  }
793
810
 
@@ -896,6 +913,10 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
896
913
  const openEndedStatus = hasOpenEndedStatusSignal(lower, raw) || taskQueueNext;
897
914
  const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext });
898
915
 
916
+ if (hasHandoffSignal(lower, raw)) {
917
+ return 'handoff';
918
+ }
919
+
899
920
  if (docsSpecific) {
900
921
  return 'docs-specific';
901
922
  }
@@ -927,6 +948,20 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
927
948
  return null;
928
949
  }
929
950
 
951
+ function hasHandoffSignal(lower, raw) {
952
+ return /\bukit:handoff\b/.test(raw)
953
+ || /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
954
+ || /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
955
+ || /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
956
+ || /\b(gom|chia|tach|tách|lên|len|xóa|clear|dọn).{0,80}\b(idea|ý tưởng|y tuong|task|công việc|cong viec|handoff)\b/.test(raw)
957
+ || /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
958
+ }
959
+
960
+ function hasHandoffClearSignal(lower, raw) {
961
+ return /\b(clear handoff|handoff clear|dọn handoff|xóa handoff|reset handoff|compact handoff)\b/.test(lower)
962
+ || (/\bukit:handoff\b/.test(raw) && /\b(clear|dọn|xóa|reset|compact)\b/.test(lower));
963
+ }
964
+
930
965
  function hasStatusUpdateSignal(lower, raw) {
931
966
  return /\b(update|refresh|write|sync|record|capture|summarize|summarise).{0,64}\b(status\.md|project status|current state|next candidates|session state)\b/.test(lower)
932
967
  || /\b(status\.md|project status).{0,64}\b(update|refresh|write|sync|record|capture|summarize|summarise)\b/.test(lower)
@@ -1459,6 +1494,108 @@ function unique(values) {
1459
1494
  return [...new Set(values.filter(Boolean))];
1460
1495
  }
1461
1496
 
1497
+ const HANDOFF_BUDGET_MAX_LINES = 200;
1498
+ const HANDOFF_FILE_MAX_LINES = 80;
1499
+ const WORKLOG_BUDGET_MAX_LINES = 600;
1500
+ const WORKLOG_BUDGET_MAX_ENTRIES = 30;
1501
+
1502
+ export async function checkHandoffBudget(rootDir) {
1503
+ const handoffDir = path.join(rootDir, 'docs', 'AI_HANDOFF');
1504
+ const checkFiles = ['ACTIVE.md', 'INDEX.md'];
1505
+ let totalLines = 0;
1506
+ const fileDetails = [];
1507
+
1508
+ for (const fileName of checkFiles) {
1509
+ const filePath = path.join(handoffDir, fileName);
1510
+ try {
1511
+ const content = await fs.readFile(filePath, 'utf8');
1512
+ const lines = content.split('\n').length;
1513
+ totalLines += lines;
1514
+ fileDetails.push({ file: fileName, lines });
1515
+ } catch {
1516
+ // File may not exist yet — not an error
1517
+ }
1518
+ }
1519
+
1520
+ // Also count task files
1521
+ const tasksDir = path.join(handoffDir, 'tasks');
1522
+ try {
1523
+ const taskFiles = await fs.readdir(tasksDir);
1524
+ for (const taskFile of taskFiles) {
1525
+ if (!taskFile.endsWith('.md')) continue;
1526
+ const content = await fs.readFile(path.join(tasksDir, taskFile), 'utf8');
1527
+ const lines = content.split('\n').length;
1528
+ totalLines += lines;
1529
+ fileDetails.push({ file: `tasks/${taskFile}`, lines });
1530
+ }
1531
+ } catch {
1532
+ // tasks dir may not exist
1533
+ }
1534
+
1535
+ const oversizedFiles = fileDetails.filter((f) => f.lines > HANDOFF_FILE_MAX_LINES);
1536
+ const overBudget = totalLines > HANDOFF_BUDGET_MAX_LINES;
1537
+
1538
+ let warning = null;
1539
+ if (overBudget) {
1540
+ warning = 'over-budget';
1541
+ } else if (oversizedFiles.length > 0) {
1542
+ warning = 'file-oversized';
1543
+ }
1544
+
1545
+ return {
1546
+ totalLines,
1547
+ maxLines: HANDOFF_BUDGET_MAX_LINES,
1548
+ fileDetails,
1549
+ oversizedFiles: oversizedFiles.map((f) => f.file),
1550
+ overBudget,
1551
+ warning,
1552
+ action: warning ? 'clear-handoff' : null,
1553
+ };
1554
+ }
1555
+
1556
+ export async function checkWorklogBudget(rootDir) {
1557
+ const worklogPath = path.join(rootDir, 'docs', 'WORKLOG.md');
1558
+ try {
1559
+ const content = await fs.readFile(worklogPath, 'utf8');
1560
+ const lines = content.split('\n').length;
1561
+ const entryMatches = content.match(/^## \d{4}-\d{2}-\d{2}/gm) ?? [];
1562
+ const entryCount = entryMatches.length;
1563
+
1564
+ const overLineBudget = lines > WORKLOG_BUDGET_MAX_LINES;
1565
+ const overEntryBudget = entryCount > WORKLOG_BUDGET_MAX_ENTRIES;
1566
+ const overBudget = overLineBudget || overEntryBudget;
1567
+
1568
+ let warning = null;
1569
+ if (overLineBudget && overEntryBudget) {
1570
+ warning = 'worklog-lines-and-entries-over';
1571
+ } else if (overLineBudget) {
1572
+ warning = 'worklog-lines-over';
1573
+ } else if (overEntryBudget) {
1574
+ warning = 'worklog-entries-over';
1575
+ }
1576
+
1577
+ return {
1578
+ lines,
1579
+ maxLines: WORKLOG_BUDGET_MAX_LINES,
1580
+ entryCount,
1581
+ maxEntries: WORKLOG_BUDGET_MAX_ENTRIES,
1582
+ overBudget,
1583
+ warning,
1584
+ action: warning ? 'compact-worklog' : null,
1585
+ };
1586
+ } catch {
1587
+ return {
1588
+ lines: 0,
1589
+ maxLines: WORKLOG_BUDGET_MAX_LINES,
1590
+ entryCount: 0,
1591
+ maxEntries: WORKLOG_BUDGET_MAX_ENTRIES,
1592
+ overBudget: false,
1593
+ warning: null,
1594
+ action: null,
1595
+ };
1596
+ }
1597
+ }
1598
+
1462
1599
  const DELEGATABLE_IMPLEMENTATION_SKILL_IDS = new Set([
1463
1600
  'delivery',
1464
1601
  'frontend',
@@ -3,7 +3,7 @@
3
3
  # Used from UserPromptSubmit and PreToolUse so end users do not need to name skills.
4
4
 
5
5
  INPUT=$(cat)
6
- PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
6
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
7
7
  STATE_FILE="$PROJECT_ROOT/.claude/ukit/skill-router-state.json"
8
8
  HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  THRESHOLD_SCRIPT="$HOOK_DIR/../ukit/runtime/compact-threshold.mjs"
@@ -3,7 +3,7 @@
3
3
  # and does not jump to blanket verification when targeted evidence already exists.
4
4
 
5
5
  INPUT=$(cat)
6
- PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
6
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
7
7
  STATE_FILE="$PROJECT_ROOT/.claude/ukit/skill-router-state.json"
8
8
  ROUTE_CACHE_FILE="$PROJECT_ROOT/.claude/ukit/route-cache.json"
9
9
  PROGRESS_FILE="$PROJECT_ROOT/.claude/ukit/verification-progress.json"
@@ -44,11 +44,12 @@ If stale or missing, downgrade confidence and verify with the smallest current t
44
44
  ## Input Order
45
45
 
46
46
  Read only what is needed:
47
- 1. `docs/STATUS.md` (or existing root `STATUS.md` fallback)
48
- 2. `docs/TASKS.md` only for queued-task prompts, “continue” with no active status, or when status points at queued work
49
- 3. `docs/CODE_MAP.md` only when navigation is needed
50
- 4. `docs/MEMORY.md` only when constraints/decisions affect the suggestion
51
- 5. routed index/tree summary only if status/tasks are stale, missing, or contradicted
47
+ 1. `docs/AI_HANDOFF/ACTIVE.md` first when the prompt explicitly names handoff / `ukit:handoff` / brainstorm-to-task flow
48
+ 2. `docs/STATUS.md` (or existing root `STATUS.md` fallback) for normal status/continue prompts
49
+ 3. `docs/TASKS.md` only for queued-task prompts, “continue” with no active status, or when status points at queued work
50
+ 4. `docs/CODE_MAP.md` only when navigation is needed
51
+ 5. `docs/MEMORY.md` only when constraints/decisions affect the suggestion
52
+ 6. routed index/tree summary only if status/tasks are stale, missing, or contradicted
52
53
 
53
54
  ## Output Shape
54
55
 
@@ -17,6 +17,11 @@ function getFilePath(payload = {}) {
17
17
  return payload.tool_input?.file_path || payload.file_path || '';
18
18
  }
19
19
 
20
+ function isHandoffAuthoringFile(relativePath) {
21
+ return relativePath === 'docs/AI_HANDOFF.md'
22
+ || relativePath.startsWith('docs/AI_HANDOFF/');
23
+ }
24
+
20
25
  async function listManifestPaths(backupsRoot) {
21
26
  const dates = await fs.readdir(backupsRoot, { withFileTypes: true }).catch(() => []);
22
27
  return dates
@@ -152,7 +157,8 @@ export async function verifyPostEdit({ projectRoot = process.cwd(), payload = {}
152
157
 
153
158
  const overChangedLines = delta.changedLines > Number(config.deltaMaxChangedLines || 120);
154
159
  const overHunks = delta.hunkCount > Number(config.deltaMaxHunks || 3);
155
- const status = risk.strict && (overChangedLines || overHunks) ? 'blocked' : 'ok';
160
+ const handoffAdvisory = isHandoffAuthoringFile(resolved.relative) && risk.labels.every((label) => ['large-file', 'multilingual-text'].includes(label));
161
+ const status = risk.strict && (overChangedLines || overHunks) ? (handoffAdvisory ? 'advisory' : 'blocked') : 'ok';
156
162
  const postEntry = {
157
163
  event: 'post-edit',
158
164
  file: resolved.relative,
@@ -169,8 +175,8 @@ export async function verifyPostEdit({ projectRoot = process.cwd(), payload = {}
169
175
 
170
176
  return {
171
177
  ...postEntry,
172
- message: status === 'blocked'
173
- ? `BLOCKED: '${resolved.relative}' exceeded Safe Patch delta budget (changedLines=${delta.changedLines}/${config.deltaMaxChangedLines}, hunks=${delta.hunkCount}/${config.deltaMaxHunks}). Review diff or restore from ${latest.entry.rollbackPath}.`
178
+ message: status === 'blocked' || status === 'advisory'
179
+ ? `${status === 'advisory' ? 'ADVISORY' : 'BLOCKED'}: '${resolved.relative}' exceeded Safe Patch delta budget (changedLines=${delta.changedLines}/${config.deltaMaxChangedLines}, hunks=${delta.hunkCount}/${config.deltaMaxHunks}). Review diff or restore from ${latest.entry.rollbackPath}.`
174
180
  : `OK: '${resolved.relative}' delta changedLines=${delta.changedLines}, hunks=${delta.hunkCount}.`,
175
181
  };
176
182
  }
@@ -193,6 +199,9 @@ async function main() {
193
199
  process.stdout.write(`${JSON.stringify(result)}\n`);
194
200
  } else if (result.status === 'ok') {
195
201
  process.stdout.write(`[ukit-safe-patch] ${result.message}\n`);
202
+ } else if (result.status === 'advisory') {
203
+ process.stderr.write(`[ukit-safe-patch] ${result.message}\n`);
204
+ process.stderr.write('[ukit-safe-patch] handoff authoring advisory — change is already written; continue updating docs/AI_HANDOFF/.\n');
196
205
  }
197
206
  if (result.status === 'blocked') {
198
207
  const advisory = isSafePatchAdvisoryOnly(runtimeConfig);
@@ -481,6 +481,7 @@ function formatDisplayRouteSummary(routeSummary = null, routingContext = {}) {
481
481
 
482
482
  const segments = [
483
483
  taskSegment,
484
+ extractRouteLineSegment(line, 'handoff'),
484
485
  extractRouteLineSegment(line, 'targets'),
485
486
  extractRouteLineSegment(line, 'tests'),
486
487
  extractRouteLineSegment(line, 'styles'),
@@ -723,11 +724,11 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
723
724
  }
724
725
 
725
726
  function shouldKeepRouteEntryForIntent(entry, intentMode) {
726
- if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific'].includes(intentMode)) {
727
+ if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific', 'handoff'].includes(intentMode)) {
727
728
  return false;
728
729
  }
729
730
 
730
- if (entry.id === 'update-status' && intentMode === 'docs-specific') {
731
+ if (entry.id === 'update-status' && ['docs-specific', 'handoff'].includes(intentMode)) {
731
732
  return false;
732
733
  }
733
734
 
@@ -831,6 +832,10 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
831
832
  const openEndedStatus = hasOpenEndedStatusSignal(lower, raw) || taskQueueNext;
832
833
  const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext });
833
834
 
835
+ if (hasHandoffSignal(lower, raw)) {
836
+ return 'handoff';
837
+ }
838
+
834
839
  if (docsSpecific) {
835
840
  return 'docs-specific';
836
841
  }
@@ -870,6 +875,20 @@ function hasStatusUpdateSignal(lower, raw) {
870
875
  || /\b(cập nhật|ghi lại|tổng kết|chốt session|bàn giao).{0,64}\b(status|trạng thái|việc tiếp theo)\b/.test(raw);
871
876
  }
872
877
 
878
+ function hasHandoffSignal(lower, raw) {
879
+ return /\bukit:handoff\b/.test(raw)
880
+ || /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
881
+ || /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
882
+ || /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
883
+ || /\b(gom|chia|tach|tách|lên|len|xóa|clear|dọn).{0,80}\b(idea|ý tưởng|y tuong|task|công việc|cong viec|handoff)\b/.test(raw)
884
+ || /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
885
+ }
886
+
887
+ function hasHandoffClearSignal(lower, raw) {
888
+ return /\b(clear handoff|handoff clear|dọn handoff|xóa handoff|reset handoff|compact handoff)\b/.test(lower)
889
+ || (/\bukit:handoff\b/.test(raw) && /\b(clear|dọn|xóa|reset|compact)\b/.test(lower));
890
+ }
891
+
873
892
  function hasOpenEndedStatusSignal(lower, raw) {
874
893
  return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/.test(lower)
875
894
  || /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project|task tiep theo|viec tiep theo trong tasks)\b/.test(lower)
@@ -1332,8 +1351,10 @@ function buildRouteSummary({
1332
1351
  ),
1333
1352
  );
1334
1353
  const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
1354
+ const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
1335
1355
  const line = [
1336
1356
  routingContext.taskType ? `task=${routingContext.taskType}` : null,
1357
+ handoffFile ? `handoff=${handoffFile}` : null,
1337
1358
  formatCompactSegment('targets', primaryTargets),
1338
1359
  formatCompactSegment('tests', relatedTests),
1339
1360
  formatCompactSegment('styles', styleFiles),
@@ -1356,6 +1377,7 @@ function buildRouteSummary({
1356
1377
  completionState,
1357
1378
  continuationState,
1358
1379
  intentMode: routingContext.intentMode ?? null,
1380
+ handoffFile,
1359
1381
  delegateHint: delegationRecommendation?.hint ?? null,
1360
1382
  nextActionType: nextAction?.type ?? null,
1361
1383
  nextActionCommand,
@@ -96,7 +96,7 @@ For clearly non-code specialist lanes (docs-only, status, task queue), skip the
96
96
  - **Non-trivial**: `docs/MEMORY.md` + `docs/PROJECT.md` + `docs/CODE_MAP.md`.
97
97
  - `docs/STATUS.md`: use for open-ended status/continue prompts or meaningful continuation context; stale status is orientation only.
98
98
  - `docs/TASKS.md`: use only for queued-task prompts or when status points at queued work; safely clean exact duplicates/completed overflow by default without deleting unfinished human-authored tasks.
99
- - `docs/WORKLOG.md`: only recent relevant entries.
99
+ - `docs/WORKLOG.md`: only recent relevant entries. Follow the Budget Rules at the top of the file; archive oldest entries to `docs/WORKLOG_ARCHIVE.md` when over limits.
100
100
  - Follow routed verification policy: targeted first, widen only when risk/shared scope justifies it, ask before blanket broad runs.
101
101
 
102
102
  ## Living Status Workflow
@@ -0,0 +1,9 @@
1
+ # Active Handoff Cycle
2
+
3
+ - Status: `ready_for_next_cycle`
4
+ - Phase: `cleared`
5
+ - Updated:
6
+ - Updated by:
7
+ - Human decision needed: `no`
8
+
9
+ <!-- Snapshot only. Rules in RULES.md. Tasks in tasks/. Plan brainstorm in PLAN.md. -->
@@ -0,0 +1,4 @@
1
+ # Handoff History
2
+
3
+ | Cycle | Date | Summary |
4
+ |-------|------|---------|
@@ -0,0 +1,6 @@
1
+ # Handoff Task Index
2
+
3
+ | ID | Title | Priority | Size | Status | File |
4
+ |----|-------|----------|------|--------|------|
5
+
6
+ Updated:
@@ -0,0 +1,5 @@
1
+ # Handoff Plan
2
+
3
+ Status: `empty`
4
+
5
+ <!-- AI ghi ý tưởng + phân tích ở đây. Sau khi tách thành tasks, clear phần dưới. -->
@@ -0,0 +1,47 @@
1
+ # Handoff Rules
2
+
3
+ ## Token Budget (MANDATORY)
4
+
5
+ - **Combined handoff reads must stay under 200 lines per request.**
6
+ - Read order: `ACTIVE.md` (if needed) → `INDEX.md` (scan tasks) → single `tasks/TASK-xxx.md` (implement one task).
7
+ - Do NOT read `RULES.md` every request — only when you need flow clarification.
8
+ - Do NOT read multiple task files in one request.
9
+ - If ACTIVE.md + INDEX.md + task file would exceed budget, read only the task file.
10
+ - Auto-compact: if any single handoff file exceeds 80 lines, trigger `clear handoff` immediately.
11
+
12
+ ## How Human Submits Ideas
13
+
14
+ - Natural language is enough: `ukit:handoff`, `gom ý tưởng`, `chia task`, `đưa vào handoff`.
15
+ - If request is already a concrete task (clear file/logic/output, small enough to do now), bypass handoff and execute directly.
16
+ - If request is broad/ambiguous/multi-step, use handoff.
17
+
18
+ ## Handoff Flow
19
+
20
+ 1. Human submits ideas → AI writes to `PLAN.md`.
21
+ 2. Human approves plan → AI splits into `tasks/TASK-xxx.md` + updates `INDEX.md`.
22
+ 3. AI implementer reads `INDEX.md` → picks task → reads `tasks/TASK-xxx.md` → implements.
23
+ 4. After implementation → update `INDEX.md` status, archive cycle if done.
24
+
25
+ ## Task Gate
26
+
27
+ A task is `ready` only when it has:
28
+ - Clear target files
29
+ - Clear action
30
+ - Dependencies stated
31
+ - Verification command
32
+ - Acceptance criteria
33
+
34
+ Missing any → `needs_breakdown`, `blocked`, or `needs_human`.
35
+
36
+ ## Clear Handoff
37
+
38
+ 1. Archive current cycle → `archive/cycle-NNN.md`.
39
+ 2. If archive > 3 files → delete oldest, append 1-line summary to `HISTORY.md`.
40
+ 3. Reset `ACTIVE.md` to empty template.
41
+ 4. Clear `INDEX.md`.
42
+ 5. Delete all files in `tasks/`.
43
+ 6. Clear `PLAN.md`.
44
+
45
+ ## Docs Sync
46
+
47
+ After cycle, update affected docs only: `WORKLOG.md`, `PROJECT.md`, `CODE_MAP.md`, `CHANGELOG.md`.
File without changes
File without changes
@@ -47,8 +47,7 @@ End users do not need to manage any of that manually.
47
47
  Complete these files before first serious use:
48
48
  - `docs/PROJECT.md`
49
49
  - `docs/MEMORY.md`
50
- - `docs/STATUS.md`
51
- - `docs/TASKS.md`
50
+ - `docs/AI_HANDOFF/`
52
51
  - `docs/WORKLOG.md`
53
52
 
54
53
  ### 4) Open your AI tool
@@ -98,8 +97,7 @@ ukit install
98
97
  Check that the docs baseline files exist and are filled in:
99
98
  - `docs/PROJECT.md`
100
99
  - `docs/MEMORY.md`
101
- - `docs/STATUS.md`
102
- - `docs/TASKS.md`
100
+ - `docs/AI_HANDOFF/`
103
101
  - `docs/WORKLOG.md`
104
102
 
105
103
  ---
@@ -44,9 +44,8 @@
44
44
 
45
45
  1. Run `ukit memory recall "<current task>"` for non-trivial work; reuse relevant `## Previous Context` before asking the user to restate prior decisions
46
46
  2. Read `docs/MEMORY.md` — architecture decisions, active constraints, known bugs
47
- 3. Read `docs/STATUS.md` for open-ended status/continue prompts or meaningful continuation context; treat stale status as orientation only
48
- 4. Read `docs/TASKS.md` only when selecting queued AI work, or when status points at queued tasks; apply safe cleanup without deleting unfinished human intent
49
- 5. Read `docs/CODE_MAP.md` if it exists structural navigation index
50
- 6. Use the installed source-code index / routed helpers to localize the smallest relevant file + test set first
51
- 7. Scan recent `docs/WORKLOG.md` entries if continuing prior work
52
- 8. Verify understanding against source before acting — **docs orient, source is truth; keep the index-first workflow intact**
47
+ 3. Read `docs/AI_HANDOFF/ACTIVE.md` when continuing cross-AI planning, task breakdown, or task implementation handoff work
48
+ 4. Read `docs/CODE_MAP.md` if it exists structural navigation index
49
+ 5. Use the installed source-code index / routed helpers to localize the smallest relevant file + test set first
50
+ 6. Scan recent `docs/WORKLOG.md` entries if continuing prior work
51
+ 7. Verify understanding against source before acting — **docs orient, source is truth; keep the index-first workflow intact**
@@ -138,11 +138,10 @@ Project đang ở đâu, làm gì tiếp?
138
138
 
139
139
  Expected UKit behavior:
140
140
  1. auto-load the hidden next-step lane
141
- 2. read `docs/STATUS.md` first and show whether it is fresh, possibly stale, stale, or missing
142
- 3. read `docs/TASKS.md` only when selecting queued work or when status points at queued tasks
141
+ 2. read `docs/AI_HANDOFF/ACTIVE.md` when the team is passing planning, task breakdown, or implementation context between AIs
142
+ 3. verify the handoff against source/index before treating it as authoritative
143
143
  4. suggest only a few actionable next candidates
144
- 5. if status/tasks are stale, verify with source/index before treating any candidate as authoritative
145
- 6. if the prompt names a concrete bug/feature/review target, keep the concrete workflow primary instead of producing a global roadmap
144
+ 5. if the prompt names a concrete bug/feature/review target, keep the concrete workflow primary instead of producing a global roadmap
146
145
 
147
146
  ---
148
147
 
@@ -2,6 +2,17 @@
2
2
 
3
3
  Track session-level execution details.
4
4
 
5
+ ## Budget Rules
6
+
7
+ Keep this file compact to save AI context tokens:
8
+
9
+ - **Max 30 entries.** When over, archive the oldest entries to `docs/WORKLOG_ARCHIVE.md`.
10
+ - **Max ~600 lines.** If over, archive oldest entries until under budget.
11
+ - Each entry should be 10-20 lines max (summary, not transcript).
12
+ - On archive: move full entry block to `docs/WORKLOG_ARCHIVE.md` (create if missing).
13
+ - Keep a compaction marker as the last line: `<!-- Entries before YYYY-MM archived to docs/WORKLOG_ARCHIVE.md. Keep this file < 600 lines. -->`
14
+ - If the user says "compact worklog" or "clean worklog", perform the archive pass and report what moved.
15
+
5
16
  For each significant action, append:
6
17
  - Date/time
7
18
  - Action taken
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.5.1",
2
+ "version": "1.5.4",
3
3
  "agent": "claude-code",
4
4
  "autonomy": {
5
5
  "level": "balanced",