@ngockhoale/ukit 1.5.0 → 1.5.2

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,30 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 1.5.2 - 2026-05-28
6
+
7
+ ### Added
8
+
9
+ - Added first-class `ukit:handoff` prompt detection and routing for explicit handoff/brainstorm-to-task flows.
10
+ - Added `intentMode: handoff` to route summaries so hooks and helpers can prioritize `docs/AI_HANDOFF.md` automatically.
11
+ - Added a reusable generic `docs/AI_HANDOFF.md` handoff template for installed projects.
12
+
13
+ ### Changed
14
+
15
+ - Handoff prompts now route through `docs-quality` skill instead of generic `update-status`, making `docs/AI_HANDOFF.md` the primary coordination artifact.
16
+ - Updated installed `next-step` skill guidance to read `docs/AI_HANDOFF.md` first for explicit handoff prompts.
17
+ - Updated `route-task` mirror and hook behavior in both source and installed artifact so `ukit install` users get consistent handoff routing.
18
+
19
+ ### Fixed
20
+
21
+ - Handoff authoring is now advisory (exit 0) in Safe Patch, so large `docs/AI_HANDOFF.md` batches no longer block the handoff workflow.
22
+ - Hard runtime/shared-risk file broad rewrites still block by default unless `advisoryOnly=true` or `UKIT_SAFE_PATCH_ADVISORY=1` is set.
23
+
24
+ ### Tests
25
+
26
+ - Added explicit handoff routing coverage in `tests/index/taskRouting.test.js`.
27
+ - Verified Safe Patch protocol still passes 12 tests including the new handoff advisory case.
28
+
5
29
  ## 1.5.0 - 2026-05-24
6
30
 
7
31
  ### 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.md`
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.md` as the default cross-AI handoff file for plan task breakdown → implementation continuity, with explicit sections so one AI can plan, another can refine tasks, and another can implement them reliably
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
@@ -84,7 +84,7 @@ items:
84
84
  requires:
85
85
  - docs-project
86
86
  - docs-memory
87
- mergeStrategy: skip
87
+ mergeStrategy: overwrite_with_backup
88
88
  variables:
89
89
  - project.name
90
90
  - project.root
@@ -107,7 +107,7 @@ items:
107
107
  requires:
108
108
  - docs-project
109
109
  - docs-memory
110
- mergeStrategy: skip
110
+ mergeStrategy: overwrite_with_backup
111
111
  variables:
112
112
  - project.name
113
113
  - project.stack
@@ -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.md
149
+ targetPath: docs/AI_HANDOFF.md
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
@@ -1302,6 +1287,15 @@ items:
1302
1287
  packs:
1303
1288
  - core
1304
1289
 
1290
+ - id: multi-antigravity-agents-link
1291
+ type: link
1292
+ sourceTemplate: .claude/agents
1293
+ targetPath: .antigravity/agents
1294
+ requires: []
1295
+ enabledByDefault: true
1296
+ packs:
1297
+ - core
1298
+
1305
1299
  - id: multi-antigravity-rules
1306
1300
  type: config
1307
1301
  sourceTemplate: adapter-presets/antigravity/rules.md
@@ -1349,6 +1343,15 @@ items:
1349
1343
  packs:
1350
1344
  - core
1351
1345
 
1346
+ - id: multi-codex-agents-link
1347
+ type: link
1348
+ sourceTemplate: .claude/agents
1349
+ targetPath: .codex/agents
1350
+ requires: []
1351
+ enabledByDefault: true
1352
+ packs:
1353
+ - core
1354
+
1352
1355
  - id: multi-codex-readme
1353
1356
  type: config
1354
1357
  sourceTemplate: .codex/README.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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",
@@ -7,12 +7,14 @@ export const OPTIONAL_ADAPTERS = [
7
7
  'multi-antigravity-ukit-link',
8
8
  'multi-antigravity-rules',
9
9
  'multi-antigravity-readme',
10
+ 'multi-antigravity-agents-link',
10
11
  ],
11
12
  managedPaths: [
12
13
  '.antigravity/skills',
13
14
  '.antigravity/ukit',
14
15
  '.antigravity/rules/rules.md',
15
16
  '.antigravity/README.md',
17
+ '.antigravity/agents',
16
18
  ],
17
19
  },
18
20
  {
@@ -24,6 +26,7 @@ export const OPTIONAL_ADAPTERS = [
24
26
  'multi-codex-readme',
25
27
  'multi-codex-settings-json',
26
28
  'multi-codex-settings-local',
29
+ 'multi-codex-agents-link',
27
30
  ],
28
31
  managedPaths: [
29
32
  '.codex/skills',
@@ -31,6 +34,7 @@ export const OPTIONAL_ADAPTERS = [
31
34
  '.codex/README.md',
32
35
  '.codex/settings.json',
33
36
  '.codex/settings.local.json',
37
+ '.codex/agents',
34
38
  ],
35
39
  },
36
40
  {
@@ -62,8 +62,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
62
62
  sessionMemoryDirExists: await pathExists(runtimePaths.sessionsDir),
63
63
  docsProjectExists: await pathExists(path.join(projectRoot, 'docs', 'PROJECT.md')),
64
64
  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')),
65
+ docsAiHandoffExists: await pathExists(path.join(projectRoot, 'docs', 'AI_HANDOFF.md')),
67
66
  docsWorklogExists: await pathExists(path.join(projectRoot, 'docs', 'WORKLOG.md')),
68
67
  allProvidersConfigured: providers.allSupported,
69
68
  ...(codexAdapterTracked
@@ -105,8 +104,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
105
104
  console.log(`[UKit] ${ok(checks.sessionMemoryDirExists)} .ukit/storage/memory/sessions/`);
106
105
  console.log(`[UKit] ${ok(checks.docsProjectExists)} docs/PROJECT.md`);
107
106
  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`);
