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
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { sanitizeTitle } from "./constants.js";
7
+ import { logDebug, logError, logWarn } from "./logger.js";
7
8
  import { STOP_WORDS } from "./stop-words.js";
8
- import { logDebug, logWarn, logError } from "./logger.js";
9
9
 
10
10
  /**
11
11
  * Print to stderr. For terminal-only UX messages, not diagnostics.
@@ -77,22 +77,79 @@ export function parseIsoTimestamp(isoStr: string): Date | null {
77
77
  export function cleanTextForSlug(text: string): string {
78
78
  if (!text) return "";
79
79
  let result = text.toLowerCase();
80
- result = result.replace(/'/g, ""); // i'm -> im, you're -> youre
81
- result = result.replace(/[^a-z0-9\s]/g, " "); // punctuation -> spaces
82
- result = result.replace(/\s+/g, " ").trim();
80
+ result = result.replaceAll('\'', ""); // i'm -> im, you're -> youre
81
+ result = result.replaceAll(/[^a-z0-9\s]/g, " "); // punctuation -> spaces
82
+ result = result.replaceAll(/\s+/g, " ").trim();
83
83
  return result;
84
84
  }
85
85
 
86
+ /**
87
+ * Generate a slug from text using AI inference with stop-word fallbacks.
88
+ * Pipeline: AI inference → stop-word post-filter → stop-word fallback → word-length fallback.
89
+ * Reusable by both context ID generation and plan archival.
90
+ * See SPEC.md §14.2
91
+ */
92
+ export function generateSlug(
93
+ text: string,
94
+ maxLen = 150,
95
+ fallbackSlug = "context",
96
+ ): string {
97
+ if (!text || !text.trim()) return fallbackSlug;
98
+
99
+ let slug: null | string = null;
100
+ const cleanedText = cleanTextForSlug(text);
101
+
102
+ // Tier 1: AI inference via generateContextIdSlug (sync — uses execFileSync)
103
+ try {
104
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef
105
+ const { generateContextIdSlug } = require("./inference.js");
106
+ const aiSlug = generateContextIdSlug(text);
107
+ if (aiSlug) {
108
+ const filteredWords = aiSlug
109
+ .split(/\s+/)
110
+ .filter(
111
+ (w: string) => !STOP_WORDS.has(w.toLowerCase()) && w.length > 1,
112
+ );
113
+ if (filteredWords.length >= 5) {
114
+ slug = sanitizeTitle(filteredWords.join(" "), maxLen);
115
+ } else {
116
+ logDebug(
117
+ "utils",
118
+ `AI slug too generic after stop-word filter (${filteredWords.length} words remain), using fallback`,
119
+ );
120
+ }
121
+ }
122
+ } catch (error: any) {
123
+ logWarn("utils", `AI slug generation failed, using fallback: ${error}`);
124
+ }
125
+
126
+ // Tier 2: Stop-word filtering on cleaned text
127
+ if (!slug) {
128
+ const words = cleanedText
129
+ .split(/\s+/)
130
+ .filter((w) => !STOP_WORDS.has(w) && w.length > 1)
131
+ .slice(0, 12);
132
+ slug = words.length >= 3
133
+ ? sanitizeTitle(words.join(" "), maxLen)
134
+ : sanitizeTitle(
135
+ cleanedText.split(/\s+/).filter((w) => w.length > 2).slice(0, 6).join(" "),
136
+ maxLen,
137
+ ) || fallbackSlug;
138
+ }
139
+
140
+ return slug;
141
+ }
142
+
86
143
  /**
87
144
  * Generate a context ID from a summary string.
88
145
  * Format: YYMMDD-HHMM-slug
89
- * 3-tier slug: AI inference → stop-word filter → simple word filter
146
+ * Delegates slug generation to generateSlug().
90
147
  * See SPEC.md §14.2
91
148
  */
