@umgbhalla/pi-gigaplan 0.1.0 → 0.1.2

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.
@@ -43,7 +43,7 @@ import {
43
43
  activePlanDirs,
44
44
  resolvePlanDir,
45
45
  loadPlan,
46
- saveState,
46
+ savePlanState,
47
47
  latestPlanRecord,
48
48
  latestPlanPath,
49
49
  latestPlanMetaPath,
@@ -173,7 +173,7 @@ function initPlan(
173
173
  last_evaluation: {},
174
174
  };
175
175
 
176
- saveState(planDir, state);
176
+ savePlanState(planDir, state);
177
177
  saveFlagRegistry(planDir, { flags: [] });
178
178
 
179
179
  return { planDir, state };
@@ -202,7 +202,7 @@ function processStepOutput(
202
202
  duration_ms: durationMs,
203
203
  result: "success",
204
204
  });
205
- saveState(planDir, state);
205
+ savePlanState(planDir, state);
206
206
  return {
207
207
  success: true,
208
208
  step: "clarify",
@@ -264,7 +264,7 @@ function processStepOutput(
264
264
  result: "success",
265
265
  output_file: planFile,
266
266
  });
267
- saveState(planDir, state);
267
+ savePlanState(planDir, state);
268
268
 
269
269
  return {
270
270
  success: true,
@@ -324,7 +324,7 @@ function processStepOutput(
324
324
  output_file: critiqueFile,
325
325
  flags_count: newFlags.length,
326
326
  });
327
- saveState(planDir, state);
327
+ savePlanState(planDir, state);
328
328
 
329
329
  return {
330
330
  success: true,
@@ -345,7 +345,7 @@ function processStepOutput(
345
345
  result: "success",
346
346
  output_file: "execution.json",
347
347
  });
348
- saveState(planDir, state);
348
+ savePlanState(planDir, state);
349
349
 
350
350
  return {
351
351
  success: true,
@@ -366,7 +366,7 @@ function processStepOutput(
366
366
  result: "success",
367
367
  output_file: "review.json",
368
368
  });
369
- saveState(planDir, state);
369
+ savePlanState(planDir, state);
370
370
 
371
371
  const criteria = (payload.criteria as Array<{ name: string; pass: boolean }>) ?? [];
372
372
  const passed = criteria.filter((c) => c.pass).length;
@@ -405,7 +405,7 @@ function runEvaluate(planDir: string, state: PlanState): StepResult {
405
405
  (evaluation.signals as Record<string, unknown>).weighted_score as number,
406
406
  ];
407
407
 
408
- state.last_evaluation = evaluation;
408
+ state.last_evaluation = evaluation as unknown as Record<string, unknown>;
409
409
  state.current_state = STATE_EVALUATED;
410
410
  state.history.push({
411
411
  step: "evaluate",
@@ -414,13 +414,13 @@ function runEvaluate(planDir: string, state: PlanState): StepResult {
414
414
  recommendation: evaluation.recommendation,
415
415
  output_file: evalFile,
416
416
  });
417
- saveState(planDir, state);
417
+ savePlanState(planDir, state);
418
418
 
419
419
  return {
420
420
  success: true,
421
421
  step: "evaluate",
422
422
  summary: `Evaluation: ${evaluation.recommendation} (${evaluation.confidence} confidence). ${evaluation.rationale}`,
423
- nextSteps: evaluation.valid_next_steps,
423
+ nextSteps: evaluation.valid_next_steps ?? [],
424
424
  artifacts: [evalFile],
425
425
  };
426
426
  }
@@ -470,7 +470,7 @@ function runGate(planDir: string, state: PlanState): StepResult {
470
470
  timestamp: nowUtc(),
471
471
  result: passed ? "success" : "failed",
472
472
  });
473
- saveState(planDir, state);
473
+ savePlanState(planDir, state);
474
474
 
