aiwcli 0.10.1 → 0.10.3

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 (110) hide show
  1. package/dist/commands/clean.js +1 -0
  2. package/dist/commands/clear.d.ts +19 -2
  3. package/dist/commands/clear.js +351 -160
  4. package/dist/commands/init/index.d.ts +1 -17
  5. package/dist/commands/init/index.js +19 -104
  6. package/dist/lib/gitignore-manager.d.ts +9 -0
  7. package/dist/lib/gitignore-manager.js +121 -0
  8. package/dist/lib/template-installer.d.ts +7 -12
  9. package/dist/lib/template-installer.js +69 -193
  10. package/dist/lib/template-settings-reconstructor.d.ts +35 -0
  11. package/dist/lib/template-settings-reconstructor.js +130 -0
  12. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/archive_plan.py +10 -2
  15. package/dist/templates/_shared/hooks/session_end.py +37 -29
  16. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  18. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/base/hook_utils.py +8 -10
  22. package/dist/templates/_shared/lib/base/inference.py +51 -62
  23. package/dist/templates/_shared/lib/base/logger.py +35 -21
  24. package/dist/templates/_shared/lib/base/stop_words.py +8 -0
  25. package/dist/templates/_shared/lib/base/utils.py +29 -8
  26. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/context/plan_manager.py +101 -2
  28. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
  29. package/dist/templates/_shared/lib-ts/base/constants.ts +299 -0
  30. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
  31. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +360 -0
  32. package/dist/templates/_shared/lib-ts/base/inference.ts +245 -0
  33. package/dist/templates/_shared/lib-ts/base/logger.ts +234 -0
  34. package/dist/templates/_shared/lib-ts/base/state-io.ts +114 -0
  35. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
  36. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +23 -0
  37. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
  38. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +432 -0
  39. package/dist/templates/_shared/lib-ts/context/context-selector.ts +497 -0
  40. package/dist/templates/_shared/lib-ts/context/context-store.ts +679 -0
  41. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +292 -0
  42. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +181 -0
  43. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +215 -0
  44. package/dist/templates/_shared/lib-ts/package.json +21 -0
  45. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -0
  46. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +65 -0
  47. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
  48. package/dist/templates/_shared/lib-ts/types.ts +151 -0
  49. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
  51. package/dist/templates/_shared/scripts/status_line.py +17 -2
  52. package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
  53. package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
  54. package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
  55. package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
  56. package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
  57. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -1
  58. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
  59. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
  60. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
  61. package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
  62. package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
  63. package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
  64. package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
  65. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
  66. package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
  67. package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
  68. package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
  69. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
  70. package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
  71. package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
  72. package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
  73. package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
  74. package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
  75. package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
  76. package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
  77. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
  78. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
  79. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
  80. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
  81. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
  82. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
  83. package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
  84. package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
  85. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +125 -40
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/utils.py +57 -13
  89. package/dist/templates/cc-native/_cc-native/plan-review.config.json +11 -7
  90. package/oclif.manifest.json +17 -2
  91. package/package.json +1 -1
  92. package/dist/lib/template-merger.d.ts +0 -47
  93. package/dist/lib/template-merger.js +0 -162
  94. package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
  95. package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
  96. package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
  97. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
  98. package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
  99. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
  100. package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
  101. package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
  102. package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
  103. package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
  104. package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
  105. package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
  106. package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
  107. package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
  108. package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
  109. package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
  110. package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Formatters for context management templates.