92
- export async function generateContextId(
149
+ export function generateContextId(
93
150
  summary: string,
94
151
  existingIds?: Set<string>,
95
- ): Promise<string> {
152
+ ): string {
96
153
  const now = new Date();
97
154
  const yy = String(now.getFullYear()).slice(2);
98
155
  const mm = String(now.getMonth() + 1).padStart(2, "0");
@@ -104,70 +161,12 @@ export async function generateContextId(
104
161
  let baseId: string;
105
162
 
106
163
  try {
107
- if (!summary || !summary.trim()) {
108
- baseId = `${timestamp}-context`;
109
- } else {
110
- let slug: string | null = null;
111
- // Pre-clean for Tier 2/3: strip punctuation so stop words match
112
- const cleanedSummary = cleanTextForSlug(summary);
113
-
114
- // Tier 1: AI inference (imported dynamically to avoid circular deps)
115
- try {
116
- const { generateContextIdSlug } = await import("./inference.js");
117
- const aiSlug = generateContextIdSlug(summary);
118
- if (aiSlug) {
119
- const filteredWords = aiSlug
120
- .split(/\s+/)
121
- .filter(
122
- (w: string) => !STOP_WORDS.has(w.toLowerCase()) && w.length > 1,
123
- );
124
- if (filteredWords.length >= 5) {
125
- slug = sanitizeTitle(filteredWords.join(" "), 150);
126
- } else {
127
- logDebug(
128
- "utils",
129
- `AI slug too generic after stop-word filter (${filteredWords.length} words remain), using fallback`,
130
- );
131
- }
132
- }
133
- } catch (e: any) {
134
- logWarn("utils", `AI context ID slug failed, using fallback: ${e}`);
135
- }
136
-
137
- // Tier 2: Stop-word filtering on cleaned text
138
- if (!slug) {
139
- try {
140
- const words = cleanedSummary
141
- .split(/\s+/)
142
- .filter((w) => !STOP_WORDS.has(w) && w.length > 1)
143
- .slice(0, 12);
144
- if (words.length >= 3) {
145
- slug = sanitizeTitle(words.join(" "), 150);
146
- } else {
147
- logDebug("utils", `Tier 2 too few content words (${words.length}), falling through to Tier 3`);
148
- }
149
- } catch (e: any) {
150
- logWarn("utils", `Stop-word fallback failed: ${e}`);
151
- }
152
- }
153
-
154
- // Tier 3: Simple word-length filter on cleaned text
155
- if (!slug || slug === "unknown") {
156
- const words = cleanedSummary
157
- .split(/\s+/)
158
- .filter((w) => w.length > 2)
159
- .slice(0, 6);
160
- slug = words.length > 0
161
- ? sanitizeTitle(words.join(" "), 150)
162
- : "context";
163
- }
164
-
165
- baseId = `${timestamp}-${slug}`;
166
- }
167
- } catch (e: any) {
164
+ const slug = generateSlug(summary);
165
+ baseId = `${timestamp}-${slug}`;
166
+ } catch (error: any) {
168
167
  logError(
169
168
  "utils",
170
- `Context ID generation failed entirely, using timestamp: ${e}`,
169
+ `Context ID generation failed entirely, using timestamp: ${error}`,
171
170
  );
172
171
  baseId = `${timestamp}-context`;
173
172
  }
@@ -180,5 +179,6 @@ export async function generateContextId(
180
179
  while (existingIds.has(`${baseId}-${counter}`)) {
181
180
  counter++;
182
181
  }
182
+
183
183
  return `${baseId}-${counter}`;
184
184
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import * as fs from "node:fs";
11
- import * as path from "node:path";
11
+ import * as _path from "node:path";
12
+
12
13
  import { parseIsoTimestamp } from "../base/utils.js";
13
14
  import type { ContextState, Task } from "../types.js";
14
15
 
@@ -41,10 +42,10 @@ export function getModeDisplay(mode: string): string {
41
42
  * Format ISO timestamp as '2 hours ago', 'yesterday', etc.
42
43
  * See SPEC.md §11.3
43
44
  */
44
- export function formatRelativeTime(isoTimestamp: string | null): string {
45
+ export function formatRelativeTime(isoTimestamp: null | string): string {
45
46
  if (!isoTimestamp) return "unknown";
46
47
 
47
- let dt = parseIsoTimestamp(isoTimestamp);
48
+ const dt = parseIsoTimestamp(isoTimestamp);
48
49
  if (!dt) return isoTimestamp.slice(0, 16);
49
50
 
50
51
  const now = new Date();
@@ -61,8 +62,10 @@ export function formatRelativeTime(isoTimestamp: string | null): string {
61
62
  if (diffMin === 0) return "just now";
62
63
  return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
63
64
  }
65
+
64
66
  return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
65
67
  }
68
+
66
69
  if (diffDays === 1) return "yesterday";
67
70
  if (diffDays < 7) return `${diffDays} days ago`;
68
71
 
@@ -77,21 +80,23 @@ export function formatRelativeTime(isoTimestamp: string | null): string {
77
80
  // Internal helpers
78
81
  // ---------------------------------------------------------------------------
79
82
 
80
- function taskAttr(task: Task | Record<string, any>, key: string, defaultVal = ""): string {
83
+ function taskAttr(task: Record<string, any> | Task, key: string, defaultVal = ""): string {
81
84
  if (typeof task === "object" && task !== null) {
82
85
  return (task as any)[key] ?? defaultVal;
83
86
  }
87
+
84
88
  return defaultVal;
85
89
  }
86
90
 
87
- function readPlanContent(planPath: string): [string | null, boolean, number] {
91
+ function readPlanContent(planPath: string): [null | string, boolean, number] {
88
92
  try {
89
93
  if (!fs.existsSync(planPath)) return [null, false, 0];
90
- const content = fs.readFileSync(planPath, "utf-8");
94
+ const content = fs.readFileSync(planPath, "utf8");
91
95
  const total = content.length;
92
96
  if (total > MAX_PLAN_INLINE_CHARS) {
93
97
  return [content.slice(0, MAX_PLAN_INLINE_CHARS), true, total];
94
98
  }
99
+
95
100
  return [content, false, total];
96
101
  } catch {
97
102
  return [null, false, 0];
@@ -100,7 +105,7 @@ function readPlanContent(planPath: string): [string | null, boolean, number] {
100
105
 
101
106
  function modeLabel(ctx: ContextState): string {
102
107
  const d = getModeDisplay(ctx.mode ?? "idle");
103
- return d ? d.replace(/^\[|\]$/g, "") : "Active";
108
+ return d ? d.replaceAll(/^\[|\]$/g, "") : "Active";
104
109
  }
105
110
 
106
111
  /**
@@ -119,7 +124,7 @@ export function buildRestoreSections(
119
124
  const savedAt = lastSession.saved_at ?? "";
120
125
  if (savedAt) {
121
126
  const reason = lastSession.save_reason ?? "";
122
- const reasonDisplay = reason ? reason.replace(/_/g, " ") : "unknown";
127
+ const reasonDisplay = reason ? reason.replaceAll('_', " ") : "unknown";
123
128
  sections.push(`**Last session ended:** ${formatRelativeTime(savedAt)} (${reasonDisplay})`);
124
129
  }
125
130
  }
@@ -138,6 +143,7 @@ export function buildRestoreSections(
138
143
  buckets[s]!.push(taskAttr(t, "subject"));
139
144
  }
140
145
  }
146
+
141
147
  if (Object.values(buckets).some(b => b.length > 0)) {
142
148
  sections.push("", `### Previous Work (${tasks.length} tasks)`, "");
143
149
  const marks: Record<string, string> = {
@@ -195,8 +201,7 @@ function resumeBlock(ctx: ContextState, projectRoot: string | undefined, modeTex
195
201
  ];
196
202
  const restore = buildRestoreSections(ctx, projectRoot, true);
197
203
  if (restore) lines.push(restore);
198
- lines.push("", "---", "", "**Instructions:**");
199
- lines.push(...instructions);
204
+ lines.push("", "---", "", "**Instructions:**", ...instructions);
200
205
  return lines.join("\n");
201
206
  }
202
207
 
@@ -218,12 +223,12 @@ export function formatHandoffContinuation(ctx: ContextState, projectRoot?: strin
218
223
 
219
224
  try {
220
225
  if (handoffPath && fs.existsSync(handoffPath)) {
221
- lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf-8"), "");
226
+ lines.push("### Previous Session Handoff", "", fs.readFileSync(handoffPath, "utf8"), "");
222
227
  } else {
223
228
  lines.push(`*Handoff document not found at \`${handoffPath}\`*`, "");
224
229
  }
225
- } catch (e: any) {
226
- lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${e}*`, "");
230
+ } catch (error: any) {
231
+ lines.push(`*Handoff document at \`${handoffPath}\` could not be read: ${error}*`, "");
227
232
  }
228
233
 
229
234
  const restore = buildRestoreSections(ctx, projectRoot, true);
@@ -266,20 +271,21 @@ export function formatContextList(contexts: ContextState[]): string {
266
271
  if (contexts.length === 0) return "No active contexts found.";
267
272
 
268
273
  const lines = ["## Active Contexts\n"];
269
- for (let i = 0; i < contexts.length; i++) {
270
- const ctx = contexts[i]!;
274
+ for (const [i, context_] of contexts.entries()) {
275
+ const ctx = context_!;
271
276
  const timeStr = formatRelativeTime(ctx.last_active);
272
277
  const md = getModeDisplay(ctx.mode ?? "idle");
273
278
  const si = md ? ` ${md}` : "";
274
- lines.push(`**${i + 1}. ${ctx.id}**${si}`);
275
- lines.push(` ${ctx.summary}`);
279
+ lines.push(`**${i + 1}. ${ctx.id}**${si}`, ` ${ctx.summary}`);
276
280
  if (ctx.method) {
277
281
  lines.push(` Method: ${ctx.method} | Last active: ${timeStr}`);
278
282
  } else {
279
283
  lines.push(` Last active: ${timeStr}`);
280
284
  }
285
+
281
286
  lines.push("");
282
287
  }
288
+
283
289
  return lines.join("\n");
284
290
  }
285
291
 
@@ -349,11 +355,11 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
349
355
  ];
350
356
 
351
357
  let selectableCount = 0;
352
- for (let i = 0; i < contexts.length; i++) {
353
- const ctx = contexts[i]!;
358
+ for (const [i, context_] of contexts.entries()) {
359
+ const ctx = context_!;
354
360
  const timeStr = formatRelativeTime(ctx.last_active);
355
361
  const mode = ctx.mode ?? "idle";
356
- const isSelectable = mode === "active" || !!ctx.handoff_path;
362
+ const isSelectable = mode === "active" || Boolean(ctx.handoff_path);
357
363
  if (isSelectable) selectableCount++;
358
364
 
359
365
  let status = "";
@@ -366,10 +372,7 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
366
372
  const summary = ctx.summary.length > 48 ? ctx.summary.slice(0, 45) + "..." : ctx.summary;
367
373
  const selTag = isSelectable ? " [selectable]" : " [end only]";
368
374
 
369
- lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`);
370
- lines.push(`| ${summary}`);
371
- lines.push(`| [${timeStr}]`);
372
- lines.push("|");
375
+ lines.push(`| ^${i + 1} ${ctx.id}${status}${selTag}`, `| ${summary}`, `| [${timeStr}]`, "|");
373
376
  }
374
377
 
375
378
  lines.push(
@@ -394,6 +397,7 @@ export function formatContextPickerStderr(contexts: ContextState[]): string {
394
397
  "+----------------------------------------------------------------+",
395
398
  );
396
399
  }
400
+
397
401
  lines.push("");
398
402
  return lines.join("\n");
399
403
  }
@@ -413,6 +417,7 @@ export function formatCommandFeedback(
413
417
  const s = ctx.summary.length > 50 ? ctx.summary.slice(0, 50) + "..." : ctx.summary;
414
418
  lines.push(`- **${ctx.id}**: ${s}`);
415
419
  }
420
+
416
421
  lines.push("");
417
422
  }
418
423
 
@@ -428,5 +433,6 @@ export function formatCommandFeedback(
428
433
  "Tasks created with TaskCreate will be persisted to this context.",
429
434
  );
430
435
  }
436
+
431
437
  return lines.join("\n");
432
438
  }
@@ -14,29 +14,30 @@
14
14
  */
15
15
 
16
16
  import * as crypto from "node:crypto";
17
- import {
18
- getContext,
19
- getAllContexts,
20
- getContextBySessionId,
21
- createContextFromPrompt,
22
- createContext,
23
- completeContext,
24
- bindSession,
25
- updateMode,
26
- } from "./context-store.js";
17
+
27
18
  import {
28
19
  formatActiveContextReminder,
20
+ formatActiveContinuation as _formatActiveContinuation,
21
+ formatCommandFeedback,
29
22
  formatContextCreated,
30
23
  formatContextPickerStderr,
31
- formatCommandFeedback,
32
24
  formatHandoffContinuation,
33
25
  formatPlanContinuation,
34
- formatActiveContinuation,
35
26
  } from "./context-formatter.js";
27
+ import {
28
+ bindSession,
29
+ completeContext,
30
+ createContext,
31
+ createContextFromPrompt,
32
+ getAllContexts,
33
+ getContext,
34
+ getContextBySessionId,
35
+ updateMode,
36
+ } from "./context-store.js";
36
37
  import { normalizePlanContent } from "./plan-manager.js";
38
+ import { logDebug, logError, logInfo, logWarn as _logWarn } from "../base/logger.js";
37
39
  import { isInternalCall } from "../base/subprocess-utils.js";
38
- import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
39
- import type { ContextState, CaretCommand } from "../types.js";
40
+ import type { CaretCommand, ContextState } from "../types.js";
40
41
 
41
42
  /** Minimum characters required for new context description. */
42
43
  const MIN_NEW_CONTEXT_CHARS = 10;
@@ -65,7 +66,7 @@ export class BlockRequest extends Error {
65
66
  export function resolveContextByPrefix(
66
67
  query: string,
67
68
  contexts: ContextState[],
68
- ): [number | null, string | null] {
69
+ ): [null | number, null | string] {
69
70
  const q = query.toLowerCase();
70
71
  const available = contexts.map(c => c.id).join(", ");
71
72
 
@@ -107,7 +108,7 @@ export function resolveContextByPrefix(
107
108
  export function parseChainedCaret(
108
109
  prompt: string,
109
110
  contexts: ContextState[],
110
- ): [CaretCommand | null, string | null] {
111
+ ): [CaretCommand | null, null | string] {
111
112
  if (!prompt.startsWith("^")) return [null, null];
112
113
 
113
114
  const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
@@ -120,7 +121,7 @@ export function parseChainedCaret(
120
121
 
121
122
  // ^N shorthand
122
123
  if (/^\d+$/.test(commandStr)) {
123
- const num = parseInt(commandStr, 10);
124
+ const num = Number.parseInt(commandStr, 10);
124
125
  if (num === 0) {
125
126
  if (remaining.length < MIN_NEW_CONTEXT_CHARS) {
126
127
  return [null,
@@ -130,21 +131,25 @@ export function parseChainedCaret(
130
131
  `Example: ^0 implement user authentication with JWT tokens`,
131
132
  ];
132
133
  }
134
+
133
135
  return [{ ends: [], select: null, new_context_desc: remaining, remaining_prompt: "" }, null];
134
136
  }
137
+
135
138
  if (num < 1 || num > contexts.length) {
136
139
  if (contexts.length === 0) {
137
140
  return [null, "No existing contexts. Use ^0 <description> to create a new one."];
138
141
  }
142
+
139
143
  return [null, `Invalid selection. Choose 1-${contexts.length} for existing contexts, or ^0 for new.`];
140
144
  }
145
+
141
146
  const ctx = contexts[num - 1]!;
142
147
  return [{ ends: [], select: ctx.id, new_context_desc: null, remaining_prompt: remaining }, null];
143
148
  }
144
149
 
145
150
  // Parse chained commands
146
151
  const ends: string[] = [];
147
- let select: string | null = null;
152
+ let select: null | string = null;
148
153
  let pos = 0;
149
154
 
150
155
  while (pos < commandStr.length) {
@@ -174,11 +179,13 @@ export function parseChainedCaret(
174
179
  if (numStart === pos) {
175
180
  return [null, `Expected number, '*', or ':prefix' after 'E' at position ${numStart + 1}`];
176
181
  }
177
- const num = parseInt(commandStr.slice(numStart, pos), 10);
182
+
183
+ const num = Number.parseInt(commandStr.slice(numStart, pos), 10);
178
184
  if (num < 1 || num > contexts.length) {
179
185
  if (contexts.length === 0) return [null, "No contexts to end."];
180
186
  return [null, `Context ^E${num} invalid. Choose 1-${contexts.length}.`];
181
187
  }
188
+
182
189
  if (pos < commandStr.length && commandStr[pos] === "+") {
183
190
  pos++;
184
191
  for (let i = num; i <= contexts.length; i++) {
@@ -208,13 +215,16 @@ export function parseChainedCaret(
208
215
  if (numStart === pos) {
209
216
  return [null, `Expected number or ':prefix' after 'S' at position ${numStart + 1}`];
210
217
  }
211
- const num = parseInt(commandStr.slice(numStart, pos), 10);
218
+
219
+ const num = Number.parseInt(commandStr.slice(numStart, pos), 10);
212
220
  if (num < 1 || num > contexts.length) {
213
221
  if (contexts.length === 0) return [null, "No contexts to select."];
214
222
  return [null, `Context ^S${num} invalid. Choose 1-${contexts.length}.`];
215
223
  }
224
+
216
225
  ctx = contexts[num - 1]!;
217
226
  }
227
+
218
228
  if (select === null) select = ctx.id;
219
229
  } else {
220
230
  return [null,
@@ -274,9 +284,9 @@ function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): Cont
274
284
  }
275
285
 
276
286
  // Tier 4 (legacy): Signature match
277
- const promptHead = prompt.slice(0, 500);
287
+ const promptHead = new Set(prompt.slice(0, 500));
278
288
  for (const ctx of hasPlanContexts) {
279
- if (ctx.plan_signature && promptHead.includes(ctx.plan_signature)) {
289
+ if (ctx.plan_signature && promptHead.has(ctx.plan_signature)) {
280
290
  logDebug("context_selector", `Tier 4 legacy signature match: ${ctx.id}`);
281
291
  return ctx;
282
292
  }
@@ -292,15 +302,15 @@ function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): Cont
292
302
  function createNewContext(
293
303
  prompt: string,
294
304
  projectRoot?: string,
295
- ): [string | null, string, string | null] {
305
+ ): [null | string, string, null | string] {
296
306
  try {
297
307
  const newCtx = createContextFromPrompt(prompt, projectRoot);
298
308
  updateMode(newCtx.id, "active", projectRoot);
299
309
  newCtx.mode = "active";
300
310
  logInfo("context_selector", `Auto-created context: ${newCtx.id}`);
301
311
  return [newCtx.id, "auto_created", formatContextCreated(newCtx)];
302
- } catch (e: any) {
303
- logError("context_selector", `Primary context creation failed: ${e}`);
312
+ } catch (error: any) {
313
+ logError("context_selector", `Primary context creation failed: ${error}`);
304
314
  try {
305
315
  const now = new Date();
306
316
  const yy = String(now.getFullYear()).slice(2);
@@ -320,8 +330,8 @@ function createNewContext(
320
330
  newCtx.mode = "active";
321
331
  logInfo("context_selector", `Fallback context created: ${newCtx.id}`);
322
332
  return [newCtx.id, "auto_created_fallback", formatContextCreated(newCtx)];
323
- } catch (e2: any) {
324
- logError("context_selector", `ALL context creation failed: ${e2}`);
333
+ } catch (error: any) {
334
+ logError("context_selector", `ALL context creation failed: ${error}`);
325
335
  return [null, "creation_failed", null];
326
336
  }
327
337
  }
@@ -335,7 +345,7 @@ function handleCaretCommand(
335
345
  prompt: string,
336
346
  contexts: ContextState[],
337
347
  projectRoot?: string,
338
- ): [string | null, string, string | null] {
348
+ ): [null | string, string, null | string] {
339
349
  if (contexts.length === 0) {
340
350
  const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
341
351
  if (!match) {
@@ -344,14 +354,16 @@ function handleCaretCommand(
344
354
  "Example: ^0 implement user authentication system",
345
355
  );
346
356
  }
357
+
347
358
  const prefixValue = match[1]!;
348
359
  const remaining = match[2] ?? "";
349
- if (!/^\d+$/.test(prefixValue) || parseInt(prefixValue, 10) !== 0) {
360
+ if (!/^\d+$/.test(prefixValue) || Number.parseInt(prefixValue, 10) !== 0) {
350
361
  throw new BlockRequest(
351
362
  "No existing contexts to select. Use ^0 <description> to create a new context.\n" +
352
363
  "Example: ^0 implement user authentication system",
353
364
  );
354
365
  }
366
+
355
367
  const description = remaining.trim();
356
368
  if (description.length < MIN_NEW_CONTEXT_CHARS) {
357
369
  throw new BlockRequest(
@@ -361,6 +373,7 @@ function handleCaretCommand(
361
373
  `Example: ^0 implement user authentication with JWT tokens`,
362
374
  );
363
375
  }
376
+
364
377
  return createNewContext(description, projectRoot);
365
378
  }
366
379
 
@@ -374,6 +387,7 @@ function handleCaretCommand(
374
387
  if (!ctxToEnd) {
375
388
  throw new BlockRequest(`Context '${ctxId}' no longer exists.\n` + formatContextPickerStderr(contexts));
376
389
  }
390
+
377
391
  completeContext(ctxToEnd.id, projectRoot);
378
392
  endedContexts.push(ctxToEnd);
379
393
  logInfo("context_selector", `Ended context: ${ctxToEnd.id}`);
@@ -384,9 +398,10 @@ function handleCaretCommand(
384
398
  if (ctxId && endedContexts.length > 0) {
385
399
  const newCtx = getContext(ctxId, projectRoot);
386
400
  const feedback = formatCommandFeedback(endedContexts, newCtx);
387
- return [ctxId, method !== "creation_failed" ? "caret_new" : method, feedback];
401
+ return [ctxId, method === "creation_failed" ? method : "caret_new", feedback];
388
402
  }
389
- return [ctxId, method !== "creation_failed" ? "caret_new" : method, output];
403
+
404
+ return [ctxId, method === "creation_failed" ? method : "caret_new", output];
390
405
  }
391
406
 
392
407
  if (cmd.select) {
@@ -394,6 +409,7 @@ function handleCaretCommand(
394
409
  if (!selectedCtx) {
395
410
  throw new BlockRequest(`Context '${cmd.select}' no longer exists.\n` + formatContextPickerStderr(contexts));
396
411
  }
412
+
397
413
  logInfo("context_selector", `Caret-selected context: ${selectedCtx.id}`);
398
414
  return [selectedCtx.id, "caret_select", formatCommandFeedback(endedContexts, selectedCtx)];
399
415
  }
@@ -408,6 +424,7 @@ function handleCaretCommand(
408
424
  "Example: implement user authentication system",
409
425
  );
410
426
  }
427
+
411
428
  throw new BlockRequest(
412
429
  feedback + "\nNo context selected.\n\nSelect a context to continue:\n" +
413
430
  formatContextPickerStderr(remainingContexts),
@@ -432,7 +449,7 @@ export function determineContext(
432
449
  prompt: string,
433
450
  sessionId?: string,
434
451
  projectRoot?: string,
435
- ): [string | null, string, string | null] {
452
+ ): [null | string, string, null | string] {
436
453
  if (isInternalCall()) {
437
454
  logDebug("context_selector", "Skipping: internal subprocess call");
438
455
  return [null, "skip_internal", null];
@@ -460,6 +477,7 @@ export function determineContext(
460
477
  "Example: implement user authentication system",
461
478
  );
462
479
  }
480
+
463
481
  throw new BlockRequest(formatContextPickerStderr(contexts));
464
482
  }
465
483