gsd-pi 2.74.0-dev.2b524c3 → 2.74.0-dev.b741afb

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 (159) hide show
  1. package/dist/cli.js +85 -0
  2. package/dist/headless-query.js +4 -1
  3. package/dist/help-text.js +23 -0
  4. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  5. package/dist/resources/extensions/gsd/auto/phases.js +45 -1
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +52 -56
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -0
  8. package/dist/resources/extensions/gsd/auto.js +8 -2
  9. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  10. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  11. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  12. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  13. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  14. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  15. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  16. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  17. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  18. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  19. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  20. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  21. package/dist/resources/extensions/gsd/graph-context.js +66 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  23. package/dist/resources/extensions/gsd/index.js +15 -2
  24. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  25. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  26. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  28. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  29. package/dist/resources/extensions/gsd/state.js +5 -1
  30. package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -0
  31. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  32. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  33. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  34. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  35. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  36. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  68. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  69. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  70. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  71. package/package.json +3 -2
  72. package/packages/daemon/package.json +2 -2
  73. package/packages/mcp-server/dist/index.d.ts +3 -0
  74. package/packages/mcp-server/dist/index.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/index.js +3 -0
  76. package/packages/mcp-server/dist/index.js.map +1 -1
  77. package/packages/mcp-server/dist/readers/graph.d.ts +87 -0
  78. package/packages/mcp-server/dist/readers/graph.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/readers/graph.js +548 -0
  80. package/packages/mcp-server/dist/readers/graph.js.map +1 -0
  81. package/packages/mcp-server/dist/readers/index.d.ts +2 -0
  82. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -1
  83. package/packages/mcp-server/dist/readers/index.js +1 -0
  84. package/packages/mcp-server/dist/readers/index.js.map +1 -1
  85. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  86. package/packages/mcp-server/dist/server.js +65 -0
  87. package/packages/mcp-server/dist/server.js.map +1 -1
  88. package/packages/mcp-server/package.json +2 -2
  89. package/packages/mcp-server/src/index.ts +15 -0
  90. package/packages/mcp-server/src/readers/graph.test.ts +426 -0
  91. package/packages/mcp-server/src/readers/graph.ts +708 -0
  92. package/packages/mcp-server/src/readers/index.ts +12 -0
  93. package/packages/mcp-server/src/server.ts +83 -0
  94. package/packages/mcp-server/tsconfig.json +1 -0
  95. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  96. package/packages/native/package.json +2 -2
  97. package/packages/native/tsconfig.tsbuildinfo +1 -0
  98. package/packages/pi-agent-core/package.json +1 -1
  99. package/packages/pi-agent-core/tsconfig.json +1 -0
  100. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  101. package/packages/pi-ai/package.json +1 -1
  102. package/packages/pi-ai/tsconfig.json +1 -0
  103. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  104. package/packages/pi-coding-agent/tsconfig.json +1 -0
  105. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  106. package/packages/pi-tui/package.json +1 -1
  107. package/packages/pi-tui/tsconfig.json +1 -0
  108. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  109. package/packages/rpc-client/package.json +1 -1
  110. package/packages/rpc-client/tsconfig.json +1 -0
  111. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  112. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  113. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +68 -1
  115. package/src/resources/extensions/gsd/auto-post-unit.ts +60 -57
  116. package/src/resources/extensions/gsd/auto-prompts.ts +13 -0
  117. package/src/resources/extensions/gsd/auto.ts +7 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  119. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  120. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  121. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  122. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  123. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  124. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  125. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  126. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  127. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  128. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  129. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  130. package/src/resources/extensions/gsd/graph-context.ts +85 -0
  131. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  132. package/src/resources/extensions/gsd/index.ts +18 -2
  133. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  134. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  135. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  136. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  137. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  138. package/src/resources/extensions/gsd/state.ts +9 -2
  139. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  140. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  141. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  142. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  143. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  146. package/src/resources/extensions/gsd/tests/graph-context.test.ts +337 -0
  147. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  148. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  149. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  150. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  151. package/src/resources/extensions/gsd/tools/complete-slice.ts +19 -0
  152. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  153. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  154. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  155. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  156. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  157. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  158. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_buildManifest.js +0 -0
  159. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_ssgManifest.js +0 -0
