aiwcli 0.12.6 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +205 -205
  10. package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -12
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -12
  12. package/dist/templates/_shared/.claude/settings.json +65 -65
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -421
  16. package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
  17. package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
  18. package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
  21. package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  42. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
  43. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
  44. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
  45. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  46. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  47. package/dist/templates/_shared/lib-ts/package.json +20 -20
  48. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  49. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  50. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  51. package/dist/templates/_shared/lib-ts/types.ts +186 -186
  52. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  53. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  54. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
  55. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
  56. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
  57. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  58. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  61. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  62. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  63. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  64. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  65. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  66. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  68. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  69. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  71. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  72. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  73. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  74. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  75. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  80. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  82. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  83. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  84. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  85. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  86. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  87. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  88. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  89. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  90. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  91. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  94. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  95. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -66
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -196
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  104. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  116. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  117. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  118. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  119. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  120. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  121. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  122. package/oclif.manifest.json +1 -1
  123. package/package.json +108 -108
  124. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
@@ -1,469 +1,469 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Save a handoff document with folder-based sharding.
4
- *
5
- * Usage:
6
- * bun .aiwcli/_shared/handoff-system/scripts/save_handoff.ts <<'EOF'
7
- * # Your handoff markdown content here (with <!-- SECTION: name --> markers)
8
- * EOF
9
- *
10
- * Or with a file:
11
- * bun .aiwcli/_shared/handoff-system/scripts/save_handoff.ts < handoff.md
12
- *
13
- * This script:
14
- * 1. Auto-resolves the active context ID
15
- * 2. Parses sections from incoming markdown using <!-- SECTION: name --> markers
16
- * 3. Creates a timestamped folder at _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
17
- * 4. Writes sharded files:
18
- * - index.md (main entry point with navigation)
19
- * - completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
20
- * - plan.md (copy of original plan if it exists)
21
- * 5. Sets handoff_path and handoff_consumed=false in state.json
22
- */
23
- import * as fs from "node:fs";
24
- import * as path from "node:path";
25
-
26
- import { getContext, saveState, getContextBySessionId, getAllContexts } from "../../lib-ts/context/context-store.js";
27
- import { getHandoffFolderPath, getProjectRoot } from "../../lib-ts/base/constants.js";
28
- import { atomicWrite } from "../../lib-ts/base/atomic-write.js";
29
- import { logInfo, logWarn, logError } from "../../lib-ts/base/logger.js";
30
- import { getGitStatusShort } from "../../lib-ts/base/git-state.js";
31
- import { eprint } from "../../lib-ts/base/utils.js";
32
-
33
- // ---------------------------------------------------------------------------
34
- // Parsing helpers
35
- // ---------------------------------------------------------------------------
36
-
37
- function parseFrontmatter(content: string): [Record<string, string>, string] {
38
- const frontmatter: Record<string, string> = {};
39
- let remaining = content;
40
-
41
- if (content.startsWith("---")) {
42
- const parts = content.split("---", 3);
43
- if (parts.length >= 3) {
44
- for (const line of parts[1]!.trim().split(/\r?\n/)) {
45
- const colonIdx = line.indexOf(":");
46
- if (colonIdx !== -1) {
47
- const key = line.slice(0, colonIdx).trim();
48
- const value = line.slice(colonIdx + 1).trim();
49
- frontmatter[key] = value;
50
- }
51
- }
52
- remaining = parts[2]!.trim();
53
- }
54
- }
55
-
56
- return [frontmatter, remaining];
57
- }
58
-
59
- function parseHandoffSections(content: string): Record<string, string> {
60
- const sections: Record<string, string> = {};
61
- let currentSection: string | null = null;
62
- const currentContent: string[] = [];
63
-
64
- for (const line of content.split(/\r?\n/)) {
65
- const marker = line.trim().match(/<!-- SECTION:\s*(\S+)\s*-->/);
66
- if (marker) {
67
- if (currentSection) {
68
- sections[currentSection] = currentContent.join("\n").trim();
69
- }
70
- currentSection = marker[1]!;
71
- currentContent.length = 0;
72
- } else if (currentSection) {
73
- currentContent.push(line);
74
- }
75
- }
76
-
77
- if (currentSection) {
78
- sections[currentSection] = currentContent.join("\n").trim();
79
- }
80
-
81
- return sections;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Plan helper
86
- // ---------------------------------------------------------------------------
87
-
88
- function getPlanPathFromContext(contextId: string, projectRoot: string): string | null {
89
- const context = getContext(contextId, projectRoot);
90
- if (!context?.plan_path) return null;
91
- try {
92
- if (fs.existsSync(context.plan_path)) return context.plan_path;
93
- } catch { /* ignore */ }
94
- return null;
95
- }
96
-
97
- // ---------------------------------------------------------------------------
98
- // File generation
99
- // ---------------------------------------------------------------------------
100
-
101
- function generateIndex(
102
- frontmatter: Record<string, string>,
103
- sections: Record<string, string>,
104
- gitStatus: string,
105
- hasPlan: boolean,
106
- ): string {
107
- const now = new Date();
108
- const isoStr = now.toISOString();
109
- const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
110
-
111
- const lines: string[] = [
112
- "---",
113
- "type: handoff",
114
- `context_id: ${frontmatter["context_id"] ?? "unknown"}`,
115
- `created_at: ${isoStr}`,
116
- `session_id: ${frontmatter["session_id"] ?? "unknown"}`,
117
- `project: ${frontmatter["project"] ?? "unknown"}`,
118
- `plan_path: ${frontmatter["plan_document"] ?? "none"}`,
119
- "---",
120
- "",
121
- `# Session Handoff - ${dateStr}`,
122
- "",
123
- ];
124
-
125
- // Summary
126
- const summary = (sections["summary"] ?? "").trim();
127
- if (summary) {
128
- const summaryText = summary
129
- .split(/\r?\n/)
130
- .filter(l => !l.trim().startsWith("##"))
131
- .join("\n")
132
- .trim();
133
- lines.push("## Summary", summaryText, "");
134
- }
135
-
136
- // Navigation
137
- lines.push(
138
- "## Quick Navigation",
139
- "",
140
- "| Document | Purpose | Priority |",
141
- "|----------|---------|----------|",
142
- "| [Dead Ends](./dead-ends.md) | Failed approaches - DO NOT RETRY | Read First |",
143
- "| [Pending](./pending.md) | Next steps and blockers | Action Items |",
144
- "| [Completed Work](./completed-work.md) | Tasks finished this session | Reference |",
145
- "| [Decisions](./decisions.md) | Technical choices and rationale | Reference |",
146
- );
147
-
148
- if (hasPlan) {
149
- lines.push("| [Plan](./plan.md) | Original plan being implemented | Reference |");
150
- }
151
-
152
- lines.push(
153
- "| [Context](./context.md) | External requirements and notes | Reference |",
154
- "",
155
- "## Continuation Instructions",
156
- "",
157
- "To continue this work in a new session:",
158
- "1. This index document provides the overview",
159
- "2. **Read [Dead Ends](./dead-ends.md) first** to avoid repeating failed approaches",
160
- "3. Check [Pending](./pending.md) for immediate next steps",
161
- "4. Reference other documents as needed",
162
- "",
163
- "## Git Status at Handoff",
164
- "```",
165
- gitStatus,
166
- "```",
167
- "",
168
- );
169
-
170
- return lines.join("\n");
171
- }
172
-
173
- function writeSectionFile(folder: string, filename: string, title: string, content: string): boolean {
174
- const text = `# ${title}\n\n${content || "(No content for this section)"}\n`;
175
- const filePath = path.join(folder, filename);
176
- const [success, error] = atomicWrite(filePath, text);
177
- if (!success) {
178
- logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
179
- return false;
180
- }
181
- return true;
182
- }
183
-
184
- // ---------------------------------------------------------------------------
185
- // Main
186
- // ---------------------------------------------------------------------------
187
-
188
- function main(): void {
189
- // Project root via shared utility (checks CLAUDE_PROJECT_DIR, falls back to cwd)
190
- const projectRoot = getProjectRoot(process.cwd());
191
-
192
- // Read content from stdin FIRST (needed to extract session_id from frontmatter)
193
- let content: string;
194
- try {
195
- content = fs.readFileSync(0, "utf-8");
196
- } catch {
197
- logError("save_handoff", "Failed to read from stdin");
198
- process.exit(1);
199
- return; // unreachable but makes TS happy
200
- }
201
-
202
- if (!content.trim()) {
203
- logError("save_handoff", "No content provided via stdin");
204
- process.exit(1);
205
- }
206
-
207
- // Parse frontmatter to extract session_id and context_id
208
- const [frontmatter, body] = parseFrontmatter(content);
209
- const frontmatterSessionId = frontmatter["session_id"] || null;
210
- const frontmatterContextId = frontmatter["context_id"] || null;
211
-
212
- // Parse arguments
213
- let explicitContextId: string | null = null;
214
- let explicitSessionId: string | null = null;
215
- const args = process.argv.slice(2);
216
- for (let i = 0; i < args.length; i++) {
217
- if (args[i] === "--context-id" && i + 1 < args.length) {
218
- explicitContextId = args[i + 1];
219
- } else if (args[i] === "--session-id" && i + 1 < args.length) {
220
- explicitSessionId = args[i + 1];
221
- }
222
- }
223
-
224
- // Six-tier context resolution:
225
- // 1a. Explicit --context-id argument
226
- // 1b. Explicit --session-id argument
227
- // 2a. session_id from frontmatter (piped through handoff content)
228
- // 2b. context_id from frontmatter (piped through handoff content)
229
- // 3. CLAUDE_SESSION_ID environment variable
230
- // 4. Most recent active context (fallback)
231
- let context: ReturnType<typeof getContext> = null;
232
- let contextId: string;
233
-
234
- if (explicitContextId) {
235
- // Tier 1a: Explicit context ID argument
236
- context = getContext(explicitContextId, projectRoot);
237
- if (!context) {
238
- eprint(`Context not found: ${explicitContextId}`);
239
- process.exit(1);
240
- }
241
- contextId = context.id;
242
- logInfo("save_handoff", `Resolved context via --context-id argument: ${contextId}`);
243
- } else if (explicitSessionId) {
244
- // Tier 1b: Explicit session ID argument
245
- context = getContextBySessionId(explicitSessionId, projectRoot);
246
- if (!context) {
247
- eprint(`No context found for session: ${explicitSessionId} (from --session-id argument)`);
248
- process.exit(1);
249
- }
250
- contextId = context.id;
251
- logInfo("save_handoff", `Resolved context via --session-id argument: ${explicitSessionId} -> ${contextId}`);
252
- } else if (frontmatterSessionId) {
253
- // Tier 2a: Frontmatter session_id (piped data)
254
- context = getContextBySessionId(frontmatterSessionId, projectRoot);
255
- if (!context) {
256
- eprint(`No context found for session: ${frontmatterSessionId} (from frontmatter)`);
257
- process.exit(1);
258
- }
259
- contextId = context.id;
260
- logInfo("save_handoff", `Resolved context via frontmatter session_id: ${frontmatterSessionId} -> ${contextId}`);
261
- } else if (frontmatterContextId) {
262
- // Tier 2b: Frontmatter context_id (piped data)
263
- context = getContext(frontmatterContextId, projectRoot);
264
- if (!context) {
265
- eprint(`No context found for context_id: ${frontmatterContextId} (from frontmatter)`);
266
- process.exit(1);
267
- }
268
- contextId = context.id;
269
- logInfo("save_handoff", `Resolved context via frontmatter context_id: ${frontmatterContextId}`);
270
- } else {
271
- const envSessionId = process.env.CLAUDE_SESSION_ID;
272
- if (envSessionId) {
273
- // Tier 2b: Environment variable
274
- context = getContextBySessionId(envSessionId, projectRoot);
275
- if (!context) {
276
- eprint(`No context found for session: ${envSessionId}`);
277
- process.exit(1);
278
- }
279
- contextId = context.id;
280
- logInfo("save_handoff", `Resolved context via CLAUDE_SESSION_ID env var: ${envSessionId} -> ${contextId}`);
281
- } else {
282
- // Tier 3: Fallback to most recent active context
283
- const activeContexts = getAllContexts("active", projectRoot);
284
- if (activeContexts.length === 0) {
285
- eprint("No active context found. Use --context-id or --session-id to specify explicitly.");
286
- eprint("Example: bun save_handoff.ts --session-id abc-123-def < handoff.md");
287
- eprint(" or: bun save_handoff.ts --context-id 260215-1234-my-context < handoff.md");
288
- process.exit(1);
289
- }
290
- context = activeContexts[0]!; // getAllContexts sorts by last_active descending
291
- contextId = context.id;
292
- logInfo("save_handoff", `Resolved context via fallback (most recent active): ${contextId}`);
293
- }
294
- }
295
-
296
- // Parse sections from body
297
- const sections = parseHandoffSections(body);
298
-
299
- logInfo("save_handoff", `Parsed ${Object.keys(sections).length} sections: ${Object.keys(sections).join(", ")}`);
300
-
301
- // Create handoff folder
302
- const handoffFolder = getHandoffFolderPath(contextId, projectRoot);
303
- fs.mkdirSync(handoffFolder, { recursive: true });
304
- logInfo("save_handoff", `Created folder: ${handoffFolder}`);
305
-
306
- // Git status
307
- const gitStatus = getGitStatusShort(projectRoot);
308
-
309
- // Check for plan
310
- const planPath = getPlanPathFromContext(contextId, projectRoot);
311
- const hasPlan = planPath !== null;
312
-
313
- // Write updated plan if Claude provided it
314
- if (sections["plan"]) {
315
- try {
316
- const updatedPlan = sections["plan"];
317
-
318
- // Write to original plan path if it exists
319
- if (planPath) {
320
- const [success, error] = atomicWrite(planPath, updatedPlan);
321
- if (success) {
322
- logInfo("save_handoff", `Plan updated at ${planPath}`);
323
- } else {
324
- logWarn("save_handoff", `Failed to update original plan: ${error}`);
325
- }
326
- }
327
-
328
- // Write to handoff folder
329
- const handoffPlanPath = path.join(handoffFolder, "plan.md");
330
- const [success, error] = atomicWrite(handoffPlanPath, updatedPlan);
331
- if (success) {
332
- logInfo("save_handoff", `Plan copied to handoff folder`);
333
- } else {
334
- logWarn("save_handoff", `Failed to copy plan to handoff: ${error}`);
335
- }
336
- } catch (e) {
337
- logWarn("save_handoff", `Plan update failed (non-critical): ${e}`);
338
- }
339
- } else if (planPath) {
340
- // Fallback: copy unchanged plan if Claude didn't provide an update
341
- try {
342
- const planContent = fs.readFileSync(planPath, "utf-8");
343
- const [success, error] = atomicWrite(path.join(handoffFolder, "plan.md"), planContent);
344
- if (success) {
345
- logInfo("save_handoff", `Copied unchanged plan from ${planPath}`);
346
- } else {
347
- logWarn("save_handoff", `Failed to copy plan: ${error}`);
348
- }
349
- } catch (e) {
350
- logWarn("save_handoff", `Failed to read plan: ${e}`);
351
- }
352
- }
353
-
354
- // Write index.md
355
- const indexContent = generateIndex(frontmatter, sections, gitStatus, hasPlan);
356
- const indexPath = path.join(handoffFolder, "index.md");
357
- {
358
- const [success, error] = atomicWrite(indexPath, indexContent);
359
- if (!success) {
360
- logError("save_handoff", `Failed to write index.md: ${error}`);
361
- process.exit(1);
362
- }
363
- }
364
-
365
- // Write section files
366
- const sectionMapping: Record<string, [string, string | null]> = {
367
- completed: ["completed-work.md", "Work Completed"],
368
- "dead-ends": ["dead-ends.md", "Dead Ends - Do Not Retry"],
369
- decisions: ["decisions.md", "Key Decisions"],
370
- pending: ["pending.md", "Pending Issues"],
371
- "next-steps": ["pending.md", null], // Append to pending.md
372
- files: ["completed-work.md", null], // Append to completed-work.md
373
- context: ["context.md", "Context for Future Sessions"],
374
- };
375
-
376
- // Track accumulated content per file
377
- const fileContents: Record<string, string[]> = {};
378
-
379
- for (const [sectionName, [filename, title]] of Object.entries(sectionMapping)) {
380
- const sectionContent = sections[sectionName];
381
- if (!sectionContent) continue;
382
-
383
- if (title === null) {
384
- // Append mode
385
- if (!fileContents[filename]) fileContents[filename] = [];
386
- fileContents[filename]!.push(sectionContent);
387
- } else {
388
- // Write mode with title
389
- if (!fileContents[filename]) {
390
- fileContents[filename] = [`# ${title}`, "", sectionContent];
391
- } else {
392
- fileContents[filename] = [`# ${title}`, "", ...fileContents[filename]!, "", sectionContent];
393
- }
394
- }
395
- }
396
-
397
- // Write all accumulated content
398
- for (const [filename, parts] of Object.entries(fileContents)) {
399
- const filePath = path.join(handoffFolder, filename);
400
- const [success, error] = atomicWrite(filePath, parts.join("\n") + "\n");
401
- if (!success) {
402
- logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
403
- }
404
- }
405
-
406
- // Ensure all expected files exist (even if empty)
407
- const expectedFiles: Record<string, string> = {
408
- "completed-work.md": "Work Completed",
409
- "dead-ends.md": "Dead Ends - Do Not Retry",
410
- "decisions.md": "Key Decisions",
411
- "pending.md": "Pending Issues & Next Steps",
412
- "context.md": "Context for Future Sessions",
413
- };
414
-
415
- for (const [filename, title] of Object.entries(expectedFiles)) {
416
- const filePath = path.join(handoffFolder, filename);
417
- if (!fs.existsSync(filePath)) {
418
- writeSectionFile(handoffFolder, filename, title, "");
419
- }
420
- }
421
-
422
- // Set handoff_path and work_consumed=false in state.json
423
- // Latest artifact wins: clear plan if it exists
424
- try {
425
- const indexPathStr = path.join(handoffFolder, "index.md");
426
- const state = getContext(contextId, projectRoot);
427
- if (state) {
428
- // Latest artifact wins: clear plan if it exists
429
- if (state.plan_path || state.plan_hash) {
430
- logInfo("save_handoff", "Handoff replaces existing plan (latest wins)");
431
- state.plan_path = null;
432
- state.plan_hash = null;
433
- state.plan_signature = null;
434
- state.plan_id = null;
435
- state.plan_anchors = [];
436
- state.plan_hash_consumed = null;
437
- }
438
-
439
- state.handoff_path = indexPathStr;
440
- state.work_consumed = false; // CHANGED: unified flag
441
- state.next_artifact_type = "handoff";
442
-
443
- const [ok, err] = saveState(contextId, state, projectRoot);
444
- if (ok) {
445
- logInfo("save_handoff", `Set handoff as staged artifact`);
446
- } else {
447
- logWarn("save_handoff", `Failed to save state: ${err}`);
448
- }
449
- } else {
450
- logWarn("save_handoff", `Could not load context state for ${contextId}`);
451
- }
452
- } catch (e) {
453
- logWarn("save_handoff", `Handoff saved but auto-resume won't work: ${e}`);
454
- }
455
-
456
- // Output success message
457
- console.log(`[OK] Created handoff folder: ${handoffFolder}`);
458
- console.log(" - index.md (entry point with navigation)");
459
-
460
- const filesCreated = fs.readdirSync(handoffFolder)
461
- .filter(f => f !== "index.md" && fs.statSync(path.join(handoffFolder, f)).isFile())
462
- .sort();
463
- console.log(` - ${filesCreated.join(", ")}`);
464
-
465
- console.log("");
466
- console.log("Handoff document saved. Use this folder for context in the next session.");
467
- }
468
-
469
- main();
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Save a handoff document with folder-based sharding.
4
+ *
5
+ * Usage:
6
+ * bun .aiwcli/_shared/handoff-system/scripts/save_handoff.ts <<'EOF'
7
+ * # Your handoff markdown content here (with <!-- SECTION: name --> markers)
8
+ * EOF
9
+ *
10
+ * Or with a file:
11
+ * bun .aiwcli/_shared/handoff-system/scripts/save_handoff.ts < handoff.md
12
+ *
13
+ * This script:
14
+ * 1. Auto-resolves the active context ID
15
+ * 2. Parses sections from incoming markdown using <!-- SECTION: name --> markers
16
+ * 3. Creates a timestamped folder at _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
17
+ * 4. Writes sharded files:
18
+ * - index.md (main entry point with navigation)
19
+ * - completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
20
+ * - plan.md (copy of original plan if it exists)
21
+ * 5. Sets handoff_path and handoff_consumed=false in state.json
22
+ */
23
+ import * as fs from "node:fs";
24
+ import * as path from "node:path";
25
+
26
+ import { getContext, saveState, getContextBySessionId, getAllContexts } from "../../lib-ts/context/context-store.js";
27
+ import { getHandoffFolderPath, getProjectRoot } from "../../lib-ts/base/constants.js";
28
+ import { atomicWrite } from "../../lib-ts/base/atomic-write.js";
29
+ import { logInfo, logWarn, logError } from "../../lib-ts/base/logger.js";
30
+ import { getGitStatusShort } from "../../lib-ts/base/git-state.js";
31
+ import { eprint } from "../../lib-ts/base/utils.js";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Parsing helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function parseFrontmatter(content: string): [Record<string, string>, string] {
38
+ const frontmatter: Record<string, string> = {};
39
+ let remaining = content;
40
+
41
+ if (content.startsWith("---")) {
42
+ const parts = content.split("---", 3);
43
+ if (parts.length >= 3) {
44
+ for (const line of parts[1]!.trim().split(/\r?\n/)) {
45
+ const colonIdx = line.indexOf(":");
46
+ if (colonIdx !== -1) {
47
+ const key = line.slice(0, colonIdx).trim();
48
+ const value = line.slice(colonIdx + 1).trim();
49
+ frontmatter[key] = value;
50
+ }
51
+ }
52
+ remaining = parts[2]!.trim();
53
+ }
54
+ }
55
+
56
+ return [frontmatter, remaining];
57
+ }
58
+
59
+ function parseHandoffSections(content: string): Record<string, string> {
60
+ const sections: Record<string, string> = {};
61
+ let currentSection: string | null = null;
62
+ const currentContent: string[] = [];
63
+
64
+ for (const line of content.split(/\r?\n/)) {
65
+ const marker = line.trim().match(/<!-- SECTION:\s*(\S+)\s*-->/);
66
+ if (marker) {
67
+ if (currentSection) {
68
+ sections[currentSection] = currentContent.join("\n").trim();
69
+ }
70
+ currentSection = marker[1]!;
71
+ currentContent.length = 0;
72
+ } else if (currentSection) {
73
+ currentContent.push(line);
74
+ }
75
+ }
76
+
77
+ if (currentSection) {
78
+ sections[currentSection] = currentContent.join("\n").trim();
79
+ }
80
+
81
+ return sections;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Plan helper
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function getPlanPathFromContext(contextId: string, projectRoot: string): string | null {
89
+ const context = getContext(contextId, projectRoot);
90
+ if (!context?.plan_path) return null;
91
+ try {
92
+ if (fs.existsSync(context.plan_path)) return context.plan_path;
93
+ } catch { /* ignore */ }
94
+ return null;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // File generation
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function generateIndex(
102
+ frontmatter: Record<string, string>,
103
+ sections: Record<string, string>,
104
+ gitStatus: string,
105
+ hasPlan: boolean,
106
+ ): string {
107
+ const now = new Date();
108
+ const isoStr = now.toISOString();
109
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
110
+
111
+ const lines: string[] = [
112
+ "---",
113
+ "type: handoff",
114
+ `context_id: ${frontmatter["context_id"] ?? "unknown"}`,
115
+ `created_at: ${isoStr}`,
116
+ `session_id: ${frontmatter["session_id"] ?? "unknown"}`,
117
+ `project: ${frontmatter["project"] ?? "unknown"}`,
118
+ `plan_path: ${frontmatter["plan_document"] ?? "none"}`,
119
+ "---",
120
+ "",
121
+ `# Session Handoff - ${dateStr}`,
122
+ "",
123
+ ];
124
+
125
+ // Summary
126
+ const summary = (sections["summary"] ?? "").trim();
127
+ if (summary) {
128
+ const summaryText = summary
129
+ .split(/\r?\n/)
130
+ .filter(l => !l.trim().startsWith("##"))
131
+ .join("\n")
132
+ .trim();
133
+ lines.push("## Summary", summaryText, "");
134
+ }
135
+
136
+ // Navigation
137
+ lines.push(
138
+ "## Quick Navigation",
139
+ "",
140
+ "| Document | Purpose | Priority |",
141
+ "|----------|---------|----------|",
142
+ "| [Dead Ends](./dead-ends.md) | Failed approaches - DO NOT RETRY | Read First |",
143
+ "| [Pending](./pending.md) | Next steps and blockers | Action Items |",
144
+ "| [Completed Work](./completed-work.md) | Tasks finished this session | Reference |",
145
+ "| [Decisions](./decisions.md) | Technical choices and rationale | Reference |",
146
+ );
147
+
148
+ if (hasPlan) {
149
+ lines.push("| [Plan](./plan.md) | Original plan being implemented | Reference |");
150
+ }
151
+
152
+ lines.push(
153
+ "| [Context](./context.md) | External requirements and notes | Reference |",
154
+ "",
155
+ "## Continuation Instructions",
156
+ "",
157
+ "To continue this work in a new session:",
158
+ "1. This index document provides the overview",
159
+ "2. **Read [Dead Ends](./dead-ends.md) first** to avoid repeating failed approaches",
160
+ "3. Check [Pending](./pending.md) for immediate next steps",
161
+ "4. Reference other documents as needed",
162
+ "",
163
+ "## Git Status at Handoff",
164
+ "```",
165
+ gitStatus,
166
+ "```",
167
+ "",
168
+ );
169
+
170
+ return lines.join("\n");
171
+ }
172
+
173
+ function writeSectionFile(folder: string, filename: string, title: string, content: string): boolean {
174
+ const text = `# ${title}\n\n${content || "(No content for this section)"}\n`;
175
+ const filePath = path.join(folder, filename);
176
+ const [success, error] = atomicWrite(filePath, text);
177
+ if (!success) {
178
+ logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
179
+ return false;
180
+ }
181
+ return true;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Main
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function main(): void {
189
+ // Project root via shared utility (checks CLAUDE_PROJECT_DIR, falls back to cwd)
190
+ const projectRoot = getProjectRoot(process.cwd());
191
+
192
+ // Read content from stdin FIRST (needed to extract session_id from frontmatter)
193
+ let content: string;
194
+ try {
195
+ content = fs.readFileSync(0, "utf-8");
196
+ } catch {
197
+ logError("save_handoff", "Failed to read from stdin");
198
+ process.exit(1);
199
+ return; // unreachable but makes TS happy
200
+ }
201
+
202
+ if (!content.trim()) {
203
+ logError("save_handoff", "No content provided via stdin");
204
+ process.exit(1);
205
+ }
206
+
207
+ // Parse frontmatter to extract session_id and context_id
208
+ const [frontmatter, body] = parseFrontmatter(content);
209
+ const frontmatterSessionId = frontmatter["session_id"] || null;
210
+ const frontmatterContextId = frontmatter["context_id"] || null;
211
+
212
+ // Parse arguments
213
+ let explicitContextId: string | null = null;
214
+ let explicitSessionId: string | null = null;
215
+ const args = process.argv.slice(2);
216
+ for (let i = 0; i < args.length; i++) {
217
+ if (args[i] === "--context-id" && i + 1 < args.length) {
218
+ explicitContextId = args[i + 1];
219
+ } else if (args[i] === "--session-id" && i + 1 < args.length) {
220
+ explicitSessionId = args[i + 1];
221
+ }
222
+ }
223
+
224
+ // Six-tier context resolution:
225
+ // 1a. Explicit --context-id argument
226
+ // 1b. Explicit --session-id argument
227
+ // 2a. session_id from frontmatter (piped through handoff content)
228
+ // 2b. context_id from frontmatter (piped through handoff content)
229
+ // 3. CLAUDE_SESSION_ID environment variable
230
+ // 4. Most recent active context (fallback)
231
+ let context: ReturnType<typeof getContext> = null;
232
+ let contextId: string;
233
+
234
+ if (explicitContextId) {
235
+ // Tier 1a: Explicit context ID argument
236
+ context = getContext(explicitContextId, projectRoot);
237
+ if (!context) {
238
+ eprint(`Context not found: ${explicitContextId}`);
239
+ process.exit(1);
240
+ }
241
+ contextId = context.id;
242
+ logInfo("save_handoff", `Resolved context via --context-id argument: ${contextId}`);
243
+ } else if (explicitSessionId) {
244
+ // Tier 1b: Explicit session ID argument
245
+ context = getContextBySessionId(explicitSessionId, projectRoot);
246
+ if (!context) {
247
+ eprint(`No context found for session: ${explicitSessionId} (from --session-id argument)`);
248
+ process.exit(1);
249
+ }
250
+ contextId = context.id;
251
+ logInfo("save_handoff", `Resolved context via --session-id argument: ${explicitSessionId} -> ${contextId}`);
252
+ } else if (frontmatterSessionId) {
253
+ // Tier 2a: Frontmatter session_id (piped data)
254
+ context = getContextBySessionId(frontmatterSessionId, projectRoot);
255
+ if (!context) {
256
+ eprint(`No context found for session: ${frontmatterSessionId} (from frontmatter)`);
257
+ process.exit(1);
258
+ }
259
+ contextId = context.id;
260
+ logInfo("save_handoff", `Resolved context via frontmatter session_id: ${frontmatterSessionId} -> ${contextId}`);
261
+ } else if (frontmatterContextId) {
262
+ // Tier 2b: Frontmatter context_id (piped data)
263
+ context = getContext(frontmatterContextId, projectRoot);
264
+ if (!context) {
265
+ eprint(`No context found for context_id: ${frontmatterContextId} (from frontmatter)`);
266
+ process.exit(1);
267
+ }
268
+ contextId = context.id;
269
+ logInfo("save_handoff", `Resolved context via frontmatter context_id: ${frontmatterContextId}`);
270
+ } else {
271
+ const envSessionId = process.env.CLAUDE_SESSION_ID;
272
+ if (envSessionId) {
273
+ // Tier 2b: Environment variable
274
+ context = getContextBySessionId(envSessionId, projectRoot);
275
+ if (!context) {
276
+ eprint(`No context found for session: ${envSessionId}`);
277
+ process.exit(1);
278
+ }
279
+ contextId = context.id;
280
+ logInfo("save_handoff", `Resolved context via CLAUDE_SESSION_ID env var: ${envSessionId} -> ${contextId}`);
281
+ } else {
282
+ // Tier 3: Fallback to most recent active context
283
+ const activeContexts = getAllContexts("active", projectRoot);
284
+ if (activeContexts.length === 0) {
285
+ eprint("No active context found. Use --context-id or --session-id to specify explicitly.");
286
+ eprint("Example: bun save_handoff.ts --session-id abc-123-def < handoff.md");
287
+ eprint(" or: bun save_handoff.ts --context-id 260215-1234-my-context < handoff.md");
288
+ process.exit(1);
289
+ }
290
+ context = activeContexts[0]!; // getAllContexts sorts by last_active descending
291
+ contextId = context.id;
292
+ logInfo("save_handoff", `Resolved context via fallback (most recent active): ${contextId}`);
293
+ }
294
+ }
295
+
296
+ // Parse sections from body
297
+ const sections = parseHandoffSections(body);
298
+
299
+ logInfo("save_handoff", `Parsed ${Object.keys(sections).length} sections: ${Object.keys(sections).join(", ")}`);
300
+
301
+ // Create handoff folder
302
+ const handoffFolder = getHandoffFolderPath(contextId, projectRoot);
303
+ fs.mkdirSync(handoffFolder, { recursive: true });
304
+ logInfo("save_handoff", `Created folder: ${handoffFolder}`);
305
+
306
+ // Git status
307
+ const gitStatus = getGitStatusShort(projectRoot);
308
+
309
+ // Check for plan
310
+ const planPath = getPlanPathFromContext(contextId, projectRoot);
311
+ const hasPlan = planPath !== null;
312
+
313
+ // Write updated plan if Claude provided it
314
+ if (sections["plan"]) {
315
+ try {
316
+ const updatedPlan = sections["plan"];
317
+
318
+ // Write to original plan path if it exists
319
+ if (planPath) {
320
+ const [success, error] = atomicWrite(planPath, updatedPlan);
321
+ if (success) {
322
+ logInfo("save_handoff", `Plan updated at ${planPath}`);
323
+ } else {
324
+ logWarn("save_handoff", `Failed to update original plan: ${error}`);
325
+ }
326
+ }
327
+
328
+ // Write to handoff folder
329
+ const handoffPlanPath = path.join(handoffFolder, "plan.md");
330
+ const [success, error] = atomicWrite(handoffPlanPath, updatedPlan);
331
+ if (success) {
332
+ logInfo("save_handoff", `Plan copied to handoff folder`);
333
+ } else {
334
+ logWarn("save_handoff", `Failed to copy plan to handoff: ${error}`);
335
+ }
336
+ } catch (e) {
337
+ logWarn("save_handoff", `Plan update failed (non-critical): ${e}`);
338
+ }
339
+ } else if (planPath) {
340
+ // Fallback: copy unchanged plan if Claude didn't provide an update
341
+ try {
342
+ const planContent = fs.readFileSync(planPath, "utf-8");
343
+ const [success, error] = atomicWrite(path.join(handoffFolder, "plan.md"), planContent);
344
+ if (success) {
345
+ logInfo("save_handoff", `Copied unchanged plan from ${planPath}`);
346
+ } else {
347
+ logWarn("save_handoff", `Failed to copy plan: ${error}`);
348
+ }
349
+ } catch (e) {
350
+ logWarn("save_handoff", `Failed to read plan: ${e}`);
351
+ }
352
+ }
353
+
354
+ // Write index.md
355
+ const indexContent = generateIndex(frontmatter, sections, gitStatus, hasPlan);
356
+ const indexPath = path.join(handoffFolder, "index.md");
357
+ {
358
+ const [success, error] = atomicWrite(indexPath, indexContent);
359
+ if (!success) {
360
+ logError("save_handoff", `Failed to write index.md: ${error}`);
361
+ process.exit(1);
362
+ }
363
+ }
364
+
365
+ // Write section files
366
+ const sectionMapping: Record<string, [string, string | null]> = {
367
+ completed: ["completed-work.md", "Work Completed"],
368
+ "dead-ends": ["dead-ends.md", "Dead Ends - Do Not Retry"],
369
+ decisions: ["decisions.md", "Key Decisions"],
370
+ pending: ["pending.md", "Pending Issues"],
371
+ "next-steps": ["pending.md", null], // Append to pending.md
372
+ files: ["completed-work.md", null], // Append to completed-work.md
373
+ context: ["context.md", "Context for Future Sessions"],
374
+ };
375
+
376
+ // Track accumulated content per file
377
+ const fileContents: Record<string, string[]> = {};
378
+
379
+ for (const [sectionName, [filename, title]] of Object.entries(sectionMapping)) {
380
+ const sectionContent = sections[sectionName];
381
+ if (!sectionContent) continue;
382
+
383
+ if (title === null) {
384
+ // Append mode
385
+ if (!fileContents[filename]) fileContents[filename] = [];
386
+ fileContents[filename]!.push(sectionContent);
387
+ } else {
388
+ // Write mode with title
389
+ if (!fileContents[filename]) {
390
+ fileContents[filename] = [`# ${title}`, "", sectionContent];
391
+ } else {
392
+ fileContents[filename] = [`# ${title}`, "", ...fileContents[filename]!, "", sectionContent];
393
+ }
394
+ }
395
+ }
396
+
397
+ // Write all accumulated content
398
+ for (const [filename, parts] of Object.entries(fileContents)) {
399
+ const filePath = path.join(handoffFolder, filename);
400
+ const [success, error] = atomicWrite(filePath, parts.join("\n") + "\n");
401
+ if (!success) {
402
+ logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
403
+ }
404
+ }
405
+
406
+ // Ensure all expected files exist (even if empty)
407
+ const expectedFiles: Record<string, string> = {
408
+ "completed-work.md": "Work Completed",
409
+ "dead-ends.md": "Dead Ends - Do Not Retry",
410
+ "decisions.md": "Key Decisions",
411
+ "pending.md": "Pending Issues & Next Steps",
412
+ "context.md": "Context for Future Sessions",
413
+ };
414
+
415
+ for (const [filename, title] of Object.entries(expectedFiles)) {
416
+ const filePath = path.join(handoffFolder, filename);
417
+ if (!fs.existsSync(filePath)) {
418
+ writeSectionFile(handoffFolder, filename, title, "");
419
+ }
420
+ }
421
+
422
+ // Set handoff_path and work_consumed=false in state.json
423
+ // Latest artifact wins: clear plan if it exists
424
+ try {
425
+ const indexPathStr = path.join(handoffFolder, "index.md");
426
+ const state = getContext(contextId, projectRoot);
427
+ if (state) {
428
+ // Latest artifact wins: clear plan if it exists
429
+ if (state.plan_path || state.plan_hash) {
430
+ logInfo("save_handoff", "Handoff replaces existing plan (latest wins)");
431
+ state.plan_path = null;
432
+ state.plan_hash = null;
433
+ state.plan_signature = null;
434
+ state.plan_id = null;
435
+ state.plan_anchors = [];
436
+ state.plan_hash_consumed = null;
437
+ }
438
+
439
+ state.handoff_path = indexPathStr;
440
+ state.work_consumed = false; // CHANGED: unified flag
441
+ state.next_artifact_type = "handoff";
442
+
443
+ const [ok, err] = saveState(contextId, state, projectRoot);
444
+ if (ok) {
445
+ logInfo("save_handoff", `Set handoff as staged artifact`);
446
+ } else {
447
+ logWarn("save_handoff", `Failed to save state: ${err}`);
448
+ }
449
+ } else {
450
+ logWarn("save_handoff", `Could not load context state for ${contextId}`);
451
+ }
452
+ } catch (e) {
453
+ logWarn("save_handoff", `Handoff saved but auto-resume won't work: ${e}`);
454
+ }
455
+
456
+ // Output success message
457
+ console.log(`[OK] Created handoff folder: ${handoffFolder}`);
458
+ console.log(" - index.md (entry point with navigation)");
459
+
460
+ const filesCreated = fs.readdirSync(handoffFolder)
461
+ .filter(f => f !== "index.md" && fs.statSync(path.join(handoffFolder, f)).isFile())
462
+ .sort();
463
+ console.log(` - ${filesCreated.join(", ")}`);
464
+
465
+ console.log("");
466
+ console.log("Handoff document saved. Use this folder for context in the next session.");
467
+ }
468
+
469
+ main();