3
+ * Constants and helpers for consistent formatting across hooks and display.
4
+ * See SPEC.md §13
5
+ */
6
+
7
+ import type { Task } from "../types.js";
8
+
9
+ // §13.1 — Legacy mode display mapping (used by handoff/document_generator)
10
+ export const LEGACY_MODE_MAP: Record<string, string> = {
11
+ planning: "[Planning]",
12
+ pending_implementation: "[Plan Ready]",
13
+ implementing: "[Implementing]",
14
+ none: "",
15
+ };
16
+
17
+ // §13.2 — Status icon mapping
18
+ export const STATUS_ICONS: Record<string, string> = {
19
+ pending: "⬜",
20
+ in_progress: "🔄",
21
+ blocked: "🚫",
22
+ completed: "✅",
23
+ };
24
+
25
+ export function getModeDisplay(mode: string): string {
26
+ return LEGACY_MODE_MAP[mode] ?? "";
27
+ }
28
+
29
+ export function getStatusIcon(status: string): string {
30
+ return STATUS_ICONS[status] ?? "⬜";
31
+ }
32
+
33
+ // §13.3 — Task rendering
34
+ export function renderTaskItem(
35
+ task: Task | Record<string, any>,
36
+ showDescription = true,
37
+ maxDescriptionLength = 100,
38
+ ): string {
39
+ const status = (task as any).status ?? "pending";
40
+ const subject = (task as any).subject ?? "";
41
+ const description = (task as any).description ?? "";
42
+
43
+ const icon = getStatusIcon(status);
44
+ const statusText = `[${status.toUpperCase()}]`;
45
+ const line = `- ${icon} ${statusText} ${subject}`;
46
+
47
+ if (showDescription && description) {
48
+ let truncated = description.slice(0, maxDescriptionLength);
49
+ if (description.length > maxDescriptionLength) {
50
+ truncated += "...";
51
+ }
52
+ return `${line}\n - ${truncated}`;
53
+ }
54
+ return line;
55
+ }
56
+
57
+ export function renderTaskList(
58
+ tasks: Array<Task | Record<string, any>>,
59
+ header = "Active Tasks",
60
+ showDescription = true,
61
+ ): string {
62
+ const lines = [`### ${header}`, ""];
63
+
64
+ if (tasks.length === 0) {
65
+ lines.push("No active tasks.");
66
+ } else {
67
+ for (const task of tasks) {
68
+ lines.push(renderTaskItem(task, showDescription));
69
+ }
70
+ }
71
+
72
+ lines.push("");
73
+ return lines.join("\n");
74
+ }
75
+
76
+ // §13.4 — Continuation headers
77
+ const CONTINUATION_HEADERS: Record<string, (id: string) => string> = {
78
+ context: (id) => `## CONTINUING CONTEXT: ${id}`,
79
+ resuming: (id) => `## RESUMING FROM HANDOFF: ${id}`,
80
+ implementing: (id) => `## CONTINUING IMPLEMENTATION: ${id}`,
81
+ handoff: (id) => `# Session Handoff: ${id}`,
82
+ };
83
+
84
+ export function formatContinuationHeader(
85
+ headerType: string,
86
+ contextId: string,
87
+ ): string {
88
+ const fn = CONTINUATION_HEADERS[headerType];
89
+ return fn ? fn(contextId) : `## ${contextId}`;
90
+ }
91
+
92
+ // §13.5 — Reason formatting
93
+ export const REASON_MAP: Record<string, string> = {
94
+ low_context: "Context window running low",
95
+ user_requested: "User requested handoff",
96
+ error_recovery: "Error recovery",
97
+ session_end: "Session ending",
98
+ };
99
+
100
+ export function formatReason(reason: string): string {
101
+ return REASON_MAP[reason] ?? reason;
102
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Plan context templates for add_plan_context hook.
3
+ * See SPEC.md §13.6
4
+ */
5
+
6
+ export function getEvaluationContextReminder(): string {
7
+ return `## CRITICAL: Write This Plan for a Different Agent
8
+
9
+ The agent executing this plan has ZERO context from this conversation — no chat history, no memory of files you explored or research you did.
10
+
11
+ **Write as if YOU are that agent. What would you need?**
12
+
13
+ ### Required Structure
14
+
15
+ \`\`\`
16
+ # Plan: <descriptive title>
17
+
18
+ ## Background
19
+ Why this change is needed (2-3 sentences)
20
+
21
+ ## Task
22
+ What exactly to build/change
23
+
24
+ ## Files
25
+ **Modify:**
26
+ - \`exact/path/to/file.py\` - What changes (reference line numbers or patterns)
27
+
28
+ **Reference:**
29
+ - \`exact/path/to/reference.py\` - Why relevant (e.g., "pattern to follow at lines 12-30")
30
+
31
+ ## Steps
32
+ 1. [Specific steps with function names, patterns, or code snippets]
33
+ 2. [Enough detail for someone who never saw this conversation]
34
+
35
+ ## Constraints
36
+ - Technical requirements, preferences, or limitations
37
+
38
+ ## Documentation
39
+ Decisions not written down are lost when this session ends. Update the nearest CLAUDE.md and MEMORY.md so the next session inherits what you learned.
40
+
41
+ **CLAUDE.md** (nearest to changed code — cascades to subdirectories):
42
+ - \`exact/path/to/CLAUDE.md\` — What to document
43
+
44
+ **What to write:**
45
+ - Architectural choices and why alternatives were rejected
46
+ - Non-obvious constraints (what breaks if this changes)
47
+ - Workarounds with context on the underlying issue
48
+ - Patterns that prevent future mistakes
49
+
50
+ **Format:** \`## Topic\` / \`**Decision:** ...\` / \`**Rationale:** ...\`
51
+
52
+ **MEMORY.md** (cross-session learning for the AI agent):
53
+ - Insight that would prevent a future mistake (e.g., "hook X silently drops field Y")
54
+
55
+ **Include when:** Architectural decisions, non-obvious constraints, workarounds, or patterns discovered during implementation.
56
+ **Omit entries for:** Routine changes with no decisions (rename, formatting, dependency bump).
57
+ When in doubt, write it — a lean entry is better than a lost decision.
58
+ \`\`\`
59
+
60
+ ### Self-Check
61
+ - [ ] Could I execute this if I forgot our entire conversation?
62
+ - [ ] Are file paths exact (not "the auth file")?
63
+ - [ ] Are implementation details specific (not "use the approach we discussed")?
64
+ - [ ] Do documentation entries capture decisions the next session would otherwise lose?`;
65
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "node16",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "outDir": "dist",
9
+ "rootDir": ".",
10
+ "declaration": true
11
+ },
12
+ "include": ["./**/*.ts"]
13
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Shared type definitions for the lib-ts library.
3
+ * All field names use snake_case for JSON backward compatibility with state.json.
4
+ * See SPEC.md §1 for full behavioral specification.
5
+ */
6
+
7
+ // §1.1
8
+ export type Mode = "idle" | "has_plan" | "has_handoff" | "active";
9
+
10
+ export interface ContextState {
11
+ id: string;
12
+ status: "active" | "completed";
13
+ summary: string;
14
+ method: string;
15
+ tags: string[];
16
+ created_at: string;
17
+ last_active: string;
18
+ mode: Mode;
19
+ plan_path: string | null;
20
+ plan_hash: string | null;
21
+ plan_signature: string | null;
22
+ plan_id: string | null;
23
+ plan_anchors: string[];
24
+ plan_consumed: boolean;
25
+ handoff_path: string | null;
26
+ handoff_consumed: boolean;
27
+ session_ids: string[];
28
+ last_session: LastSession | null;
29
+ tasks: Task[];
30
+ }
31
+
32
+ // §1.2
33
+ export interface GitState {
34
+ branch?: string;
35
+ uncommitted_files?: string[];
36
+ last_commit_short?: string;
37
+ }
38
+
39
+ export interface LastSession {
40
+ session_id?: string;
41
+ saved_at?: string;
42
+ save_reason?: string;
43
+ transcript_path?: string;
44
+ context_remaining_pct?: number;
45
+ git_state?: GitState;
46
+ }
47
+
48
+ // §1.3
49
+ export interface Task {
50
+ id: string;
51
+ subject: string;
52
+ description: string;
53
+ active_form: string;
54
+ status: "pending" | "in_progress" | "completed" | "blocked";
55
+ created_at: string;
56
+ completed_at: string | null;
57
+ evidence: string;
58
+ work_summary: string;
59
+ files_changed: string[];
60
+ session_id?: string;
61
+ }
62
+
63
+ // §1.4
64
+ export interface IndexEntry {
65
+ summary: string;
66
+ mode: string;
67
+ last_active: string;
68
+ }
69
+
70
+ export interface IndexFile {
71
+ version: "3.0";
72
+ updated_at: string;
73
+ sessions: Record<string, string>;
74
+ contexts: Record<string, IndexEntry>;
75
+ }
76
+
77
+ // §1.5
78
+ export interface LogEntry {
79
+ ts: string;
80
+ level: "debug" | "info" | "warn" | "error";
81
+ hook: string;
82
+ msg: string;
83
+ component?: string;
84
+ data?: any;
85
+ tb?: string;
86
+ }
87
+
88
+ // §1.6
89
+ export interface HookInput {
90
+ hook_event_name: string;
91
+ tool_name?: string;
92
+ tool_input?: Record<string, any>;
93
+ tool_result?: string;
94
+ session_id?: string;
95
+ cwd?: string;
96
+ transcript_path?: string;
97
+ context_window?: {
98
+ current_usage?: {
99
+ cache_read_input_tokens?: number;
100
+ input_tokens?: number;
101
+ cache_creation_input_tokens?: number;
102
+ output_tokens?: number;
103
+ };
104
+ context_window_size?: number;
105
+ };
106
+ permission_mode?: string;
107
+ source?: string;
108
+ }
109
+
110
+ // §1.7
111
+ export interface HookOutput {
112
+ hookSpecificOutput?: {
113
+ additionalContext?: string;
114
+ permissionDecision?: "allow" | "deny";
115
+ permissionDecisionReason?: string;
116
+ };
117
+ }
118
+
119
+ // §1.8
120
+ export interface InferenceResult {
121
+ success: boolean;
122
+ output: string;
123
+ error?: string;
124
+ latency_ms: number;
125
+ }
126
+
127
+ // §1.9
128
+ export interface HandoffDocument {
129
+ context_id: string;
130
+ context_summary: string;
131
+ session_id: string;
132
+ reason: string;
133
+ created_at: string;
134
+ plan_path: string | null;
135
+ context_folder: string;
136
+ events_log_path: string;
137
+ active_tasks: Task[];
138
+ completed_tasks_this_session: Array<{ subject: string }>;
139
+ work_summary: string;
140
+ next_steps: string[];
141
+ important_notes: string[];
142
+ file_path: string | null;
143
+ }
144
+
145
+ // §1.10
146
+ export interface CaretCommand {
147
+ ends: string[];
148
+ select: string | null;
149
+ new_context_desc: string | null;
150
+ remaining_prompt: string;
151
+ }
@@ -0,0 +1,359 @@
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 <context_id> <<'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 <context_id> < handoff.md
12
+ *
13
+ * This script:
14
+ * 1. Parses sections from incoming markdown using <!-- SECTION: name --> markers
15
+ * 2. Creates a timestamped folder at _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
16
+ * 3. Writes sharded files:
17
+ * - index.md (main entry point with navigation)
18
+ * - completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
19
+ * - plan.md (copy of original plan if it exists)
20
+ * 4. Sets handoff_path and handoff_consumed=false in state.json
21
+ */
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+
25
+ import { getContext, saveState } from "../lib-ts/context/context-store.js";
26
+ import { getHandoffFolderPath, getProjectRoot } from "../lib-ts/base/constants.js";
27
+ import { atomicWrite } from "../lib-ts/base/atomic-write.js";
28
+ import { logInfo, logWarn, logError } from "../lib-ts/base/logger.js";
29
+ import { getGitStatusShort } from "../lib-ts/base/git-state.js";
30
+ import { eprint } from "../lib-ts/base/utils.js";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Parsing helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function parseFrontmatter(content: string): [Record<string, string>, string] {
37
+ const frontmatter: Record<string, string> = {};
38
+ let remaining = content;
39
+
40
+ if (content.startsWith("---")) {
41
+ const parts = content.split("---", 3);
42
+ if (parts.length >= 3) {
43
+ for (const line of parts[1]!.trim().split("\n")) {
44
+ const colonIdx = line.indexOf(":");
45
+ if (colonIdx !== -1) {
46
+ const key = line.slice(0, colonIdx).trim();
47
+ const value = line.slice(colonIdx + 1).trim();
48
+ frontmatter[key] = value;
49
+ }
50
+ }
51
+ remaining = parts[2]!.trim();
52
+ }
53
+ }
54
+
55
+ return [frontmatter, remaining];
56
+ }
57
+
58
+ function parseHandoffSections(content: string): Record<string, string> {
59
+ const sections: Record<string, string> = {};
60
+ let currentSection: string | null = null;
61
+ const currentContent: string[] = [];
62
+
63
+ for (const line of content.split("\n")) {
64
+ const marker = line.trim().match(/<!-- SECTION:\s*(\S+)\s*-->/);
65
+ if (marker) {
66
+ if (currentSection) {
67
+ sections[currentSection] = currentContent.join("\n").trim();
68
+ }
69
+ currentSection = marker[1]!;
70
+ currentContent.length = 0;
71
+ } else if (currentSection) {
72
+ currentContent.push(line);
73
+ }
74
+ }
75
+
76
+ if (currentSection) {
77
+ sections[currentSection] = currentContent.join("\n").trim();
78
+ }
79
+
80
+ return sections;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Plan helper
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function getPlanPathFromContext(contextId: string, projectRoot: string): string | null {
88
+ const context = getContext(contextId, projectRoot);
89
+ if (!context?.plan_path) return null;
90
+ try {
91
+ if (fs.existsSync(context.plan_path)) return context.plan_path;
92
+ } catch { /* ignore */ }
93
+ return null;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // File generation
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function generateIndex(
101
+ frontmatter: Record<string, string>,
102
+ sections: Record<string, string>,
103
+ gitStatus: string,
104
+ hasPlan: boolean,
105
+ ): string {
106
+ const now = new Date();
107
+ const isoStr = now.toISOString();
108
+ 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")}`;
109
+
110
+ const lines: string[] = [
111
+ "---",
112
+ "type: handoff",
113
+ `context_id: ${frontmatter["context_id"] ?? "unknown"}`,
114
+ `created_at: ${isoStr}`,
115
+ `session_id: ${frontmatter["session_id"] ?? "unknown"}`,
116
+ `project: ${frontmatter["project"] ?? "unknown"}`,
117
+ `plan_path: ${frontmatter["plan_document"] ?? "none"}`,
118
+ "---",
119
+ "",
120
+ `# Session Handoff - ${dateStr}`,
121
+ "",
122
+ ];
123
+
124
+ // Summary
125
+ const summary = (sections["summary"] ?? "").trim();
126
+ if (summary) {
127
+ const summaryText = summary
128
+ .split("\n")
129
+ .filter(l => !l.trim().startsWith("##"))
130
+ .join("\n")
131
+ .trim();
132
+ lines.push("## Summary", summaryText, "");
133
+ }
134
+
135
+ // Navigation
136
+ lines.push(
137
+ "## Quick Navigation",
138
+ "",
139
+ "| Document | Purpose | Priority |",
140
+ "|----------|---------|----------|",
141
+ "| [Dead Ends](./dead-ends.md) | Failed approaches - DO NOT RETRY | Read First |",
142
+ "| [Pending](./pending.md) | Next steps and blockers | Action Items |",
143
+ "| [Completed Work](./completed-work.md) | Tasks finished this session | Reference |",
144
+ "| [Decisions](./decisions.md) | Technical choices and rationale | Reference |",
145
+ );
146
+
147
+ if (hasPlan) {
148
+ lines.push("| [Plan](./plan.md) | Original plan being implemented | Reference |");
149
+ }
150
+
151
+ lines.push(
152
+ "| [Context](./context.md) | External requirements and notes | Reference |",
153
+ "",
154
+ "## Continuation Instructions",
155
+ "",
156
+ "To continue this work in a new session:",
157
+ "1. This index document provides the overview",
158
+ "2. **Read [Dead Ends](./dead-ends.md) first** to avoid repeating failed approaches",
159
+ "3. Check [Pending](./pending.md) for immediate next steps",
160
+ "4. Reference other documents as needed",
161
+ "",
162
+ "## Git Status at Handoff",
163
+ "```",
164
+ gitStatus,
165
+ "```",
166
+ "",
167
+ );
168
+
169
+ return lines.join("\n");
170
+ }
171
+
172
+ function writeSectionFile(folder: string, filename: string, title: string, content: string): boolean {
173
+ const text = `# ${title}\n\n${content || "(No content for this section)"}\n`;
174
+ const filePath = path.join(folder, filename);
175
+ const [success, error] = atomicWrite(filePath, text);
176
+ if (!success) {
177
+ logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
178
+ return false;
179
+ }
180
+ return true;
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Main
185
+ // ---------------------------------------------------------------------------
186
+
187
+ function main(): void {
188
+ if (process.argv.length < 3) {
189
+ eprint(
190
+ "Usage: bun save_handoff.ts <context_id> < content.md\n" +
191
+ " bun save_handoff.ts <context_id> <<'EOF'\n" +
192
+ " ... markdown content with <!-- SECTION: name --> markers ...\n" +
193
+ " EOF",
194
+ );
195
+ process.exit(1);
196
+ }
197
+
198
+ const contextId = process.argv[2]!;
199
+
200
+ // Read content from stdin
201
+ let content: string;
202
+ try {
203
+ content = fs.readFileSync(0, "utf-8");
204
+ } catch {
205
+ logError("save_handoff", "Failed to read from stdin");
206
+ process.exit(1);
207
+ return; // unreachable but makes TS happy
208
+ }
209
+
210
+ if (!content.trim()) {
211
+ logError("save_handoff", "No content provided via stdin");
212
+ process.exit(1);
213
+ }
214
+
215
+ // Project root via shared utility (checks CLAUDE_PROJECT_DIR, falls back to cwd)
216
+ const projectRoot = getProjectRoot(process.cwd());
217
+
218
+ // Verify context exists
219
+ const context = getContext(contextId, projectRoot);
220
+ if (!context) {
221
+ logError("save_handoff", `Context not found: ${contextId}`);
222
+ process.exit(1);
223
+ }
224
+
225
+ // Parse frontmatter and sections
226
+ const [frontmatter, body] = parseFrontmatter(content);
227
+ const sections = parseHandoffSections(body);
228
+
229
+ logInfo("save_handoff", `Parsed ${Object.keys(sections).length} sections: ${Object.keys(sections).join(", ")}`);
230
+
231
+ // Create handoff folder
232
+ const handoffFolder = getHandoffFolderPath(contextId, projectRoot);
233
+ fs.mkdirSync(handoffFolder, { recursive: true });
234
+ logInfo("save_handoff", `Created folder: ${handoffFolder}`);
235
+
236
+ // Git status
237
+ const gitStatus = getGitStatusShort(projectRoot);
238
+
239
+ // Check for plan
240
+ const planPath = getPlanPathFromContext(contextId, projectRoot);
241
+ const hasPlan = planPath !== null;
242
+
243
+ // Copy plan if exists
244
+ if (planPath) {
245
+ try {
246
+ const planContent = fs.readFileSync(planPath, "utf-8");
247
+ const [success, error] = atomicWrite(path.join(handoffFolder, "plan.md"), planContent);
248
+ if (success) {
249
+ logInfo("save_handoff", `Copied plan from ${planPath}`);
250
+ } else {
251
+ logWarn("save_handoff", `Failed to copy plan: ${error}`);
252
+ }
253
+ } catch (e) {
254
+ logWarn("save_handoff", `Failed to read plan: ${e}`);
255
+ }
256
+ }
257
+
258
+ // Write index.md
259
+ const indexContent = generateIndex(frontmatter, sections, gitStatus, hasPlan);
260
+ const indexPath = path.join(handoffFolder, "index.md");
261
+ {
262
+ const [success, error] = atomicWrite(indexPath, indexContent);
263
+ if (!success) {
264
+ logError("save_handoff", `Failed to write index.md: ${error}`);
265
+ process.exit(1);
266
+ }
267
+ }
268
+
269
+ // Write section files
270
+ const sectionMapping: Record<string, [string, string | null]> = {
271
+ completed: ["completed-work.md", "Work Completed"],
272
+ "dead-ends": ["dead-ends.md", "Dead Ends - Do Not Retry"],
273
+ decisions: ["decisions.md", "Key Decisions"],
274
+ pending: ["pending.md", "Pending Issues"],
275
+ "next-steps": ["pending.md", null], // Append to pending.md
276
+ files: ["completed-work.md", null], // Append to completed-work.md
277
+ context: ["context.md", "Context for Future Sessions"],
278
+ };
279
+
280
+ // Track accumulated content per file
281
+ const fileContents: Record<string, string[]> = {};
282
+
283
+ for (const [sectionName, [filename, title]] of Object.entries(sectionMapping)) {
284
+ const sectionContent = sections[sectionName];
285
+ if (!sectionContent) continue;
286
+
287
+ if (title === null) {
288
+ // Append mode
289
+ if (!fileContents[filename]) fileContents[filename] = [];
290
+ fileContents[filename]!.push(sectionContent);
291
+ } else {
292
+ // Write mode with title
293
+ if (!fileContents[filename]) {
294
+ fileContents[filename] = [`# ${title}`, "", sectionContent];
295
+ } else {
296
+ fileContents[filename] = [`# ${title}`, "", ...fileContents[filename]!, "", sectionContent];
297
+ }
298
+ }
299
+ }
300
+
301
+ // Write all accumulated content
302
+ for (const [filename, parts] of Object.entries(fileContents)) {
303
+ const filePath = path.join(handoffFolder, filename);
304
+ const [success, error] = atomicWrite(filePath, parts.join("\n") + "\n");
305
+ if (!success) {
306
+ logWarn("save_handoff", `Failed to write ${filename}: ${error}`);
307
+ }
308
+ }
309
+
310
+ // Ensure all expected files exist (even if empty)
311
+ const expectedFiles: Record<string, string> = {
312
+ "completed-work.md": "Work Completed",
313
+ "dead-ends.md": "Dead Ends - Do Not Retry",
314
+ "decisions.md": "Key Decisions",
315
+ "pending.md": "Pending Issues & Next Steps",
316
+ "context.md": "Context for Future Sessions",
317
+ };
318
+
319
+ for (const [filename, title] of Object.entries(expectedFiles)) {
320
+ const filePath = path.join(handoffFolder, filename);
321
+ if (!fs.existsSync(filePath)) {
322
+ writeSectionFile(handoffFolder, filename, title, "");
323
+ }
324
+ }
325
+
326
+ // Set handoff_path and handoff_consumed=false in state.json
327
+ try {
328
+ const indexPathStr = path.join(handoffFolder, "index.md");
329
+ const state = getContext(contextId, projectRoot);
330
+ if (state) {
331
+ state.handoff_path = indexPathStr;
332
+ state.handoff_consumed = false;
333
+ const [ok, err] = saveState(contextId, state, projectRoot);
334
+ if (ok) {
335
+ logInfo("save_handoff", `Set handoff_path: ${indexPathStr}`);
336
+ } else {
337
+ logWarn("save_handoff", `Failed to save state: ${err}`);
338
+ }
339
+ } else {
340
+ logWarn("save_handoff", `Could not load context state for ${contextId}`);
341
+ }
342
+ } catch (e) {
343
+ logWarn("save_handoff", `Handoff saved but auto-resume won't work (context update failed): ${e}`);
344
+ }
345
+
346
+ // Output success message
347
+ console.log(`[OK] Created handoff folder: ${handoffFolder}`);
348
+ console.log(" - index.md (entry point with navigation)");
349
+
350
+ const filesCreated = fs.readdirSync(handoffFolder)
351
+ .filter(f => f !== "index.md" && fs.statSync(path.join(handoffFolder, f)).isFile())
352
+ .sort();
353
+ console.log(` - ${filesCreated.join(", ")}`);
354
+
355
+ console.log("");
356
+ console.log("Handoff document saved. Use this folder for context in the next session.");
357
+ }
358
+
359
+ main();