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.
- package/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +205 -205
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -64
- package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
- package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -183
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -130
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -180
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +690 -690
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- 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 "
|
|
27
|
-
import { getHandoffFolderPath, getProjectRoot } from "
|
|
28
|
-
import { atomicWrite } from "
|
|
29
|
-
import { logInfo, logWarn, logError } from "
|
|
30
|
-
import { getGitStatusShort } from "
|
|
31
|
-
import { eprint } from "
|
|
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
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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();
|