planpong 0.3.0 → 0.5.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 (59) hide show
  1. package/dist/src/config/defaults.js +1 -0
  2. package/dist/src/config/defaults.js.map +1 -1
  3. package/dist/src/config/loader.d.ts +1 -0
  4. package/dist/src/config/loader.js +3 -0
  5. package/dist/src/config/loader.js.map +1 -1
  6. package/dist/src/core/apply-edits.d.ts +40 -0
  7. package/dist/src/core/apply-edits.js +220 -0
  8. package/dist/src/core/apply-edits.js.map +1 -0
  9. package/dist/src/core/convergence.d.ts +57 -4
  10. package/dist/src/core/convergence.js +134 -6
  11. package/dist/src/core/convergence.js.map +1 -1
  12. package/dist/src/core/loop.js +3 -3
  13. package/dist/src/core/loop.js.map +1 -1
  14. package/dist/src/core/operations.d.ts +14 -1
  15. package/dist/src/core/operations.js +592 -56
  16. package/dist/src/core/operations.js.map +1 -1
  17. package/dist/src/core/plan-diff.d.ts +23 -0
  18. package/dist/src/core/plan-diff.js +135 -0
  19. package/dist/src/core/plan-diff.js.map +1 -0
  20. package/dist/src/core/session.d.ts +11 -0
  21. package/dist/src/core/session.js +51 -1
  22. package/dist/src/core/session.js.map +1 -1
  23. package/dist/src/mcp/tools/get-feedback.d.ts +16 -0
  24. package/dist/src/mcp/tools/get-feedback.js +118 -114
  25. package/dist/src/mcp/tools/get-feedback.js.map +1 -1
  26. package/dist/src/mcp/tools/revise.d.ts +16 -0
  27. package/dist/src/mcp/tools/revise.js +76 -61
  28. package/dist/src/mcp/tools/revise.js.map +1 -1
  29. package/dist/src/mcp/tools/status.js +15 -1
  30. package/dist/src/mcp/tools/status.js.map +1 -1
  31. package/dist/src/prompts/planner.d.ts +34 -1
  32. package/dist/src/prompts/planner.js +272 -17
  33. package/dist/src/prompts/planner.js.map +1 -1
  34. package/dist/src/prompts/reviewer.d.ts +14 -1
  35. package/dist/src/prompts/reviewer.js +84 -1
  36. package/dist/src/prompts/reviewer.js.map +1 -1
  37. package/dist/src/providers/claude.d.ts +3 -0
  38. package/dist/src/providers/claude.js +151 -13
  39. package/dist/src/providers/claude.js.map +1 -1
  40. package/dist/src/providers/codex.d.ts +3 -0
  41. package/dist/src/providers/codex.js +150 -14
  42. package/dist/src/providers/codex.js.map +1 -1
  43. package/dist/src/providers/types.d.ts +69 -3
  44. package/dist/src/schemas/config.d.ts +3 -0
  45. package/dist/src/schemas/config.js +6 -0
  46. package/dist/src/schemas/config.js.map +1 -1
  47. package/dist/src/schemas/json-schema.d.ts +21 -0
  48. package/dist/src/schemas/json-schema.js +172 -0
  49. package/dist/src/schemas/json-schema.js.map +1 -0
  50. package/dist/src/schemas/metrics.d.ts +171 -0
  51. package/dist/src/schemas/metrics.js +49 -0
  52. package/dist/src/schemas/metrics.js.map +1 -0
  53. package/dist/src/schemas/revision.d.ts +166 -2
  54. package/dist/src/schemas/revision.js +35 -2
  55. package/dist/src/schemas/revision.js.map +1 -1
  56. package/dist/src/schemas/session.d.ts +6 -0
  57. package/dist/src/schemas/session.js +10 -0
  58. package/dist/src/schemas/session.js.map +1 -1
  59. package/package.json +4 -2
@@ -13,41 +13,151 @@ function cleanEnv() {
13
13
  }
14
14
  return env;
15
15
  }