475
475
  return {
476
476
  success: passed,
@@ -492,7 +492,8 @@ export default function gigaplanExtension(pi: ExtensionAPI) {
492
492
  // Widget state
493
493
  let activePlan: { name: string; state: string; step: string } | null = null;
494
494
 
495
- function updateWidget(ctx: any) {
495
+ function updateWidget(ctx?: any) {
496
+ if (!ctx?.ui) return;
496
497
  if (!activePlan) {
497
498
  ctx.ui.setStatus("gigaplan", "");
498
499
  return;
@@ -514,11 +515,14 @@ export default function gigaplanExtension(pi: ExtensionAPI) {
514
515
  }
515
516
 
516
517
  // Ask for configuration
517
- const robustness = await ctx.ui.select("Robustness level", [
518
- { label: "Light — pragmatic, fast", value: "light" },
519
- { label: "Standard — balanced (default)", value: "standard" },
520
- { label: "Thorough — exhaustive review", value: "thorough" },
521
- ]);
518
+ const ROBUSTNESS_OPTIONS = ["Light — pragmatic, fast", "Standard — balanced (default)", "Thorough — exhaustive review"];
519
+ const ROBUSTNESS_MAP: Record<string, string> = {
520
+ [ROBUSTNESS_OPTIONS[0]]: "light",
521
+ [ROBUSTNESS_OPTIONS[1]]: "standard",
522
+ [ROBUSTNESS_OPTIONS[2]]: "thorough",
523
+ };
524
+ const robustnessChoice = await ctx.ui.select("Robustness level", ROBUSTNESS_OPTIONS);
525
+ const robustness = ROBUSTNESS_MAP[robustnessChoice ?? ""] ?? "standard";
522
526
 
523
527
  const autoApprove = await ctx.ui.confirm(
524
528
  "Auto-approve?",
@@ -528,14 +532,14 @@ export default function gigaplanExtension(pi: ExtensionAPI) {
528
532
  // Initialize
529
533
  const root = ctx.cwd;
530
534
  const { planDir, state } = initPlan(root, idea, {
531
- robustness: robustness ?? "standard",
535
+ robustness,
532
536
  autoApprove,
533
537
  });
534
538
 
535
539
  activePlan = { name: state.name, state: state.current_state, step: "clarify" };
536
540
  updateWidget(ctx);
537
541
 
538
- ctx.ui.notify(`Plan "${state.name}" initialized. Starting orchestration...`, "success");
542
+ ctx.ui.notify(`Plan "${state.name}" initialized. Starting orchestration...`, "info");
539
543
 
540
544
  // Send orchestration prompt to the LLM
541
545
  const orchestrationPrompt = `You are now in **gigaplan mode**. A structured plan has been initialized.
@@ -613,7 +617,7 @@ Start now with the **clarify** step.`;
613
617
  } catch (e) {
614
618
  return {
615
619
  content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
616
- details: { error: true },
620
+ details: { error: true } as any,
617
621
  };
618
622
  }
619
623
  },
@@ -633,7 +637,7 @@ Start now with the **clarify** step.`;
633
637
  durationMs: Type.Optional(Type.Number({ description: "How long the step took in ms" })),
634
638
  }),
635
639
 
636
- async execute(_id, params) {
640
+ async execute(_id, params, _signal, _onUpdate, ctx) {
637
641
  try {
638
642
  const state = readJson(path.join(params.planDir, "state.json")) as PlanState;
639
643
  let result: StepResult;
@@ -659,7 +663,7 @@ Start now with the **clarify** step.`;
659
663
  if (activePlan) {
660
664
  activePlan.state = result.step;
661
665
  activePlan.step = result.nextSteps[0] ?? "done";
662
- updateWidget(null as any); // TODO: need ctx
666
+ updateWidget(ctx);
663
667
  }
664
668
 
665
669
  const nextAction = result.nextSteps.length > 0
@@ -682,7 +686,7 @@ Start now with the **clarify** step.`;
682
686
  } catch (e) {
683
687
  return {
684
688
  content: [{ type: "text", text: `Error advancing: ${e instanceof Error ? e.message : String(e)}` }],
685
- details: { error: true },
689
+ details: { error: true } as any,
686
690
  };
687
691
  }
688
692
  },
@@ -789,14 +793,14 @@ Start now with the **clarify** step.`;
789
793
  timestamp: nowUtc(),
790
794
  message: `add-note: ${params.note}`,
791
795
  });
792
- saveState(params.planDir, state);
796
+ savePlanState(params.planDir, state);
793
797
  return { content: [{ type: "text", text: `Note added. Continue with the current step.` }], details: {} };
794
798
  }
795
799
 
796
800
  case "abort": {
797
801
  state.current_state = STATE_ABORTED;
798
802
  state.history.push({ step: "override", timestamp: nowUtc(), message: "aborted" });
799
- saveState(params.planDir, state);
803
+ savePlanState(params.planDir, state);
800
804
  return { content: [{ type: "text", text: `Plan "${state.name}" aborted.` }], details: {} };
801
805
  }
802
806
 
@@ -808,10 +812,10 @@ Start now with the **clarify** step.`;
808
812
  timestamp: nowUtc(),
809
813
  message: "force-proceed (bypassed gate)",
810
814
  });
811
- saveState(params.planDir, state);
815
+ savePlanState(params.planDir, state);
812
816
  return {
813
817
  content: [{ type: "text", text: "Force-proceeded to gate. Next step: execute." }],
814
- details: { nextSteps: ["execute"] },
818
+ details: { nextSteps: ["execute"] } as any,
815
819
  };
816
820
  }
817
821
 
@@ -823,10 +827,10 @@ Start now with the **clarify** step.`;
823
827
  timestamp: nowUtc(),
824
828
  message: "skip (user override to SKIP)",
825
829
  });
