aiwcli 0.12.3 → 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 (125) 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 -64
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
  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 -0
  16. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
  17. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
  18. package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
  21. package/dist/templates/_shared/{workflows → 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 -183
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
  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 -130
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
  42. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  43. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
  44. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
  45. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
  46. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  47. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  48. package/dist/templates/_shared/lib-ts/package.json +20 -20
  49. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  50. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  51. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  52. package/dist/templates/_shared/lib-ts/types.ts +186 -180
  53. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  54. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  55. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
  56. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
  57. package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
  58. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  61. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  62. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  63. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  64. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  65. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  66. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  67. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  68. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  69. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  71. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  72. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  73. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  74. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  75. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  80. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  82. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  83. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  84. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  85. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  86. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  87. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  88. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  89. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  90. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  91. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  94. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  95. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  104. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  116. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  117. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  118. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  119. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  120. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  121. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  122. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  123. package/oclif.manifest.json +1 -1
  124. package/package.json +108 -108
  125. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
@@ -1,358 +1,469 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Save a handoff document with folder-based sharding.
4
- *
5
- * Usage:
6
- * bun .aiwcli/_shared/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/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 } 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
- // Resolve context ID from session (requires CLAUDE_SESSION_ID env var)
193
- const sessionId = process.env.CLAUDE_SESSION_ID;
194
-
195
- if (!sessionId) {
196
- eprint("CLAUDE_SESSION_ID not set. This script must be run from within a Claude Code session.");
197
- process.exit(1);
198
- }
199
-
200
- const context = getContextBySessionId(sessionId, projectRoot);
201
- if (!context) {
202
- eprint(`No context found for session: ${sessionId}`);
203
- process.exit(1);
204
- }
205
-
206
- const contextId = context.id;
207
- logInfo("save_handoff", `Resolved context via session ID: ${sessionId} -> ${contextId}`);
208
-
209
- // Read content from stdin
210
- let content: string;
211
- try {
212
- content = fs.readFileSync(0, "utf-8");
213
- } catch {
214
- logError("save_handoff", "Failed to read from stdin");
215
- process.exit(1);
216
- return; // unreachable but makes TS happy
217
- }
218
-
219
- if (!content.trim()) {
220
- logError("save_handoff", "No content provided via stdin");
221
- process.exit(1);
222
- }
223
-
224
- // Parse frontmatter and sections
225
- const [frontmatter, body] = parseFrontmatter(content);
226
- const sections = parseHandoffSections(body);
227
-
228
- logInfo("save_handoff", `Parsed ${Object.keys(sections).length} sections: ${Object.keys(sections).join(", ")}`);
229
-
230
- // Create handoff folder
231
- const handoffFolder = getHandoffFolderPath(contextId, projectRoot);
232
- fs.mkdirSync(handoffFolder, { recursive: true });
233
- logInfo("save_handoff", `Created folder: ${handoffFolder}`);
234
-
235
- // Git status
236
- const gitStatus = getGitStatusShort(projectRoot);
237
-
238
- // Check for plan
239
- const planPath = getPlanPathFromContext(contextId, projectRoot);
240
- const hasPlan = planPath !== null;
241
-
242
- // Copy plan if exists
243
- if (planPath) {
244
- try {
245
- const planContent = fs.readFileSync(planPath, "utf-8");
246
- const [success, error] = atomicWrite(path.join(handoffFolder, "plan.md"), planContent);
247
- if (success) {
248
- logInfo("save_handoff", `Copied plan from ${planPath}`);
249
- } else {
250
- logWarn("save_handoff", `Failed to copy plan: ${error}`);
251
- }
252
- } catch (e) {
253
- logWarn("save_handoff", `Failed to read plan: ${e}`);
254
- }
255
- }
256
-
257
- // Write index.md
258
- const indexContent = generateIndex(frontmatter, sections, gitStatus, hasPlan);
259
- const indexPath = path.join(handoffFolder, "index.md");
260
- {
261
- const [success, error] = atomicWrite(indexPath, indexContent);
262
- if (!success) {
263
- logError("save_handoff", `Failed to write index.md: ${error}`);
264
- process.exit(1);
265
- }
266
- }
267
-
268
- // Write section files
269
- const sectionMapping: Record<string, [string, string | null]> = {
270
- completed: ["completed-work.md", "Work Completed"],
271
- "dead-ends": ["dead-ends.md", "Dead Ends - Do Not Retry"],
272
- decisions: ["decisions.md", "Key Decisions"],
273
- pending: ["pending.md", "Pending Issues"],
274
- "next-steps": ["pending.md", null], // Append to pending.md
275
- files: ["completed-work.md", null], // Append to completed-work.md
276
- context: ["context.md", "Context for Future Sessions"],
277
- };
278
-
279
- // Track accumulated content per file
280
- const fileContents: Record<string, string[]> = {};
281
-
282
- for (const [sectionName, [filename, title]] of Object.entries(sectionMapping)) {
283
- const sectionContent = sections[sectionName];
284
- if (!sectionContent) continue;
285
-
286
- if (title === null) {
287
- // Append mode
288
- if (!fileContents[filename]) fileContents[filename] = [];
289
- fileContents[filename]!.push(sectionContent);
290
- } else {
291
- // Write mode with title
292
- if (!fileContents[filename]) {
293
- fileContents[filename] = [`# ${title}`, "", sectionContent];
294
- } else {
295
- fileContents[filename] = [`# ${title}`, "", ...fileContents[filename]!, "", sectionContent];
296
- }
297
- }
298
- }
299
-
300
- // Write all accumulated content
301
- for (const [filename, parts] of Object.entries(fileContents)) {
302
- const filePath = path.join(handoffFolder, filename);
303
- const [success, error] = atomicWrite(filePath, parts.join("\n") + "\n");
304
- if (!success) {
305
- logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
306
- }
307
- }
308
-
309
- // Ensure all expected files exist (even if empty)
310
- const expectedFiles: Record<string, string> = {
311
- "completed-work.md": "Work Completed",
312
- "dead-ends.md": "Dead Ends - Do Not Retry",
313
- "decisions.md": "Key Decisions",
314
- "pending.md": "Pending Issues & Next Steps",
315
- "context.md": "Context for Future Sessions",
316
- };
317
-
318
- for (const [filename, title] of Object.entries(expectedFiles)) {
319
- const filePath = path.join(handoffFolder, filename);
320
- if (!fs.existsSync(filePath)) {
321
- writeSectionFile(handoffFolder, filename, title, "");
322
- }
323
- }
324
-
325
- // Set handoff_path and handoff_consumed=false in state.json
326
- try {
327
- const indexPathStr = path.join(handoffFolder, "index.md");
328
- const state = getContext(contextId, projectRoot);
329
- if (state) {
330
- state.handoff_path = indexPathStr;
331
- state.handoff_consumed = false;
332
- const [ok, err] = saveState(contextId, state, projectRoot);
333
- if (ok) {
334
- logInfo("save_handoff", `Set handoff_path: ${indexPathStr}`);
335
- } else {
336
- logWarn("save_handoff", `Failed to save state: ${err}`);
337
- }
338
- } else {
339
- logWarn("save_handoff", `Could not load context state for ${contextId}`);
340
- }
341
- } catch (e) {
342
- logWarn("save_handoff", `Handoff saved but auto-resume won't work (context update failed): ${e}`);
343
- }
344
-
345
- // Output success message
346
- console.log(`[OK] Created handoff folder: ${handoffFolder}`);
347
- console.log(" - index.md (entry point with navigation)");
348
-
349
- const filesCreated = fs.readdirSync(handoffFolder)
350
- .filter(f => f !== "index.md" && fs.statSync(path.join(handoffFolder, f)).isFile())
351
- .sort();
352
- console.log(` - ${filesCreated.join(", ")}`);
353
-
354
- console.log("");
355
- console.log("Handoff document saved. Use this folder for context in the next session.");
356
- }
357
-
358
- 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();