16
+ /**
17
+ * Parse claude's `--output-format json` envelope and extract the
18
+ * `structured_output` field as a JSON string ready for downstream parsing.
19
+ * Returns null if the envelope is malformed or the field is missing.
20
+ *
21
+ * Envelope shape (subset):
22
+ * {
23
+ * "type": "result",
24
+ * "is_error": false,
25
+ * "result": "",
26
+ * "structured_output": { ...model's constrained JSON... },
27
+ * ...
28
+ * }
29
+ */
30
+ function extractStructuredOutput(stdout) {
31
+ try {
32
+ const envelope = JSON.parse(stdout);
33
+ if (envelope &&
34
+ typeof envelope === "object" &&
35
+ "structured_output" in envelope &&
36
+ envelope.structured_output !== null &&
37
+ typeof envelope.structured_output === "object") {
38
+ return JSON.stringify(envelope.structured_output);
39
+ }
40
+ }
41
+ catch {
42
+ // Not JSON — may indicate a pre-envelope error or auth failure
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Classify a CLI invocation failure as `capability` (downgrade-eligible) or
48
+ * `fatal` (terminal). Capability errors indicate the CLI doesn't support the
49
+ * requested structured output flag; fatal errors are everything else.
50
+ */
51
+ function classifyError(stderr, exitCode) {
52
+ const lower = stderr.toLowerCase();
53
+ const capabilityIndicators = [
54
+ "unknown flag",
55
+ "unknown option",
56
+ "unrecognized",
57
+ "invalid schema",
58
+ "invalid json schema",
59
+ "json-schema",
60
+ "unsupported",
61
+ ];
62
+ const isCapability = capabilityIndicators.some((indicator) => lower.includes(indicator));
63
+ return {
64
+ kind: isCapability ? "capability" : "fatal",
65
+ message: stderr.slice(0, 500) || `claude exited with code ${exitCode}`,
66
+ exitCode,
67
+ stderr,
68
+ };
69
+ }
16
70
  export class ClaudeProvider {
17
71
  name = "claude";
72
+ capabilityCache = null;
18
73
  async invoke(prompt, options) {
19
- // claude -p reads prompt from stdin when no positional arg is given
20
- const args = ["-p", "--output-format", "text"];
74
+ // claude -p reads prompt from stdin when no positional arg is given.
75
+ // --bare skips hooks/MCP/auto-memory/CLAUDE.md/plugin-sync for faster
76
+ // subprocess startup, but it bypasses OAuth/keychain — only safe to use
77
+ // when ANTHROPIC_API_KEY is set.
78
+ const args = ["-p"];
79
+ if (process.env.ANTHROPIC_API_KEY) {
80
+ args.push("--bare");
81
+ }
82
+ if (options.jsonSchema) {
83
+ // With a schema, use --output-format json so the response envelope
84
+ // includes a `structured_output` field containing the model's
85
+ // constrained JSON as a native object. --output-format text drops
86
+ // the structured_output field entirely.
87
+ args.push("--output-format", "json", "--json-schema", JSON.stringify(options.jsonSchema));
88
+ }
89
+ else {
90
+ args.push("--output-format", "text");
91
+ }
21
92
  if (options.model) {
22
93
  args.push("--model", options.model);
23
94
  }
95
+ // Persistent conversation. `--session-id` creates a new session with the
96
+ // given UUID; `--resume` continues an existing one. Caller must use one
97
+ // or the other, never both. Lets us drop heavy "current plan + prior
98
+ // decisions" stuffing on round 2+ since the model retains context.
99
+ if (options.newSessionId && options.resumeSessionId) {
100
+ throw new Error("claude provider: newSessionId and resumeSessionId are mutually exclusive");
101
+ }
102
+ if (options.newSessionId) {
103
+ args.push("--session-id", options.newSessionId);
104
+ }
105
+ else if (options.resumeSessionId) {
106
+ args.push("--resume", options.resumeSessionId);
107
+ }
24
108
  const start = Date.now();
25
109
  try {
26
110
  const result = await execa("claude", args, {
27
111
  cwd: options.cwd,
28
112
  preferLocal: true,
29
- timeout: options.timeout ?? 300_000,
113
+ timeout: options.timeout ?? 600_000,
30
114
  reject: false,
31
115
  env: cleanEnv(),
32
116
  extendEnv: false,
33
117
  input: prompt,
34
118
  });
35
- if (result.exitCode !== 0) {
36
- process.stderr.write(`[claude-provider] exit=${result.exitCode} stderr=${result.stderr?.slice(0, 500)}\n`);
119
+ const duration = Date.now() - start;
120
+ const exitCode = result.exitCode ?? 1;
121
+ // claude -p can exit non-zero with valid stdout. Treat presence of
122
+ // stdout as success even on non-zero exit.
123
+ // Canonical session ID for the response: echoes the input UUID
124
+ // (claude accepts external UUIDs as session IDs, so input == output).
125
+ const sessionId = options.newSessionId ?? options.resumeSessionId;
126
+ if (result.stdout && result.stdout.trim().length > 0) {
127
+ if (options.jsonSchema) {
128
+ // Parse claude's envelope and extract structured_output.
129
+ const extracted = extractStructuredOutput(result.stdout);
130
+ if (extracted === null) {
131
+ return {
132
+ ok: false,
133
+ error: {
134
+ kind: "capability",
135
+ message: `claude returned envelope without structured_output field: ${result.stdout.slice(0, 300)}`,
136
+ exitCode,
137
+ stderr: result.stderr,
138
+ },
139
+ duration,
140
+ };
141
+ }
142
+ return { ok: true, output: extracted, duration, sessionId };
143
+ }
144
+ return { ok: true, output: result.stdout, duration, sessionId };
37
145
  }
146
+ // No usable output — classify the failure
147
+ process.stderr.write(`[claude-provider] exit=${exitCode} stderr=${result.stderr?.slice(0, 500)}\n`);
38
148
  return {
39
- content: result.stdout,
40
- exitCode: result.exitCode ?? 1,
41
- duration: Date.now() - start,
149
+ ok: false,
150
+ error: classifyError(result.stderr ?? "", exitCode),
151
+ duration,
42
152
  };
43
153
  }
44
154
  catch (error) {
155
+ const duration = Date.now() - start;
156
+ const message = error instanceof Error ? error.message : "Unknown error invoking claude";
45
157
  return {
46
- content: error instanceof Error
47
- ? error.message
48
- : "Unknown error invoking claude",
49
- exitCode: 1,
50
- duration: Date.now() - start,
158
+ ok: false,
159
+ error: { kind: "fatal", message, exitCode: 1 },
160
+ duration,
51
161
  };
52
162
  }
53
163
  }
@@ -66,6 +176,34 @@ export class ClaudeProvider {
66
176
  return false;
67
177
  }
68
178
  }
179
+ async checkStructuredOutputSupport() {
180
+ if (this.capabilityCache !== null) {
181
+ return this.capabilityCache;
182
+ }
183
+ try {
184
+ const result = await execa("claude", ["--help"], {
185
+ preferLocal: true,
186
+ timeout: 5_000,
187
+ reject: false,
188
+ env: cleanEnv(),
189
+ extendEnv: false,
190
+ });
191
+ const helpText = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
192
+ const supported = helpText.includes("--json-schema");
193
+ this.capabilityCache = supported;
194
+ if (!supported) {
195
+ process.stderr.write(`[planpong] Structured output not supported by claude — using legacy parsing\n`);
196
+ }
197
+ return supported;
198
+ }
199
+ catch {
200
+ this.capabilityCache = false;
201
+ return false;
202
+ }
203
+ }
204
+ markNonCapable() {
205
+ this.capabilityCache = false;
206
+ }
69
207
  getModels() {
70
208
  return MODELS;
71
209
  }
@@ -1 +1 @@
1
- {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../src/providers/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAG9B,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAE3C;;;GAGG;AACH,SAAS,QAAQ;IACf,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,IAAI,GAAG,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAChD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,cAAc;IACzB,IAAI,GAAG,QAAQ,CAAC;IAEhB,KAAK,CAAC,MAAM,CACV,MAAc,EACd,OAAsB;QAEtB,oEAAoE;QACpE,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC;QAE/C,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE;gBACzC,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,OAAO;gBACnC,MAAM,EAAE,KAAK;gBACb,GAAG,EAAE,QAAQ,EAAE;gBACf,SAAS,EAAE,KAAK;gBAChB,KAAK,EAAE,MAAM;aACd,CAAC,CAAC;YAEH,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,0BAA0B,MAAM,CAAC,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CACrF,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,MAAM;gBACtB,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,CAAC;gBAC9B,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;aAC7B,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,OAAO,EACL,KAAK,YAAY,KAAK;oBACpB,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,+BAA+B;gBACrC,QAAQ,EAAE,CAAC;gBACX,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;aAC7B,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,EAAE;gBAClD,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,KAAK;gBACb,GAAG,EAAE,QAAQ,EAAE;gBACf,SAAS,EAAE,KAAK;aACjB,CAAC,CAAC;YACH,OAAO,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,SAAS;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,eAAe;QACb,oEAAoE;QACpE,OAAO,CAAC,SAAS,CAAC,CAAC;IACrB,CAAC;CACF"}
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../src/providers/claude.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,11 @@
1
1
  import type { Provider, InvokeOptions, ProviderResponse } from "./types.js";
2
2
  export declare class CodexProvider implements Provider {
3
3
  name: string;
4
+ private capabilityCache;
4
5
  invoke(prompt: string, options: InvokeOptions): Promise<ProviderResponse>;
5
6
  isAvailable(): Promise<boolean>;
7
+ checkStructuredOutputSupport(): Promise<boolean>;
8
+ markNonCapable(): void;
6
9
  getModels(): string[];
7
10
  getEffortLevels(): string[];
8
11
  }
@@ -1,14 +1,79 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { readFileSync, unlinkSync } from "node:fs";
2
+ import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { execa } from "execa";
6
6
  const MODELS = ["gpt-5.3-codex", "o3-pro", "o3", "o4-mini"];
7
7
  const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
8
+ /**
9
+ * Classify a CLI invocation failure as `capability` (downgrade-eligible) or
10
+ * `fatal` (terminal). Capability errors indicate the CLI doesn't support the
11
+ * requested structured output flag; fatal errors are everything else.
12
+ *
13
+ * Patterns must be narrow — codex's normal session header includes flag
14
+ * names like "output-schema:" in its info output, so substring matches on
15
+ * the flag name alone produce false positives.
16
+ */
17
+ function extractCodexThreadId(stdout) {
18
+ if (!stdout)
19
+ return undefined;
20
+ // The first non-empty line of `codex exec --json` stdout is a
21
+ // `thread.started` or `thread.resumed` event with `thread_id`. Scan
22
+ // the first ~10 lines defensively in case the model emits anything
23
+ // else first.
24
+ const lines = stdout.split("\n").slice(0, 10);
25
+ for (const line of lines) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed.startsWith("{"))
28
+ continue;
29
+ try {
30
+ const evt = JSON.parse(trimmed);
31
+ if ((evt.type === "thread.started" || evt.type === "thread.resumed") &&
32
+ typeof evt.thread_id === "string") {
33
+ return evt.thread_id;
34
+ }
35
+ }
36
+ catch {
37
+ continue;
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ function classifyError(stderr, exitCode) {
43
+ const lower = stderr.toLowerCase();
44
+ const capabilityPatterns = [
45
+ /\bunknown (?:flag|option|argument)\b/,
46
+ /\bunrecognized (?:flag|option|argument)\b/,
47
+ /\bunexpected (?:flag|option|argument)\b/,
48
+ /\binvalid_json_schema\b/,
49
+ /\binvalid schema\b/,
50
+ /\bschema is not supported\b/,
51
+ /\bstructured output (?:not|isn't) supported\b/,
52
+ ];
53
+ const isCapability = capabilityPatterns.some((pattern) => pattern.test(lower));
54
+ return {
55
+ kind: isCapability ? "capability" : "fatal",
56
+ message: stderr.slice(0, 500) || `codex exited with code ${exitCode}`,
57
+ exitCode,
58
+ stderr,
59
+ };
60
+ }
8
61
  export class CodexProvider {
9
62
  name = "codex";
63
+ capabilityCache = null;
10
64
  async invoke(prompt, options) {
11
- const args = ["exec"];
65
+ // codex doesn't accept an externally-generated session UUID. The first
66
+ // call always creates a fresh thread; we capture `thread_id` from the
67
+ // `--json` event stream on stdout and the caller persists it. Resume
68
+ // subsequent calls via `codex exec resume <id>` (subcommand form).
69
+ if (options.newSessionId) {
70
+ // Silent ignore — codex generates its own ID. The caller will get
71
+ // the actual ID back via ProviderResponse.sessionId.
72
+ }
73
+ const isResume = options.resumeSessionId != null && options.resumeSessionId.length > 0;
74
+ const args = isResume
75
+ ? ["exec", "resume", options.resumeSessionId]
76
+ : ["exec"];
12
77
  if (options.model) {
13
78
  args.push("-m", options.model);
14
79
  }
@@ -18,6 +83,23 @@ export class CodexProvider {
18
83
  // Write clean output to a temp file to avoid parsing header/footer
19
84
  const outFile = join(tmpdir(), `planpong-codex-${randomBytes(6).toString("hex")}.txt`);
20
85
  args.push("-o", outFile);
86
+ // Always enable --json so we can capture the thread_id event from
87
+ // stdout. The `-o` file still receives the agent's clean text output;
88
+ // `--json` only changes stdout/stderr streaming.
89
+ args.push("--json");
90
+ // Optional structured output schema
91
+ let schemaFile = null;
92
+ if (options.jsonSchema) {
93
+ schemaFile = join(tmpdir(), `planpong-codex-schema-${randomBytes(6).toString("hex")}.json`);
94
+ try {
95
+ writeFileSync(schemaFile, JSON.stringify(options.jsonSchema));
96
+ args.push("--output-schema", schemaFile);
97
+ }
98
+ catch (error) {
99
+ // If we can't write the schema file, fall through without structured output
100
+ schemaFile = null;
101
+ }
102
+ }
21
103
  // Use stdin for prompt (CLI arg has length limits)
22
104
  args.push("-");
23
105
  const start = Date.now();
@@ -25,38 +107,66 @@ export class CodexProvider {
25
107
  const result = await execa("codex", args, {
26
108
  cwd: options.cwd,
27
109
  preferLocal: true,
28
- timeout: options.timeout ?? 300_000,
110
+ timeout: options.timeout ?? 600_000,
29
111
  reject: false,
30
112
  input: prompt,
31
113
  });
32
- let content;
114
+ const duration = Date.now() - start;
115
+ const exitCode = result.exitCode ?? 1;
116
+ let content = "";
33
117
  try {
34
118
  content = readFileSync(outFile, "utf-8");
35
119
  }
36
120
  catch {
37
121
  // Fall back to stdout if output file wasn't created
38
- content = result.stdout;
122
+ content = result.stdout ?? "";
39
123
  }
40
- // Clean up temp file
124
+ // Clean up temp files
41
125
  try {
42
126
  unlinkSync(outFile);
43
127
  }
44
128
  catch {
45
129
  // ignore
46
130
  }
131
+ if (schemaFile) {
132
+ try {
133
+ unlinkSync(schemaFile);
134
+ }
135
+ catch {
136
+ // ignore
137
+ }
138
+ }
139
+ if (content && content.trim().length > 0) {
140
+ // Capture thread_id from --json stdout. The first event is
141
+ // `thread.started` (fresh) or `thread.resumed` (resume). Both
142
+ // carry `thread_id`. We treat parse failures as "no session
143
+ // tracking" rather than as errors — sessions are an
144
+ // optimization, not a correctness requirement.
145
+ const sessionId = extractCodexThreadId(result.stdout);
146
+ return { ok: true, output: content, duration, sessionId };
147
+ }
47
148
  return {
48
- content,
49
- exitCode: result.exitCode ?? 1,
50
- duration: Date.now() - start,
149
+ ok: false,
150
+ error: classifyError(result.stderr ?? "", exitCode),
151
+ duration,
51
152
  };
52
153
  }
53
154
  catch (error) {
155
+ const duration = Date.now() - start;
156
+ // Cleanup on error path
157
+ if (schemaFile) {
158
+ try {
159
+ unlinkSync(schemaFile);
160
+ }
161
+ catch {
162
+ // ignore
163
+ }
164
+ }
165
+ const message = error instanceof Error ? error.message : "Unknown error invoking codex";
54
166
  return {
55
- content: error instanceof Error
56
- ? error.message
57
- : "Unknown error invoking codex",
58
- exitCode: 1,
59
- duration: Date.now() - start,
167
+ ok: false,
168
+ error: { kind: "fatal", message, exitCode: 1 },
169
+ duration,
60
170
  };
61
171
  }
62
172
  }
@@ -73,6 +183,32 @@ export class CodexProvider {
73
183
  return false;
74
184
  }
75
185
  }
186
+ async checkStructuredOutputSupport() {
187
+ if (this.capabilityCache !== null) {
188
+ return this.capabilityCache;
189
+ }
190
+ try {
191
+ const result = await execa("codex", ["exec", "--help"], {
192
+ preferLocal: true,
193
+ timeout: 5_000,
194
+ reject: false,
195
+ });
196
+ const helpText = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
197
+ const supported = helpText.includes("--output-schema");
198
+ this.capabilityCache = supported;
199
+ if (!supported) {
200
+ process.stderr.write(`[planpong] Structured output not supported by codex — using legacy parsing\n`);
201
+ }
202
+ return supported;
203
+ }
204
+ catch {
205
+ this.capabilityCache = false;
206
+ return false;
207
+ }
208
+ }
209
+ markNonCapable() {
210
+ this.capabilityCache = false;
211
+ }
76
212
  getModels() {
77
213
  return MODELS;
78
214
  }
@@ -1 +1 @@
1
- {"version":3,"file":"codex.js","sourceRoot":"","sources":["../../../src/providers/codex.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"codex.js","sourceRoot":"","sources":["../../../src/providers/codex.ts"],"names":[],"mappings":"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"}
@@ -3,16 +3,82 @@ export interface InvokeOptions {
3
3
  model?: string;
4
4
  effort?: string;
5
5
  timeout?: number;
6
+ /**
7
+ * JSON Schema to constrain model output. When set, providers pass this to
8
+ * their respective structured-output flags (`--json-schema` for claude,
9
+ * `--output-schema` for codex).
10
+ */
11
+ jsonSchema?: Record<string, unknown>;
12
+ /**
13
+ * Initialize a NEW persistent conversation with this UUID. Mutually
14
+ * exclusive with `resumeSessionId`. After the first call succeeds, the
15
+ * caller must use `resumeSessionId` on subsequent calls.
16
+ */
17
+ newSessionId?: string;
18
+ /**
19
+ * Resume an EXISTING persistent conversation. The model retains context
20
+ * from prior turns. Only valid for sessions previously created with
21
+ * `newSessionId`.
22
+ */
23
+ resumeSessionId?: string;
6
24
  }
7
- export interface ProviderResponse {
8
- content: string;
25
+ /**
26
+ * Provider invocation error categories. Used by the operations-layer state
27
+ * machine to decide whether to downgrade or fail terminally.
28
+ *
29
+ * - `capability`: schema rejected, flag unrecognized at runtime, structured
30
+ * output format error. Indicates the CLI doesn't support the requested
31
+ * structured output mode. Downgrade-eligible.
32
+ * - `fatal`: auth failure, timeout, network/transport error, non-zero exit
33
+ * with no output. Unrelated to structured output capability. Terminal.
34
+ */
35
+ export type ProviderErrorKind = "capability" | "fatal";
36
+ export interface ProviderError {
37
+ kind: ProviderErrorKind;
38
+ message: string;
9
39
  exitCode: number;
10
- duration: number;
40
+ stderr?: string;
11
41
  }
42
+ /**
43
+ * Discriminated result of a single provider invocation. Providers are
44
+ * single-shot — they perform one invocation and return either the output
45
+ * or a typed error. They do NOT retry or downgrade internally; that is
46
+ * the operations-layer state machine's job.
47
+ *
48
+ * `sessionId` (success only): the canonical conversation ID the provider
49
+ * used. For providers that accept an externally-generated UUID (claude),
50
+ * this echoes the input; for providers that generate their own IDs
51
+ * (codex), this is parsed from the provider's output. Caller persists
52
+ * this and passes it as `resumeSessionId` on subsequent calls to keep
53
+ * the same conversation thread.
54
+ */
55
+ export type ProviderResponse = {
56
+ ok: true;
57
+ output: string;
58
+ duration: number;
59
+ sessionId?: string;
60
+ } | {
61
+ ok: false;
62
+ error: ProviderError;
63
+ duration: number;
64
+ };
12
65
  export interface Provider {
13
66
  name: string;
14
67
  invoke(prompt: string, options: InvokeOptions): Promise<ProviderResponse>;
15
68
  isAvailable(): Promise<boolean>;
16
69
  getModels(): string[];
17
70
  getEffortLevels(): string[];
71
+ /**
72
+ * Probe the underlying CLI to determine whether structured output is
73
+ * supported. Result is cached for the session lifetime. If the probe
74
+ * fails or times out, returns false (use legacy path).
75
+ */
76
+ checkStructuredOutputSupport(): Promise<boolean>;
77
+ /**
78
+ * Mark this provider as non-capable for the remainder of the session.
79
+ * Called by the state machine after a runtime structured-output failure
80
+ * (capability error or JSON.parse failure) to prevent re-attempting
81
+ * structured output on subsequent rounds.
82
+ */
83
+ markNonCapable(): void;
18
84
  }
@@ -42,6 +42,7 @@ export declare const PlanpongConfigSchema: z.ZodObject<{
42
42
  plans_dir: z.ZodDefault<z.ZodString>;
43
43
  max_rounds: z.ZodDefault<z.ZodNumber>;
44
44
  human_in_loop: z.ZodDefault<z.ZodBoolean>;
45
+ revision_mode: z.ZodDefault<z.ZodEnum<["edits", "full"]>>;
45
46
  }, "strip", z.ZodTypeAny, {
46
47
  planner: {
47
48
  provider: string;
@@ -56,6 +57,7 @@ export declare const PlanpongConfigSchema: z.ZodObject<{
56
57
  plans_dir: string;
57
58
  max_rounds: number;
58
59
  human_in_loop: boolean;
60
+ revision_mode: "edits" | "full";
59
61
  }, {
60
62
  planner: {
61
63
  provider: string;
@@ -70,6 +72,7 @@ export declare const PlanpongConfigSchema: z.ZodObject<{
70
72
  plans_dir?: string | undefined;
71
73
  max_rounds?: number | undefined;
72
74
  human_in_loop?: boolean | undefined;
75
+ revision_mode?: "edits" | "full" | undefined;
73
76
  }>;
74
77
  export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
75
78
  export type PlanpongConfig = z.infer<typeof PlanpongConfigSchema>;
@@ -10,5 +10,11 @@ export const PlanpongConfigSchema = z.object({
10
10
  plans_dir: z.string().default("docs/plans"),
11
11
  max_rounds: z.number().int().min(1).max(50).default(10),
12
12
  human_in_loop: z.boolean().default(true),
13
+ // Initial release defaults to `"full"`. Edits mode is opt-in via
14
+ // planpong.yaml until benchmark thresholds (first-pass success ≥80%,
15
+ // retry rate <30%, no rounds increase) are confirmed. `"full"` is the
16
+ // kill switch — risk + detail phases use the direction-phase schema and
17
+ // skip the edit applier entirely.
18
+ revision_mode: z.enum(["edits", "full"]).default("full"),
13
19
  });
14
20
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/schemas/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,oBAAoB;IAC7B,QAAQ,EAAE,oBAAoB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACvD,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACzC,CAAC,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/schemas/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,oBAAoB;IAC7B,QAAQ,EAAE,oBAAoB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACvD,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACxC,iEAAiE;IACjE,qEAAqE;IACrE,sEAAsE;IACtE,wEAAwE;IACxE,kCAAkC;IAClC,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;CACzD,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { ReviewPhase } from "../prompts/reviewer.js";
2
+ export declare const DirectionFeedbackJsonSchema: Record<string, unknown>;
3
+ export declare const RiskFeedbackJsonSchema: Record<string, unknown>;
4
+ export declare const ReviewFeedbackJsonSchema: Record<string, unknown>;
5
+ export declare const PlannerRevisionJsonSchema: Record<string, unknown>;
6
+ /**
7
+ * Get the JSON Schema appropriate for a given review phase.
8
+ */
9
+ export declare function getFeedbackJsonSchemaForPhase(phase: ReviewPhase): Record<string, unknown>;
10
+ /**
11
+ * Get the JSON Schema for a planner revision response, selecting the
12
+ * shape based on phase and the configured revision mode.
13
+ *
14
+ * - Direction phase always emits `updated_plan` (sweeping rewrites are
15
+ * allowed in round 1).
16
+ * - Risk + detail phase with `revisionMode: "edits"` emits an `edits[]`
17
+ * array — the planner cannot fall back to full output.
18
+ * - Risk + detail phase with `revisionMode: "full"` keeps the full-plan
19
+ * shape (kill switch).
20
+ */
21
+ export declare function getRevisionJsonSchema(phase: ReviewPhase, revisionMode: "edits" | "full"): Record<string, unknown>;