gsd-pi 2.29.0-dev.77f06e2 → 2.29.0-dev.953d788

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 (80) hide show
  1. package/README.md +17 -24
  2. package/dist/resources/extensions/bg-shell/process-manager.ts +0 -13
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
  4. package/dist/resources/extensions/gsd/auto-post-unit.ts +3 -6
  5. package/dist/resources/extensions/gsd/auto-recovery.ts +22 -16
  6. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
  7. package/dist/resources/extensions/gsd/auto.ts +15 -0
  8. package/dist/resources/extensions/gsd/commands-handlers.ts +1 -20
  9. package/dist/resources/extensions/gsd/commands-logs.ts +14 -13
  10. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
  11. package/dist/resources/extensions/gsd/commands.ts +22 -55
  12. package/dist/resources/extensions/gsd/dashboard-overlay.ts +1 -2
  13. package/dist/resources/extensions/gsd/json-persistence.ts +1 -16
  14. package/dist/resources/extensions/gsd/queue-order.ts +11 -10
  15. package/dist/resources/extensions/gsd/session-status-io.ts +41 -23
  16. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  17. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  18. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
  19. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  20. package/dist/resources/extensions/mcporter/index.ts +525 -0
  21. package/dist/resources/extensions/remote-questions/discord-adapter.ts +19 -8
  22. package/dist/resources/extensions/remote-questions/slack-adapter.ts +17 -11
  23. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
  24. package/package.json +1 -1
  25. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -13
  27. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  28. package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -13
  29. package/src/resources/extensions/bg-shell/process-manager.ts +0 -13
  30. package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
  31. package/src/resources/extensions/gsd/auto-post-unit.ts +3 -6
  32. package/src/resources/extensions/gsd/auto-recovery.ts +22 -16
  33. package/src/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
  34. package/src/resources/extensions/gsd/auto.ts +15 -0
  35. package/src/resources/extensions/gsd/commands-handlers.ts +1 -20
  36. package/src/resources/extensions/gsd/commands-logs.ts +14 -13
  37. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
  38. package/src/resources/extensions/gsd/commands.ts +22 -55
  39. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -2
  40. package/src/resources/extensions/gsd/json-persistence.ts +1 -16
  41. package/src/resources/extensions/gsd/queue-order.ts +11 -10
  42. package/src/resources/extensions/gsd/session-status-io.ts +41 -23
  43. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  44. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  45. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
  46. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  47. package/src/resources/extensions/mcporter/index.ts +525 -0
  48. package/src/resources/extensions/remote-questions/discord-adapter.ts +19 -8
  49. package/src/resources/extensions/remote-questions/slack-adapter.ts +17 -11
  50. package/src/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
  51. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
  52. package/dist/resources/extensions/gsd/prompts/workflow-start.md +0 -28
  53. package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
  54. package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
  55. package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
  56. package/dist/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
  57. package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
  58. package/dist/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
  59. package/dist/resources/extensions/gsd/workflow-templates/registry.json +0 -85
  60. package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
  61. package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
  62. package/dist/resources/extensions/gsd/workflow-templates/spike.md +0 -69
  63. package/dist/resources/extensions/gsd/workflow-templates.ts +0 -241
  64. package/dist/resources/extensions/mcp-client/index.ts +0 -459
  65. package/dist/resources/extensions/remote-questions/http-client.ts +0 -76
  66. package/src/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
  67. package/src/resources/extensions/gsd/prompts/workflow-start.md +0 -28
  68. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
  69. package/src/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
  70. package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
  71. package/src/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
  72. package/src/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
  73. package/src/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
  74. package/src/resources/extensions/gsd/workflow-templates/registry.json +0 -85
  75. package/src/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
  76. package/src/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
  77. package/src/resources/extensions/gsd/workflow-templates/spike.md +0 -69
  78. package/src/resources/extensions/gsd/workflow-templates.ts +0 -241
  79. package/src/resources/extensions/mcp-client/index.ts +0 -459
  80. package/src/resources/extensions/remote-questions/http-client.ts +0 -76