@@ -34,6 +34,7 @@ import {
34
34
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
35
35
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
36
36
  import { logWarning } from "./workflow-logger.js";
37
+ import { inlineGraphSubgraph } from "./graph-context.js";
37
38
 
38
39
  // ─── Preamble Cap ─────────────────────────────────────────────────────────────
39
40
 
@@ -1175,6 +1176,10 @@ export async function buildResearchSlicePrompt(
1175
1176
  const knowledgeInlineRS = await inlineKnowledgeScoped(base, keywords);
1176
1177
  if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
1177
1178
 
1179
+ // Knowledge graph: subgraph for this slice (graceful — skipped if no graph.json)
1180
+ const graphBlockRS = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { budget: 3000 });
1181
+ if (graphBlockRS) inlined.push(graphBlockRS);
1182
+
1178
1183
  inlined.push(inlineTemplate("research", "Research"));
1179
1184
 
1180
1185
  const depContent = await inlineDependencySummaries(mid, sid, base);
@@ -1250,6 +1255,10 @@ export async function buildPlanSlicePrompt(
1250
1255
  const knowledgeInlinePS = await inlineKnowledgeScoped(base, keywordsPS);
1251
1256
  if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
1252
1257
 
1258
+ // Knowledge graph: subgraph for this slice (graceful — skipped if no graph.json)
1259
+ const graphBlockPS = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { budget: 3000 });
1260
+ if (graphBlockPS) inlined.push(graphBlockPS);
1261
+
1253
1262
  inlined.push(inlineTemplate("plan", "Slice Plan"));
1254
1263
  if (inlineLevel === "full") {
1255
1264
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
@@ -1366,12 +1375,16 @@ export async function buildExecuteTaskPrompt(
1366
1375
  // Only include if it has content (not a "not found" result)
1367
1376
  const knowledgeContent = knowledgeInlineET && !knowledgeInlineET.includes("not found") ? knowledgeInlineET : null;
1368
1377
 
1378
+ // Knowledge graph: tight subgraph for this task (graceful — skipped if no graph.json)
1379
+ const graphBlockET = await inlineGraphSubgraph(base, `${tid} ${tTitle}`, { budget: 2000 });
1380
+
1369
1381
  const inlinedTemplates = inlineLevel === "minimal"
1370
1382
  ? inlineTemplate("task-summary", "Task Summary")
1371
1383
  : [
1372
1384
  inlineTemplate("task-summary", "Task Summary"),
1373
1385
  inlineTemplate("decisions", "Decisions"),
1374
1386
  ...(knowledgeContent ? [knowledgeContent] : []),
1387
+ ...(graphBlockET ? [graphBlockET] : []),
1375
1388
  ].join("\n\n---\n\n");
1376
1389
 
1377
1390
  const taskSummaryPath = join(base, `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`);
@@ -195,6 +195,7 @@ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.j
195
195
  import { startUnitSupervision } from "./auto-timers.js";
196
196
  import { runPostUnitVerification } from "./auto-verification.js";
197
197
  import {
198
+ autoCommitUnit,
198
199
  postUnitPreVerification,
199
200
  postUnitPostVerification,
200
201
  } from "./auto-post-unit.js";
@@ -1180,6 +1181,7 @@ function buildLoopDeps(): LoopDeps {
1180
1181
  getMainBranch,
1181
1182
  // Unit closeout + runtime records
1182
1183
  closeoutUnit,
1184
+ autoCommitUnit,
1183
1185
  recordOutcome,
1184
1186
  writeLock,
1185
1187
  captureAvailableSkills,
@@ -1400,6 +1402,11 @@ export async function startAuto(
1400
1402
  s.stepMode = requestedStepMode;
1401
1403
  s.cmdCtx = ctx;
1402
1404
  s.basePath = base;
1405
+ // Ensure the workflow-logger audit log is pinned to the project root
1406
+ // even when auto-mode is entered via a path that bypasses the
1407
+ // bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
1408
+ // (e.g. /clear resume, hot-reload).
1409
+ setLogBasePath(base);
1403
1410
  s.unitDispatchCount.clear();
1404
1411
  s.unitLifetimeDispatches.clear();
1405
1412
  if (!getLedger()) initMetrics(base);
@@ -2,7 +2,6 @@
2
2
 
3
3
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
4
4
 
5
- import { registerGSDCommand } from "../commands.js";
6
5
  import { registerExitCommand } from "../exit-command.js";
7
6
  import { registerWorktreeCommand } from "../worktree-command.js";
8
7
  import { registerDbTools } from "./db-tools.js";
@@ -12,6 +11,7 @@ import { registerQueryTools } from "./query-tools.js";
12
11
  import { registerHooks } from "./register-hooks.js";
13
12
  import { registerShortcuts } from "./register-shortcuts.js";
14
13
  import { writeCrashLog } from "./crash-log.js";
14
+ import { logWarning } from "../workflow-logger.js";
15
15
 
16
16
  export { writeCrashLog } from "./crash-log.js";
17
17
 
@@ -58,7 +58,8 @@ function installEpipeGuard(): void {
58
58
  }
59
59
 
60
60
  export function registerGsdExtension(pi: ExtensionAPI): void {
61
- registerGSDCommand(pi);
61
+ // Note: registerGSDCommand is called by index.ts before this function,
62
+ // so we intentionally skip it here to avoid double-registration.
62
63
  registerWorktreeCommand(pi);
63
64
  registerExitCommand(pi);
64
65
 
@@ -71,10 +72,25 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
71
72
  },
72
73
  });
73
74
 
74
- registerDynamicTools(pi);
75
- registerDbTools(pi);
76
- registerJournalTools(pi);
77
- registerQueryTools(pi);
78
- registerShortcuts(pi);
79
- registerHooks(pi);
75
+ // Wrap non-critical registrations individually so one failure
76
+ // doesn't prevent the others from loading.
77
+ const nonCriticalRegistrations: Array<[string, () => void]> = [
78
+ ["dynamic-tools", () => registerDynamicTools(pi)],
79
+ ["db-tools", () => registerDbTools(pi)],
80
+ ["journal-tools", () => registerJournalTools(pi)],
81
+ ["query-tools", () => registerQueryTools(pi)],
82
+ ["shortcuts", () => registerShortcuts(pi)],
83
+ ["hooks", () => registerHooks(pi)],
84
+ ];
85
+
86
+ for (const [name, register] of nonCriticalRegistrations) {
87
+ try {
88
+ register();
89
+ } catch (err) {
90
+ logWarning(
91
+ "bootstrap",
92
+ `Failed to register ${name}: ${err instanceof Error ? err.message : String(err)}`,
93
+ );
94
+ }
95
+ }
80
96
  }
@@ -15,7 +15,7 @@ export interface GsdCommandDefinition {
15
15
  type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
16
16
 
17
17
  export const GSD_COMMAND_DESCRIPTION =
18
- "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications";
18
+ "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests";
19
19
 
20
20
  export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
21
21
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -74,6 +74,12 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
74
74
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
75
75
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
76
76
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
77
+ { cmd: "ship", desc: "Create PR from milestone artifacts and open for review" },
78
+ { cmd: "do", desc: "Route freeform text to the right GSD command" },
79
+ { cmd: "session-report", desc: "Session cost, tokens, and work summary" },
80
+ { cmd: "backlog", desc: "Manage backlog items (add, promote, remove, list)" },
81
+ { cmd: "pr-branch", desc: "Create clean PR branch filtering .gsd/ commits" },
82
+ { cmd: "add-tests", desc: "Generate tests for completed slices" },
77
83
  ];
78
84
 
79
85
  const NESTED_COMPLETIONS: CompletionMap = {
@@ -244,6 +250,25 @@ const NESTED_COMPLETIONS: CompletionMap = {
244
250
  { cmd: "stats", desc: "Show file count, description coverage, and generation time" },
245
251
  { cmd: "help", desc: "Show usage and available subcommands" },
246
252
  ],
253
+ ship: [
254
+ { cmd: "--dry-run", desc: "Preview PR without creating" },
255
+ { cmd: "--draft", desc: "Open as draft PR" },
256
+ { cmd: "--base", desc: "Override target branch (default: main)" },
257
+ { cmd: "--force", desc: "Ship even with pending tasks" },
258
+ ],
259
+ "session-report": [
260
+ { cmd: "--json", desc: "Machine-readable JSON output" },
261
+ { cmd: "--save", desc: "Save report to .gsd/reports/" },
262
+ ],
263
+ backlog: [
264
+ { cmd: "add", desc: "Add item to backlog" },
265
+ { cmd: "promote", desc: "Promote backlog item to active slice" },
266
+ { cmd: "remove", desc: "Remove backlog item" },
267
+ ],
268
+ "pr-branch": [
269
+ { cmd: "--dry-run", desc: "Preview what would be filtered" },
270
+ { cmd: "--name", desc: "Custom branch name" },
271
+ ],
247
272
  };
248
273
 
249
274
  function filterOptions(
@@ -11,6 +11,9 @@ import { handleExport } from "../../export.js";
11
11
  import { handleHistory } from "../../history.js";
12
12
  import { handleUndo } from "../../undo.js";
13
13
  import { handleRemote } from "../../../remote-questions/mod.js";
14
+ import { handleShip } from "../../commands-ship.js";
15
+ import { handleSessionReport } from "../../commands-session-report.js";
16
+ import { handlePrBranch } from "../../commands-pr-branch.js";
14
17
  import { projectRoot } from "../context.js";
15
18
 
16
19
  export async function handleOpsCommand(trimmed: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<boolean> {
@@ -216,5 +219,22 @@ Examples:
216
219
  await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi);
217
220
  return true;
218
221
  }
222
+ if (trimmed === "ship" || trimmed.startsWith("ship ")) {
223
+ await handleShip(trimmed.replace(/^ship\s*/, "").trim(), ctx, pi);
224
+ return true;
225
+ }
226
+ if (trimmed === "session-report" || trimmed.startsWith("session-report ")) {
227
+ await handleSessionReport(trimmed.replace(/^session-report\s*/, "").trim(), ctx);
228
+ return true;
229
+ }
230
+ if (trimmed === "pr-branch" || trimmed.startsWith("pr-branch ")) {
231
+ await handlePrBranch(trimmed.replace(/^pr-branch\s*/, "").trim(), ctx);
232
+ return true;
233
+ }
234
+ if (trimmed === "add-tests" || trimmed.startsWith("add-tests ")) {
235
+ const { handleAddTests } = await import("../../commands-add-tests.js");
236
+ await handleAddTests(trimmed.replace(/^add-tests\s*/, "").trim(), ctx, pi);
237
+ return true;
238
+ }
219
239
  return false;
220
240
  }
@@ -38,6 +38,67 @@ const WORKFLOW_USAGE = [
38
38
  " resume — Resume paused custom workflow auto-mode",
39
39
  ].join("\n");
40
40
 
41
+ function splitWorkflowRunArgs(input: string): string[] {
42
+ const tokens: string[] = [];
43
+ let current = "";
44
+ let quote: '"' | "'" | null = null;
45
+ let escapeNext = false;
46
+
47
+ for (const ch of input) {
48
+ if (escapeNext) {
49
+ current += ch;
50
+ escapeNext = false;
51
+ continue;
52
+ }
53
+
54
+ if (ch === "\\") {
55
+ escapeNext = true;
56
+ continue;
57
+ }
58
+
59
+ if (quote) {
60
+ if (ch === quote) {
61
+ quote = null;
62
+ } else {
63
+ current += ch;
64
+ }
65
+ continue;
66
+ }
67
+
68
+ if (ch === '"' || ch === "'") {
69
+ quote = ch;
70
+ continue;
71
+ }
72
+
73
+ if (/\s/.test(ch)) {
74
+ if (current) {
75
+ tokens.push(current);
76
+ current = "";
77
+ }
78
+ continue;
79
+ }
80
+
81
+ current += ch;
82
+ }
83
+
84
+ if (escapeNext) current += "\\";
85
+ if (current) tokens.push(current);
86
+ return tokens;
87
+ }
88
+
89
+ export function parseWorkflowRunArgs(args: string): { defName: string; overrides: Record<string, string> } {
90
+ const parts = splitWorkflowRunArgs(args);
91
+ const defName = parts[0] ?? "";
92
+ const overrides: Record<string, string> = {};
93
+ for (let i = 1; i < parts.length; i++) {
94
+ const eqIdx = parts[i].indexOf("=");
95
+ if (eqIdx > 0) {
96
+ overrides[parts[i].slice(0, eqIdx)] = parts[i].slice(eqIdx + 1);
97
+ }
98
+ }
99
+ return { defName, overrides };
100
+ }
101
+
41
102
  async function handleCustomWorkflow(
42
103
  sub: string,
43
104
  ctx: ExtensionCommandContext,
@@ -62,15 +123,7 @@ async function handleCustomWorkflow(
62
123
  ctx.ui.notify("Usage: /gsd workflow run <name> [param=value ...]", "warning");
63
124
  return true;
64
125
  }
65
- const parts = args.split(/\s+/);
66
- const defName = parts[0];
67
- const overrides: Record<string, string> = {};
68
- for (let i = 1; i < parts.length; i++) {
69
- const eqIdx = parts[i].indexOf("=");
70
- if (eqIdx > 0) {
71
- overrides[parts[i].slice(0, eqIdx)] = parts[i].slice(eqIdx + 1);
72
- }
73
- }
126
+ const { defName, overrides } = parseWorkflowRunArgs(args);
74
127
  try {
75
128
  const base = projectRoot();
76
129
  const runDir = createRun(base, defName, Object.keys(overrides).length > 0 ? overrides : undefined);
@@ -168,6 +221,18 @@ async function handleCustomWorkflow(
168
221
  }
169
222
 
170
223
  export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<boolean> {
224
+ // ── /gsd do — natural language routing (must be early to route to other commands) ──
225
+ if (trimmed === "do" || trimmed.startsWith("do ")) {
226
+ const { handleDo } = await import("../../commands-do.js");
227
+ await handleDo(trimmed.replace(/^do\s*/, "").trim(), ctx, pi);
228
+ return true;
229
+ }
230
+ // ── Backlog management ──
231
+ if (trimmed === "backlog" || trimmed.startsWith("backlog ")) {
232
+ const { handleBacklog } = await import("../../commands-backlog.js");
233
+ await handleBacklog(trimmed.replace(/^backlog\s*/, "").trim(), ctx, pi);
234
+ return true;
235
+ }
171
236
  // ── Custom workflow commands (`/gsd workflow ...`) ──
172
237
  if (trimmed === "workflow" || trimmed.startsWith("workflow ")) {
173
238
  const sub = trimmed.slice("workflow".length).trim();
@@ -0,0 +1,137 @@
1
+ /**
2
+ * GSD Command — /gsd add-tests
3
+ *
4
+ * Generates tests for a completed slice by dispatching an LLM prompt
5
+ * with implementation context (summaries, changed files, test patterns).
6
+ */
7
+
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
+
10
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import { deriveState } from "./state.js";
14
+ import { gsdRoot, resolveSliceFile } from "./paths.js";
15
+ import { loadPrompt } from "./prompt-loader.js";
16
+
17
+ function findLastCompletedSlice(basePath: string, milestoneId: string): string | null {
18
+ // Scan disk for slices that have a SUMMARY.md (indicating completion)
19
+ const slicesDir = join(gsdRoot(basePath), "milestones", milestoneId, "slices");
20
+ if (!existsSync(slicesDir)) return null;
21
+
22
+ try {
23
+ const entries = readdirSync(slicesDir, { withFileTypes: true })
24
+ .filter((e) => e.isDirectory() && /^S\d+$/.test(e.name))
25
+ .sort((a, b) => b.name.localeCompare(a.name)); // reverse order — latest first
26
+
27
+ for (const entry of entries) {
28
+ const summaryPath = join(slicesDir, entry.name, `${entry.name}-SUMMARY.md`);
29
+ if (existsSync(summaryPath)) return entry.name;
30
+ }
31
+ } catch {
32
+ // non-fatal
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function readSliceSummary(basePath: string, milestoneId: string, sliceId: string): { title: string; content: string } {
38
+ const summaryPath = resolveSliceFile(basePath, milestoneId, sliceId, "SUMMARY");
39
+ if (summaryPath && existsSync(summaryPath)) {
40
+ const content = readFileSync(summaryPath, "utf-8");
41
+ const titleMatch = content.match(/^#\s+(.+)/m);
42
+ return { title: titleMatch?.[1] ?? sliceId, content };
43
+ }
44
+ return { title: sliceId, content: "(no summary available)" };
45
+ }
46
+
47
+ function detectTestPatterns(basePath: string): string {
48
+ const patterns: string[] = [];
49
+
50
+ // Check for common test configs
51
+ const checks = [
52
+ { file: "jest.config.ts", name: "Jest" },
53
+ { file: "jest.config.js", name: "Jest" },
54
+ { file: "vitest.config.ts", name: "Vitest" },
55
+ { file: "vitest.config.js", name: "Vitest" },
56
+ { file: ".mocharc.yml", name: "Mocha" },
57
+ ];
58
+
59
+ for (const check of checks) {
60
+ if (existsSync(join(basePath, check.file))) {
61
+ patterns.push(`Framework: ${check.name} (${check.file})`);
62
+ }
63
+ }
64
+
65
+ // Look for existing test files to infer patterns
66
+ const testDirs = ["tests", "test", "src/__tests__", "__tests__"];
67
+ for (const dir of testDirs) {
68
+ const fullDir = join(basePath, dir);
69
+ if (existsSync(fullDir)) {
70
+ try {
71
+ const files = readdirSync(fullDir).filter((f) => f.endsWith(".test.ts") || f.endsWith(".spec.ts") || f.endsWith(".test.js"));
72
+ if (files.length > 0) {
73
+ patterns.push(`Test directory: ${dir}/ (${files.length} test files)`);
74
+ // Read first test file for patterns
75
+ const samplePath = join(fullDir, files[0]);
76
+ const sample = readFileSync(samplePath, "utf-8").slice(0, 500);
77
+ patterns.push(`Sample pattern from ${files[0]}:\n${sample}`);
78
+ break;
79
+ }
80
+ } catch {
81
+ // non-fatal
82
+ }
83
+ }
84
+ }
85
+
86
+ return patterns.length > 0 ? patterns.join("\n") : "No test framework detected. Use Node.js built-in test runner.";
87
+ }
88
+
89
+ export async function handleAddTests(
90
+ args: string,
91
+ ctx: ExtensionCommandContext,
92
+ pi: ExtensionAPI,
93
+ ): Promise<void> {
94
+ const basePath = process.cwd();
95
+ const state = await deriveState(basePath);
96
+
97
+ if (!state.activeMilestone) {
98
+ ctx.ui.notify("No active milestone.", "warning");
99
+ return;
100
+ }
101
+
102
+ const milestoneId = state.activeMilestone.id;
103
+
104
+ // Determine target
105
+ const targetId = args.trim() || findLastCompletedSlice(basePath, milestoneId);
106
+ if (!targetId) {
107
+ ctx.ui.notify(
108
+ "No completed slices found. Specify a slice ID: /gsd add-tests S03",
109
+ "warning",
110
+ );
111
+ return;
112
+ }
113
+
114
+ // Gather context
115
+ const summary = readSliceSummary(basePath, milestoneId, targetId);
116
+ const testPatterns = detectTestPatterns(basePath);
117
+
118
+ ctx.ui.notify(`Generating tests for ${targetId}: "${summary.title}"...`, "info");
119
+
120
+ try {
121
+ const prompt = loadPrompt("add-tests", {
122
+ sliceId: targetId,
123
+ sliceTitle: summary.title,
124
+ sliceSummary: summary.content,
125
+ existingTestPatterns: testPatterns,
126
+ workingDirectory: basePath,
127
+ });
128
+
129
+ pi.sendMessage(
130
+ { customType: "gsd-add-tests", content: prompt, display: false },
131
+ { triggerTurn: true },
132
+ );
133
+ } catch (err) {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ ctx.ui.notify(`Failed to dispatch test generation: ${msg}`, "error");
136
+ }
137
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * GSD Command — /gsd backlog
3
+ *
4
+ * Structured backlog management with 999.x numbering.
5
+ * Items stored in .gsd/BACKLOG.md as markdown checklist.
6
+ * Items can be promoted to active slices via add-slice.
7
+ */
8
+
9
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
10
+
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+
14
+ import { gsdRoot } from "./paths.js";
15
+
16
+ interface BacklogItem {
17
+ id: string;
18
+ title: string;
19
+ done: boolean;
20
+ note: string;
21
+ }
22
+
23
+ function backlogPath(basePath: string): string {
24
+ return join(gsdRoot(basePath), "BACKLOG.md");
25
+ }
26
+
27
+ function parseBacklog(basePath: string): BacklogItem[] {
28
+ const filePath = backlogPath(basePath);
29
+ if (!existsSync(filePath)) return [];
30
+
31
+ const content = readFileSync(filePath, "utf-8");
32
+ const items: BacklogItem[] = [];
33
+
34
+ for (const line of content.split("\n")) {
35
+ const match = line.match(/^- \[([ x])\] (999\.\d+) — (.+?)(?:\s*\((.+)\))?$/);
36
+ if (match) {
37
+ items.push({
38
+ id: match[2],
39
+ title: match[3].trim(),
40
+ done: match[1] === "x",
41
+ note: match[4] ?? "",
42
+ });
43
+ }
44
+ }
45
+
46
+ return items;
47
+ }
48
+
49
+ function writeBacklog(basePath: string, items: BacklogItem[]): void {
50
+ const filePath = backlogPath(basePath);
51
+ mkdirSync(dirname(filePath), { recursive: true });
52
+ const lines = ["# Backlog\n"];
53
+ for (const item of items) {
54
+ const check = item.done ? "x" : " ";
55
+ const note = item.note ? ` (${item.note})` : "";
56
+ lines.push(`- [${check}] ${item.id} — ${item.title}${note}`);
57
+ }
58
+ lines.push(""); // trailing newline
59
+ writeFileSync(filePath, lines.join("\n"), "utf-8");
60
+ }
61
+
62
+ function nextBacklogId(items: BacklogItem[]): string {
63
+ let maxNum = 0;
64
+ for (const item of items) {
65
+ const match = item.id.match(/^999\.(\d+)$/);
66
+ if (match) {
67
+ const num = parseInt(match[1], 10);
68
+ if (num > maxNum) maxNum = num;
69
+ }
70
+ }
71
+ return `999.${maxNum + 1}`;
72
+ }
73
+
74
+ async function listBacklog(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
75
+ const items = parseBacklog(basePath);
76
+ if (items.length === 0) {
77
+ ctx.ui.notify("Backlog is empty. Add items with /gsd backlog add <title>", "info");
78
+ return;
79
+ }
80
+
81
+ const lines = ["Backlog:\n"];
82
+ for (const item of items) {
83
+ const status = item.done ? "✓" : "○";
84
+ const note = item.note ? ` (${item.note})` : "";
85
+ lines.push(` ${status} ${item.id} — ${item.title}${note}`);
86
+ }
87
+ const pending = items.filter((i) => !i.done).length;
88
+ lines.push(`\n${pending} pending, ${items.length - pending} promoted/done`);
89
+ ctx.ui.notify(lines.join("\n"), "info");
90
+ }
91
+
92
+ async function addBacklogItem(basePath: string, title: string, ctx: ExtensionCommandContext): Promise<void> {
93
+ if (!title) {
94
+ ctx.ui.notify("Usage: /gsd backlog add <title>", "warning");
95
+ return;
96
+ }
97
+
98
+ const items = parseBacklog(basePath);
99
+ const id = nextBacklogId(items);
100
+ const date = new Date().toISOString().slice(0, 10);
101
+
102
+ items.push({ id, title: title.replace(/^['"]|['"]$/g, ""), done: false, note: `added ${date}` });
103
+ writeBacklog(basePath, items);
104
+
105
+ ctx.ui.notify(`Added ${id}: "${title}"`, "success");
106
+ }
107
+
108
+ async function promoteBacklogItem(
109
+ basePath: string,
110
+ itemId: string,
111
+ ctx: ExtensionCommandContext,
112
+ pi: ExtensionAPI,
113
+ ): Promise<void> {
114
+ if (!itemId) {
115
+ ctx.ui.notify("Usage: /gsd backlog promote <id>\nExample: /gsd backlog promote 999.1", "warning");
116
+ return;
117
+ }
118
+
119
+ const items = parseBacklog(basePath);
120
+ const item = items.find((i) => i.id === itemId);
121
+
122
+ if (!item) {
123
+ ctx.ui.notify(`Backlog item ${itemId} not found.`, "warning");
124
+ return;
125
+ }
126
+
127
+ if (item.done) {
128
+ ctx.ui.notify(`${itemId} is already promoted/done.`, "info");
129
+ return;
130
+ }
131
+
132
+ // Promote — currently requires single-writer engine (not yet available)
133
+ // Mark as promoted in backlog for now; slice creation will be available with the engine.
134
+ item.done = true;
135
+ item.note = `promoted ${new Date().toISOString().slice(0, 10)}`;
136
+ writeBacklog(basePath, items);
137
+ ctx.ui.notify(`Promoted ${itemId}: "${item.title}" — add it to the roadmap manually or wait for engine slice commands.`, "info");
138
+ }
139
+
140
+ async function removeBacklogItem(basePath: string, itemId: string, ctx: ExtensionCommandContext): Promise<void> {
141
+ if (!itemId) {
142
+ ctx.ui.notify("Usage: /gsd backlog remove <id>", "warning");
143
+ return;
144
+ }
145
+
146
+ const items = parseBacklog(basePath);
147
+ const idx = items.findIndex((i) => i.id === itemId);
148
+
149
+ if (idx === -1) {
150
+ ctx.ui.notify(`Backlog item ${itemId} not found.`, "warning");
151
+ return;
152
+ }
153
+
154
+ const removed = items.splice(idx, 1)[0];
155
+ writeBacklog(basePath, items);
156
+ ctx.ui.notify(`Removed ${removed.id}: "${removed.title}"`, "success");
157
+ }
158
+
159
+ export async function handleBacklog(
160
+ args: string,
161
+ ctx: ExtensionCommandContext,
162
+ pi: ExtensionAPI,
163
+ ): Promise<void> {
164
+ const basePath = process.cwd();
165
+ const parts = args.trim().split(/\s+/);
166
+ const sub = parts[0] ?? "";
167
+ const rest = parts.slice(1).join(" ");
168
+
169
+ switch (sub) {
170
+ case "":
171
+ return listBacklog(basePath, ctx);
172
+ case "add":
173
+ return addBacklogItem(basePath, rest, ctx);
174
+ case "promote":
175
+ return promoteBacklogItem(basePath, rest.trim(), ctx, pi);
176
+ case "remove":
177
+ return removeBacklogItem(basePath, rest.trim(), ctx);
178
+ default:
179
+ // Treat as implicit add
180
+ return addBacklogItem(basePath, args, ctx);
181
+ }
182
+ }