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.
- package/dist/commands/clear.d.ts +8 -0
- package/dist/commands/clear.js +86 -0
- package/dist/lib/bmad-installer.d.ts +2 -27
- package/dist/lib/bmad-installer.js +3 -43
- package/dist/lib/claude-settings-types.d.ts +2 -1
- package/dist/lib/env-compat.d.ts +0 -8
- package/dist/lib/env-compat.js +0 -12
- package/dist/lib/git/index.d.ts +0 -1
- package/dist/lib/gitignore-manager.d.ts +0 -2
- package/dist/lib/gitignore-manager.js +1 -1
- package/dist/lib/hooks-merger.d.ts +1 -15
- package/dist/lib/hooks-merger.js +1 -1
- package/dist/lib/index.d.ts +3 -7
- package/dist/lib/index.js +3 -11
- package/dist/lib/output.d.ts +2 -1
- package/dist/lib/settings-hierarchy.d.ts +1 -13
- package/dist/lib/settings-hierarchy.js +1 -1
- package/dist/lib/template-installer.d.ts +5 -9
- package/dist/lib/template-installer.js +2 -12
- package/dist/lib/template-linter.d.ts +3 -10
- package/dist/lib/template-linter.js +2 -2
- package/dist/lib/template-resolver.d.ts +6 -0
- package/dist/lib/template-resolver.js +10 -0
- package/dist/lib/template-settings-reconstructor.d.ts +1 -1
- package/dist/lib/template-settings-reconstructor.js +17 -24
- package/dist/lib/terminal.d.ts +3 -14
- package/dist/lib/terminal.js +0 -4
- package/dist/lib/version.d.ts +2 -11
- package/dist/lib/version.js +2 -2
- package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
- package/dist/lib/windsurf-hooks-merger.js +1 -1
- package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
- package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
- package/dist/templates/_shared/hooks-ts/session_start.ts +15 -20
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -14
- package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +174 -43
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
- package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -162
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +26 -30
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +12 -13
- package/dist/templates/_shared/lib-ts/package.json +1 -2
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
- package/dist/templates/_shared/lib-ts/types.ts +17 -2
- package/dist/templates/_shared/scripts/resume_handoff.ts +62 -38
- package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
- package/dist/templates/_shared/scripts/status_line.ts +102 -148
- package/dist/templates/_shared/workflows/handoff.md +1 -1
- package/dist/templates/cc-native/.claude/settings.json +183 -175
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +316 -176
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +38 -0
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -0
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +15 -15
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +11 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +227 -114
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +64 -16
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +23 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +7 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -218
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +27 -111
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +5 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +3 -5
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +104 -132
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +22 -13
- package/oclif.manifest.json +1 -1
- package/package.json +2 -3
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -106
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
- /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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 {
|
|
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
|
|
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
|
|
33
|
+
export function archivePlan(
|
|
36
34
|
planPath: string,
|
|
37
35
|
contextId: string,
|
|
38
36
|
projectRoot?: string,
|
|
39
|
-
):
|
|
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, "
|
|
48
|
-
} catch (
|
|
49
|
-
logError("plan_manager", `Failed to read plan: ${
|
|
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(
|
|
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
|
-
):
|
|
142
|
+
): string | null {
|
|
147
143
|
// 1. Check state.json plan_path first
|
|
148
144
|
try {
|
|
149
|
-
|
|
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 (
|
|
154
|
-
logWarn("plan_manager", `Failed to check state.json plan_path: ${
|
|
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().
|
|
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.
|
|
197
|
-
result = result.
|
|
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(
|
|
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):
|
|
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, "
|
|
256
|
-
} catch (
|
|
257
|
-
logWarn("plan_manager", `Failed to read transcript: ${
|
|
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.
|
|
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):
|
|
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):
|
|
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.
|
|
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, "
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
):
|
|
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, "
|
|
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(
|
|
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",
|