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
package/dist/cli.js CHANGED
@@ -141,6 +141,91 @@ if (cliFlags.messages[0] === 'update') {
141
141
  await runUpdate();
142
142
  process.exit(0);
143
143
  }
144
+ // ---------------------------------------------------------------------------
145
+ // Graph subcommand — `gsd graph build|status|query|diff`
146
+ // ---------------------------------------------------------------------------
147
+ if (cliFlags.messages[0] === 'graph') {
148
+ const sub = cliFlags.messages[1];
149
+ const { buildGraph, writeGraph, graphStatus, graphQuery, graphDiff, resolveGsdRoot } = await import('@gsd-build/mcp-server');
150
+ const projectDir = process.cwd();
151
+ const gsdRoot = resolveGsdRoot(projectDir);
152
+ if (!sub || sub === 'build') {
153
+ try {
154
+ const graph = await buildGraph(projectDir);
155
+ await writeGraph(gsdRoot, graph);
156
+ process.stdout.write(`Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges\n`);
157
+ }
158
+ catch (err) {
159
+ process.stderr.write(`[gsd] graph build failed: ${err instanceof Error ? err.message : String(err)}\n`);
160
+ process.exit(1);
161
+ }
162
+ }
163
+ else if (sub === 'status') {
164
+ try {
165
+ const result = await graphStatus(projectDir);
166
+ if (!result.exists) {
167
+ process.stdout.write('Graph: not built yet. Run: gsd graph build\n');
168
+ }
169
+ else {
170
+ process.stdout.write(`Graph status:\n`);
171
+ process.stdout.write(` exists: ${result.exists}\n`);
172
+ process.stdout.write(` nodes: ${result.nodeCount}\n`);
173
+ process.stdout.write(` edges: ${result.edgeCount}\n`);
174
+ process.stdout.write(` stale: ${result.stale}\n`);
175
+ process.stdout.write(` ageHours: ${result.ageHours !== undefined ? result.ageHours.toFixed(2) : 'n/a'}\n`);
176
+ process.stdout.write(` lastBuild: ${result.lastBuild ?? 'n/a'}\n`);
177
+ }
178
+ }
179
+ catch (err) {
180
+ process.stderr.write(`[gsd] graph status failed: ${err instanceof Error ? err.message : String(err)}\n`);
181
+ process.exit(1);
182
+ }
183
+ }
184
+ else if (sub === 'query') {
185
+ const term = cliFlags.messages[2];
186
+ if (!term) {
187
+ process.stderr.write('Usage: gsd graph query <term>\n');
188
+ process.exit(1);
189
+ }
190
+ try {
191
+ const result = await graphQuery(projectDir, term);
192
+ if (result.nodes.length === 0) {
193
+ process.stdout.write(`No nodes found for term: "${term}"\n`);
194
+ }
195
+ else {
196
+ process.stdout.write(`Query results for "${term}" (${result.nodes.length} nodes, ${result.edges.length} edges):\n`);
197
+ for (const node of result.nodes) {
198
+ process.stdout.write(` [${node.type}] ${node.label} (${node.confidence})\n`);
199
+ }
200
+ }
201
+ }
202
+ catch (err) {
203
+ process.stderr.write(`[gsd] graph query failed: ${err instanceof Error ? err.message : String(err)}\n`);
204
+ process.exit(1);
205
+ }
206
+ }
207
+ else if (sub === 'diff') {
208
+ try {
209
+ const result = await graphDiff(projectDir);
210
+ process.stdout.write(`Graph diff:\n`);
211
+ process.stdout.write(` nodes added: ${result.nodes.added.length}\n`);
212
+ process.stdout.write(` nodes removed: ${result.nodes.removed.length}\n`);
213
+ process.stdout.write(` nodes changed: ${result.nodes.changed.length}\n`);
214
+ process.stdout.write(` edges added: ${result.edges.added.length}\n`);
215
+ process.stdout.write(` edges removed: ${result.edges.removed.length}\n`);
216
+ }
217
+ catch (err) {
218
+ process.stderr.write(`[gsd] graph diff failed: ${err instanceof Error ? err.message : String(err)}\n`);
219
+ process.exit(1);
220
+ }
221
+ }
222
+ else {
223
+ process.stderr.write(`Unknown graph command: ${sub}\n`);
224
+ process.stderr.write('Commands: build, status, query <term>, diff\n');
225
+ process.exit(1);
226
+ }
227
+ process.exit(0);
228
+ }
144
229
  exitIfManagedResourcesAreNewer(agentDir);