107
+ console.log(`[UKit] ${ok(checks.docsAiHandoffExists)} docs/AI_HANDOFF.md`);
110
108
  console.log(`[UKit] ${ok(checks.docsWorklogExists)} docs/WORKLOG.md`);
111
109
  if (codexAdapterTracked) {
112
110
  console.log(`[UKit] ${ok(checks.codexReadmeExists)} .codex/README.md`);
@@ -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.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.md, 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.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
51
51
  }
@@ -2,6 +2,16 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { writeFileAtomic, copyFileSafe, createDirectoryLink, removeLinkOnly } from './fileOps.js';
4
4
 
5
+ const TCC_PROTECTED_PREFIXES = ['.codex/', '.antigravity/'];
6
+
7
+ function isTccProtectedPath(filePath) {
8
+ const normalized = filePath.replace(/\\/g, '/');
9
+ return TCC_PROTECTED_PREFIXES.some((prefix) => {
10
+ const idx = normalized.indexOf(`/${prefix}`);
11
+ return idx !== -1 || normalized.startsWith(prefix);
12
+ });
13
+ }
14
+
5
15
  export async function applyDiffResults(diffResults, { backupRoot, projectRoot } = {}) {
6
16
  const writes = [];
7
17
  let skippedUpdates = 0;
@@ -12,14 +22,9 @@ export async function applyDiffResults(diffResults, { backupRoot, projectRoot }
12
22
  }
13
23
 
14
24
  if (entry.type === 'link') {
15
- // Remove existing symlink before recreating.
16
- // Use removeLinkOnly — NOT removeLinkOrDir — to protect real directories that may
17
- // contain user content. If the path is not a symlink, skip and warn instead of
18
- // recursively deleting it.
19
25
  if (entry.action === 'update') {
20
26
  const removed = await removeLinkOnly(entry.targetPath);
21
27
  if (!removed) {
22
- // Determine what actually exists at this path for a precise warning message.
23
28
  let existingKind = 'unknown path';
24
29
  try {
25
30
  const stat = await fs.lstat(entry.targetPath);
@@ -34,7 +39,18 @@ export async function applyDiffResults(diffResults, { backupRoot, projectRoot }
34
39
  continue;
35
40
  }
36
41
  }
37
- await createDirectoryLink(entry.linkTarget, entry.targetPath);
42
+ try {
43
+ await createDirectoryLink(entry.linkTarget, entry.targetPath);
44
+ } catch (linkError) {
45
+ if (linkError.code === 'EPERM' || linkError.code === 'EACCES') {
46
+ console.warn(
47
+ `[UKit] Warning: skipping link creation for ${entry.targetPath} — permission denied. Run 'ukit install' from within the target tool to create it.`,
48
+ );
49
+ skippedUpdates += 1;
50
+ continue;
51
+ }
52
+ throw linkError;
53
+ }
38
54
  writes.push({
39
55
  id: entry.id,
40
56
  targetPath: entry.targetPath,
@@ -54,7 +70,18 @@ export async function applyDiffResults(diffResults, { backupRoot, projectRoot }
54
70
  const relPath = path.relative(projectRoot, entry.targetPath);
55
71
  const timestamp = Date.now();
56
72
  const backupPath = path.join(backupRoot, `${relPath}.${timestamp}.bak`);
57
- await copyFileSafe(entry.targetPath, backupPath);
73
+ try {
74
+ await copyFileSafe(entry.targetPath, backupPath);
75
+ } catch (backupError) {
76
+ if (backupError.code === 'EPERM' || backupError.code === 'EACCES') {
77
+ console.warn(
78
+ `[UKit] Warning: skipping backup for ${entry.targetPath} — permission denied.`,
79
+ );
80
+ skippedUpdates += 1;
81
+ continue;
82
+ }
83
+ throw backupError;
84
+ }
58
85
  }
59
86
 
60
87
  const binaryEntry = Buffer.isBuffer(entry.renderedContent);
@@ -73,7 +100,21 @@ export async function applyDiffResults(diffResults, { backupRoot, projectRoot }
73
100
  )
74
101
  : entry.renderedContent;
75
102
 
76
- await writeFileAtomic(entry.targetPath, nextContent);
103
+ try {
104
+ await writeFileAtomic(entry.targetPath, nextContent);
105
+ } catch (writeError) {
106
+ if (
107
+ (writeError.code === 'EPERM' || writeError.code === 'EACCES') &&
108
+ isTccProtectedPath(entry.targetPath)
109
+ ) {
110
+ console.warn(
111
+ `[UKit] Warning: skipping write for ${entry.targetPath} — permission denied (macOS TCC). Run 'ukit install' from within the owning tool to update it.`,
112
+ );
113
+ skippedUpdates += 1;
114
+ continue;
115
+ }
116
+ throw writeError;
117
+ }
77
118
  if (typeof entry.mode === 'number') {
78
119
  await fs.chmod(entry.targetPath, entry.mode);
79
120
  }
@@ -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.0',
52
+ version: '1.5.2',
53
53
  agent: 'claude-code',
54
54
  autonomy: {
55
55
  level: 'balanced',
@@ -243,8 +243,10 @@ export function buildRouteSummary({
243
243
  ),
244
244
  );
245
245
  const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
246
+ const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF.md' : null;
246
247
  const summaryLine = [
247
248
  routingContext.taskType ? `task=${routingContext.taskType}` : null,
249
+ handoffFile ? `handoff=${handoffFile}` : null,
248
250
  formatCompactSegment('targets', primaryTargets),
249
251
  formatCompactSegment('tests', relatedTests),
250
252
  formatCompactSegment('styles', styleFiles),
@@ -267,6 +269,7 @@ export function buildRouteSummary({
267
269
  completionState,
268
270
  continuationState,
269
271
  intentMode: routingContext.intentMode ?? null,
272
+ handoffFile,
270
273
  delegateHint: delegationRecommendation?.hint ?? null,
271
274
  nextActionType: nextAction?.type ?? null,
272
275
  nextActionCommand,
@@ -783,11 +786,11 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
783
786
  }
784
787
 
785
788
  function shouldKeepRouteEntryForIntent(entry, intentMode) {
786
- if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific'].includes(intentMode)) {
789
+ if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific', 'handoff'].includes(intentMode)) {
787
790
  return false;
788
791
  }
789
792
 
790
- if (entry.id === 'update-status' && intentMode === 'docs-specific') {
793
+ if (entry.id === 'update-status' && ['docs-specific', 'handoff'].includes(intentMode)) {
791
794
  return false;
792
795
  }
793
796
 
@@ -896,6 +899,10 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
896
899
  const openEndedStatus = hasOpenEndedStatusSignal(lower, raw) || taskQueueNext;
897
900
  const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext });
898
901
 
902
+ if (hasHandoffSignal(lower, raw)) {
903
+ return 'handoff';
904
+ }
905
+
899
906
  if (docsSpecific) {
900
907
  return 'docs-specific';
901
908
  }
@@ -927,6 +934,20 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
927
934
  return null;
928
935
  }
929
936
 
937
+ function hasHandoffSignal(lower, raw) {
938
+ return /\bukit:handoff\b/.test(raw)
939
+ || /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
940
+ || /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
941
+ || /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
942
+ || /\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)
943
+ || /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
944
+ }
945
+
946
+ function hasHandoffClearSignal(lower, raw) {
947
+ return /\b(clear handoff|handoff clear|dọn handoff|xóa handoff|reset handoff|compact handoff)\b/.test(lower)
948
+ || (/\bukit:handoff\b/.test(raw) && /\b(clear|dọn|xóa|reset|compact)\b/.test(lower));
949
+ }
950
+
930
951
  function hasStatusUpdateSignal(lower, raw) {
931
952
  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
953
  || /\b(status\.md|project status).{0,64}\b(update|refresh|write|sync|record|capture|summarize|summarise)\b/.test(lower)
@@ -3,11 +3,13 @@ export const OPTIONAL_ADAPTER_ITEM_IDS = new Set([
3
3
  'multi-antigravity-ukit-link',
4
4
  'multi-antigravity-rules',
5
5
  'multi-antigravity-readme',
6
+ 'multi-antigravity-agents-link',
6
7
  'multi-codex-skills-link',
7
8
  'multi-codex-ukit-link',
8
9
  'multi-codex-readme',
9
10
  'multi-codex-settings-json',
10
11
  'multi-codex-settings-local',
12
+ 'multi-codex-agents-link',
11
13
  'multi-opencode-config',
12
14
  ]);
13
15
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ukit-small-task-maintainer
3
3
  description: "Internal UKit maintenance subagent for low-risk, reversible UKit decisions. Use proactively when UKit needs to decide or perform safe cleanup such as pruning docs/TASKS.md, classifying queued work, choosing whether compact/summarization is appropriate, summarizing local docs/status, or maintaining small UKit runtime queues. Do not use for product implementation, security, release/publish, data-loss, architecture, or risky/shared code changes."
4
- model: inherit
4
+ model: unic-lite
5
5
  color: cyan
6
6
  tools: ["Read", "Grep", "Glob", "Edit", "Write"]
7
7
  ---
@@ -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.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,10 @@ 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
+ }
23
+
20
24
  async function listManifestPaths(backupsRoot) {
21
25
  const dates = await fs.readdir(backupsRoot, { withFileTypes: true }).catch(() => []);
22
26
  return dates
@@ -152,7 +156,8 @@ export async function verifyPostEdit({ projectRoot = process.cwd(), payload = {}
152
156
 
153
157
  const overChangedLines = delta.changedLines > Number(config.deltaMaxChangedLines || 120);
154
158
  const overHunks = delta.hunkCount > Number(config.deltaMaxHunks || 3);
155
- const status = risk.strict && (overChangedLines || overHunks) ? 'blocked' : 'ok';
159
+ const handoffAdvisory = isHandoffAuthoringFile(resolved.relative) && risk.labels.every((label) => ['large-file', 'multilingual-text'].includes(label));
160
+ const status = risk.strict && (overChangedLines || overHunks) ? (handoffAdvisory ? 'advisory' : 'blocked') : 'ok';
156
161
  const postEntry = {
157
162
  event: 'post-edit',
158
163
  file: resolved.relative,
@@ -169,8 +174,8 @@ export async function verifyPostEdit({ projectRoot = process.cwd(), payload = {}
169
174
 
170
175
  return {
171
176
  ...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}.`
177
+ message: status === 'blocked' || status === 'advisory'
178
+ ? `${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
179
  : `OK: '${resolved.relative}' delta changedLines=${delta.changedLines}, hunks=${delta.hunkCount}.`,
175
180
  };
176
181
  }
@@ -193,6 +198,9 @@ async function main() {
193
198
  process.stdout.write(`${JSON.stringify(result)}\n`);
194
199
  } else if (result.status === 'ok') {
195
200
  process.stdout.write(`[ukit-safe-patch] ${result.message}\n`);
201
+ } else if (result.status === 'advisory') {
202
+ process.stderr.write(`[ukit-safe-patch] ${result.message}\n`);
203
+ process.stderr.write('[ukit-safe-patch] handoff authoring advisory — change is already written; continue updating docs/AI_HANDOFF.md.\n');
196
204
  }
197
205
  if (result.status === 'blocked') {
198
206
  const advisory = isSafePatchAdvisoryOnly(runtimeConfig);
@@ -723,11 +723,11 @@ async function selectActiveSkills({ rootDir, promptText, commandText, targetFile
723
723
  }
724
724
 
725
725
  function shouldKeepRouteEntryForIntent(entry, intentMode) {
726
- if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific'].includes(intentMode)) {
726
+ if (entry.id === 'next-step' && ['scoped-advice', 'docs-specific', 'handoff'].includes(intentMode)) {
727
727
  return false;
728
728
  }
729
729
 
730
- if (entry.id === 'update-status' && intentMode === 'docs-specific') {
730
+ if (entry.id === 'update-status' && ['docs-specific', 'handoff'].includes(intentMode)) {
731
731
  return false;
732
732
  }
733
733
 
@@ -835,6 +835,10 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
835
835
  return 'docs-specific';
836
836
  }
837
837
 
838
+ if (hasHandoffSignal(lower, raw)) {
839
+ return 'handoff';
840
+ }
841
+
838
842
  if (statusUpdate) {
839
843
  return 'status-update';
840
844
  }
@@ -870,6 +874,20 @@ function hasStatusUpdateSignal(lower, raw) {
870
874
  || /\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
875
  }
872
876
 
877
+ function hasHandoffSignal(lower, raw) {
878
+ return /\bukit:handoff\b/.test(raw)
879
+ || /\b(ai handoff|handoff phase|handoff mode|clear handoff|update handoff|start handoff|handoff clear)\b/.test(lower)
880
+ || /\b(brainstorm|idea dump|ideas?).{0,80}\b(handoff|tasks?|taskify|split|breakdown)\b/.test(lower)
881
+ || /\b(handoff|tasks?|taskify|split|breakdown).{0,80}\b(brainstorm|idea dump|ideas?)\b/.test(lower)
882
+ || /\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)
883
+ || /\b(bàn giao|ban giao).{0,80}\b(ai|task|công việc|cong viec)\b/.test(raw);
884
+ }
885
+
886
+ function hasHandoffClearSignal(lower, raw) {
887
+ return /\b(clear handoff|handoff clear|dọn handoff|xóa handoff|reset handoff|compact handoff)\b/.test(lower)
888
+ || (/\bukit:handoff\b/.test(raw) && /\b(clear|dọn|xóa|reset|compact)\b/.test(lower));
889
+ }
890
+
873
891
  function hasOpenEndedStatusSignal(lower, raw) {
874
892
  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
893
  || /\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)