aiwcli 0.10.3 → 0.11.0

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 (189) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -9,20 +9,21 @@
9
9
 
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
- import { readStateJson, writeStateJson, toDict, dictToState } from "../base/state-io.js";
12
+
13
13
  import { atomicWrite } from "../base/atomic-write.js";
14
14
  import {
15
+ getArchiveContextDir,
16
+ getArchiveDir,
17
+ getArchiveIndexPath,
15
18
  getContextDir,
16
19
  getContextsDir,
17
20
  getIndexPath,
18
- getArchiveDir,
19
- getArchiveContextDir,
20
- getArchiveIndexPath,
21
21
  validateContextId,
22
22
  } from "../base/constants.js";
23
- import { logDebug, logInfo, logWarn, logError, setContextPath } from "../base/logger.js";
24
- import { nowIso, generateContextId } from "../base/utils.js";
25
- import type { ContextState, IndexFile, IndexEntry, Mode } from "../types.js";
23
+ import { logDebug as _logDebug, logError, logInfo, logWarn, setContextPath } from "../base/logger.js";
24
+ import { dictToState as _dictToState, readStateJson, toDict as _toDict, writeStateJson } from "../base/state-io.js";
25
+ import { generateContextId, nowIso } from "../base/utils.js";
26
+ import type { ContextState, IndexEntry, IndexFile, Mode } from "../types.js";
26
27
 
27
28
  const INDEX_VERSION = "3.0";
28
29
 
@@ -34,12 +35,13 @@ function loadIndex(projectRoot?: string): IndexFile {
34
35
  const indexPath = getIndexPath(projectRoot);
35
36
  if (fs.existsSync(indexPath)) {
36
37
  try {
37
- const raw = fs.readFileSync(indexPath, "utf-8");
38
+ const raw = fs.readFileSync(indexPath, "utf8");
38
39
  return JSON.parse(raw) as IndexFile;
39
- } catch (e: any) {
40
- logWarn("context_store", `Failed to read index, recreating: ${e}`);
40
+ } catch (error: any) {
41
+ logWarn("context_store", `Failed to read index, recreating: ${error}`);
41
42
  }
42
43
  }
44
+
43
45
  return { version: INDEX_VERSION, updated_at: nowIso(), sessions: {}, contexts: {} };
44
46
  }
45
47
 
@@ -50,6 +52,7 @@ function saveIndex(index: IndexFile, projectRoot?: string): boolean {
50
52
  if (!success) {
51
53
  logWarn("context_store", `Failed to write index: ${error}`);
52
54
  }
55
+
53
56
  return success;
54
57
  }
55
58
 
@@ -69,7 +72,7 @@ function migrateContextJson(contextId: string, projectRoot?: string): ContextSta
69
72
  if (!fs.existsSync(legacyPath)) return null;
70
73
 
71
74
  try {
72
- const data = JSON.parse(fs.readFileSync(legacyPath, "utf-8"));
75
+ const data = JSON.parse(fs.readFileSync(legacyPath, "utf8"));
73
76
  const inFlight = data.in_flight ?? {};
74
77
  const oldMode = inFlight.mode ?? "none";
75
78
  const MODE_MIGRATION: Record<string, string> = {
@@ -104,8 +107,8 @@ function migrateContextJson(contextId: string, projectRoot?: string): ContextSta
104
107
  last_session: null,
105
108
  tasks: [],
106
109
  };
107
- } catch (e: any) {
108
- logWarn("context_store", `Failed to migrate context.json for '${contextId}': ${e}`);
110
+ } catch (error: any) {
111
+ logWarn("context_store", `Failed to migrate context.json for '${contextId}': ${error}`);
109
112
  return null;
110
113
  }
111
114
  }
@@ -134,7 +137,7 @@ export function saveState(
134
137
  contextId: string,
135
138
  state: ContextState,
136
139
  projectRoot?: string,
137
- ): [boolean, string | null] {
140
+ ): [boolean, null | string] {
138
141
  // Ensure the state ID matches
139
142
  state.id = contextId;
140
143
 
@@ -152,10 +155,12 @@ export function saveState(
152
155
  if (!index.sessions) index.sessions = {} as Record<string, string>;
153
156
  index.sessions[sid] = contextId;
154
157
  }
158
+
155
159
  const indexOk = saveIndex(index, projectRoot);
156
160
  if (!indexOk) {
157
161
  return [true, "state.json saved but index.json update failed"];
158
162
  }
163
+
159
164
  return [true, null];
160
165
  }