145
230
  // Early TTY check — must come before heavy initialization to avoid dangling
146
231
  // handles that prevent process.exit() from completing promptly.
@@ -33,7 +33,9 @@ async function loadExtensionModules() {
33
33
  const dispatchModule = await jiti.import(gsdExtensionPath('auto-dispatch.ts'), {});
34
34
  const sessionModule = await jiti.import(gsdExtensionPath('session-status-io.ts'), {});
35
35
  const prefsModule = await jiti.import(gsdExtensionPath('preferences.ts'), {});
36
+ const autoStartModule = await jiti.import(gsdExtensionPath('auto-start.ts'), {});
36
37
  return {
38
+ openProjectDbIfPresent: autoStartModule.openProjectDbIfPresent,
37
39
  deriveState: stateModule.deriveState,
38
40
  resolveDispatch: dispatchModule.resolveDispatch,
39
41
  readAllSessionStatuses: sessionModule.readAllSessionStatuses,
@@ -42,7 +44,8 @@ async function loadExtensionModules() {
42
44
  }
43
45
  // ─── Implementation ─────────────────────────────────────────────────────────
44
46
  export async function handleQuery(basePath) {
45
- const { deriveState, resolveDispatch, readAllSessionStatuses, loadEffectiveGSDPreferences } = await loadExtensionModules();
47
+ const { openProjectDbIfPresent, deriveState, resolveDispatch, readAllSessionStatuses, loadEffectiveGSDPreferences, } = await loadExtensionModules();
48
+ await openProjectDbIfPresent(basePath);
46
49
  const state = await deriveState(basePath);
47
50
  // Derive next dispatch action
48
51
  let next;
package/dist/help-text.js CHANGED
@@ -84,6 +84,28 @@ const SUBCOMMAND_HELP = {
84
84
  ' gsd worktree remove old-branch Remove a specific worktree',
85
85
  ' gsd worktree remove old-branch --force Remove even with unmerged changes',
86
86
  ].join('\n'),
87
+ graph: [
88
+ 'Usage: gsd graph <subcommand> [options]',
89
+ '',
90
+ 'Manage the GSD project knowledge graph. Reads .gsd/ artifacts and builds',
91
+ 'a queryable graph of milestones, slices, tasks, rules, patterns, and lessons.',
92
+ '',
93
+ 'Subcommands:',
94
+ ' build Parse .gsd/ artifacts (STATE.md, milestone ROADMAPs, slice PLANs,',
95
+ ' KNOWLEDGE.md) and write .gsd/graphs/graph.json atomically.',
96
+ ' query Search graph nodes by term (BFS from seed matches, budget-trimmed).',
97
+ ' Returns matching nodes and reachable edges within the token budget.',
98
+ ' status Show whether graph.json exists, its age, node/edge counts, and',
99
+ ' whether it is stale (built more than 24 hours ago).',
100
+ ' diff Compare current graph.json with .last-build-snapshot.json.',
101
+ ' Returns added, removed, and changed nodes and edges.',
102
+ '',
103
+ 'Examples:',
104
+ ' gsd graph build Build the graph from .gsd/ artifacts',
105
+ ' gsd graph status Check graph age and node/edge counts',
106
+ ' gsd graph query auth Find nodes related to "auth"',
107
+ ' gsd graph diff Show changes since last snapshot',
108
+ ].join('\n'),
87
109
  headless: [
88
110
  'Usage: gsd headless [flags] [command] [args...]',
89
111
  '',
@@ -165,6 +187,7 @@ export function printHelp(version) {
165
187
  process.stdout.write(' worktree <cmd> Manage worktrees (list, merge, clean, remove)\n');
166
188
  process.stdout.write(' auto [args] Run auto-mode without TUI (pipeable)\n');
167
189
  process.stdout.write(' headless [cmd] [args] Run /gsd commands without TUI (default: auto)\n');
190
+ process.stdout.write(' graph <subcommand> Manage knowledge graph (build, query, status, diff)\n');
168
191
  process.stdout.write('\nRun gsd <subcommand> --help for subcommand-specific help.\n');
169
192
  }
170
193
  export function printSubcommandHelp(subcommand, version) {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Leaf node in the import DAG.
5
5
  */
6
+ import { summarizeLogs } from "../workflow-logger.js";
6
7
  /**
7
8
  * Pattern matching ENOENT errors with a file path.
8
9
  * Matches: "ENOENT: no such file or directory, access '/path/to/file'"
@@ -22,13 +23,19 @@ const ENOENT_PATH_RE = /ENOENT[^']*'([^']+)'/;
22
23
  export function detectStuck(window) {
23
24
  if (window.length < 2)
24
25
  return null;
26
+ // Peek (not drain) the workflow-logger buffer so stuck reasons can surface
27
+ // the underlying diagnostic context (projection failures, DB degradations,
28
+ // reconcile warnings) that usually explains *why* the loop is stuck. The
29
+ // auto-loop's finalize step owns the buffer lifecycle — this is read-only.
30
+ const loggerSummary = summarizeLogs();
31
+ const suffix = loggerSummary ? ` — ${loggerSummary}` : "";
25
32
  const last = window[window.length - 1];
26
33
  const prev = window[window.length - 2];
27
34
  // Rule 1: Same error repeated consecutively
28
35
  if (last.error && prev.error && last.error === prev.error) {
29
36
  return {
30
37
  stuck: true,
31
- reason: `Same error repeated: ${last.error.slice(0, 200)}`,
38
+ reason: `Same error repeated: ${last.error.slice(0, 200)}${suffix}`,
32
39
  };
33
40
  }
34
41
  // Rule 2: Same unit 3+ consecutive times
@@ -37,7 +44,7 @@ export function detectStuck(window) {
37
44
  if (lastThree.every((u) => u.key === last.key)) {
38
45
  return {
39
46
  stuck: true,
40
- reason: `${last.key} derived 3 consecutive times without progress`,
47
+ reason: `${last.key} derived 3 consecutive times without progress${suffix}`,
41
48
  };
42
49
  }
43
50
  }
@@ -49,7 +56,7 @@ export function detectStuck(window) {
49
56
  w[0].key !== w[1].key) {
50
57
  return {
51
58
  stuck: true,
52
- reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
59
+ reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}${suffix}`,
53
60
  };
54
61
  }
55
62
  }
@@ -67,7 +74,7 @@ export function detectStuck(window) {
67
74
  if (count >= 2) {
68
75
  return {
69
76
  stuck: true,
70
- reason: `Missing file referenced twice: ${filePath} (ENOENT)`,
77
+ reason: `Missing file referenced twice: ${filePath} (ENOENT)${suffix}`,
71
78
  };
72
79
  }
73
80
  enoentPaths.set(filePath, count);
@@ -16,7 +16,7 @@ import { MergeConflictError } from "../git-service.js";
16
16
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
17
17
  import { join, basename, dirname, parse as parsePath } from "node:path";
18
18
  import { existsSync, cpSync, readdirSync } from "node:fs";
19
- import { logWarning, logError } from "../workflow-logger.js";
19
+ import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
20
20
  import { gsdRoot } from "../paths.js";
21
21
  import { atomicWriteSync } from "../atomic-write.js";
22
22
  import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
@@ -100,6 +100,22 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
100
100
  }
101
101
  await deps.stopAuto(ctx, pi, reason);
102
102
  }
103
+ async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
104
+ ic.deps.emitJournalEvent({
105
+ ts: new Date().toISOString(),
106
+ flowId: ic.flowId,
107
+ seq: ic.nextSeq(),
108
+ eventType: "unit-end",
109
+ data: {
110
+ unitType,
111
+ unitId,
112
+ status: "cancelled",
113
+ artifactVerified: false,
114
+ ...(errorContext ? { errorContext } : {}),
115
+ },
116
+ causedBy: { flowId: ic.flowId, seq: unitStartSeq },
117
+ });
118
+ }
103
119
  // ─── runPreDispatch ───────────────────────────────────────────────────────────
104
120
  /**
105
121
  * Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
@@ -776,6 +792,10 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
776
792
  s.currentUnit.type === unitType &&
777
793
  s.currentUnit.id === unitId);
778
794
  const previousTier = s.currentUnitRouting?.tier;
795
+ // Scope workflow-logger buffer to this unit so post-finalize drains are
796
+ // per-unit. Without this, the module-level _buffer accumulates across every
797
+ // unit in the same Node process (see workflow-logger.ts module header).
798
+ _resetLogs();
779
799
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
780
800
  setCurrentPhase(unitType);
781
801
  s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
@@ -975,6 +995,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
975
995
  // Provider-error pause: pauseAuto already handled cleanup and scheduled
976
996
  // recovery. Don't hard-stop — just break out of the loop (#2762).
977
997
  if (unitResult.errorContext?.category === "provider") {
998
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
978
999
  debugLog("autoLoop", { phase: "exit", reason: "provider-pause", isTransient: unitResult.errorContext.isTransient });
979
1000
  return { action: "break", reason: "provider-pause" };
980
1001
  }
@@ -988,9 +1009,16 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
988
1009
  ctx.ui.notify(`Session creation timed out for ${unitType} ${unitId}. Pausing auto-mode (recoverable).`, "warning");
989
1010
  debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId });
990
1011
  await deps.pauseAuto(ctx, pi);
1012
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1013
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
991
1014
  return { action: "break", reason: "session-timeout" };
992
1015
  }
993
1016
  // All other cancelled states (structural errors, non-transient failures): hard stop
1017
+ if (s.currentUnit) {
1018
+ await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
1019
+ }
1020
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1021
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
994
1022
  ctx.ui.notify(`Session creation failed for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`, "warning");
995
1023
  await deps.stopAuto(ctx, pi, `Session creation failed: ${unitResult.errorContext?.message ?? "unknown"}`);
996
1024
  debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
@@ -1124,6 +1152,9 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1124
1152
  // cannot mutate state for the next unit (#3757).
1125
1153
  s.currentUnit = null;
1126
1154
  clearCurrentPhase();
1155
+ // Drop any logger entries from the timed-out unit so they don't bleed
1156
+ // into the next iteration's drain.
1157
+ drainLogs();
1127
1158
  loopState.consecutiveFinalizeTimeouts++;
1128
1159
  debugLog("autoLoop", {
1129
1160
  phase: "pre-verification-timeout",
@@ -1199,6 +1230,9 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1199
1230
  // cannot mutate state for the next unit (#3757).
1200
1231
  s.currentUnit = null;
1201
1232
  clearCurrentPhase();
1233
+ // Drop any logger entries from the timed-out unit so they don't bleed
1234
+ // into the next iteration's drain.
1235
+ drainLogs();
1202
1236
  loopState.consecutiveFinalizeTimeouts++;
1203
1237
  debugLog("autoLoop", {
1204
1238
  phase: "post-verification-timeout",
@@ -1230,5 +1264,15 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1230
1264
  }
1231
1265
  // Both pre and post verification completed without timeout — reset counter
1232
1266
  loopState.consecutiveFinalizeTimeouts = 0;
1267
+ // Surface accumulated workflow-logger issues for this unit to the user.
1268
+ // Warnings/errors logged during the unit are buffered in the logger and
1269
+ // drained here so the user sees a single consolidated post-unit alert.
1270
+ if (hasAnyIssues()) {
1271
+ const { logs } = drainAndSummarize();
1272
+ if (logs.length > 0) {
1273
+ const severity = logs.some((e) => e.severity === "error") ? "error" : "warning";
1274
+ ctx.ui.notify(formatForNotification(logs), severity);
1275
+ }
1276
+ }
1233
1277
  return { action: "next", data: undefined };
1234
1278
  }
@@ -189,6 +189,57 @@ export function buildStepCompleteMessage(nextState) {
189
189
  return `Step complete. Next: ${next.label}\n`
190
190
  + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
191
191
  }
192
+ export async function autoCommitUnit(basePath, unitType, unitId, ctx) {
193
+ try {
194
+ let taskContext;
195
+ if (unitType === "execute-task") {
196
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
197
+ if (mid && sid && tid) {
198
+ const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
199
+ if (summaryPath) {
200
+ try {
201
+ const summaryContent = await loadFile(summaryPath);
202
+ if (summaryContent) {
203
+ const summary = parseSummary(summaryContent);
204
+ let ghIssueNumber;
205
+ try {
206
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
207
+ ghIssueNumber = getTaskIssueNumberForCommit(basePath, mid, sid, tid) ?? undefined;
208
+ }
209
+ catch (err) {
210
+ logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
211
+ }
212
+ taskContext = {
213
+ taskId: `${sid}/${tid}`,
214
+ taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
215
+ oneLiner: summary.oneLiner || undefined,
216
+ keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
217
+ issueNumber: ghIssueNumber,
218
+ };
219
+ }
220
+ }
221
+ catch (e) {
222
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ _resetHasChangesCache();
228
+ if (LIFECYCLE_ONLY_UNITS.has(unitType)) {
229
+ return null;
230
+ }
231
+ const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId, taskContext);
232
+ if (commitMsg) {
233
+ ctx?.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
234
+ }
235
+ return commitMsg;
236
+ }
237
+ catch (e) {
238
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
239
+ ctx?.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
240
+ return null;
241
+ }
242
+ }
192
243
  /**
193
244
  * Pre-verification processing: parallel worker signal check, cache invalidation,
194
245
  * auto-commit, doctor run, state rebuild, worktree sync, artifact verification.
@@ -224,62 +275,7 @@ export async function postUnitPreVerification(pctx, opts) {
224
275
  // Auto-commit
225
276
  if (s.currentUnit) {
226
277
  const unit = s.currentUnit;
227
- try {
228
- let taskContext;
229
- if (s.currentUnit.type === "execute-task") {
230
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
231
- if (mid && sid && tid) {
232
- const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
233
- if (summaryPath) {
234
- try {
235
- const summaryContent = await loadFile(summaryPath);
236
- if (summaryContent) {
237
- const summary = parseSummary(summaryContent);
238
- // Look up GitHub issue number for commit linking
239
- let ghIssueNumber;
240
- try {
241
- const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
242
- ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
243
- }
244
- catch (err) {
245
- // GitHub sync not available — skip
246
- logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
247
- }
248
- taskContext = {
249
- taskId: `${sid}/${tid}`,
250
- taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
251
- oneLiner: summary.oneLiner || undefined,
252
- keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
253
- issueNumber: ghIssueNumber,
254
- };
255
- }
256
- }
257
- catch (e) {
258
- debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
259
- }
260
- }
261
- }
262
- }
263
- // Invalidate the nativeHasChanges cache before auto-commit (#1853).
264
- // The cache has a 10-second TTL and is keyed by basePath. A stale
265
- // `false` result causes autoCommit to skip staging entirely, leaving
266
- // code files only in the working tree where they are destroyed by
267
- // `git worktree remove --force` during teardown.
268
- _resetHasChangesCache();
269
- // Skip auto-commit for lifecycle-only units (#2553) — they only touch
270
- // `.gsd/` internal state files. Those files are picked up by the next
271
- // actual task commit via smartStage().
272
- if (!LIFECYCLE_ONLY_UNITS.has(s.currentUnit.type)) {
273
- const commitMsg = autoCommitCurrentBranch(s.basePath, s.currentUnit.type, s.currentUnit.id, taskContext);
274
- if (commitMsg) {
275
- ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
276
- }
277
- }
278
- }
279
- catch (e) {
280
- debugLog("postUnit", { phase: "auto-commit", error: String(e) });
281
- ctx.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
282
- }
278
+ await autoCommitUnit(s.basePath, unit.type, unit.id, ctx);
283
279
  // GitHub sync (non-blocking, opt-in)
284
280
  await runSafely("postUnit", "github-sync", async () => {
285
281
  const { runGitHubSync } = await import("../github-sync/sync.js");
@@ -20,6 +20,7 @@ import { assertGateCoverage, getGatesForTurn, } from "./gate-registry.js";
20
20
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
21
21
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
22
22
  import { logWarning } from "./workflow-logger.js";
23
+ import { inlineGraphSubgraph } from "./graph-context.js";
23
24
  // ─── Preamble Cap ─────────────────────────────────────────────────────────────
24
25
  const MAX_PREAMBLE_CHARS = 30_000;
25
26
  function capPreamble(preamble) {
@@ -1040,6 +1041,10 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
1040
1041
  const knowledgeInlineRS = await inlineKnowledgeScoped(base, keywords);
1041
1042
  if (knowledgeInlineRS)
1042
1043
  inlined.push(knowledgeInlineRS);
1044
+ // Knowledge graph: subgraph for this slice (graceful — skipped if no graph.json)
1045
+ const graphBlockRS = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { budget: 3000 });
1046
+ if (graphBlockRS)
1047
+ inlined.push(graphBlockRS);
1043
1048
  inlined.push(inlineTemplate("research", "Research"));
1044
1049
  const depContent = await inlineDependencySummaries(mid, sid, base);
1045
1050
  const activeOverrides = await loadActiveOverrides(base);
@@ -1111,6 +1116,10 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
1111
1116
  const knowledgeInlinePS = await inlineKnowledgeScoped(base, keywordsPS);
1112
1117
  if (knowledgeInlinePS)
1113
1118
  inlined.push(knowledgeInlinePS);
1119
+ // Knowledge graph: subgraph for this slice (graceful — skipped if no graph.json)
1120
+ const graphBlockPS = await inlineGraphSubgraph(base, `${sid} ${sTitle}`, { budget: 3000 });
1121
+ if (graphBlockPS)
1122
+ inlined.push(graphBlockPS);
1114
1123
  inlined.push(inlineTemplate("plan", "Slice Plan"));
1115
1124
  if (inlineLevel === "full") {
1116
1125
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
@@ -1194,12 +1203,15 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
1194
1203
  : null;
1195
1204
  // Only include if it has content (not a "not found" result)
1196
1205
  const knowledgeContent = knowledgeInlineET && !knowledgeInlineET.includes("not found") ? knowledgeInlineET : null;
1206
+ // Knowledge graph: tight subgraph for this task (graceful — skipped if no graph.json)
1207
+ const graphBlockET = await inlineGraphSubgraph(base, `${tid} ${tTitle}`, { budget: 2000 });
1197
1208
  const inlinedTemplates = inlineLevel === "minimal"
1198
1209
  ? inlineTemplate("task-summary", "Task Summary")
1199
1210
  : [
1200
1211
  inlineTemplate("task-summary", "Task Summary"),
1201
1212
  inlineTemplate("decisions", "Decisions"),
1202
1213
  ...(knowledgeContent ? [knowledgeContent] : []),
1214
+ ...(graphBlockET ? [graphBlockET] : []),
1203
1215
  ].join("\n\n---\n\n");
1204
1216
  const taskSummaryPath = join(base, `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`);
1205
1217
  const activeOverrides = await loadActiveOverrides(base);
@@ -36,7 +36,7 @@ import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.
36
36
  import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
37
37
  import { deactivateGSD } from "../shared/gsd-phase-state.js";
38
38
  import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
39
- import { logWarning } from "./workflow-logger.js";
39
+ import { setLogBasePath, logWarning } from "./workflow-logger.js";
40
40
  import { homedir } from "node:os";
41
41
  import { join } from "node:path";
42
42
  import { pathToFileURL } from "node:url";
@@ -61,7 +61,7 @@ import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.j
61
61
  // ── Extracted modules ──────────────────────────────────────────────────────
62
62
  import { startUnitSupervision } from "./auto-timers.js";
63
63
  import { runPostUnitVerification } from "./auto-verification.js";
64
- import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
64
+ import { autoCommitUnit, postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
65
65
  import { bootstrapAutoSession, openProjectDbIfPresent } from "./auto-start.js";
66
66
  import { initHealthWidget } from "./health-widget.js";
67
67
  import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight } from "./auto-loop.js";
@@ -884,6 +884,7 @@ function buildLoopDeps() {
884
884
  getMainBranch,
885
885
  // Unit closeout + runtime records
886
886
  closeoutUnit,
887
+ autoCommitUnit,
887
888
  recordOutcome,
888
889
  writeLock,
889
890
  captureAvailableSkills,
@@ -1081,6 +1082,11 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1081
1082
  s.stepMode = requestedStepMode;
1082
1083
  s.cmdCtx = ctx;
1083
1084
  s.basePath = base;
1085
+ // Ensure the workflow-logger audit log is pinned to the project root
1086
+ // even when auto-mode is entered via a path that bypasses the
1087
+ // bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
1088
+ // (e.g. /clear resume, hot-reload).
1089
+ setLogBasePath(base);
1084
1090
  s.unitDispatchCount.clear();
1085
1091
  s.unitLifetimeDispatches.clear();
1086
1092
  if (!getLedger())
@@ -1,5 +1,4 @@
1
1
  // GSD2 — Extension registration: wires all GSD tools, commands, and hooks into pi
2
- import { registerGSDCommand } from "../commands.js";
3
2
  import { registerExitCommand } from "../exit-command.js";
4
3
  import { registerWorktreeCommand } from "../worktree-command.js";
5
4
  import { registerDbTools } from "./db-tools.js";
@@ -9,6 +8,7 @@ import { registerQueryTools } from "./query-tools.js";
9
8
  import { registerHooks } from "./register-hooks.js";
10
9
  import { registerShortcuts } from "./register-shortcuts.js";
11
10
  import { writeCrashLog } from "./crash-log.js";
11
+ import { logWarning } from "../workflow-logger.js";
12
12
  export { writeCrashLog } from "./crash-log.js";
13
13
  export function handleRecoverableExtensionProcessError(err) {
14
14
  if (err.code === "EPIPE") {
@@ -52,7 +52,8 @@ function installEpipeGuard() {
52
52
  }
53
53
  }
54
54
  export function registerGsdExtension(pi) {
55
- registerGSDCommand(pi);
55
+ // Note: registerGSDCommand is called by index.ts before this function,
56
+ // so we intentionally skip it here to avoid double-registration.
56
57
  registerWorktreeCommand(pi);
57
58
  registerExitCommand(pi);
58
59
  installEpipeGuard();
@@ -62,10 +63,22 @@ export function registerGsdExtension(pi) {
62
63
  process.exit(0);
63
64
  },
64
65
  });
65
- registerDynamicTools(pi);
66
- registerDbTools(pi);
67
- registerJournalTools(pi);
68
- registerQueryTools(pi);
69
- registerShortcuts(pi);
70
- registerHooks(pi);
66
+ // Wrap non-critical registrations individually so one failure
67
+ // doesn't prevent the others from loading.
68
+ const nonCriticalRegistrations = [
69
+ ["dynamic-tools", () => registerDynamicTools(pi)],
70
+ ["db-tools", () => registerDbTools(pi)],
71
+ ["journal-tools", () => registerJournalTools(pi)],
72
+ ["query-tools", () => registerQueryTools(pi)],
73
+ ["shortcuts", () => registerShortcuts(pi)],
74
+ ["hooks", () => registerHooks(pi)],
75
+ ];
76
+ for (const [name, register] of nonCriticalRegistrations) {
77
+ try {
78
+ register();
79
+ }
80
+ catch (err) {
81
+ logWarning("bootstrap", `Failed to register ${name}: ${err instanceof Error ? err.message : String(err)}`);
82
+ }
83
+ }
71
84
  }
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { loadRegistry } from "../workflow-templates.js";
5
5
  import { resolveProjectRoot } from "../worktree.js";
6
6
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
7
- export const GSD_COMMAND_DESCRIPTION = "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";
7
+ export const GSD_COMMAND_DESCRIPTION = "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";
8
8
  export const TOP_LEVEL_SUBCOMMANDS = [
9
9
  { cmd: "help", desc: "Categorized command reference with descriptions" },
10
10
  { cmd: "next", desc: "Explicit step mode (same as /gsd)" },
@@ -62,6 +62,12 @@ export const TOP_LEVEL_SUBCOMMANDS = [
62
62
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
63
63
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
64
64
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
65
+ { cmd: "ship", desc: "Create PR from milestone artifacts and open for review" },
66
+ { cmd: "do", desc: "Route freeform text to the right GSD command" },
67
+ { cmd: "session-report", desc: "Session cost, tokens, and work summary" },
68
+ { cmd: "backlog", desc: "Manage backlog items (add, promote, remove, list)" },
69
+ { cmd: "pr-branch", desc: "Create clean PR branch filtering .gsd/ commits" },
70
+ { cmd: "add-tests", desc: "Generate tests for completed slices" },
65
71
  ];
66
72
  const NESTED_COMPLETIONS = {
67
73
  auto: [
@@ -231,6 +237,25 @@ const NESTED_COMPLETIONS = {
231
237
  { cmd: "stats", desc: "Show file count, description coverage, and generation time" },
232
238
  { cmd: "help", desc: "Show usage and available subcommands" },
233
239
  ],
240
+ ship: [
241
+ { cmd: "--dry-run", desc: "Preview PR without creating" },
242
+ { cmd: "--draft", desc: "Open as draft PR" },
243
+ { cmd: "--base", desc: "Override target branch (default: main)" },
244
+ { cmd: "--force", desc: "Ship even with pending tasks" },
245
+ ],
246
+ "session-report": [
247
+ { cmd: "--json", desc: "Machine-readable JSON output" },
248
+ { cmd: "--save", desc: "Save report to .gsd/reports/" },
249
+ ],
250
+ backlog: [
251
+ { cmd: "add", desc: "Add item to backlog" },
252
+ { cmd: "promote", desc: "Promote backlog item to active slice" },
253
+ { cmd: "remove", desc: "Remove backlog item" },
254
+ ],
255
+ "pr-branch": [
256
+ { cmd: "--dry-run", desc: "Preview what would be filtered" },
257
+ { cmd: "--name", desc: "Custom branch name" },
258
+ ],
234
259
  };
235
260
  function filterOptions(partial, options, prefix = "") {
236
261
  const normalizedPrefix = prefix ? `${prefix} ` : "";
@@ -8,6 +8,9 @@ import { handleExport } from "../../export.js";
8
8
  import { handleHistory } from "../../history.js";
9
9
  import { handleUndo } from "../../undo.js";
10
10
  import { handleRemote } from "../../../remote-questions/mod.js";
11
+ import { handleShip } from "../../commands-ship.js";
12
+ import { handleSessionReport } from "../../commands-session-report.js";
13
+ import { handlePrBranch } from "../../commands-pr-branch.js";
11
14
  import { projectRoot } from "../context.js";
12
15
  export async function handleOpsCommand(trimmed, ctx, pi) {
13
16
  if (trimmed === "init") {
@@ -213,5 +216,22 @@ Examples:
213
216
  await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi);
214
217
  return true;
215
218
  }
219
+ if (trimmed === "ship" || trimmed.startsWith("ship ")) {
220
+ await handleShip(trimmed.replace(/^ship\s*/, "").trim(), ctx, pi);
221
+ return true;
222
+ }
223
+ if (trimmed === "session-report" || trimmed.startsWith("session-report ")) {
224
+ await handleSessionReport(trimmed.replace(/^session-report\s*/, "").trim(), ctx);
225
+ return true;
226
+ }
227
+ if (trimmed === "pr-branch" || trimmed.startsWith("pr-branch ")) {
228
+ await handlePrBranch(trimmed.replace(/^pr-branch\s*/, "").trim(), ctx);
229
+ return true;
230
+ }
231
+ if (trimmed === "add-tests" || trimmed.startsWith("add-tests ")) {
232
+ const { handleAddTests } = await import("../../commands-add-tests.js");
233
+ await handleAddTests(trimmed.replace(/^add-tests\s*/, "").trim(), ctx, pi);
234
+ return true;
235
+ }
216
236
  return false;
217
237
  }