@umgbhalla/pi-gigaplan 0.1.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.
@@ -0,0 +1,847 @@
1
+ /**
2
+ * pi-gigaplan extension
3
+ *
4
+ * Structured AI planning with cross-model critique.
5
+ * Registers /gigaplan command and gigaplan_status tool.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "@sinclair/typebox";
10
+ import { Text } from "@mariozechner/pi-tui";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ import {
15
+ PlanState,
16
+ PlanConfig,
17
+ FlagRecord,
18
+ FlagRegistry,
19
+ EvaluationResult,
20
+ STATE_INITIALIZED,
21
+ STATE_CLARIFIED,
22
+ STATE_PLANNED,
23
+ STATE_CRITIQUED,
24
+ STATE_EVALUATED,
25
+ STATE_GATED,
26
+ STATE_EXECUTED,
27
+ STATE_DONE,
28
+ STATE_ABORTED,
29
+ TERMINAL_STATES,
30
+ DEFAULT_AGENT_ROUTING,
31
+ ROBUSTNESS_LEVELS,
32
+ nowUtc,
33
+ slugify,
34
+ jsonDump,
35
+ sha256Text,
36
+ atomicWriteText,
37
+ atomicWriteJson,
38
+ readJson,
39
+ ensureRuntimeLayout,
40
+ gigaplanRoot,
41
+ plansRoot,
42
+ schemasRoot,
43
+ activePlanDirs,
44
+ resolvePlanDir,
45
+ loadPlan,
46
+ saveState,
47
+ latestPlanRecord,
48
+ latestPlanPath,
49
+ latestPlanMetaPath,
50
+ loadFlagRegistry,
51
+ saveFlagRegistry,
52
+ unresolvedSignificantFlags,
53
+ scopeCreepFlags,
54
+ FLAG_BLOCKING_STATUSES,
55
+ GigaplanError,
56
+ } from "../src/core.js";
57
+
58
+ import { buildEvaluation } from "../src/evaluation.js";
59
+ import {
60
+ buildStepConfig,
61
+ parseStepOutput,
62
+ validatePayload,
63
+ sessionKeyFor,
64
+ } from "../src/workers.js";
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // State machine: valid transitions
68
+ // ---------------------------------------------------------------------------
69
+
70
+ const VALID_TRANSITIONS: Record<string, string[]> = {
71
+ [STATE_INITIALIZED]: [STATE_CLARIFIED],
72
+ [STATE_CLARIFIED]: [STATE_PLANNED],
73
+ [STATE_PLANNED]: [STATE_CRITIQUED],
74
+ [STATE_CRITIQUED]: [STATE_EVALUATED],
75
+ [STATE_EVALUATED]: [STATE_PLANNED, STATE_GATED, STATE_ABORTED], // integrate→planned, skip→gated, abort
76
+ [STATE_GATED]: [STATE_EXECUTED],
77
+ [STATE_EXECUTED]: [STATE_DONE],
78
+ };
79
+
80
+ function requireState(state: PlanState, ...expected: string[]): void {
81
+ if (!expected.includes(state.current_state)) {
82
+ throw new GigaplanError(
83
+ "invalid_state",
84
+ `Expected state ${expected.join(" or ")}, got ${state.current_state}`,
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Infer the next step(s) from the current state.
91
+ */
92
+ function inferNextSteps(state: PlanState): string[] {
93
+ switch (state.current_state) {
94
+ case STATE_INITIALIZED: return ["clarify"];
95
+ case STATE_CLARIFIED: return ["plan"];
96
+ case STATE_PLANNED: return ["critique"];
97
+ case STATE_CRITIQUED: return ["evaluate"];
98
+ case STATE_EVALUATED: {
99
+ const rec = (state.last_evaluation as Record<string, unknown>)?.recommendation as string;
100
+ if (rec === "CONTINUE") return ["integrate"];
101
+ if (rec === "SKIP") return ["gate"];
102
+ if (rec === "ABORT") return ["abort"];
103
+ return ["override"]; // ESCALATE
104
+ }
105
+ case STATE_GATED: return ["execute"];
106
+ case STATE_EXECUTED: return ["review"];
107
+ default: return [];
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Step handlers
113
+ // ---------------------------------------------------------------------------
114
+
115
+ interface StepResult {
116
+ success: boolean;
117
+ step: string;
118
+ summary: string;
119
+ nextSteps: string[];
120
+ artifacts?: string[];
121
+ }
122
+
123
+ /**
124
+ * Initialize a new plan.
125
+ */
126
+ function initPlan(
127
+ root: string,
128
+ idea: string,
129
+ options: {
130
+ name?: string;
131
+ maxIterations?: number;
132
+ budgetUsd?: number;
133
+ autoApprove?: boolean;
134
+ robustness?: string;
135
+ } = {},
136
+ ): { planDir: string; state: PlanState } {
137
+ ensureRuntimeLayout(root);
138
+
139
+ const name = options.name ?? slugify(idea);
140
+ const planDir = path.join(plansRoot(root), name);
141
+
142
+ if (fs.existsSync(path.join(planDir, "state.json"))) {
143
+ throw new GigaplanError("plan_exists", `Plan "${name}" already exists`);
144
+ }
145
+
146
+ fs.mkdirSync(planDir, { recursive: true });
147
+
148
+ const state: PlanState = {
149
+ name,
150
+ idea,
151
+ current_state: STATE_INITIALIZED,
152
+ iteration: 0,
153
+ created_at: nowUtc(),
154
+ config: {
155
+ max_iterations: options.maxIterations ?? 3,
156
+ budget_usd: options.budgetUsd ?? 25.0,
157
+ project_dir: root,
158
+ auto_approve: options.autoApprove ?? false,
159
+ robustness: options.robustness ?? "standard",
160
+ },
161
+ sessions: {},
162
+ plan_versions: [],
163
+ history: [],
164
+ meta: {
165
+ significant_counts: [],
166
+ weighted_scores: [],
167
+ plan_deltas: [],
168
+ recurring_critiques: [],
169
+ total_cost_usd: 0,
170
+ overrides: [],
171
+ notes: [],
172
+ },
173
+ last_evaluation: {},
174
+ };
175
+
176
+ saveState(planDir, state);
177
+ saveFlagRegistry(planDir, { flags: [] });
178
+
179
+ return { planDir, state };
180
+ }
181
+
182
+ /**
183
+ * Process a step's output after a subagent completes.
184
+ * Updates state, saves artifacts, advances the state machine.
185
+ */
186
+ function processStepOutput(
187
+ step: string,
188
+ planDir: string,
189
+ state: PlanState,
190
+ payload: Record<string, unknown>,
191
+ durationMs: number,
192
+ ): StepResult {
193
+ const iteration = state.iteration;
194
+
195
+ switch (step) {
196
+ case "clarify": {
197
+ state.clarification = payload;
198
+ state.current_state = STATE_CLARIFIED;
199
+ state.history.push({
200
+ step: "clarify",
201
+ timestamp: nowUtc(),
202
+ duration_ms: durationMs,
203
+ result: "success",
204
+ });
205
+ saveState(planDir, state);
206
+ return {
207
+ success: true,
208
+ step: "clarify",
209
+ summary: `Clarified: ${(payload.intent_summary as string) ?? "done"}`,
210
+ nextSteps: ["plan"],
211
+ artifacts: [],
212
+ };
213
+ }
214
+
215
+ case "plan":
216
+ case "integrate": {
217
+ const newIteration = iteration + 1;
218
+ state.iteration = newIteration;
219
+
220
+ // Save plan markdown
221
+ const planText = (payload.plan as string) ?? "";
222
+ const planFile = `plan_v${newIteration}.md`;
223
+ atomicWriteText(path.join(planDir, planFile), planText);
224
+
225
+ // Save plan metadata
226
+ const meta = {
227
+ success_criteria: payload.success_criteria ?? [],
228
+ assumptions: payload.assumptions ?? [],
229
+ questions: payload.questions ?? [],
230
+ changes_summary: payload.changes_summary,
231
+ flags_addressed: payload.flags_addressed,
232
+ };
233
+ atomicWriteJson(
234
+ path.join(planDir, `plan_v${newIteration}.meta.json`),
235
+ meta,
236
+ );
237
+
238
+ // Update plan versions
239
+ state.plan_versions.push({
240
+ version: newIteration,
241
+ file: planFile,
242
+ hash: sha256Text(planText),
243
+ timestamp: nowUtc(),
244
+ });
245
+
246
+ // Handle flags addressed (integrate only)
247
+ if (step === "integrate" && Array.isArray(payload.flags_addressed)) {
248
+ const registry = loadFlagRegistry(planDir);
249
+ for (const flagId of payload.flags_addressed as string[]) {
250
+ const flag = registry.flags.find((f) => f.id === flagId);
251
+ if (flag) {
252
+ flag.status = "addressed";
253
+ flag.addressed_in = `plan_v${newIteration}`;
254
+ }
255
+ }
256
+ saveFlagRegistry(planDir, registry);
257
+ }
258
+
259
+ state.current_state = STATE_PLANNED;
260
+ state.history.push({
261
+ step,
262
+ timestamp: nowUtc(),
263
+ duration_ms: durationMs,
264
+ result: "success",
265
+ output_file: planFile,
266
+ });
267
+ saveState(planDir, state);
268
+
269
+ return {
270
+ success: true,
271
+ step,
272
+ summary: `Plan v${newIteration} created`,
273
+ nextSteps: ["critique"],
274
+ artifacts: [planFile],
275
+ };
276
+ }
277
+
278
+ case "critique": {
279
+ // Save critique artifact
280
+ const critiqueFile = `critique_v${iteration}.json`;
281
+ atomicWriteJson(path.join(planDir, critiqueFile), payload);
282
+
283
+ // Update flag registry
284
+ const registry = loadFlagRegistry(planDir);
285
+ const newFlags = (payload.flags as FlagRecord[]) ?? [];
286
+ const verifiedIds = new Set((payload.verified_flag_ids as string[]) ?? []);
287
+
288
+ // Mark verified flags
289
+ for (const flag of registry.flags) {
290
+ if (flag.id && verifiedIds.has(flag.id)) {
291
+ flag.status = "verified";
292
+ flag.verified = true;
293
+ flag.verified_in = `critique_v${iteration}`;
294
+ }
295
+ }
296
+
297
+ // Add new flags
298
+ for (const newFlag of newFlags) {
299
+ const existing = registry.flags.find((f) => f.id === newFlag.id);
300
+ if (existing) {
301
+ existing.concern = newFlag.concern;
302
+ existing.category = newFlag.category;
303
+ existing.severity_hint = newFlag.severity_hint;
304
+ existing.evidence = newFlag.evidence;
305
+ existing.status = "open";
306
+ existing.severity = newFlag.severity_hint === "likely-significant" ? "significant" : "minor";
307
+ } else {
308
+ registry.flags.push({
309
+ ...newFlag,
310
+ raised_in: `critique_v${iteration}`,
311
+ status: "open",
312
+ severity: newFlag.severity_hint === "likely-significant" ? "significant" : "minor",
313
+ });
314
+ }
315
+ }
316
+ saveFlagRegistry(planDir, registry);
317
+
318
+ state.current_state = STATE_CRITIQUED;
319
+ state.history.push({
320
+ step: "critique",
321
+ timestamp: nowUtc(),
322
+ duration_ms: durationMs,
323
+ result: "success",
324
+ output_file: critiqueFile,
325
+ flags_count: newFlags.length,
326
+ });
327
+ saveState(planDir, state);
328
+
329
+ return {
330
+ success: true,
331
+ step: "critique",
332
+ summary: `Critique: ${newFlags.length} flags raised, ${verifiedIds.size} verified`,
333
+ nextSteps: ["evaluate"],
334
+ artifacts: [critiqueFile],
335
+ };
336
+ }
337
+
338
+ case "execute": {
339
+ atomicWriteJson(path.join(planDir, "execution.json"), payload);
340
+ state.current_state = STATE_EXECUTED;
341
+ state.history.push({
342
+ step: "execute",
343
+ timestamp: nowUtc(),
344
+ duration_ms: durationMs,
345
+ result: "success",
346
+ output_file: "execution.json",
347
+ });
348
+ saveState(planDir, state);
349
+
350
+ return {
351
+ success: true,
352
+ step: "execute",
353
+ summary: `Executed. Files changed: ${(payload.files_changed as string[])?.length ?? 0}`,
354
+ nextSteps: ["review"],
355
+ artifacts: ["execution.json"],
356
+ };
357
+ }
358
+
359
+ case "review": {
360
+ atomicWriteJson(path.join(planDir, "review.json"), payload);
361
+ state.current_state = STATE_DONE;
362
+ state.history.push({
363
+ step: "review",
364
+ timestamp: nowUtc(),
365
+ duration_ms: durationMs,
366
+ result: "success",
367
+ output_file: "review.json",
368
+ });
369
+ saveState(planDir, state);
370
+
371
+ const criteria = (payload.criteria as Array<{ name: string; pass: boolean }>) ?? [];
372
+ const passed = criteria.filter((c) => c.pass).length;
373
+ return {
374
+ success: true,
375
+ step: "review",
376
+ summary: `Review: ${passed}/${criteria.length} criteria passed. ${(payload.issues as string[])?.length ?? 0} issues.`,
377
+ nextSteps: [],
378
+ artifacts: ["review.json"],
379
+ };
380
+ }
381
+
382
+ default:
383
+ throw new GigaplanError("unsupported_step", `Unknown step: ${step}`);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Run the evaluate step (pure logic, no LLM needed).
389
+ */
390
+ function runEvaluate(planDir: string, state: PlanState): StepResult {
391
+ requireState(state, STATE_CRITIQUED);
392
+
393
+ const evaluation = buildEvaluation(planDir, state);
394
+ const evalFile = `evaluation_v${state.iteration}.json`;
395
+ atomicWriteJson(path.join(planDir, evalFile), evaluation);
396
+
397
+ // Update meta tracking
398
+ const registry = loadFlagRegistry(planDir);
399
+ const sigCount = registry.flags.filter(
400
+ (f) => f.severity === "significant" && f.status !== "verified",
401
+ ).length;
402
+ state.meta.significant_counts = [...(state.meta.significant_counts ?? []), sigCount];
403
+ state.meta.weighted_scores = [
404
+ ...(state.meta.weighted_scores ?? []),
405
+ (evaluation.signals as Record<string, unknown>).weighted_score as number,
406
+ ];
407
+
408
+ state.last_evaluation = evaluation;
409
+ state.current_state = STATE_EVALUATED;
410
+ state.history.push({
411
+ step: "evaluate",
412
+ timestamp: nowUtc(),
413
+ result: "success",
414
+ recommendation: evaluation.recommendation,
415
+ output_file: evalFile,
416
+ });
417
+ saveState(planDir, state);
418
+
419
+ return {
420
+ success: true,
421
+ step: "evaluate",
422
+ summary: `Evaluation: ${evaluation.recommendation} (${evaluation.confidence} confidence). ${evaluation.rationale}`,
423
+ nextSteps: evaluation.valid_next_steps,
424
+ artifacts: [evalFile],
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Run gate checks (pure logic).
430
+ */
431
+ function runGate(planDir: string, state: PlanState): StepResult {
432
+ requireState(state, STATE_EVALUATED);
433
+
434
+ const projectDir = state.config.project_dir ?? process.cwd();
435
+ const checks: Record<string, boolean> = {
436
+ project_exists: fs.existsSync(projectDir),
437
+ project_writable: (() => {
438
+ try { fs.accessSync(projectDir, fs.constants.W_OK); return true; } catch { return false; }
439
+ })(),
440
+ plan_exists: state.plan_versions.length > 0,
441
+ has_success_criteria: (() => {
442
+ try {
443
+ const meta = readJson(latestPlanMetaPath(planDir, state)) as Record<string, unknown>;
444
+ return Array.isArray(meta.success_criteria) && meta.success_criteria.length > 0;
445
+ } catch { return false; }
446
+ })(),
447
+ };
448
+
449
+ const registry = loadFlagRegistry(planDir);
450
+ const unresolved = unresolvedSignificantFlags(registry);
451
+ checks.no_unresolved_flags = unresolved.length === 0;
452
+
453
+ const passed = Object.values(checks).every(Boolean);
454
+
455
+ const gate = {
456
+ passed,
457
+ preflight_results: checks,
458
+ unresolved_flags: unresolved.map((f) => ({ id: f.id, concern: f.concern })),
459
+ timestamp: nowUtc(),
460
+ };
461
+
462
+ atomicWriteJson(path.join(planDir, "gate.json"), gate);
463
+
464
+ if (passed) {
465
+ state.current_state = STATE_GATED;
466
+ }
467
+
468
+ state.history.push({
469
+ step: "gate",
470
+ timestamp: nowUtc(),
471
+ result: passed ? "success" : "failed",
472
+ });
473
+ saveState(planDir, state);
474
+
475
+ return {
476
+ success: passed,
477
+ step: "gate",
478
+ summary: passed
479
+ ? "Gate passed. Ready for execution."
480
+ : `Gate failed: ${Object.entries(checks).filter(([, v]) => !v).map(([k]) => k).join(", ")}`,
481
+ nextSteps: passed ? ["execute"] : ["integrate"],
482
+ artifacts: ["gate.json"],
483
+ };
484
+ }
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // Extension
488
+ // ---------------------------------------------------------------------------
489
+
490
+ export default function gigaplanExtension(pi: ExtensionAPI) {
491
+
492
+ // Widget state
493
+ let activePlan: { name: string; state: string; step: string } | null = null;
494
+
495
+ function updateWidget(ctx: any) {
496
+ if (!activePlan) {
497
+ ctx.ui.setStatus("gigaplan", "");
498
+ return;
499
+ }
500
+ ctx.ui.setStatus(
501
+ "gigaplan",
502
+ `📋 ${activePlan.name} [${activePlan.state}] → ${activePlan.step}`,
503
+ );
504
+ }
505
+
506
+ // ── /gigaplan command ──
507
+ pi.registerCommand("gigaplan", {
508
+ description: "Start a structured planning session: /gigaplan <idea>",
509
+ handler: async (args, ctx) => {
510
+ const idea = (args ?? "").trim();
511
+ if (!idea) {
512
+ ctx.ui.notify("Usage: /gigaplan <description of what to build>", "warning");
513
+ return;
514
+ }
515
+
516
+ // 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
+ ]);
522
+
523
+ const autoApprove = await ctx.ui.confirm(
524
+ "Auto-approve?",
525
+ "Skip manual gate approval and auto-advance through all steps?",
526
+ );
527
+
528
+ // Initialize
529
+ const root = ctx.cwd;
530
+ const { planDir, state } = initPlan(root, idea, {
531
+ robustness: robustness ?? "standard",
532
+ autoApprove,
533
+ });
534
+
535
+ activePlan = { name: state.name, state: state.current_state, step: "clarify" };
536
+ updateWidget(ctx);
537
+
538
+ ctx.ui.notify(`Plan "${state.name}" initialized. Starting orchestration...`, "success");
539
+
540
+ // Send orchestration prompt to the LLM
541
+ const orchestrationPrompt = `You are now in **gigaplan mode**. A structured plan has been initialized.
542
+
543
+ ## Plan: ${state.name}
544
+ - **Idea:** ${idea}
545
+ - **Robustness:** ${robustness}
546
+ - **Auto-approve:** ${autoApprove}
547
+ - **Plan directory:** ${planDir}
548
+
549
+ ## Workflow
550
+
551
+ Execute each step by spawning a subagent using the \`subagent\` tool. After each subagent completes, use the \`gigaplan_advance\` tool to process the output and advance the state machine.
552
+
553
+ ### Steps (in order):
554
+ 1. **clarify** → Spawn subagent to clarify the idea
555
+ 2. **plan** → Spawn subagent to create implementation plan
556
+ 3. **critique** → Spawn subagent (different model!) to independently critique
557
+ 4. **evaluate** → Use \`gigaplan_advance\` with step="evaluate" (no subagent needed)
558
+ 5. Based on evaluation:
559
+ - CONTINUE → **integrate** (spawn subagent to revise plan) → back to critique
560
+ - SKIP → **gate** (use \`gigaplan_advance\` with step="gate")
561
+ - ESCALATE → Ask user for override decision
562
+ - ABORT → Stop
563
+ 6. After gate passes: **execute** → Spawn subagent to implement
564
+ 7. **review** → Spawn subagent to validate
565
+
566
+ ### Subagent spawning pattern:
567
+ For each LLM step, use the \`gigaplan_step\` tool which returns the subagent config, then spawn it.
568
+
569
+ Start now with the **clarify** step.`;
570
+
571
+ pi.sendUserMessage(orchestrationPrompt);
572
+ },
573
+ });
574
+
575
+ // ── gigaplan_step tool — get subagent config for a step ──
576
+ pi.registerTool({
577
+ name: "gigaplan_step",
578
+ label: "Gigaplan Step",
579
+ description:
580
+ "Get the subagent configuration for a gigaplan step. Returns the task prompt, " +
581
+ "agent name, and output path. Use this before spawning a subagent for each step.",
582
+ parameters: Type.Object({
583
+ planDir: Type.String({ description: "Path to the plan directory (.gigaplan/plans/<name>)" }),
584
+ step: Type.String({ description: "Step to run: clarify, plan, critique, integrate, execute, review" }),
585
+ }),
586
+
587
+ async execute(_id, params) {
588
+ try {
589
+ const state = readJson(path.join(params.planDir, "state.json")) as PlanState;
590
+ const config = buildStepConfig(params.step, state, params.planDir);
591
+
592
+ return {
593
+ content: [{
594
+ type: "text",
595
+ text: `Subagent config for step "${params.step}":\n\n` +
596
+ `**Name:** ${config.name}\n` +
597
+ `**Agent:** ${config.agent}\n` +
598
+ `**Output path:** ${config.outputPath}\n` +
599
+ `**Tools:** ${config.tools}\n\n` +
600
+ `Spawn this as an autonomous subagent with the task below. ` +
601
+ `After it completes, call gigaplan_advance with step="${params.step}".`,
602
+ }],
603
+ details: {
604
+ name: config.name,
605
+ agent: config.agent,
606
+ task: config.task,
607
+ model: config.model,
608
+ tools: config.tools,
609
+ outputPath: config.outputPath,
610
+ interactive: false,
611
+ },
612
+ };
613
+ } catch (e) {
614
+ return {
615
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
616
+ details: { error: true },
617
+ };
618
+ }
619
+ },
620
+ });
621
+
622
+ // ── gigaplan_advance tool — process output and advance state ──
623
+ pi.registerTool({
624
+ name: "gigaplan_advance",
625
+ label: "Gigaplan Advance",
626
+ description:
627
+ "Process a completed gigaplan step and advance the state machine. " +
628
+ "For LLM steps (clarify, plan, critique, integrate, execute, review): reads the output file written by the subagent. " +
629
+ "For logic steps (evaluate, gate): runs the logic directly.",
630
+ parameters: Type.Object({
631
+ planDir: Type.String({ description: "Path to the plan directory" }),
632
+ step: Type.String({ description: "Step that just completed" }),
633
+ durationMs: Type.Optional(Type.Number({ description: "How long the step took in ms" })),
634
+ }),
635
+
636
+ async execute(_id, params) {
637
+ try {
638
+ const state = readJson(path.join(params.planDir, "state.json")) as PlanState;
639
+ let result: StepResult;
640
+
641
+ if (params.step === "evaluate") {
642
+ result = runEvaluate(params.planDir, state);
643
+ } else if (params.step === "gate") {
644
+ result = runGate(params.planDir, state);
645
+ } else {
646
+ // LLM step — read subagent output
647
+ const outputPath = path.join(params.planDir, `${params.step}_output.json`);
648
+ const payload = parseStepOutput(params.step, outputPath);
649
+ result = processStepOutput(
650
+ params.step,
651
+ params.planDir,
652
+ state,
653
+ payload,
654
+ params.durationMs ?? 0,
655
+ );
656
+ }
657
+
658
+ // Update widget
659
+ if (activePlan) {
660
+ activePlan.state = result.step;
661
+ activePlan.step = result.nextSteps[0] ?? "done";
662
+ updateWidget(null as any); // TODO: need ctx
663
+ }
664
+
665
+ const nextAction = result.nextSteps.length > 0
666
+ ? `\n\n**Next step(s):** ${result.nextSteps.join(", ")}`
667
+ : "\n\n**Plan complete!**";
668
+
669
+ return {
670
+ content: [{
671
+ type: "text",
672
+ text: `**${result.step}** — ${result.summary}${nextAction}`,
673
+ }],
674
+ details: {
675
+ success: result.success,
676
+ step: result.step,
677
+ summary: result.summary,
678
+ nextSteps: result.nextSteps,
679
+ artifacts: result.artifacts,
680
+ },
681
+ };
682
+ } catch (e) {
683
+ return {
684
+ content: [{ type: "text", text: `Error advancing: ${e instanceof Error ? e.message : String(e)}` }],
685
+ details: { error: true },
686
+ };
687
+ }
688
+ },
689
+ });
690
+
691
+ // ── gigaplan_status tool ──
692
+ pi.registerTool({
693
+ name: "gigaplan_status",
694
+ label: "Gigaplan Status",
695
+ description: "Show the status of gigaplan plans in the current project.",
696
+ parameters: Type.Object({
697
+ planName: Type.Optional(Type.String({ description: "Specific plan name (optional)" })),
698
+ }),
699
+
700
+ async execute(_id, params, _signal, _onUpdate, ctx) {
701
+ const root = ctx.cwd;
702
+ const gigaplan = gigaplanRoot(root);
703
+
704
+ if (!fs.existsSync(gigaplan)) {
705
+ return {
706
+ content: [{ type: "text", text: "No .gigaplan directory found. Run /gigaplan to start." }],
707
+ details: {},
708
+ };
709
+ }
710
+
711
+ const dirs = activePlanDirs(root);
712
+ if (dirs.length === 0) {
713
+ return {
714
+ content: [{ type: "text", text: "No plans found." }],
715
+ details: {},
716
+ };
717
+ }
718
+
719
+ if (params.planName) {
720
+ const planDir = path.join(plansRoot(root), params.planName);
721
+ if (!fs.existsSync(path.join(planDir, "state.json"))) {
722
+ return {
723
+ content: [{ type: "text", text: `Plan "${params.planName}" not found.` }],
724
+ details: {},
725
+ };
726
+ }
727
+ const state = readJson(path.join(planDir, "state.json")) as PlanState;
728
+ const registry = loadFlagRegistry(planDir);
729
+ const unresolved = unresolvedSignificantFlags(registry);
730
+ const nextSteps = inferNextSteps(state);
731
+
732
+ const lines = [
733
+ `# Plan: ${state.name}`,
734
+ `**State:** ${state.current_state}`,
735
+ `**Iteration:** ${state.iteration}`,
736
+ `**Idea:** ${state.idea}`,
737
+ `**Robustness:** ${state.config.robustness ?? "standard"}`,
738
+ `**Next steps:** ${nextSteps.join(", ") || "none"}`,
739
+ `**Total flags:** ${registry.flags.length} (${unresolved.length} unresolved significant)`,
740
+ `**History:** ${state.history.map((h) => h.step).join(" → ")}`,
741
+ ];
742
+
743
+ return {
744
+ content: [{ type: "text", text: lines.join("\n") }],
745
+ details: { state, planDir },
746
+ };
747
+ }
748
+
749
+ // List all plans
750
+ const lines = dirs.map((d) => {
751
+ const s = readJson(path.join(d, "state.json")) as PlanState;
752
+ const next = inferNextSteps(s);
753
+ return `• **${s.name}** [${s.current_state}] iter=${s.iteration} → ${next[0] ?? "done"}`;
754
+ });
755
+
756
+ return {
757
+ content: [{ type: "text", text: `## Gigaplan Plans\n\n${lines.join("\n")}` }],
758
+ details: { plans: dirs.map((d) => path.basename(d)) },
759
+ };
760
+ },
761
+ });
762
+
763
+ // ── gigaplan_override tool ──
764
+ pi.registerTool({
765
+ name: "gigaplan_override",
766
+ label: "Gigaplan Override",
767
+ description: "Manual intervention on a gigaplan: add-note, abort, force-proceed, or skip.",
768
+ parameters: Type.Object({
769
+ planDir: Type.String({ description: "Path to the plan directory" }),
770
+ action: Type.String({ description: "Override action: add-note, abort, force-proceed, skip" }),
771
+ note: Type.Optional(Type.String({ description: "Note text (for add-note action)" })),
772
+ }),
773
+
774
+ async execute(_id, params) {
775
+ try {
776
+ const state = readJson(path.join(params.planDir, "state.json")) as PlanState;
777
+
778
+ switch (params.action) {
779
+ case "add-note": {
780
+ if (!params.note) {
781
+ return { content: [{ type: "text", text: "Error: note text required for add-note" }], details: {} };
782
+ }
783
+ state.meta.notes = [
784
+ ...(state.meta.notes ?? []),
785
+ { note: params.note, timestamp: nowUtc() },
786
+ ];
787
+ state.history.push({
788
+ step: "override",
789
+ timestamp: nowUtc(),
790
+ message: `add-note: ${params.note}`,
791
+ });
792
+ saveState(params.planDir, state);
793
+ return { content: [{ type: "text", text: `Note added. Continue with the current step.` }], details: {} };
794
+ }
795
+
796
+ case "abort": {
797
+ state.current_state = STATE_ABORTED;
798
+ state.history.push({ step: "override", timestamp: nowUtc(), message: "aborted" });
799
+ saveState(params.planDir, state);
800
+ return { content: [{ type: "text", text: `Plan "${state.name}" aborted.` }], details: {} };
801
+ }
802
+
803
+ case "force-proceed": {
804
+ state.current_state = STATE_GATED;
805
+ state.meta.user_approved_gate = true;
806
+ state.history.push({
807
+ step: "override",
808
+ timestamp: nowUtc(),
809
+ message: "force-proceed (bypassed gate)",
810
+ });
811
+ saveState(params.planDir, state);
812
+ return {
813
+ content: [{ type: "text", text: "Force-proceeded to gate. Next step: execute." }],
814
+ details: { nextSteps: ["execute"] },
815
+ };
816
+ }
817
+
818
+ case "skip": {
819
+ state.last_evaluation = { recommendation: "SKIP" };
820
+ state.current_state = STATE_EVALUATED;
821
+ state.history.push({
822
+ step: "override",
823
+ timestamp: nowUtc(),
824
+ message: "skip (user override to SKIP)",
825
+ });
826
+ saveState(params.planDir, state);
827
+ return {
828
+ content: [{ type: "text", text: "Skipped to gate. Next step: gate." }],
829
+ details: { nextSteps: ["gate"] },
830
+ };
831
+ }
832
+
833
+ default:
834
+ return {
835
+ content: [{ type: "text", text: `Unknown action: ${params.action}. Use: add-note, abort, force-proceed, skip` }],
836
+ details: {},
837
+ };
838
+ }
839
+ } catch (e) {
840
+ return {
841
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
842
+ details: { error: true },
843
+ };
844
+ }
845
+ },
846
+ });
847
+ }