161
166
 
@@ -165,7 +170,7 @@ export function saveState(
165
170
  * See SPEC.md §7.4
166
171
  */
167
172
  export function createContext(
168
- contextId: string | null,
173
+ contextId: null | string,
169
174
  summary: string,
170
175
  method = "",
171
176
  projectRoot?: string,
@@ -185,23 +190,8 @@ export function createContext(
185
190
  } catch { /* ignore */ }
186
191
  }
187
192
  }
188
- // generateContextId is async but we need sync here — use a simple fallback
189
- const now = new Date();
190
- const yy = String(now.getFullYear()).slice(2);
191
- const mm = String(now.getMonth() + 1).padStart(2, "0");
192
- const dd = String(now.getDate()).padStart(2, "0");
193
- const hh = String(now.getHours()).padStart(2, "0");
194
- const min = String(now.getMinutes()).padStart(2, "0");
195
- const timestamp = `${yy}${mm}${dd}-${hh}${min}`;
196
- const words = summary.toLowerCase().split(/\s+/).filter(w => w.length > 2).slice(0, 6);
197
- const slug = words.length > 0 ? words.join("-").replace(/[^a-z0-9-]/g, "").slice(0, 50) : "context";
198
- contextId = `${timestamp}-${slug}`;
199
-
200
- if (existingIds.has(contextId)) {
201
- let counter = 2;
202
- while (existingIds.has(`${contextId}-${counter}`)) counter++;
203
- contextId = `${contextId}-${counter}`;
204
- }
193
+
194
+ contextId = generateContextId(summary, existingIds);
205
195
  }
206
196
 
207
197
  contextId = validateContextId(contextId);
@@ -251,6 +241,7 @@ export function getContext(contextId: string, projectRoot?: string): ContextStat
251
241
  } catch {
252
242
  return null;
253
243
  }
244
+
254
245
  return loadState(contextId, projectRoot);
255
246
  }
256
247
 
