aiwcli 0.10.3 → 0.11.1

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 (191) 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 +107 -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/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -3,6 +3,8 @@
3
3
  * See SPEC.md §5.10
4
4
  */
5
5
 
6
+ import { execSync, execFile } from "node:child_process";
7
+
6
8
  /**
7
9
  * Check if this is an internal subprocess call.
8
10
  * All hooks should check this and return early to prevent recursion.
@@ -21,3 +23,143 @@ export function getInternalSubprocessEnv(): Record<string, string | undefined> {
21
23
  AIWCLI_INTERNAL_CALL: "true",
22
24
  };
23
25
  }
26
+
27
+ /**
28
+ * Find an executable on the system PATH.
29
+ * Uses `where` on Windows, `which` on Unix.
30
+ * On Windows, prefers .cmd/.exe over extensionless shims since
31
+ * execFileSync cannot spawn extensionless shell scripts.
32
+ * Returns the first match or null if not found.
33
+ */
34
+ export function findExecutable(name: string): string | null {
35
+ try {
36
+ const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
37
+ const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true })
38
+ .trim()
39
+ .split(/\r?\n/)
40
+ .map((l) => l.trim())
41
+ .filter(Boolean);
42
+
43
+ if (lines.length === 0) return null;
44
+
45
+ // On Windows, `where` may return an extensionless shim first (e.g. npm creates
46
+ // both `claude` and `claude.cmd`). execFileSync can't spawn the extensionless
47
+ // one, so prefer .cmd or .exe.
48
+ if (process.platform === "win32") {
49
+ const preferred = lines.find((l) => /\.(cmd|exe)$/i.test(l));
50
+ return preferred ?? lines[0] ?? null;
51
+ }
52
+
53
+ return lines[0] ?? null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Type guard for Node.js child_process exec errors.
61
+ * ExecSync throws objects with these extra properties on non-zero exit or timeout.
62
+ */
63
+ export interface ExecSyncError {
64
+ killed: boolean;
65
+ signal: string | null;
66
+ stdout: Buffer | string;
67
+ stderr: Buffer | string;
68
+ status: number | null;
69
+ message: string;
70
+ }
71
+
72
+ /** Check if an unknown error is an ExecSync error with process info. */
73
+ export function isExecSyncError(e: unknown): e is ExecSyncError {
74
+ return (
75
+ typeof e === "object" &&
76
+ e !== null &&
77
+ "killed" in e &&
78
+ "signal" in e
79
+ );
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Async Subprocess Execution
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Result from an async subprocess execution.
88
+ * Never throws — callers inspect fields to determine outcome.
89
+ */
90
+ export interface ExecResult {
91
+ stdout: string;
92
+ stderr: string;
93
+ exitCode: number;
94
+ killed: boolean;
95
+ signal: string | null;
96
+ }
97
+
98
+ /** Options for execFileAsync. */
99
+ export interface ExecAsyncOptions {
100
+ /** Data piped to the child's stdin. */
101
+ input?: string;
102
+ /** Timeout in milliseconds (not seconds). */
103
+ timeout?: number;
104
+ /** Environment variables for the child process. */
105
+ env?: Record<string, string | undefined>;
106
+ /** Maximum bytes on stdout/stderr. Default: 10 MB. */
107
+ maxBuffer?: number;
108
+ /** Use shell for execution. Required on Windows for .cmd files. */
109
+ shell?: boolean;
110
+ }
111
+
112
+ /**
113
+ * Async subprocess execution that does NOT block the event loop.
114
+ * Drop-in replacement for execFileSync in Promise-based parallel patterns.
115
+ *
116
+ * Returns ExecResult on both success and non-zero exit.
117
+ * On timeout: result.killed = true, result.signal = "SIGTERM".
118
+ * On spawn failure: result.exitCode = -1, result.stderr contains error.
119
+ */
120
+ export function execFileAsync(
121
+ file: string,
122
+ args: string[],
123
+ options?: ExecAsyncOptions,
124
+ ): Promise<ExecResult> {
125
+ return new Promise((resolve) => {
126
+ const child = execFile(
127
+ file,
128
+ args,
129
+ {
130
+ encoding: "utf-8",
131
+ timeout: options?.timeout ?? 0,
132
+ env: options?.env as NodeJS.ProcessEnv,
133
+ maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024,
134
+ shell: options?.shell,
135
+ },
136
+ (error, stdout, stderr) => {
137
+ if (error) {
138
+ // execFile callback error includes process exit info
139
+ const errObj = error as unknown as Record<string, unknown>;
140
+ resolve({
141
+ stdout: String(stdout ?? ""),
142
+ stderr: String(stderr ?? ""),
143
+ exitCode: typeof errObj.code === "number" ? errObj.code : (error as any).status ?? 1,
144
+ killed: Boolean(errObj.killed),
145
+ signal: typeof errObj.signal === "string" ? errObj.signal : null,
146
+ });
147
+ } else {
148
+ resolve({
149
+ stdout: String(stdout ?? ""),
150
+ stderr: String(stderr ?? ""),
151
+ exitCode: 0,
152
+ killed: false,
153
+ signal: null,
154
+ });
155
+ }
156
+ },
157
+ );
158
+
159
+ // Pipe input to stdin if provided
160
+ if (options?.input != null && child.stdin) {
161
+ child.stdin.write(options.input);
162
+ child.stdin.end();
163
+ }
164
+ });
165
+ }
@@ -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
  }