aiwcli 0.11.0 → 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.
- package/dist/templates/_shared/hooks-ts/session_end.ts +4 -1
- package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/logger.ts +15 -18
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +18 -15
- 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/scripts/resume_handoff.ts +62 -38
- package/dist/templates/_shared/scripts/save_handoff.ts +24 -24
- package/dist/templates/_shared/scripts/status_line.ts +101 -147
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +239 -133
- 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 +139 -56
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +22 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +1 -0
- 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 +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +133 -13
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +6 -6
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +5 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +30 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +118 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +21 -0
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
|
@@ -73,6 +73,12 @@ export function parseJsonMaybe(
|
|
|
73
73
|
`Parsed JSON (${parseMethod}) missing/empty fields: ${JSON.stringify(missing)}`,
|
|
74
74
|
);
|
|
75
75
|
logDebug("parse", `Keys present: ${JSON.stringify(Object.keys(obj))}`);
|
|
76
|
+
// Heuristic extraction grabbed the wrong object — reject it.
|
|
77
|
+
// Strict parse still returns partial objects (caller handles defaults).
|
|
78
|
+
if (parseMethod === "heuristic") {
|
|
79
|
+
logWarn("parse", "Rejecting heuristic result due to missing required fields");
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -137,7 +143,7 @@ export function coerceToReview(
|
|
|
137
143
|
"coerce",
|
|
138
144
|
`verdict=${obj.verdict}, issues_count=${Array.isArray(obj.issues) ? (obj.issues as unknown[]).length : 0}`,
|
|
139
145
|
);
|
|
140
|
-
}
|
|
146
|
+
}
|
|
141
147
|
|
|
142
148
|
if (!obj.issues) {
|
|
143
149
|
logDebug("coerce", "issues array empty or missing");
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* See cc-native-plan-review-spec.md §4.8
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { logDebug, logInfo, logWarn, logError } from "../../_shared/lib-ts/base/logger.js";
|
|
7
|
+
import { getInternalSubprocessEnv, findExecutable, execFileAsync } from "../../_shared/lib-ts/base/subprocess-utils.js";
|
|
6
8
|
import { parseCliOutput } from "./cli-output-parser.js";
|
|
7
|
-
import type { AgentConfig,
|
|
9
|
+
import type { AgentConfig, OrchestratorConfig, OrchestratorResult, ComplexityCategory } from "./types.js";
|
|
8
10
|
import { ORCHESTRATOR_SCHEMA } from "./types.js";
|
|
9
|
-
import { logDebug, logError, logInfo, logWarn } from "../../_shared/lib-ts/base/logger.js";
|
|
10
|
-
import { execFileAsync, findExecutable, getInternalSubprocessEnv } from "../../_shared/lib-ts/base/subprocess-utils.js";
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Constants
|
|
@@ -175,6 +175,7 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning`
|
|
|
175
175
|
timeout: config.timeout * 1000,
|
|
176
176
|
env: env as Record<string, string>,
|
|
177
177
|
maxBuffer: 10 * 1024 * 1024,
|
|
178
|
+
shell: process.platform === "win32",
|
|
178
179
|
});
|
|
179
180
|
|
|
180
181
|
if (result.killed || result.signal === "SIGTERM") {
|
|
@@ -211,7 +212,7 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning`
|
|
|
211
212
|
let category = (obj.category as string) ?? "code";
|
|
212
213
|
if (!categories.includes(category)) category = "code";
|
|
213
214
|
|
|
214
|
-
let
|
|
215
|
+
let selectedAgents = obj.selectedAgents;
|
|
215
216
|
if (!Array.isArray(selectedAgents)) selectedAgents = [];
|
|
216
217
|
|
|
217
218
|
const reasoning = String(obj.reasoning ?? "").trim() || "No reasoning provided";
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Agent-based plan reviewer with multi-provider support.
|
|
3
|
+
* Routes to Claude or Codex CLI based on agent.provider field.
|
|
4
4
|
* See cc-native-plan-review-spec.md §4.10
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { logDebug, logInfo, logWarn, logError } from "../../../_shared/lib-ts/base/logger.js";
|
|
11
|
+
import { getInternalSubprocessEnv, findExecutable, execFileAsync } from "../../../_shared/lib-ts/base/subprocess-utils.js";
|
|
11
12
|
import { parseCliOutput } from "../cli-output-parser.js";
|
|
13
|
+
import { parseJsonMaybe, coerceToReview } from "../json-parser.js";
|
|
12
14
|
import { debugLog, debugRaw } from "../debug.js";
|
|
13
|
-
import { coerceToReview } from "../json-parser.js";
|
|
14
15
|
import type { AgentConfig, ReviewerResult, ReviewOptions } from "../types.js";
|
|
15
16
|
import { AGENT_REVIEW_PROMPT_PREFIX } from "../types.js";
|
|
16
17
|
import { makeResult } from "./types.js";
|
|
17
18
|
import type { Reviewer } from "./types.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Agent reviewer — runs a
|
|
21
|
+
* Agent reviewer — runs a CLI instance with a custom persona.
|
|
21
22
|
*/
|
|
22
23
|
export class AgentReviewer implements Reviewer {
|
|
23
24
|
constructor(private agent: AgentConfig) {}
|
|
@@ -39,7 +40,8 @@ export class AgentReviewer implements Reviewer {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Run a single
|
|
43
|
+
* Run a single agent to review the plan.
|
|
44
|
+
* Routes to provider-specific implementation based on agent.provider.
|
|
43
45
|
* Never throws — returns error ReviewerResult on failure.
|
|
44
46
|
*/
|
|
45
47
|
export async function runAgentReview(
|
|
@@ -49,6 +51,24 @@ export async function runAgentReview(
|
|
|
49
51
|
timeout: number,
|
|
50
52
|
contextPath?: string,
|
|
51
53
|
sessionName = "unknown",
|
|
54
|
+
): Promise<ReviewerResult> {
|
|
55
|
+
if (agent.provider === "codex") {
|
|
56
|
+
return runAgentReviewCodex(plan, agent, schema, timeout, contextPath, sessionName);
|
|
57
|
+
}
|
|
58
|
+
// Default: Claude (existing implementation)
|
|
59
|
+
return runAgentReviewClaude(plan, agent, schema, timeout, contextPath, sessionName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run a single Claude Code agent to review the plan.
|
|
64
|
+
*/
|
|
65
|
+
async function runAgentReviewClaude(
|
|
66
|
+
plan: string,
|
|
67
|
+
agent: AgentConfig,
|
|
68
|
+
schema: Record<string, unknown>,
|
|
69
|
+
timeout: number,
|
|
70
|
+
contextPath?: string,
|
|
71
|
+
sessionName = "unknown",
|
|
52
72
|
): Promise<ReviewerResult> {
|
|
53
73
|
const claudePath = findExecutable("claude");
|
|
54
74
|
if (!claudePath) {
|
|
@@ -82,7 +102,7 @@ ${plan}
|
|
|
82
102
|
cmdArgs.push("--system-prompt", fullPrompt);
|
|
83
103
|
}
|
|
84
104
|
|
|
85
|
-
logInfo(agent.name, `Running with model: ${agent.model}, timeout: ${timeout}s`);
|
|
105
|
+
logInfo(agent.name, `Running Claude with model: ${agent.model}, timeout: ${timeout}s`);
|
|
86
106
|
|
|
87
107
|
const env = getInternalSubprocessEnv();
|
|
88
108
|
|
|
@@ -91,10 +111,11 @@ ${plan}
|
|
|
91
111
|
timeout: timeout * 1000,
|
|
92
112
|
env: env as Record<string, string>,
|
|
93
113
|
maxBuffer: 10 * 1024 * 1024,
|
|
114
|
+
shell: process.platform === "win32",
|
|
94
115
|
});
|
|
95
116
|
|
|
96
117
|
if (result.killed || result.signal === "SIGTERM") {
|
|
97
|
-
logWarn(agent.name, `TIMEOUT after ${timeout}s`);
|
|
118
|
+
logWarn(agent.name, `Claude TIMEOUT after ${timeout}s`);
|
|
98
119
|
return makeResult(agent.name, false, "error", {}, "", `${agent.name} timed out after ${timeout}s`);
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -116,12 +137,12 @@ ${plan}
|
|
|
116
137
|
if (err) {
|
|
117
138
|
debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stderr", err);
|
|
118
139
|
}
|
|
119
|
-
|
|
120
140
|
debugLog(contextPath, sessionName, `agent:${agent.name}`, "subprocess_info", {
|
|
121
141
|
exit_code: result.exitCode,
|
|
122
142
|
stdout_len: raw.length,
|
|
123
143
|
stderr_len: err.length,
|
|
124
144
|
model: agent.model,
|
|
145
|
+
provider: "claude",
|
|
125
146
|
timeout,
|
|
126
147
|
});
|
|
127
148
|
}
|
|
@@ -146,10 +167,109 @@ ${plan}
|
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
const [ok, verdict, norm] = coerceToReview(
|
|
149
|
-
obj as
|
|
170
|
+
obj as Record<string, unknown> | null,
|
|
150
171
|
"Retry or check agent configuration.",
|
|
151
172
|
);
|
|
152
173
|
|
|
153
174
|
return makeResult(agent.name, ok, verdict, norm, raw, err);
|
|
154
175
|
}
|
|
155
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Run a single Codex CLI agent to review the plan.
|
|
179
|
+
* Adapts the codex.ts reviewer pattern for agent-based review with persona.
|
|
180
|
+
*/
|
|
181
|
+
async function runAgentReviewCodex(
|
|
182
|
+
plan: string,
|
|
183
|
+
agent: AgentConfig,
|
|
184
|
+
schema: Record<string, unknown>,
|
|
185
|
+
timeout: number,
|
|
186
|
+
contextPath?: string,
|
|
187
|
+
sessionName = "unknown",
|
|
188
|
+
): Promise<ReviewerResult> {
|
|
189
|
+
const codexPath = findExecutable("codex");
|
|
190
|
+
if (!codexPath) {
|
|
191
|
+
logWarn(agent.name, "Codex CLI not found on PATH, skipping");
|
|
192
|
+
return makeResult(agent.name, false, "skip", {}, "", "codex CLI not found on PATH");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Codex has no --system-prompt flag, so we prepend the agent persona to stdin.
|
|
196
|
+
const fullPrompt = [
|
|
197
|
+
AGENT_REVIEW_PROMPT_PREFIX,
|
|
198
|
+
"---",
|
|
199
|
+
agent.system_prompt || "",
|
|
200
|
+
"---",
|
|
201
|
+
`Return ONLY a JSON object matching this schema:\n${JSON.stringify(schema)}`,
|
|
202
|
+
"",
|
|
203
|
+
"PLAN:",
|
|
204
|
+
"<<<",
|
|
205
|
+
plan,
|
|
206
|
+
">>>",
|
|
207
|
+
].join("\n\n");
|
|
208
|
+
|
|
209
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `codex-agent-${agent.name}-`));
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const schemaPath = path.join(tmpDir, "schema.json");
|
|
213
|
+
const outPath = path.join(tmpDir, "output.json");
|
|
214
|
+
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
215
|
+
|
|
216
|
+
const cmdArgs = ["exec", "--sandbox", "read-only"];
|
|
217
|
+
if (agent.model) cmdArgs.push("--model", agent.model);
|
|
218
|
+
cmdArgs.push("--output-schema", schemaPath, "-o", outPath, "-");
|
|
219
|
+
|
|
220
|
+
logInfo(agent.name, `Running Codex with model: ${agent.model}, timeout: ${timeout}s`);
|
|
221
|
+
|
|
222
|
+
const result = await execFileAsync(codexPath, cmdArgs, {
|
|
223
|
+
input: fullPrompt,
|
|
224
|
+
timeout: timeout * 1000,
|
|
225
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
226
|
+
shell: process.platform === "win32",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (result.killed || result.signal === "SIGTERM") {
|
|
230
|
+
logWarn(agent.name, `Codex TIMEOUT after ${timeout}s`);
|
|
231
|
+
return makeResult(agent.name, false, "error", {}, "", `${agent.name} (codex) timed out after ${timeout}s`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!result.stdout && !result.stderr && !fs.existsSync(outPath) && result.exitCode !== 0) {
|
|
235
|
+
logError(agent.name, `Codex exited with code ${result.exitCode} and no output`);
|
|
236
|
+
return makeResult(agent.name, false, "error", {}, "", `${agent.name} (codex) failed (exit ${result.exitCode})`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Read output: prefer temp file, fallback to stdout
|
|
240
|
+
let raw = "";
|
|
241
|
+
if (fs.existsSync(outPath)) {
|
|
242
|
+
raw = fs.readFileSync(outPath, "utf-8");
|
|
243
|
+
}
|
|
244
|
+
const err = result.stderr.trim();
|
|
245
|
+
|
|
246
|
+
// Debug logging
|
|
247
|
+
if (contextPath) {
|
|
248
|
+
debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stdout", raw || result.stdout);
|
|
249
|
+
if (err) debugRaw(contextPath, sessionName, `agent:${agent.name}`, "stderr", err);
|
|
250
|
+
debugLog(contextPath, sessionName, `agent:${agent.name}`, "subprocess_info", {
|
|
251
|
+
exit_code: result.exitCode,
|
|
252
|
+
stdout_len: (raw || result.stdout).length,
|
|
253
|
+
stderr_len: err.length,
|
|
254
|
+
model: agent.model,
|
|
255
|
+
provider: "codex",
|
|
256
|
+
timeout,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Parse output
|
|
261
|
+
const obj = parseJsonMaybe(raw) ?? parseJsonMaybe(result.stdout);
|
|
262
|
+
const [ok, verdict, norm] = coerceToReview(
|
|
263
|
+
obj,
|
|
264
|
+
"Retry or check Codex CLI auth/config.",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return makeResult(agent.name, ok, verdict, norm, raw || result.stdout, err);
|
|
268
|
+
} finally {
|
|
269
|
+
try {
|
|
270
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
271
|
+
} catch (e) {
|
|
272
|
+
logDebug(agent.name, `Failed to cleanup temp dir ${tmpDir}: ${e}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { coerceToReview, parseJsonMaybe } from "../json-parser.js";
|
|
10
|
+
import { logDebug, logInfo, logWarn, logError } from "../../../_shared/lib-ts/base/logger.js";
|
|
11
|
+
import { findExecutable, execFileAsync } from "../../../_shared/lib-ts/base/subprocess-utils.js";
|
|
12
|
+
import { parseJsonMaybe, coerceToReview } from "../json-parser.js";
|
|
14
13
|
import { REVIEW_PROMPT_PREFIX } from "../types.js";
|
|
15
14
|
import type { ReviewerResult, ReviewOptions } from "../types.js";
|
|
16
15
|
import { makeResult } from "./types.js";
|
|
@@ -29,7 +28,7 @@ export class CodexReviewer implements Reviewer {
|
|
|
29
28
|
async review(
|
|
30
29
|
plan: string,
|
|
31
30
|
schema: Record<string, unknown>,
|
|
32
|
-
|
|
31
|
+
options: ReviewOptions,
|
|
33
32
|
): Promise<ReviewerResult> {
|
|
34
33
|
return runCodexReview(plan, schema, this.settings);
|
|
35
34
|
}
|
|
@@ -91,6 +90,7 @@ ${plan}
|
|
|
91
90
|
input: prompt,
|
|
92
91
|
timeout: timeout * 1000,
|
|
93
92
|
maxBuffer: 10 * 1024 * 1024,
|
|
93
|
+
shell: process.platform === "win32",
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
if (result.killed || result.signal === "SIGTERM") {
|
|
@@ -107,7 +107,7 @@ ${plan}
|
|
|
107
107
|
|
|
108
108
|
let raw = "";
|
|
109
109
|
if (fs.existsSync(outPath)) {
|
|
110
|
-
raw = fs.readFileSync(outPath, "
|
|
110
|
+
raw = fs.readFileSync(outPath, "utf-8");
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const obj = parseJsonMaybe(raw) ?? parseJsonMaybe(result.stdout);
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* See cc-native-plan-review-spec.md §4.12
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { logDebug,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { logDebug, logInfo, logWarn, logError } from "../../../_shared/lib-ts/base/logger.js";
|
|
8
|
+
import { findExecutable, execFileAsync } from "../../../_shared/lib-ts/base/subprocess-utils.js";
|
|
9
|
+
import { parseJsonMaybe, coerceToReview } from "../json-parser.js";
|
|
10
10
|
import type { ReviewerResult, ReviewOptions } from "../types.js";
|
|
11
11
|
import { makeResult } from "./types.js";
|
|
12
12
|
import type { Reviewer } from "./types.js";
|
|
@@ -24,7 +24,7 @@ export class GeminiReviewer implements Reviewer {
|
|
|
24
24
|
async review(
|
|
25
25
|
plan: string,
|
|
26
26
|
schema: Record<string, unknown>,
|
|
27
|
-
|
|
27
|
+
options: ReviewOptions,
|
|
28
28
|
): Promise<ReviewerResult> {
|
|
29
29
|
return runGeminiReview(plan, schema, this.settings);
|
|
30
30
|
}
|
|
@@ -78,6 +78,7 @@ ${JSON.stringify(schema)}
|
|
|
78
78
|
input: plan,
|
|
79
79
|
timeout: timeout * 1000,
|
|
80
80
|
maxBuffer: 10 * 1024 * 1024,
|
|
81
|
+
shell: process.platform === "win32",
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
if (result.killed || result.signal === "SIGTERM") {
|
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
|
|
10
|
-
import { validatePlanPath } from "./constants.js";
|
|
11
|
-
import type { IterationEntry, IterationState } from "./types.js";
|
|
8
|
+
import * as path from "node:path";
|
|
12
9
|
import { atomicWrite } from "../../_shared/lib-ts/base/atomic-write.js";
|
|
13
|
-
import {
|
|
10
|
+
import { logInfo, logWarn, logError } from "../../_shared/lib-ts/base/logger.js";
|
|
14
11
|
import { nowIso } from "../../_shared/lib-ts/base/utils.js";
|
|
12
|
+
import { validatePlanPath } from "./constants.js";
|
|
13
|
+
import type { IterationState, IterationEntry } from "./types.js";
|
|
15
14
|
|
|
16
15
|
// ---------------------------------------------------------------------------
|
|
17
16
|
// Constants
|
|
@@ -19,10 +18,10 @@ import { nowIso } from "../../_shared/lib-ts/base/utils.js";
|
|
|
19
18
|
|
|
20
19
|
const STATE_SCHEMA_VERSION = "1.0.0";
|
|
21
20
|
|
|
22
|
-
const DEFAULT_REVIEW_ITERATIONS: Record<string, number> = {
|
|
21
|
+
export const DEFAULT_REVIEW_ITERATIONS: Record<string, number> = {
|
|
23
22
|
simple: 1,
|
|
24
|
-
medium:
|
|
25
|
-
high:
|
|
23
|
+
medium: 3,
|
|
24
|
+
high: 5,
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +43,7 @@ export function getStateFilePath(planPath: string): string {
|
|
|
44
43
|
/**
|
|
45
44
|
* Load state file with schema validation and migration.
|
|
46
45
|
*/
|
|
47
|
-
export function loadState(planPath: string):
|
|
46
|
+
export function loadState(planPath: string): Record<string, unknown> | null {
|
|
48
47
|
try {
|
|
49
48
|
const stateFile = getStateFilePath(planPath);
|
|
50
49
|
|
|
@@ -53,7 +52,7 @@ export function loadState(planPath: string): null | Record<string, unknown> {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
const state = JSON.parse(
|
|
56
|
-
fs.readFileSync(stateFile, "
|
|
55
|
+
fs.readFileSync(stateFile, "utf-8"),
|
|
57
56
|
) as Record<string, unknown>;
|
|
58
57
|
|
|
59
58
|
// Handle schema version (backward compatible)
|
|
@@ -73,13 +72,12 @@ export function loadState(planPath: string): null | Record<string, unknown> {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
return state;
|
|
76
|
-
} catch (
|
|
77
|
-
if (
|
|
78
|
-
logError("state", `SECURITY: Invalid plan path: ${
|
|
75
|
+
} catch (e: unknown) {
|
|
76
|
+
if (e instanceof Error && e.message.includes("Invalid plan path")) {
|
|
77
|
+
logError("state", `SECURITY: Invalid plan path: ${e}`);
|
|
79
78
|
} else {
|
|
80
|
-
logError("state", `Failed to load state: ${
|
|
81
|
-
}
|
|
82
|
-
|
|
79
|
+
logError("state", `Failed to load state: ${e}`);
|
|
80
|
+
}
|
|
83
81
|
return null;
|
|
84
82
|
}
|
|
85
83
|
}
|
|
@@ -111,13 +109,12 @@ export function saveStateToPlan(
|
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
return true;
|
|
114
|
-
} catch (
|
|
115
|
-
if (
|
|
116
|
-
logError("state", `SECURITY: Invalid plan path: ${
|
|
112
|
+
} catch (e: unknown) {
|
|
113
|
+
if (e instanceof Error && e.message.includes("Invalid plan path")) {
|
|
114
|
+
logError("state", `SECURITY: Invalid plan path: ${e}`);
|
|
117
115
|
} else {
|
|
118
|
-
logError("state", String(
|
|
119
|
-
}
|
|
120
|
-
|
|
116
|
+
logError("state", String(e));
|
|
117
|
+
}
|
|
121
118
|
return false;
|
|
122
119
|
}
|
|
123
120
|
}
|
|
@@ -132,16 +129,14 @@ export function deleteState(planPath: string): boolean {
|
|
|
132
129
|
if (fs.existsSync(stateFile)) {
|
|
133
130
|
fs.unlinkSync(stateFile);
|
|
134
131
|
logInfo("state", `Deleted state file: ${stateFile}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
132
|
+
}
|
|
137
133
|
return true;
|
|
138
|
-
} catch (
|
|
139
|
-
if (
|
|
140
|
-
logError("state", `SECURITY: Invalid plan path in delete: ${
|
|
134
|
+
} catch (e: unknown) {
|
|
135
|
+
if (e instanceof Error && e.message.includes("Invalid plan path")) {
|
|
136
|
+
logError("state", `SECURITY: Invalid plan path in delete: ${e}`);
|
|
141
137
|
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
logWarn("state", `Failed to delete state file: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
logWarn("state", `Failed to delete state file: ${e}`);
|
|
145
140
|
return false;
|
|
146
141
|
}
|
|
147
142
|
}
|
|
@@ -178,6 +173,9 @@ export function getIterationState(
|
|
|
178
173
|
max: reviewIterations[complexity] ?? 1,
|
|
179
174
|
complexity,
|
|
180
175
|
history: [],
|
|
176
|
+
graduated: [],
|
|
177
|
+
passStreaks: {},
|
|
178
|
+
lastPlanHash: "",
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
181
|
|
|
@@ -209,7 +207,7 @@ export function shouldContinueIterating(
|
|
|
209
207
|
verdict: string,
|
|
210
208
|
config?: Record<string, unknown>,
|
|
211
209
|
): boolean {
|
|
212
|
-
const
|
|
210
|
+
const current = iteration.current;
|
|
213
211
|
const maxIter = iteration.max;
|
|
214
212
|
|
|
215
213
|
// At or past max iterations
|
|
@@ -225,8 +223,7 @@ export function shouldContinueIterating(
|
|
|
225
223
|
let earlyExit = true;
|
|
226
224
|
if (config) {
|
|
227
225
|
earlyExit = (config.earlyExitOnAllPass as boolean) ?? true;
|
|
228
|
-
}
|
|
229
|
-
|
|
226
|
+
}
|
|
230
227
|
if (earlyExit && verdict === "pass") {
|
|
231
228
|
logInfo(
|
|
232
229
|
"state",
|