aiwcli 0.11.0 → 0.12.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 (118) hide show
  1. package/dist/commands/clear.d.ts +8 -0
  2. package/dist/commands/clear.js +86 -0
  3. package/dist/lib/bmad-installer.d.ts +2 -27
  4. package/dist/lib/bmad-installer.js +3 -43
  5. package/dist/lib/claude-settings-types.d.ts +2 -1
  6. package/dist/lib/env-compat.d.ts +0 -8
  7. package/dist/lib/env-compat.js +0 -12
  8. package/dist/lib/git/index.d.ts +0 -1
  9. package/dist/lib/gitignore-manager.d.ts +0 -2
  10. package/dist/lib/gitignore-manager.js +1 -1
  11. package/dist/lib/hooks-merger.d.ts +1 -15
  12. package/dist/lib/hooks-merger.js +1 -1
  13. package/dist/lib/index.d.ts +3 -7
  14. package/dist/lib/index.js +3 -11
  15. package/dist/lib/output.d.ts +2 -1
  16. package/dist/lib/settings-hierarchy.d.ts +1 -13
  17. package/dist/lib/settings-hierarchy.js +1 -1
  18. package/dist/lib/template-installer.d.ts +5 -9
  19. package/dist/lib/template-installer.js +2 -12
  20. package/dist/lib/template-linter.d.ts +3 -10
  21. package/dist/lib/template-linter.js +2 -2
  22. package/dist/lib/template-resolver.d.ts +6 -0
  23. package/dist/lib/template-resolver.js +10 -0
  24. package/dist/lib/template-settings-reconstructor.d.ts +1 -1
  25. package/dist/lib/template-settings-reconstructor.js +17 -24
  26. package/dist/lib/terminal.d.ts +3 -14
  27. package/dist/lib/terminal.js +0 -4
  28. package/dist/lib/version.d.ts +2 -11
  29. package/dist/lib/version.js +2 -2
  30. package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
  31. package/dist/lib/windsurf-hooks-merger.js +1 -1
  32. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  33. package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
  34. package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
  35. package/dist/templates/_shared/hooks-ts/session_start.ts +15 -20
  36. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -14
  37. package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
  38. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  39. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +174 -43
  40. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
  41. package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
  42. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -162
  43. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
  44. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
  45. package/dist/templates/_shared/lib-ts/package.json +1 -2
  46. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
  47. package/dist/templates/_shared/lib-ts/types.ts +17 -2
  48. package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
  49. package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
  50. package/dist/templates/_shared/scripts/status_line.ts +102 -148
  51. package/dist/templates/_shared/workflows/handoff.md +1 -1
  52. package/dist/templates/cc-native/.claude/settings.json +183 -175
  53. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
  54. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  55. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
  56. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +316 -176
  57. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +38 -0
  58. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -0
  59. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
  60. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +15 -15
  61. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
  62. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +227 -114
  63. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +64 -16
  64. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +23 -3
  65. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -4
  67. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
  68. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -218
  69. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +27 -111
  72. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
  73. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +5 -3
  74. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
  75. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -0
  76. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
  77. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
  78. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +3 -5
  80. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
  81. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +104 -132
  82. package/dist/templates/cc-native/_cc-native/plan-review.config.json +22 -13
  83. package/oclif.manifest.json +1 -1
  84. package/package.json +2 -3
  85. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
  86. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
  87. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -106
  88. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
  89. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
  90. /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
  91. /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
  92. /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
  93. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
  94. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
  95. /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
  96. /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
  97. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
  98. /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
  99. /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
  100. /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
  101. /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
  102. /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
  103. /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
  104. /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
  105. /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
  106. /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
  107. /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
  108. /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
  109. /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
  110. /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
  111. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
  112. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
  113. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
  114. /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
  115. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
  116. /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
  117. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
  118. /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-STRENGTH.md → plan-review/VERIFY-STRENGTH.md} +0 -0