@@ -288,6 +279,7 @@ export function getAllContexts(
288
279
  try {
289
280
  if (!fs.statSync(fullPath).isDirectory()) continue;
290
281
  } catch { continue; }
282
+
291
283
  const state = loadState(entry, projectRoot);
292
284
  if (state && (!status || state.status === status)) {
293
285
  results.push(state);
@@ -306,7 +298,7 @@ export function getAllContexts(
306
298
  */
307
299
  export function updateContext(
308
300
  contextId: string,
309
- updates: Partial<Pick<ContextState, "summary" | "tags" | "method">>,
301
+ updates: Partial<Pick<ContextState, "method" | "summary" | "tags">>,
310
302
  projectRoot?: string,
311
303
  ): ContextState | null {
312
304
  const state = getContext(contextId, projectRoot);
@@ -356,6 +348,7 @@ export function getContextBySessionId(
356
348
  return state;
357
349
  }
358
350
  }
351
+
359
352
  return null;
360
353
  }
361
354
 
@@ -387,6 +380,7 @@ export function bindSession(
387
380
  if (!state.session_ids.includes(sessionId)) {
388
381
  state.session_ids.push(sessionId);
389
382
  }
383
+
390
384
  state.last_active = nowIso();
391
385
 
392
386
  const [success] = saveState(contextId, state, projectRoot);
@@ -402,13 +396,13 @@ export function updateMode(
402
396
  mode: Mode,
403
397
  projectRoot?: string,
404
398
  opts?: {
405
- plan_path?: string;
406
- plan_hash?: string;
407
- plan_signature?: string;
408
- plan_id?: string;
399
+ handoff_consumed?: boolean;
409
400
  plan_anchors?: string[];
410
401
  plan_consumed?: boolean;
411
- handoff_consumed?: boolean;
402
+ plan_hash?: string;
403
+ plan_id?: string;
404
+ plan_path?: string;
405
+ plan_signature?: string;
412
406
  },
413
407
  ): ContextState | null {
414
408
  const state = getContext(contextId, projectRoot);
@@ -506,6 +500,7 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
506
500
  logWarn("context_store", `Cannot archive: context '${contextId}' not found`);
507
501
  return null;
508
502
  }
503
+
509
504
  if (state.status !== "completed") {
510
505
  logWarn("context_store", `Cannot archive: context '${contextId}' not completed`);
511
506
  return null;
@@ -524,8 +519,8 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
524
519
 
525
520
  try {
526
521
  fs.renameSync(sourceDir, archiveDest);
527
- } catch (e: any) {
528
- logError("context_store", `Failed to move context to archive: ${e}`);
522
+ } catch (error: any) {
523
+ logError("context_store", `Failed to move context to archive: ${error}`);
529
524
  return null;
530
525
  }
531
526
 
@@ -536,6 +531,7 @@ export function archiveContext(contextId: string, projectRoot?: string): Context
536
531
  for (const [sid, cid] of Object.entries(sessions)) {
537
532
  if (cid === contextId) delete sessions[sid];
538
533
  }
534
+
539
535
  saveIndex(index, projectRoot);
540
536
 
541
537
  // Add to archive index
@@ -555,6 +551,7 @@ export function reopenContext(contextId: string, projectRoot?: string): ContextS
555
551
  if (!state) {
556
552
  state = restoreFromArchive(contextId, projectRoot);
557
553
  }
554
+
558
555
  if (!state) return null;
559
556
 
560
557
  if (state.status === "active") {
@@ -595,6 +592,35 @@ export function createContextFromPrompt(
595
592
  );
596
593
  }
597
594
 
595
+ /**
596
+ * Find the active context ID programmatically.
597
+ * Checks CONTEXT_ID env var first, then searches for the single active context.
598
+ * Returns null if no active context or multiple active contexts found.
599
+ */
600
+ export function findActiveContextId(projectRoot?: string): null | string {
601
+ // Env var takes priority
602
+ const envId = process.env.CONTEXT_ID;
603
+ if (envId) {
604
+ const ctx = getContext(envId, projectRoot);
605
+ if (ctx) return ctx.id;
606
+ }
607
+
608
+ // Search for active contexts
609
+ const active = getAllContexts("active", projectRoot)
610
+ .filter(c => c.mode === "active" || c.mode === "has_plan" || c.mode === "has_handoff");
611
+
612
+ if (active.length === 1) return active[0]!.id;
613
+ if (active.length > 1) {
614
+ // Multiple active — try to find the most recently active
615
+ const sorted = active.sort((a, b) =>
616
+ (b.last_active ?? "").localeCompare(a.last_active ?? ""),
617
+ );
618
+ return sorted[0]!.id;
619
+ }
620
+
621
+ return null;
622
+ }
623
+
598
624
  // ---------------------------------------------------------------------------
599
625
  // Archive helpers
600
626
  // ---------------------------------------------------------------------------
@@ -613,9 +639,9 @@ function updateArchiveIndex(state: ContextState, projectRoot?: string): boolean
613
639
 
614
640
  if (fs.existsSync(archiveIndexPath)) {
615
641
  try {
616
- archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf-8"));
617
- } catch (e: any) {
618
- logWarn("context_store", `Failed to read archive index, recreating: ${e}`);
642
+ archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf8"));
643
+ } catch (error_: any) {
644
+ logWarn("context_store", `Failed to read archive index, recreating: ${error_}`);
619
645
  }
620
646
  }
621
647
 
@@ -627,6 +653,7 @@ function updateArchiveIndex(state: ContextState, projectRoot?: string): boolean
627
653
  if (!success) {
628
654
  logWarn("context_store", `Failed to write archive index: ${error}`);
629
655
  }
656
+
630
657
  return success;
631
658
  }
632
659
 
@@ -642,8 +669,8 @@ function restoreFromArchive(contextId: string, projectRoot?: string): ContextSta
642
669
 
643
670
  try {
644
671
  fs.renameSync(archiveDir, activeDir);
645
- } catch (e: any) {
646
- logError("context_store", `Failed to restore context from archive: ${e}`);
672
+ } catch (error: any) {
673
+ logError("context_store", `Failed to restore context from archive: ${error}`);
647
674
  return null;
648
675
  }
649
676
 
@@ -660,7 +687,7 @@ function removeFromArchiveIndex(contextId: string, projectRoot?: string): boolea
660
687
  if (!fs.existsSync(archiveIndexPath)) return true;
661
688
 
662
689
  try {
663
- const archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf-8")) as IndexFile;
690
+ const archiveIndex = JSON.parse(fs.readFileSync(archiveIndexPath, "utf8")) as IndexFile;
664
691
  if (archiveIndex.contexts[contextId]) {
665
692
  delete archiveIndex.contexts[contextId];
666
693
  archiveIndex.updated_at = nowIso();
@@ -671,9 +698,10 @@ function removeFromArchiveIndex(contextId: string, projectRoot?: string): boolea
671
698
  return false;
672
699
  }
673
700
  }
701
+
674
702
  return true;
675
- } catch (e: any) {
676
- logWarn("context_store", `Failed to read archive index: ${e}`);
703
+ } catch (error: any) {
704
+ logWarn("context_store", `Failed to read archive index: ${error}`);
677
705
  return false;
678
706
  }
679
707
  }
@@ -8,14 +8,16 @@
8
8
  * - extractPlanPathFromResult: parse plan path from ExitPlanMode output
9
9
  */
10
10
 
11
+ import * as crypto from "node:crypto";
11
12
  import * as fs from "node:fs";
12
13
  import * as path from "node:path";
13
- import * as crypto from "node:crypto";
14
- import { getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
14
+
15
15
  import { atomicWrite } from "../base/atomic-write.js";
16
+ import { getContextDir as _getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
17
+ import { logDebug, logError, logInfo, logWarn } from "../base/logger.js";
16
18
  import { readStateJson } from "../base/state-io.js";
17
- import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
18
- import type { ContextState } from "../types.js";
19
+ import { generateSlug } from "../base/utils.js";
20
+ import type { ContextState as _ContextState } from "../types.js";
19
21
 
20
22
  // ---------------------------------------------------------------------------
21
23
  // Plan archival
@@ -34,7 +36,7 @@ export async function archivePlan(
34
36
  planPath: string,
35
37
  contextId: string,
36
38
  projectRoot?: string,
37
- ): Promise<[string | null, string | null, string | null]> {
39
+ ): Promise<[null | string, null | string, null | string]> {
38
40
  if (!fs.existsSync(planPath)) {
39
41
  logWarn("plan_manager", `Plan file not found: ${planPath}`);
40
42
  return [null, null, null];
@@ -42,9 +44,9 @@ export async function archivePlan(
42
44
 
43
45
  let content: string;
44
46
  try {
45
- content = fs.readFileSync(planPath, "utf-8");
46
- } catch (e: any) {
47
- logError("plan_manager", `Failed to read plan: ${e}`);
47
+ content = fs.readFileSync(planPath, "utf8");
48
+ } catch (error_: any) {
49
+ logError("plan_manager", `Failed to read plan: ${error_}`);
48
50
  return [null, null, null];
49
51
  }
50
52
 
@@ -69,23 +71,12 @@ export async function archivePlan(
69
71
  String(now.getMinutes()).padStart(2, "0"),
70
72
  ].join("");
71
73
 
72
- // Try AI inference for a descriptive slug
73
- let slug: string | null = null;
74
- try {
75
- const { generateContextIdSlug } = await import("../base/inference.js");
76
- const aiSlug = generateContextIdSlug(content.slice(0, 500), 5);
77
- if (aiSlug) {
78
- slug = sanitizeTitle(aiSlug, 60);
79
- }
80
- } catch {
81
- // AI inference unavailable — use filename fallback
82
- }
83
-
84
- // Fallback: use plan filename
85
- if (!slug) {
86
- const stem = path.basename(planPath, path.extname(planPath));
87
- slug = sanitizeTitle(stem, 30);
88
- }
74
+ // Extract a clean summary from plan content for slug generation.
75
+ // Headings describe the plan's intent better than raw markdown body.
76
+ const summary = extractPlanSummary(content);
77
+ const slug = generateSlug(summary, 60, sanitizeTitle(
78
+ path.basename(planPath, path.extname(planPath)), 30,
79
+ ));
89
80
 
90
81
  let archiveName = `${dateStr}-${slug}.md`;
91
82
  let archivePath = path.join(plansDir, archiveName);
@@ -109,6 +100,37 @@ export async function archivePlan(
109
100
  return [archivePath, planHash, planSignature];
110
101
  }
111
102
 
103
+ /**
104
+ * Extract a human-readable summary from plan markdown content.
105
+ * Pulls headings and the first substantial paragraph, producing
106
+ * text suitable for the AI slug generator (which expects conversational input).
107
+ */
108
+ function extractPlanSummary(content: string): string {
109
+ const lines = content.split("\n");
110
+ const parts: string[] = [];
111
+ let firstParagraph = "";
112
+
113
+ for (const line of lines) {
114
+ const trimmed = line.trim();
115
+ // Collect markdown headings (strip # prefix)
116
+ if (trimmed.startsWith("#")) {
117
+ const heading = trimmed.replace(/^#+\s*/, "");
118
+ if (heading.length > 2) parts.push(heading);
119
+ }
120
+
121
+ // Grab first substantial non-heading line as context
122
+ if (!firstParagraph && !trimmed.startsWith("#") && trimmed.length > 20) {
123
+ firstParagraph = trimmed.slice(0, 120);
124
+ }
125
+
126
+ // Enough material for the AI
127
+ if (parts.length >= 5) break;
128
+ }
129
+
130
+ if (firstParagraph) parts.push(firstParagraph);
131
+ return parts.join(" ").slice(0, 500) || content.slice(0, 500);
132
+ }
133
+
112
134
  // ---------------------------------------------------------------------------
113
135
  // Plan lookup
114
136
  // ---------------------------------------------------------------------------
@@ -121,15 +143,15 @@ export async function archivePlan(
121
143
  export function findLatestPlan(
122
144
  contextId: string,
123
145
  projectRoot?: string,
124
- ): string | null {
146
+ ): null | string {
125
147
  // 1. Check state.json plan_path first
126
148
  try {
127
149
  const state = readStateJson(contextId, projectRoot);
128
150
  if (state?.plan_path && fs.existsSync(state.plan_path)) {
129
151
  return state.plan_path;
130
152
  }
131
- } catch (e: any) {
132
- logWarn("plan_manager", `Failed to check state.json plan_path: ${e}`);
153
+ } catch (error: any) {
154
+ logWarn("plan_manager", `Failed to check state.json plan_path: ${error}`);
133
155
  }
134
156
 
135
157
  // 2. Fall back to most recent .md in plans/ dir
@@ -162,7 +184,7 @@ export function findLatestPlan(
162
184
  * See SPEC.md §9.4
163
185
  */
164
186
  export function generatePlanId(): string {
165
- return crypto.randomUUID().replace(/-/g, "").slice(0, 8);
187
+ return crypto.randomUUID().replaceAll('-', "").slice(0, 8);
166
188
  }
167
189
 
168
190
  /**
@@ -171,8 +193,8 @@ export function generatePlanId(): string {
171
193
  * See SPEC.md §9.5
172
194
  */
173
195
  export function normalizePlanContent(text: string): string {
174
- let result = text.replace(/<[^>]+>/g, "");
175
- result = result.replace(/\s+/g, " ").trim();
196
+ let result = text.replaceAll(/<[^>]+>/g, "");
197
+ result = result.replaceAll(/\s+/g, " ").trim();
176
198
  return result;
177
199
  }
178
200
 
@@ -190,8 +212,10 @@ export function extractPlanAnchors(content: string, maxAnchors = 5): string[] {
190
212
  } else if (anchors.length === 0 && trimmed.length > 20) {
191
213
  anchors.push(trimmed.slice(0, 80));
192
214
  }
215
+
193
216
  if (anchors.length >= maxAnchors) break;
194
217
  }
218
+
195
219
  return anchors;
196
220
  }
197
221
 
@@ -206,7 +230,7 @@ const MAX_TRANSCRIPT_SIZE = 50 * 1024 * 1024; // 50 MB
206
230
  * Searches in reverse for the most recent Write tool call targeting .claude/plans/.
207
231
  * See SPEC.md §9.7
208
232
  */
209
- export function findPlanPathInTranscript(transcriptPath: string): string | null {
233
+ export function findPlanPathInTranscript(transcriptPath: string): null | string {
210
234
  if (!transcriptPath) return null;
211
235
 
212
236
  if (!fs.existsSync(transcriptPath)) {
@@ -228,9 +252,9 @@ export function findPlanPathInTranscript(transcriptPath: string): string | null
228
252
 
229
253
  let lines: string[];
230
254
  try {
231
- lines = fs.readFileSync(transcriptPath, "utf-8").split("\n");
232
- } catch (e: any) {
233
- logWarn("plan_manager", `Failed to read transcript: ${e}`);
255
+ lines = fs.readFileSync(transcriptPath, "utf8").split("\n");
256
+ } catch (error: any) {
257
+ logWarn("plan_manager", `Failed to read transcript: ${error}`);
234
258
  return null;
235
259
  }
236
260
 
@@ -262,7 +286,7 @@ export function findPlanPathInTranscript(transcriptPath: string): string | null
262
286
  if (!filePath) continue;
263
287
 
264
288
  // Check if path contains .claude/plans/ as consecutive parts
265
- const parts = filePath.replace(/\\/g, "/").split("/");
289
+ const parts = filePath.replaceAll('\\', "/").split("/");
266
290
  for (let j = 0; j < parts.length - 1; j++) {
267
291
  if (parts[j] === ".claude" && parts[j + 1] === "plans") {
268
292
  logInfo("plan_manager", `Extracted plan path from transcript: ${filePath}`);
@@ -285,7 +309,7 @@ export function findPlanPathInTranscript(transcriptPath: string): string | null
285
309
  * Parses the pattern: "Your plan has been saved to: <path>"
286
310
  * See SPEC.md §9.8
287
311
  */
288
- export function extractPlanPathFromResult(toolResult: string): string | null {
312
+ export function extractPlanPathFromResult(toolResult: string): null | string {
289
313
  if (!toolResult) return null;
290
314
  const match = toolResult.match(/Your plan has been saved to:\s*(.+\.md)/);
291
315
  return match ? match[1]!.trim() : null;
@@ -6,10 +6,10 @@
6
6
  * Uses state-io for I/O to avoid circular imports with context-store.
7
7
  */
8
8
 
9
- import { readStateJson, writeStateJson, toDict } from "../base/state-io.js";
10
9
  import { logWarn } from "../base/logger.js";
10
+ import { readStateJson, toDict as _toDict, writeStateJson } from "../base/state-io.js";
11
11
  import { nowIso } from "../base/utils.js";
12
- import type { ContextState, Task } from "../types.js";
12
+ import type { ContextState as _ContextState, Task } from "../types.js";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Public API
@@ -27,7 +27,7 @@ export function generateNextTaskId(contextId: string, projectRoot?: string): str
27
27
  for (const t of tasks) {
28
28
  const match = /^aiw-(\d+)$/.exec(t.id);
29
29
  if (match) {
30
- const num = parseInt(match[1]!, 10);
30
+ const num = Number.parseInt(match[1]!, 10);
31
31
  if (num > maxNum) maxNum = num;
32
32
  }
33
33
  }
@@ -46,7 +46,7 @@ export function addTask(
46
46
  activeForm = "",
47
47
  sessionId = "",
48
48
  projectRoot?: string,
49
- ): Task | null {
49
+ ): null | Task {
50
50
  const state = readStateJson(contextId, projectRoot);
51
51
  if (!state) return null;
52
52
 
@@ -80,11 +80,11 @@ export function updateTask(
80
80
  contextId: string,
81
81
  taskId: string,
82
82
  opts?: {
83
- status?: string;
84
83
  evidence?: string;
85
- work_summary?: string;
86
84
  files_changed?: string[];
87
85
  session_id?: string;
86
+ status?: string;
87
+ work_summary?: string;
88
88
  },
89
89
  projectRoot?: string,
90
90
  ): boolean {
@@ -99,6 +99,7 @@ export function updateTask(
99
99
  task.completed_at = nowIso();
100
100
  }
101
101
  }
102
+
102
103
  if (opts?.evidence) task.evidence = opts.evidence;
103
104
  if (opts?.work_summary) task.work_summary = opts.work_summary;
104
105
  if (opts?.files_changed !== undefined) task.files_changed = opts.files_changed;
@@ -167,12 +168,15 @@ export function generateTaskSummary(contextId: string, projectRoot?: string): st
167
168
  const ws = t.work_summary ? `\n Work: ${t.work_summary}` : "";
168
169
  lines.push(`- [x] ${t.id}: ${t.subject}${ws}`);
169
170
  }
171
+
170
172
  for (const t of inProgress) {
171
173
  lines.push(`- [~] ${t.id}: ${t.subject}`);
172
174
  }
175
+
173
176
  for (const t of pending) {
174
177
  lines.push(`- [ ] ${t.id}: ${t.subject}`);
175
178
  }
179
+
176
180
  for (const t of blocked) {
177
181
  lines.push(`- [!] ${t.id}: ${t.subject}`);
178
182
  }
@@ -6,17 +6,18 @@
6
6
  * work to a new session (typically due to context window limits).
7
7
  */
8
8
 
9
+ import * as crypto from "node:crypto";
9
10
  import * as fs from "node:fs";
10
11
  import * as path from "node:path";
11
- import * as crypto from "node:crypto";
12
- import { getContextHandoffsDir, getContextDir } from "../base/constants.js";
12
+
13
13
  import { atomicWrite } from "../base/atomic-write.js";
14
- import { logInfo, logError } from "../base/logger.js";
14
+ import { getContextDir, getContextHandoffsDir } from "../base/constants.js";
15
+ import { logError, logInfo } from "../base/logger.js";
15
16
  import { nowIso } from "../base/utils.js";
16
- import { getContext, saveState } from "../context/context-store.js";
17
+ import { getContext, saveState as _saveState } from "../context/context-store.js";
17
18
  import { getTasks } from "../context/task-tracker.js";
18
- import { renderTaskList, formatContinuationHeader, formatReason } from "../templates/formatters.js";
19
- import type { HandoffDocument, Task } from "../types.js";
19
+ import { formatContinuationHeader, formatReason, renderTaskList } from "../templates/formatters.js";
20
+ import type { HandoffDocument, Task as _Task } from "../types.js";
20
21
 
21
22
  /**
22
23
  * Generate and save a handoff document for a context.
@@ -124,8 +125,7 @@ function renderHandoffMarkdown(doc: HandoffDocument): string {
124
125
  );
125
126
 
126
127
  // Active tasks
127
- lines.push(renderTaskList(doc.active_tasks, "Active Tasks", true).trimEnd());
128
- lines.push("");
128
+ lines.push(renderTaskList(doc.active_tasks, "Active Tasks", true).trimEnd(), "");
129
129
 
130
130
  // Completed this session
131
131
  if (doc.completed_tasks_this_session.length > 0) {
@@ -133,8 +133,7 @@ function renderHandoffMarkdown(doc: HandoffDocument): string {
133
133
  doc.completed_tasks_this_session as any[],
134
134
  "Completed This Session",
135
135
  false,
136
- ).trimEnd());
137
- lines.push("");
136
+ ).trimEnd(), "");
138
137
  }
139
138
 
140
139
  // Work summary
@@ -148,6 +147,7 @@ function renderHandoffMarkdown(doc: HandoffDocument): string {
148
147
  for (let i = 0; i < doc.next_steps.length; i++) {
149
148
  lines.push(`${i + 1}. ${doc.next_steps[i]}`);
150
149
  }
150
+
151
151
  lines.push("");
152
152
  }
153
153
 
@@ -157,6 +157,7 @@ function renderHandoffMarkdown(doc: HandoffDocument): string {
157
157
  for (const note of doc.important_notes) {
158
158
  lines.push(`- ${note}`);
159
159
  }
160
+
160
161
  lines.push("");
161
162
  }
162
163