826
- saveState(params.planDir, state);
830
+ savePlanState(params.planDir, state);
827
831
  return {
828
832
  content: [{ type: "text", text: "Skipped to gate. Next step: gate." }],
829
- details: { nextSteps: ["gate"] },
833
+ details: { nextSteps: ["gate"] } as any,
830
834
  };
831
835
  }
832
836
 
@@ -839,7 +843,7 @@ Start now with the **clarify** step.`;
839
843
  } catch (e) {
840
844
  return {
841
845
  content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
842
- details: { error: true },
846
+ details: { error: true } as any,
843
847
  };
844
848
  }
845
849
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umgbhalla/pi-gigaplan",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Structured AI planning with cross-model critique — gigaplan as a pi extension",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,7 +40,7 @@ For **evaluate** and **gate** steps: skip the subagent, just call `gigaplan_adva
40
40
  ```
41
41
  clarify → plan → critique → evaluate
42
42
 
43
- CONTINUE → integrate → plan (loop)
43
+ CONTINUE → integrate → critique (loop)
44
44
  SKIP → gate → execute → review → done
45
45
  ESCALATE → ask user → override
46
46
  ABORT → done
package/src/evaluation.ts CHANGED
@@ -270,7 +270,7 @@ export function buildEvaluation(planDir: string, state: PlanState): EvaluationRe
270
270
  const robustness = configuredRobustness(state);
271
271
  const skipThreshold = ROBUSTNESS_SKIP_THRESHOLDS[robustness] ?? 2.0;
272
272
  const stagnationFactor = ROBUSTNESS_STAGNATION_FACTORS[robustness] ?? 0.9;
273
- const openScopeCreep = scopeCreepFlags(flagRegistry, FLAG_BLOCKING_STATUSES);
273
+ const openScopeCreep = scopeCreepFlags(flagRegistry, { statuses: FLAG_BLOCKING_STATUSES });
274
274
  const significantCount = flagRegistry.flags.filter(
275
275
  (f) => f.severity === "significant" && f.status !== "verified",
276
276
  ).length;
package/src/prompts.ts CHANGED
@@ -4,7 +4,8 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import {
6
6
  PlanState,
7
- CliError,
7
+ FlagRecord,
8
+ GigaplanError,
8
9
  latestPlanPath,
9
10
  readJson,
10
11
  latestPlanMetaPath,
@@ -23,7 +24,7 @@ function clarifyPrompt(state: PlanState, planDir: string): string {
23
24
  const notes = state.meta?.notes ?? [];
24
25
  const notesBlock =
25
26
  notes.length > 0
26
- ? notes.map((n: { note: string }) => `- ${n.note}`).join("\n")
27
+ ? notes.map((n: Record<string, unknown>) => `- ${n.note}`).join("\n")
27
28
  : "- None";
28
29
  return `
29
30
  You are a planning assistant. The user has proposed the following idea:
@@ -52,7 +53,7 @@ function planPrompt(state: PlanState, planDir: string): string {
52
53
  const notes = state.meta?.notes ?? [];
53
54
  const notesBlock =
54
55
  notes.length > 0
55
- ? notes.map((n: { note: string }) => `- ${n.note}`).join("\n")
56
+ ? notes.map((n: Record<string, unknown>) => `- ${n.note}`).join("\n")
56
57
  : "- None";
57
58
  const clarification = state.clarification ?? {};
58
59
  const refined = clarification.refined_idea ?? "";
@@ -103,7 +104,7 @@ function integratePrompt(state: PlanState, planDir: string): string {
103
104
  );
104
105
  const evaluation = readJson(evaluatePath);
105
106
  const unresolved = unresolvedSignificantFlags(flagRegistry);
106
- const openFlags = unresolved.map((flag: Record<string, unknown>) => ({
107
+ const openFlags = unresolved.map((flag) => ({
107
108
  id: flag.id,
108
109
  severity: flag.severity,
109
110
  status: flag.status,
@@ -151,10 +152,10 @@ function critiquePrompt(state: PlanState, planDir: string): string {
151
152
  const flagRegistry = loadFlagRegistry(planDir);
152
153
  const robustness = configuredRobustness(state);
153
154
  const unresolved = flagRegistry.flags
154
- .filter((flag: Record<string, unknown>) =>
155
+ .filter((flag) =>
155
156
  ["addressed", "open", "disputed"].includes(flag.status as string)
156
157
  )
157
- .map((flag: Record<string, unknown>) => ({
158
+ .map((flag) => ({
158
159
  id: flag.id,
159
160
  concern: flag.concern,
160
161
  status: flag.status,
@@ -255,7 +256,7 @@ function reviewClaudePrompt(state: PlanState, planDir: string): string {
255
256
  const latestMeta = readJson(latestPlanMetaPath(planDir, state));
256
257
  const execution = readJson(path.join(planDir, "execution.json"));
257
258
  const gate = readJson(path.join(planDir, "gate.json"));
258
- const diffSummary = collectGitDiffSummary(projectDir);
259
+ const diffSummary = collectGitDiffSummary(projectDir ?? process.cwd());
259
260
  return `
260
261
  Review the execution critically against user intent and observable success criteria.
261
262
 
@@ -294,7 +295,7 @@ function reviewCodexPrompt(state: PlanState, planDir: string): string {
294
295
  );
295
296
  const latestMeta = readJson(latestPlanMetaPath(planDir, state));
296
297
  const execution = readJson(path.join(planDir, "execution.json"));
297
- const diffSummary = collectGitDiffSummary(projectDir);
298
+ const diffSummary = collectGitDiffSummary(projectDir ?? process.cwd());
298
299
  return `
299
300
  Review the implementation against the success criteria.
300
301
 
@@ -358,7 +359,7 @@ export function createPrompt(
358
359
  agent === "codex" ? CODEX_PROMPT_BUILDERS : CLAUDE_PROMPT_BUILDERS;
359
360
  const builder = builders[step];
360
361
  if (!builder) {
361
- throw new CliError(
362
+ throw new GigaplanError(
362
363
  "unsupported_step",
363
364
  `Unsupported ${agent} step '${step}'`
364
365
  );
package/src/workers.ts CHANGED
@@ -60,6 +60,82 @@ function getRequiredKeys(step: string): string[] {
60
60
  return (schema?.required as string[] | undefined) ?? [];
61
61
  }
62
62
 
63
+ function getStepSchema(step: string): Record<string, unknown> | undefined {
64
+ const filename = STEP_SCHEMA_FILENAMES[step];
65
+ if (!filename) return undefined;
66
+ return SCHEMAS[filename] as Record<string, unknown> | undefined;
67
+ }
68
+
69
+ function describeType(value: unknown): string {
70
+ if (Array.isArray(value)) return "array";
71
+ if (value === null) return "null";
72
+ return typeof value;
73
+ }
74
+
75
+ function validateValueAgainstSchema(
76
+ value: unknown,
77
+ schema: Record<string, unknown> | undefined,
78
+ keyPath: string,
79
+ ): void {
80
+ if (!schema) return;
81
+
82
+ const expectedType = schema.type as string | undefined;
83
+ if (expectedType === "string" && typeof value !== "string") {
84
+ throw new GigaplanError(
85
+ "parse_error",
86
+ `${keyPath} must be a string, got ${describeType(value)}`,
87
+ );
88
+ }
89
+ if (expectedType === "boolean" && typeof value !== "boolean") {
90
+ throw new GigaplanError(
91
+ "parse_error",
92
+ `${keyPath} must be a boolean, got ${describeType(value)}`,
93
+ );
94
+ }
95
+ if (expectedType === "array") {
96
+ if (!Array.isArray(value)) {
97
+ throw new GigaplanError(
98
+ "parse_error",
99
+ `${keyPath} must be an array, got ${describeType(value)}`,
100
+ );
101
+ }
102
+ const itemSchema = schema.items as Record<string, unknown> | undefined;
103
+ value.forEach((item, index) => {
104
+ validateValueAgainstSchema(item, itemSchema, `${keyPath}[${index}]`);
105
+ });
106
+ return;
107
+ }
108
+ if (expectedType === "object") {
109
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
110
+ throw new GigaplanError(
111
+ "parse_error",
112
+ `${keyPath} must be an object, got ${describeType(value)}`,
113
+ );
114
+ }
115
+
116
+ const obj = value as Record<string, unknown>;
117
+ const required = (schema.required as string[] | undefined) ?? [];
118
+ const missing = required.filter((k) => !(k in obj));
119
+ if (missing.length > 0) {
120
+ throw new GigaplanError(
121
+ "parse_error",
122
+ `${keyPath} missing required keys: ${missing.join(", ")}`,
123
+ );
124
+ }
125
+
126
+ const properties = (schema.properties as Record<string, unknown> | undefined) ?? {};
127
+ for (const [prop, propSchema] of Object.entries(properties)) {
128
+ if (prop in obj) {
129
+ validateValueAgainstSchema(
130
+ obj[prop],
131
+ propSchema as Record<string, unknown>,
132
+ keyPath === "payload" ? prop : `${keyPath}.${prop}`,
133
+ );
134
+ }
135
+ }
136
+ }
137
+ }
138
+
63
139
  // ---------------------------------------------------------------------------
64
140
  // Validation
65
141
  // ---------------------------------------------------------------------------
@@ -73,6 +149,8 @@ export function validatePayload(step: string, payload: Record<string, unknown>):
73
149
  `${step} output missing required keys: ${missing.join(", ")}`,
74
150
  );
75
151
  }
152
+
153
+ validateValueAgainstSchema(payload, getStepSchema(step), "payload");
76
154
  }
77
155
 
78
156
  // ---------------------------------------------------------------------------
@@ -104,6 +182,44 @@ export function resolveAgent(step: string, state: PlanState): AgentRouting {
104
182
  // Build subagent task
105
183
  // ---------------------------------------------------------------------------
106
184
 
185
+ function stepOutputChecklist(step: string): string {
186
+ switch (step) {
187
+ case "clarify":
188
+ return [
189
+ '- Top-level keys: `questions`, `refined_idea`, `intent_summary`.',
190
+ '- `questions` must be an array of objects with `question` and `context`.',
191
+ ].join("\n");
192
+ case "plan":
193
+ return [
194
+ '- Top-level keys: `plan`, `questions`, `success_criteria`, `assumptions`.',
195
+ '- `plan` must be a markdown string, not an array or object.',
196
+ ].join("\n");
197
+ case "integrate":
198
+ return [
199
+ '- Top-level keys: `plan`, `changes_summary`, `flags_addressed`, `assumptions`, `success_criteria`, `questions`.',
200
+ '- `plan` must be a markdown string.',
201
+ '- `flags_addressed` must contain exact flag IDs from critique.',
202
+ ].join("\n");
203
+ case "critique":
204
+ return [
205
+ '- Top-level keys: `flags`, `verified_flag_ids`, `disputed_flag_ids`.',
206
+ '- Use `flags`, not `significant_issues` or any alternate field name.',
207
+ '- Each `flags[]` item must include exactly: `id`, `concern`, `category`, `severity_hint`, `evidence`.',
208
+ ].join("\n");
209
+ case "execute":
210
+ return [
211
+ '- Top-level keys: `output`, `files_changed`, `commands_run`, `deviations`.',
212
+ ].join("\n");
213
+ case "review":
214
+ return [
215
+ '- Top-level keys: `criteria`, `issues`, `summary`.',
216
+ '- Each `criteria[]` item must include `name`, `pass`, `evidence`.',
217
+ ].join("\n");
218
+ default:
219
+ return "- Follow the schema exactly.";
220
+ }
221
+ }
222
+
107
223
  /**
108
224
  * Build the full task prompt for a subagent.
109
225
  * Includes the step prompt + instructions to write structured output.
@@ -131,6 +247,9 @@ ${prompt}
131
247
  You MUST write your response as a valid JSON object to this file:
132
248
  \`${outputPath}\`
133
249
 
250
+ Checklist:
251
+ ${stepOutputChecklist(step)}
252
+
134
253
  The JSON must conform to this schema:
135
254
  \`\`\`json
136
255
  ${JSON.stringify(strict, null, 2)}