@@ -1,162 +1,181 @@
1
- /**
2
- * Subprocess environment utilities.
3
- * See SPEC.md §5.10
4
- */
5
-
6
- import { execFile, execSync } from "node:child_process";
7
-
8
- /**
9
- * Check if this is an internal subprocess call.
10
- * All hooks should check this and return early to prevent recursion.
11
- */
12
- export function isInternalCall(): boolean {
13
- return process.env.AIWCLI_INTERNAL_CALL === "true";
14
- }
15
-
16
- /**
17
- * Get environment for internal subprocess calls.
18
- * Returns a copy of process.env with AIWCLI_INTERNAL_CALL=true.
19
- */
20
- export function getInternalSubprocessEnv(): Record<string, string | undefined> {
21
- return {
22
- ...process.env,
23
- AIWCLI_INTERNAL_CALL: "true",
24
- };
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): null | string {
35
- try {
36
- const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
37
- const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] })
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
- message: string;
66
- signal: null | string;
67
- status: null | number;
68
- stderr: Buffer | string;
69
- stdout: Buffer | 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
- exitCode: number;
92
- killed: boolean;
93
- signal: null | string;
94
- stderr: string;
95
- stdout: string;
96
- }
97
-
98
- /** Options for execFileAsync. */
99
- export interface ExecAsyncOptions {
100
- /** Environment variables for the child process. */
101
- env?: Record<string, string | undefined>;
102
- /** Data piped to the child's stdin. */
103
- input?: string;
104
- /** Maximum bytes on stdout/stderr. Default: 10 MB. */
105
- maxBuffer?: number;
106
- /** Timeout in milliseconds (not seconds). */
107
- timeout?: number;
108
- }
109
-
110
- /**
111
- * Async subprocess execution that does NOT block the event loop.
112
- * Drop-in replacement for execFileSync in Promise-based parallel patterns.
113
- *
114
- * Returns ExecResult on both success and non-zero exit.
115
- * On timeout: result.killed = true, result.signal = "SIGTERM".
116
- * On spawn failure: result.exitCode = -1, result.stderr contains error.
117
- */
118
- export function execFileAsync(
119
- file: string,
120
- args: string[],
121
- options?: ExecAsyncOptions,
122
- ): Promise<ExecResult> {
123
- return new Promise((resolve) => {
124
- const child = execFile(
125
- file,
126
- args,
127
- {
128
- encoding: "utf-8",
129
- timeout: options?.timeout ?? 0,
130
- env: options?.env as NodeJS.ProcessEnv,
131
- maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024,
132
- },
133
- (error, stdout, stderr) => {
134
- if (error) {
135
- // execFile callback error includes process exit info
136
- const errObj = error as unknown as Record<string, unknown>;
137
- resolve({
138
- stdout: String(stdout ?? ""),
139
- stderr: String(stderr ?? ""),
140
- exitCode: typeof errObj.code === "number" ? errObj.code : (error as any).status ?? 1,
141
- killed: Boolean(errObj.killed),
142
- signal: typeof errObj.signal === "string" ? errObj.signal : null,
143
- });
144
- } else {
145
- resolve({
146
- stdout: String(stdout ?? ""),
147
- stderr: String(stderr ?? ""),
148
- exitCode: 0,
149
- killed: false,
150
- signal: null,
151
- });
152
- }
153
- },
154
- );
155
-
156
- // Pipe input to stdin if provided
157
- if (options?.input !== null && options?.input !== undefined && child.stdin) {
158
- child.stdin.write(options.input);
159
- child.stdin.end();
160
- }
161
- });
162
- }
1
+ /**
2
+ * Subprocess environment utilities.
3
+ * See SPEC.md §5.10
4
+ */
5
+
6
+ import { execSync, execFile } from "node:child_process";
7
+
8
+ /**
9
+ * Check if this is an internal subprocess call.
10
+ * All hooks should check this and return early to prevent recursion.
11
+ */
12
+ export function isInternalCall(): boolean {
13
+ return process.env.AIWCLI_INTERNAL_CALL === "true";
14
+ }
15
+
16
+ /**
17
+ * Get environment for internal subprocess calls.
18
+ * Returns a copy of process.env with AIWCLI_INTERNAL_CALL=true and
19
+ * Claude Code nesting-detection env vars removed so subprocess
20
+ * claude instances can run without being blocked.
21
+ */
22
+ export function getInternalSubprocessEnv(): Record<string, string | undefined> {
23
+ const env = {
24
+ ...process.env,
25
+ AIWCLI_INTERNAL_CALL: "true",
26
+ };
27
+ // Explicitly delete vars that block subprocess calls (set to undefined does not work)
28
+ delete env.CLAUDECODE;
29
+ delete env.CLAUDE_CODE_ENTRYPOINT;
30
+ return env;
31
+ }
32
+ /**
33
+ * Find an executable on the system PATH.
34
+ * Uses `where` on Windows, `which` on Unix.
35
+ * On Windows, prefers .cmd/.exe over extensionless shims since
36
+ * execFileSync cannot spawn extensionless shell scripts.
37
+ * Returns the first match or null if not found.
38
+ */
39
+ export function findExecutable(name: string): string | null {
40
+ try {
41
+ const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
42
+ const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true })
43
+ .trim()
44
+ .split(/\r?\n/)
45
+ .map((l) => l.trim())
46
+ .filter(Boolean);
47
+
48
+ if (lines.length === 0) return null;
49
+
50
+ // On Windows, `where` may return an extensionless shim first (e.g. npm creates
51
+ // both `claude` and `claude.cmd`). execFileSync can't spawn the extensionless
52
+ // one, so prefer .cmd or .exe.
53
+ if (process.platform === "win32") {
54
+ const preferred = lines.find((l) => /\.(cmd|exe)$/i.test(l));
55
+ return preferred ?? lines[0] ?? null;
56
+ }
57
+
58
+ return lines[0] ?? null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Type guard for Node.js child_process exec errors.
66
+ * ExecSync throws objects with these extra properties on non-zero exit or timeout.
67
+ */
68
+ export interface ExecSyncError {
69
+ killed: boolean;
70
+ signal: string | null;
71
+ stdout: Buffer | string;
72
+ stderr: Buffer | string;
73
+ status: number | null;
74
+ message: string;
75
+ }
76
+
77
+ /** Check if an unknown error is an ExecSync error with process info. */
78
+ export function isExecSyncError(e: unknown): e is ExecSyncError {
79
+ return (
80
+ typeof e === "object" &&
81
+ e !== null &&
82
+ "killed" in e &&
83
+ "signal" in e
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Quote a string for use as a cmd.exe argument when shell: true.
89
+ * Wraps in double quotes and escapes inner double quotes as "".
90
+ * On non-Windows platforms, returns the string unchanged (execFile
91
+ * handles quoting automatically without shell).
92
+ */
93
+ export function shellQuoteWin(arg: string): string {
94
+ if (process.platform !== "win32") return arg;
95
+ return '"' + arg.replace(/"/g, '""') + '"';
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Async Subprocess Execution
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Result from an async subprocess execution.
104
+ * Never throws callers inspect fields to determine outcome.
105
+ */
106
+ export interface ExecResult {
107
+ stdout: string;
108
+ stderr: string;
109
+ exitCode: number;
110
+ killed: boolean;
111
+ signal: string | null;
112
+ }
113
+
114
+ /** Options for execFileAsync. */
115
+ export interface ExecAsyncOptions {
116
+ /** Data piped to the child's stdin. */
117
+ input?: string;
118
+ /** Timeout in milliseconds (not seconds). */
119
+ timeout?: number;
120
+ /** Environment variables for the child process. */
121
+ env?: Record<string, string | undefined>;
122
+ /** Maximum bytes on stdout/stderr. Default: 10 MB. */
123
+ maxBuffer?: number;
124
+ /** Use shell for execution. Required on Windows for .cmd files. */
125
+ shell?: boolean;
126
+ }
127
+
128
+ /**
129
+ * Async subprocess execution that does NOT block the event loop.
130
+ * Drop-in replacement for execFileSync in Promise-based parallel patterns.
131
+ *
132
+ * Returns ExecResult on both success and non-zero exit.
133
+ * On timeout: result.killed = true, result.signal = "SIGTERM".
134
+ * On spawn failure: result.exitCode = -1, result.stderr contains error.
135
+ */
136
+ export function execFileAsync(
137
+ file: string,
138
+ args: string[],
139
+ options?: ExecAsyncOptions,
140
+ ): Promise<ExecResult> {
141
+ return new Promise((resolve) => {
142
+ const child = execFile(
143
+ file,
144
+ args,
145
+ {
146
+ encoding: "utf-8",
147
+ timeout: options?.timeout ?? 0,
148
+ env: options?.env as NodeJS.ProcessEnv,
149
+ maxBuffer: options?.maxBuffer ?? 10 * 1024 * 1024,
150
+ shell: options?.shell,
151
+ },
152
+ (error, stdout, stderr) => {
153
+ if (error) {
154
+ // execFile callback error includes process exit info
155
+ const errObj = error as unknown as Record<string, unknown>;
156
+ resolve({
157
+ stdout: String(stdout ?? ""),
158
+ stderr: String(stderr ?? ""),
159
+ exitCode: typeof errObj.code === "number" ? errObj.code : (error as any).status ?? 1,
160
+ killed: Boolean(errObj.killed),
161
+ signal: typeof errObj.signal === "string" ? errObj.signal : null,
162
+ });
163
+ } else {
164
+ resolve({
165
+ stdout: String(stdout ?? ""),
166
+ stderr: String(stderr ?? ""),
167
+ exitCode: 0,
168
+ killed: false,
169
+ signal: null,
170
+ });
171
+ }
172
+ },
173
+ );
174
+
175
+ // Pipe input to stdin if provided
176
+ if (options?.input != null && child.stdin) {
177
+ child.stdin.write(options.input);
178
+ child.stdin.end();
179
+ }
180
+ });
181
+ }
@@ -8,16 +8,14 @@
8
8
  * - extractPlanPathFromResult: parse plan path from ExitPlanMode output
9
9
  */
10
10
 
11
- import * as crypto from "node:crypto";
12
11
  import * as fs from "node:fs";
13
12
  import * as path from "node:path";
14
-
13
+ import * as crypto from "node:crypto";
14
+ import { getContextDir, getContextPlansDir, sanitizeTitle } from "../base/constants.js";
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";
18
- import { readStateJson } from "../base/state-io.js";
16
+ import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
19
17
  import { generateSlug } from "../base/utils.js";
20
- import type { ContextState as _ContextState } from "../types.js";
18
+ import type { ContextState } from "../types.js";
21
19
 
22
20
  // ---------------------------------------------------------------------------
23
21
  // Plan archival
@@ -32,11 +30,11 @@ import type { ContextState as _ContextState } from "../types.js";
32
30
  * Returns [archivedPath, planHash, planSignature] on success,
33
31
  * or [null, null, null] on error.
34
32
  */
35
- export async function archivePlan(
33
+ export function archivePlan(
36
34
  planPath: string,
37
35
  contextId: string,
38
36
  projectRoot?: string,
39
- ): Promise<[null | string, null | string, null | string]> {
37
+ ): [string | null, string | null, string | null] {
40
38
  if (!fs.existsSync(planPath)) {
41
39
  logWarn("plan_manager", `Plan file not found: ${planPath}`);
42
40
  return [null, null, null];
@@ -44,9 +42,9 @@ export async function archivePlan(
44
42
 
45
43
  let content: string;
46
44
  try {
47
- content = fs.readFileSync(planPath, "utf8");
48
- } catch (error_: any) {
49
- logError("plan_manager", `Failed to read plan: ${error_}`);
45
+ content = fs.readFileSync(planPath, "utf-8");
46
+ } catch (e: any) {
47
+ logError("plan_manager", `Failed to read plan: ${e}`);
50
48
  return [null, null, null];
51
49
  }
52
50
 
@@ -106,7 +104,7 @@ export async function archivePlan(
106
104
  * text suitable for the AI slug generator (which expects conversational input).
107
105
  */
108
106
  function extractPlanSummary(content: string): string {
109
- const lines = content.split("\n");
107
+ const lines = content.split(/\r?\n/);
110
108
  const parts: string[] = [];
111
109
  let firstParagraph = "";
112
110
 
@@ -117,12 +115,10 @@ function extractPlanSummary(content: string): string {
117
115
  const heading = trimmed.replace(/^#+\s*/, "");
118
116
  if (heading.length > 2) parts.push(heading);
119
117
  }
120
-
121
118
  // Grab first substantial non-heading line as context
122
119
  if (!firstParagraph && !trimmed.startsWith("#") && trimmed.length > 20) {
123
120
  firstParagraph = trimmed.slice(0, 120);
124
121
  }
125
-
126
122
  // Enough material for the AI
127
123
  if (parts.length >= 5) break;
128
124
  }
@@ -143,15 +139,17 @@ function extractPlanSummary(content: string): string {
143
139
  export function findLatestPlan(
144
140
  contextId: string,
145
141
  projectRoot?: string,
146
- ): null | string {
142
+ ): string | null {
147
143
  // 1. Check state.json plan_path first
148
144
  try {
149
- const state = readStateJson(contextId, projectRoot);
145
+ // Dynamic import to avoid circular dependency at module level
146
+ const stateIo = require("../base/state-io.js");
147
+ const state = stateIo.readStateJson(contextId, projectRoot);
150
148
  if (state?.plan_path && fs.existsSync(state.plan_path)) {
151
149
  return state.plan_path;
152
150
  }
153
- } catch (error: any) {
154
- logWarn("plan_manager", `Failed to check state.json plan_path: ${error}`);
151
+ } catch (e: any) {
152
+ logWarn("plan_manager", `Failed to check state.json plan_path: ${e}`);
155
153
  }
156
154
 
157
155
  // 2. Fall back to most recent .md in plans/ dir
@@ -184,7 +182,7 @@ export function findLatestPlan(
184
182
  * See SPEC.md §9.4
185
183
  */
186
184
  export function generatePlanId(): string {
187
- return crypto.randomUUID().replaceAll('-', "").slice(0, 8);
185
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 8);
188
186
  }
189
187
 
190
188
  /**
@@ -193,8 +191,8 @@ export function generatePlanId(): string {
193
191
  * See SPEC.md §9.5
194
192
  */
195
193
  export function normalizePlanContent(text: string): string {
196
- let result = text.replaceAll(/<[^>]+>/g, "");
197
- result = result.replaceAll(/\s+/g, " ").trim();
194
+ let result = text.replace(/<[^>]+>/g, "");
195
+ result = result.replace(/\s+/g, " ").trim();
198
196
  return result;
199
197
  }
200
198
 
@@ -205,17 +203,15 @@ export function normalizePlanContent(text: string): string {
205
203
  */
206
204
  export function extractPlanAnchors(content: string, maxAnchors = 5): string[] {
207
205
  const anchors: string[] = [];
208
- for (const line of content.split("\n")) {
206
+ for (const line of content.split(/\r?\n/)) {
209
207
  const trimmed = line.trim();
210
208
  if (trimmed.startsWith("#") && trimmed.length > 3) {
211
209
  anchors.push(trimmed.slice(0, 80));
212
210
  } else if (anchors.length === 0 && trimmed.length > 20) {
213
211
  anchors.push(trimmed.slice(0, 80));
214
212
  }
215
-
216
213
  if (anchors.length >= maxAnchors) break;
217
214
  }
218
-
219
215
  return anchors;
220
216
  }
221
217
 
@@ -230,7 +226,7 @@ const MAX_TRANSCRIPT_SIZE = 50 * 1024 * 1024; // 50 MB
230
226
  * Searches in reverse for the most recent Write tool call targeting .claude/plans/.
231
227
  * See SPEC.md §9.7
232
228
  */
233
- export function findPlanPathInTranscript(transcriptPath: string): null | string {
229
+ export function findPlanPathInTranscript(transcriptPath: string): string | null {
234
230
  if (!transcriptPath) return null;
235
231
 
236
232
  if (!fs.existsSync(transcriptPath)) {
@@ -252,9 +248,9 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
252
248
 
253
249
  let lines: string[];
254
250
  try {
255
- lines = fs.readFileSync(transcriptPath, "utf8").split("\n");
256
- } catch (error: any) {
257
- logWarn("plan_manager", `Failed to read transcript: ${error}`);
251
+ lines = fs.readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
252
+ } catch (e: any) {
253
+ logWarn("plan_manager", `Failed to read transcript: ${e}`);
258
254
  return null;
259
255
  }
260
256
 
@@ -286,7 +282,7 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
286
282
  if (!filePath) continue;
287
283
 
288
284
  // Check if path contains .claude/plans/ as consecutive parts
289
- const parts = filePath.replaceAll('\\', "/").split("/");
285
+ const parts = filePath.replace(/\\/g, "/").split("/");
290
286
  for (let j = 0; j < parts.length - 1; j++) {
291
287
  if (parts[j] === ".claude" && parts[j + 1] === "plans") {
292
288
  logInfo("plan_manager", `Extracted plan path from transcript: ${filePath}`);
@@ -309,7 +305,7 @@ export function findPlanPathInTranscript(transcriptPath: string): null | string
309
305
  * Parses the pattern: "Your plan has been saved to: <path>"
310
306
  * See SPEC.md §9.8
311
307
  */
312
- export function extractPlanPathFromResult(toolResult: string): null | string {
308
+ export function extractPlanPathFromResult(toolResult: string): string | null {
313
309
  if (!toolResult) return null;
314
310
  const match = toolResult.match(/Your plan has been saved to:\s*(.+\.md)/);
315
311
  return match ? match[1]!.trim() : null;
@@ -7,8 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as fs from "node:fs";
10
- import * as path from "node:path";
11
-
10
+ import * as path from "node:path";
12
11
  import { getContextHandoffsDir } from "../base/constants.js";
13
12
  import { getContext } from "../context/context-store.js";
14
13
  import type { HandoffSections } from "../types.js";
@@ -19,7 +18,7 @@ import type { HandoffSections } from "../types.js";
19
18
  * (YYYY-MM-DD-HHMM format ensures lexicographic = chronological).
20
19
  * Returns full path to most recent folder, or null.
21
20
  */
22
- export function findLatestHandoff(contextId: string, projectRoot?: string): null | string {
21
+ export function findLatestHandoff(contextId: string, projectRoot?: string): string | null {
23
22
  const handoffsDir = getContextHandoffsDir(contextId, projectRoot);
24
23
 
25
24
  try {
@@ -30,7 +29,7 @@ export function findLatestHandoff(contextId: string, projectRoot?: string): null
30
29
  .sort();
31
30
 
32
31
  if (entries.length === 0) return null;
33
- return path.join(handoffsDir, entries.at(-1)!);
32
+ return path.join(handoffsDir, entries[entries.length - 1]!);
34
33
  } catch {
35
34
  return null;
36
35
  }
@@ -65,7 +64,7 @@ export function readHandoffSections(handoffFolder: string): HandoffSections {
65
64
  const filePath = path.join(handoffFolder, filename);
66
65
  try {
67
66
  if (fs.existsSync(filePath)) {
68
- sections[key as keyof HandoffSections] = fs.readFileSync(filePath, "utf8");
67
+ sections[key as keyof HandoffSections] = fs.readFileSync(filePath, "utf-8");
69
68
  }
70
69
  } catch {
71
70
  // graceful — leave as null
@@ -87,11 +86,11 @@ export function getHandoffTimestamp(handoffFolder: string): Date | null {
87
86
 
88
87
  const [, year, month, day, hour, minute] = match;
89
88
  const date = new Date(
90
- Number.parseInt(year!, 10),
91
- Number.parseInt(month!, 10) - 1,
92
- Number.parseInt(day!, 10),
93
- Number.parseInt(hour!, 10),
94
- Number.parseInt(minute!, 10),
89
+ parseInt(year!, 10),
90
+ parseInt(month!, 10) - 1,
91
+ parseInt(day!, 10),
92
+ parseInt(hour!, 10),
93
+ parseInt(minute!, 10),
95
94
  );
96
95
 
97
96
  return isNaN(date.getTime()) ? null : date;
@@ -106,12 +105,12 @@ export function getHandoffPlanReference(
106
105
  handoffFolder: string,
107
106
  contextId: string,
108
107
  projectRoot?: string,
109
- ): null | string {
108
+ ): string | null {
110
109
  // Try plan.md frontmatter
111
110
  const planMdPath = path.join(handoffFolder, "plan.md");
112
111
  try {
113
112
  if (fs.existsSync(planMdPath)) {
114
- const content = fs.readFileSync(planMdPath, "utf8");
113
+ const content = fs.readFileSync(planMdPath, "utf-8");
115
114
  const frontmatter = parseFrontmatter(content);
116
115
  if (frontmatter["plan_path"]) {
117
116
  const pp = frontmatter["plan_path"];
@@ -146,7 +145,7 @@ function parseFrontmatter(content: string): Record<string, string> {
146
145
  const parts = content.split("---", 3);
147
146
  if (parts.length < 3) return frontmatter;
148
147
 
149
- for (const line of parts[1]!.trim().split("\n")) {
148
+ for (const line of parts[1]!.trim().split(/\r?\n/)) {
150
149
  const colonIdx = line.indexOf(":");
151
150
  if (colonIdx !== -1) {
152
151
  const key = line.slice(0, colonIdx).trim();
@@ -7,8 +7,7 @@
7
7
  "test:unit": "mocha '__tests__/base/**/*.test.ts' '__tests__/templates/**/*.test.ts'",
8
8
  "test:contract": "mocha '__tests__/context/**/*.test.ts' '__tests__/handoff/**/*.test.ts'",
9
9
  "test:integration": "mocha '__tests__/integration/**/*.test.ts'",
10
- "test:parity": "mocha '__tests__/integration/python-parity.test.ts'",
11
- "fixtures": "python __tests__/fixtures/generate_fixtures.py"
10
+ "test:parity": "mocha '__tests__/integration/python-parity.test.ts'"
12
11
  },
13
12
  "devDependencies": {
14
13
  "mocha": "^10.0.0",