@@ -22,6 +22,7 @@ import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSumma
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
24
24
  import { writeVerificationJSON } from "./verification-evidence.js";
25
+ export { inlinePriorMilestoneSummary } from "./files.js";
25
26
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
27
  import {
27
28
  gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
@@ -191,6 +192,12 @@ import {
191
192
  NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
192
193
  } from "./auto/session.js";
193
194
  import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
195
+ export {
196
+ MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
197
+ MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
198
+ NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
199
+ } from "./auto/session.js";
200
+ export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
194
201
 
195
202
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
196
203
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -261,6 +268,8 @@ export function shouldUseWorktreeIsolation(): boolean {
261
268
  * Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
262
269
  * running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
263
270
  */
271
+ // Re-export budget utilities for external consumers
272
+ export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
264
273
 
265
274
  /** Wrapper: register SIGTERM handler and store reference. */
266
275
  function registerSigtermHandler(currentBasePath: string): void {
@@ -273,6 +282,8 @@ function deregisterSigtermHandler(): void {
273
282
  s.sigtermHandler = null;
274
283
  }
275
284
 
285
+ export { type AutoDashboardData } from "./auto-dashboard.js";
286
+
276
287
  export function getAutoDashboardData(): AutoDashboardData {
277
288
  const ledger = getLedger();
278
289
  const totals = ledger ? getProjectTotals(ledger.units) : null;
@@ -923,6 +934,8 @@ async function showStepWizard(
923
934
  }
924
935
  }
925
936
 
937
+ // describeNextUnit is imported from auto-dashboard.ts and re-exported
938
+ export { describeNextUnit } from "./auto-dashboard.js";
926
939
 
927
940
  /** Thin wrapper: delegates to auto-dashboard.ts, passing state accessors. */
928
941
  function updateProgressWidget(
@@ -1892,3 +1905,5 @@ export async function dispatchHookUnit(
1892
1905
  }
1893
1906
 
1894
1907
 
1908
+ // Direct phase dispatch → auto-direct-dispatch.ts
1909
+ export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
@@ -19,26 +19,7 @@ import {
19
19
  filterDoctorIssues,
20
20
  } from "./doctor.js";
21
21
  import { isAutoActive } from "./auto.js";
22
- import { projectRoot } from "./commands.js";
23
- import { loadPrompt } from "./prompt-loader.js";
24
-
25
- export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
26
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
27
- const workflow = readFileSync(workflowPath, "utf-8");
28
- const prompt = loadPrompt("doctor-heal", {
29
- doctorSummary: reportText,
30
- structuredIssues,
31
- scopeLabel: scope ?? "active milestone / blocking scope",
32
- doctorCommandSuffix: scope ? ` ${scope}` : "",
33
- });
34
-
35
- const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
36
-
37
- pi.sendMessage(
38
- { customType: "gsd-doctor-heal", content, display: false },
39
- { triggerTurn: true },
40
- );
41
- }
22
+ import { projectRoot, dispatchDoctorHeal } from "./commands.js";
42
23
 
43
24
  export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
44
25
  const trimmed = args.trim();
@@ -14,7 +14,6 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
14
  import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { gsdRoot } from "./paths.js";
17
- import { loadJsonFileOrNull } from "./json-persistence.js";
18
17
 
19
18
  // ─── Types ──────────────────────────────────────────────────────────────────
20
19
 
@@ -332,18 +331,20 @@ async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): P
332
331
 
333
332
  // Metrics summary
334
333
  const metricsPath = join(gsdRoot(basePath), "metrics.json");
335
- const isMetrics = (d: unknown): d is { units: Array<Record<string, unknown>> } =>
336
- d !== null && typeof d === "object" && "units" in d! && Array.isArray((d as Record<string, unknown>).units);
337
- const metrics = loadJsonFileOrNull(metricsPath, isMetrics);
338
- if (metrics && metrics.units.length > 0) {
339
- const units = metrics.units;
340
- const totalCost = units.reduce((sum: number, u) => sum + ((u.cost as number) ?? 0), 0);
341
- const totalTokens = units.reduce((sum: number, u) => {
342
- const t = u.tokens as Record<string, number> | undefined;
343
- return sum + (t?.total ?? 0);
344
- }, 0);
345
- lines.push("");
346
- lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
334
+ if (existsSync(metricsPath)) {
335
+ try {
336
+ const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
337
+ const units = metrics?.units;
338
+ if (Array.isArray(units) && units.length > 0) {
339
+ const totalCost = units.reduce((sum: number, u: Record<string, unknown>) => sum + ((u.cost as number) ?? 0), 0);
340
+ const totalTokens = units.reduce((sum: number, u: Record<string, unknown>) => {
341
+ const t = u.tokens as Record<string, number> | undefined;
342
+ return sum + (t?.total ?? 0);
343
+ }, 0);
344
+ lines.push("");
345
+ lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
346
+ }
347
+ } catch { /* ignore */ }
347
348
  }
348
349
 
349
350
  lines.push("");
@@ -260,57 +260,27 @@ async function configureModels(ctx: ExtensionCommandContext, prefs: Record<strin
260
260
  group.push(m);
261
261
  }
262
262
  const providers = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
263
- // Sort models within each provider
264
- for (const group of byProvider.values()) {
265
- group.sort((a, b) => a.id.localeCompare(b.id));
266
- }
267
263
 
268
- // Build provider menu with model counts
269
- const providerOptions = providers.map(p => {
270
- const count = byProvider.get(p)!.length;
271
- return `${p} (${count} models)`;
272
- });
273
- providerOptions.push("(keep current)", "(clear)", "(type manually)");
264
+ const modelOptions: string[] = [];
265
+ for (const provider of providers) {
266
+ const group = byProvider.get(provider)!;
267
+ modelOptions.push(`─── ${provider} (${group.length}) ───`);
268
+ for (const m of group) {
269
+ modelOptions.push(`${m.id} · ${m.provider}`);
270
+ }
271
+ }
272
+ modelOptions.push("(keep current)", "(clear)");
274
273
 
275
274
  for (const phase of modelPhases) {
276
275
  const current = models[phase] ?? "";
277
- const phaseLabel = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}`;
278
-
279
- // Step 1: pick provider
280
- const providerChoice = await ctx.ui.select(`${phaseLabel} — choose provider:`, providerOptions);
281
- if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(keep current)") continue;
282
-
283
- if (providerChoice === "(clear)") {
284
- delete models[phase];
285
- continue;
286
- }
287
-
288
- if (providerChoice === "(type manually)") {
289
- const input = await ctx.ui.input(
290
- `${phaseLabel} — enter model ID:`,
291
- current || "e.g. claude-sonnet-4-20250514",
292
- );
293
- if (input !== null && input !== undefined) {
294
- const val = input.trim();
295
- if (val) models[phase] = val;
296
- }
297
- continue;
298
- }
299
-
300
- // Step 2: pick model within provider
301
- const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
302
- const group = byProvider.get(providerName);
303
- if (!group) continue;
304
-
305
- const modelOptions = group.map(m => m.id);
306
- modelOptions.push("(keep current)", "(clear)");
276
+ const title = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`;
277
+ const choice = await ctx.ui.select(title, modelOptions);
307
278
 
308
- const modelChoice = await ctx.ui.select(`${phaseLabel} ${providerName}:`, modelOptions);
309
- if (modelChoice && typeof modelChoice === "string" && modelChoice !== "(keep current)") {
310
- if (modelChoice === "(clear)") {
279
+ if (choice && typeof choice === "string" && choice !== "(keep current)") {
280
+ if (choice === "(clear)") {
311
281
  delete models[phase];
312
282
  } else {
313
- models[phase] = modelChoice;
283
+ models[phase] = choice.split(" · ")[0];
314
284
  }
315
285
  }
316
286
  }
@@ -13,8 +13,7 @@ import { deriveState } from "./state.js";
13
13
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
14
14
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
15
15
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
16
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
17
- import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
16
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
18
17
  import { resolveProjectRoot } from "./worktree.js";
19
18
  import { assertSafeDirectory } from "./validate-directory.js";
20
19
  import {
@@ -22,6 +21,8 @@ import {
22
21
  getProjectGSDPreferencesPath,
23
22
  loadEffectiveGSDPreferences,
24
23
  } from "./preferences.js";
24
+ import { loadPrompt } from "./prompt-loader.js";
25
+
25
26
  import { handleRemote } from "../remote-questions/mod.js";
26
27
  import { handleQuick } from "./quick.js";
27
28
  import { handleHistory } from "./history.js";
@@ -43,9 +44,26 @@ import { handleInspect } from "./commands-inspect.js";
43
44
  import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
44
45
  import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
45
46
  import { handleLogs } from "./commands-logs.js";
46
- import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
47
47
 
48
48
 
49
+ export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
50
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
51
+ const workflow = readFileSync(workflowPath, "utf-8");
52
+ const prompt = loadPrompt("doctor-heal", {
53
+ doctorSummary: reportText,
54
+ structuredIssues,
55
+ scopeLabel: scope ?? "active milestone / blocking scope",
56
+ doctorCommandSuffix: scope ? ` ${scope}` : "",
57
+ });
58
+
59
+ const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
60
+
61
+ pi.sendMessage(
62
+ { customType: "gsd-doctor-heal", content, display: false },
63
+ { triggerTurn: true },
64
+ );
65
+ }
66
+
49
67
  /** Resolve the effective project root, accounting for worktree paths. */
50
68
  export function projectRoot(): string {
51
69
  const root = resolveProjectRoot(process.cwd());
@@ -55,7 +73,7 @@ export function projectRoot(): string {
55
73
 
56
74
  export function registerGSDCommand(pi: ExtensionAPI): void {
57
75
  pi.registerCommand("gsd", {
58
- description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
76
+ description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
59
77
  getArgumentCompletions: (prefix: string) => {
60
78
  const subcommands = [
61
79
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -98,8 +116,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
98
116
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
99
117
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
100
118
  { cmd: "update", desc: "Update GSD to the latest version" },
101
- { cmd: "start", desc: "Start a workflow template (bugfix, spike, feature, etc.)" },
102
- { cmd: "templates", desc: "List available workflow templates" },
103
119
  ];
104
120
  const parts = prefix.trim().split(/\s+/);
105
121
 
@@ -285,42 +301,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
285
301
  .map((s) => ({ value: `knowledge ${s.cmd}`, label: s.cmd, description: s.desc }));
286
302
  }
287
303
 
288
- if (parts[0] === "start" && parts.length <= 2) {
289
- const subPrefix = parts[1] ?? "";
290
- const subs = [
291
- { cmd: "bugfix", desc: "Triage, fix, test, and ship a bug fix" },
292
- { cmd: "small-feature", desc: "Lightweight feature with optional discussion" },
293
- { cmd: "spike", desc: "Research, prototype, and document findings" },
294
- { cmd: "hotfix", desc: "Minimal: fix it, test it, ship it" },
295
- { cmd: "refactor", desc: "Inventory, plan waves, migrate, verify" },
296
- { cmd: "security-audit", desc: "Scan, triage, remediate, re-scan" },
297
- { cmd: "dep-upgrade", desc: "Assess, upgrade, fix breaks, verify" },
298
- { cmd: "full-project", desc: "Complete GSD workflow with full ceremony" },
299
- { cmd: "resume", desc: "Resume an in-progress workflow" },
300
- { cmd: "--list", desc: "List all available templates" },
301
- { cmd: "--dry-run", desc: "Preview workflow without executing" },
302
- ];
303
- return subs
304
- .filter((s) => s.cmd.startsWith(subPrefix))
305
- .map((s) => ({ value: `start ${s.cmd}`, label: s.cmd, description: s.desc }));
306
- }
307
-
308
- if (parts[0] === "templates" && parts.length <= 2) {
309
- const subPrefix = parts[1] ?? "";
310
- const subs = [
311
- { cmd: "info", desc: "Show detailed template info" },
312
- ];
313
- return subs
314
- .filter((s) => s.cmd.startsWith(subPrefix))
315
- .map((s) => ({ value: `templates ${s.cmd}`, label: s.cmd, description: s.desc }));
316
- }
317
-
318
- if (parts[0] === "templates" && parts[1] === "info" && parts.length <= 3) {
319
- const namePrefix = parts[2] ?? "";
320
- return getTemplateCompletions(namePrefix)
321
- .map((c) => ({ value: `templates ${c.value}`, label: c.label, description: c.description }));
322
- }
323
-
324
304
  if (parts[0] === "doctor") {
325
305
  const modePrefix = parts[1] ?? "";
326
306
  const modes = [
@@ -812,17 +792,6 @@ Examples:
812
792
  return;
813
793
  }
814
794
 
815
- // ─── Workflow Templates ────────────────────────────────────────
816
- if (trimmed === "start" || trimmed.startsWith("start ")) {
817
- await handleStart(trimmed.replace(/^start\s*/, "").trim(), ctx, pi);
818
- return;
819
- }
820
-
821
- if (trimmed === "templates" || trimmed.startsWith("templates ")) {
822
- await handleTemplates(trimmed.replace(/^templates\s*/, "").trim(), ctx);
823
- return;
824
- }
825
-
826
795
  if (trimmed === "") {
827
796
  // Bare /gsd defaults to step mode
828
797
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
@@ -841,8 +810,6 @@ function showHelp(ctx: ExtensionCommandContext): void {
841
810
  const lines = [
842
811
  "GSD — Get Shit Done\n",
843
812
  "WORKFLOW",
844
- " /gsd start <tpl> Start a workflow template (bugfix, spike, feature, hotfix, etc.)",
845
- " /gsd templates List available workflow templates [info <name>]",
846
813
  " /gsd Run next unit in step mode (same as /gsd next)",
847
814
  " /gsd next Execute next task, then pause [--dry-run] [--verbose]",
848
815
  " /gsd auto Run all queued units continuously [--verbose]",
@@ -11,8 +11,7 @@ import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
11
11
  import { deriveState } from "./state.js";
12
12
  import { loadFile, parseRoadmap, parsePlan } from "./files.js";
13
13
  import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
14
- import { getAutoDashboardData } from "./auto.js";
15
- import type { AutoDashboardData } from "./auto-dashboard.js";
14
+ import { getAutoDashboardData, type AutoDashboardData } from "./auto.js";
16
15
  import {
17
16
  getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
18
17
  aggregateByModel, aggregateCacheHitRate, formatCost, formatTokenCount, formatCostProjection,
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  /**
@@ -50,18 +50,3 @@ export function saveJsonFile<T>(filePath: string, data: T): void {
50
50
  // Non-fatal — don't let persistence failures break operation
51
51
  }
52
52
  }
53
-
54
- /**
55
- * Write a JSON file atomically (write to .tmp, then rename).
56
- * Creates parent directories as needed. Non-fatal on error.
57
- */
58
- export function writeJsonFileAtomic<T>(filePath: string, data: T): void {
59
- try {
60
- mkdirSync(dirname(filePath), { recursive: true });
61
- const tmp = filePath + ".tmp";
62
- writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
63
- renameSync(tmp, filePath);
64
- } catch {
65
- // Non-fatal — don't let persistence failures break operation
66
- }
67
- }
@@ -9,10 +9,10 @@
9
9
  * survives branch switches and is shared across sessions.
10
10
  */
11
11
 
12
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
12
13
  import { join } from "node:path";
13
14
  import { gsdRoot } from "./paths.js";
14
15
  import { milestoneIdSort } from "./milestone-ids.js";
15
- import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
16
16
 
17
17
  // ─── Types ───────────────────────────────────────────────────────────────────
18
18
 
@@ -45,12 +45,6 @@ function queueOrderPath(basePath: string): string {
45
45
  return join(gsdRoot(basePath), "QUEUE-ORDER.json");
46
46
  }
47
47
 
48
- // ─── Type Guards ─────────────────────────────────────────────────────────────
49
-
50
- function isQueueOrderFile(data: unknown): data is QueueOrderFile {
51
- return data !== null && typeof data === "object" && "order" in data! && Array.isArray((data as QueueOrderFile).order);
52
- }
53
-
54
48
  // ─── Read / Write ────────────────────────────────────────────────────────────
55
49
 
56
50
  /**
@@ -58,8 +52,15 @@ function isQueueOrderFile(data: unknown): data is QueueOrderFile {
58
52
  * the file is corrupt/unreadable.
59
53
  */
60
54
  export function loadQueueOrder(basePath: string): string[] | null {
61
- const data = loadJsonFileOrNull(queueOrderPath(basePath), isQueueOrderFile);
62
- return data?.order ?? null;
55
+ const p = queueOrderPath(basePath);
56
+ if (!existsSync(p)) return null;
57
+ try {
58
+ const data: QueueOrderFile = JSON.parse(readFileSync(p, "utf-8"));
59
+ if (!Array.isArray(data.order)) return null;
60
+ return data.order;
61
+ } catch {
62
+ return null;
63
+ }
63
64
  }
64
65
 
65
66
  /**
@@ -70,7 +71,7 @@ export function saveQueueOrder(basePath: string, order: string[]): void {
70
71
  order,
71
72
  updatedAt: new Date().toISOString(),
72
73
  };
73
- saveJsonFile(queueOrderPath(basePath), data);
74
+ writeFileSync(queueOrderPath(basePath), JSON.stringify(data, null, 2) + "\n", "utf-8");
74
75
  }
75
76
 
76
77
  // ─── Sorting ─────────────────────────────────────────────────────────────────
@@ -11,6 +11,9 @@
11
11
  */
12
12
 
13
13
  import {
14
+ writeFileSync,
15
+ readFileSync,
16
+ renameSync,
14
17
  unlinkSync,
15
18
  readdirSync,
16
19
  mkdirSync,
@@ -18,7 +21,6 @@ import {
18
21
  } from "node:fs";
19
22
  import { join } from "node:path";
20
23
  import { gsdRoot } from "./paths.js";
21
- import { loadJsonFileOrNull, writeJsonFileAtomic } from "./json-persistence.js";
22
24
 
23
25
  // ─── Types ─────────────────────────────────────────────────────────────────
24
26
 
@@ -47,16 +49,9 @@ export interface SignalMessage {
47
49
  const PARALLEL_DIR = "parallel";
48
50
  const STATUS_SUFFIX = ".status.json";
49
51
  const SIGNAL_SUFFIX = ".signal.json";
52
+ const TMP_SUFFIX = ".tmp";
50
53
  const DEFAULT_STALE_TIMEOUT_MS = 30_000;
51
54
 
52
- function isSessionStatus(data: unknown): data is SessionStatus {
53
- return data !== null && typeof data === "object" && "milestoneId" in data && "pid" in data;
54
- }
55
-
56
- function isSignalMessage(data: unknown): data is SignalMessage {
57
- return data !== null && typeof data === "object" && "signal" in data && "sentAt" in data;
58
- }
59
-
60
55
  // ─── Helpers ───────────────────────────────────────────────────────────────
61
56
 
62
57
  function parallelDir(basePath: string): string {
@@ -91,13 +86,25 @@ function isPidAlive(pid: number): boolean {
91
86
 
92
87
  /** Write session status atomically (write to .tmp, then rename). */
93
88
  export function writeSessionStatus(basePath: string, status: SessionStatus): void {
94
- ensureParallelDir(basePath);
95
- writeJsonFileAtomic(statusPath(basePath, status.milestoneId), status);
89
+ try {
90
+ ensureParallelDir(basePath);
91
+ const dest = statusPath(basePath, status.milestoneId);
92
+ const tmp = dest + TMP_SUFFIX;
93
+ writeFileSync(tmp, JSON.stringify(status, null, 2), "utf-8");
94
+ renameSync(tmp, dest);
95
+ } catch { /* non-fatal */ }
96
96
  }
97
97
 
98
98
  /** Read a specific milestone's session status. */
99
99
  export function readSessionStatus(basePath: string, milestoneId: string): SessionStatus | null {
100
- return loadJsonFileOrNull(statusPath(basePath, milestoneId), isSessionStatus);
100
+ try {
101
+ const p = statusPath(basePath, milestoneId);
102
+ if (!existsSync(p)) return null;
103
+ const raw = readFileSync(p, "utf-8");
104
+ return JSON.parse(raw) as SessionStatus;
105
+ } catch {
106
+ return null;
107
+ }
101
108
  }
102
109
 
103
110
  /** Read all session status files from .gsd/parallel/. */
@@ -107,10 +114,13 @@ export function readAllSessionStatuses(basePath: string): SessionStatus[] {
107
114
 
108
115
  const results: SessionStatus[] = [];
109
116
  try {
110
- for (const entry of readdirSync(dir)) {
117
+ const entries = readdirSync(dir);
118
+ for (const entry of entries) {
111
119
  if (!entry.endsWith(STATUS_SUFFIX)) continue;
112
- const status = loadJsonFileOrNull(join(dir, entry), isSessionStatus);
113
- if (status) results.push(status);
120
+ try {
121
+ const raw = readFileSync(join(dir, entry), "utf-8");
122
+ results.push(JSON.parse(raw) as SessionStatus);
123
+ } catch { /* skip corrupt files */ }
114
124
  }
115
125
  } catch { /* non-fatal */ }
116
126
  return results;
@@ -128,19 +138,27 @@ export function removeSessionStatus(basePath: string, milestoneId: string): void
128
138
 
129
139
  /** Write a signal file for a worker to consume. */
130
140
  export function sendSignal(basePath: string, milestoneId: string, signal: SessionSignal): void {
131
- ensureParallelDir(basePath);
132
- const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
133
- writeJsonFileAtomic(signalPath(basePath, milestoneId), msg);
141
+ try {
142
+ ensureParallelDir(basePath);
143
+ const dest = signalPath(basePath, milestoneId);
144
+ const tmp = dest + TMP_SUFFIX;
145
+ const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
146
+ writeFileSync(tmp, JSON.stringify(msg, null, 2), "utf-8");
147
+ renameSync(tmp, dest);
148
+ } catch { /* non-fatal */ }
134
149
  }
135
150
 
136
151
  /** Read and delete a signal file (atomic consume). Returns null if no signal pending. */
137
152
  export function consumeSignal(basePath: string, milestoneId: string): SignalMessage | null {
138
- const p = signalPath(basePath, milestoneId);
139
- const msg = loadJsonFileOrNull(p, isSignalMessage);
140
- if (msg) {
141
- try { unlinkSync(p); } catch { /* non-fatal */ }
153
+ try {
154
+ const p = signalPath(basePath, milestoneId);
155
+ if (!existsSync(p)) return null;
156
+ const raw = readFileSync(p, "utf-8");
157
+ unlinkSync(p);
158
+ return JSON.parse(raw) as SignalMessage;
159
+ } catch {
160
+ return null;
142
161
  }
143
- return msg;
144
162
  }
145
163
 
146
164
  // ─── Stale Detection ───────────────────────────────────────────────────────
@@ -5,7 +5,7 @@ import {
5
5
  getBudgetAlertLevel,
6
6
  getBudgetEnforcementAction,
7
7
  getNewBudgetAlertLevel,
8
- } from "../auto-budget.js";
8
+ } from "../auto.js";
9
9
 
10
10
  test("getBudgetAlertLevel returns the expected threshold bucket", () => {
11
11
  assert.equal(getBudgetAlertLevel(0.10), 0);
@@ -17,8 +17,8 @@ import { tmpdir } from "node:os";
17
17
  import {
18
18
  _getUnitConsecutiveSkips,
19
19
  _resetUnitConsecutiveSkips,
20
+ MAX_CONSECUTIVE_SKIPS,
20
21
  } from "../auto.ts";
21
- import { MAX_CONSECUTIVE_SKIPS } from "../auto/session.ts";
22
22
  import { persistCompletedKey, removePersistedKey, loadPersistedKeys } from "../auto-recovery.ts";
23
23
  import { createTestContext } from "./test-helpers.ts";
24
24