gsd-pi 2.59.0-dev.d77b3dd → 2.60.0-dev.2580e65

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 (198) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +7 -4
  2. package/dist/resources/extensions/gsd/auto/phases.js +15 -7
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +21 -8
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +6 -3
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +58 -9
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +3 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.js +36 -20
  8. package/dist/resources/extensions/gsd/auto-recovery.js +37 -18
  9. package/dist/resources/extensions/gsd/auto-start.js +9 -5
  10. package/dist/resources/extensions/gsd/auto-timers.js +11 -5
  11. package/dist/resources/extensions/gsd/auto-unit-closeout.js +5 -3
  12. package/dist/resources/extensions/gsd/auto-verification.js +3 -2
  13. package/dist/resources/extensions/gsd/auto-worktree.js +120 -55
  14. package/dist/resources/extensions/gsd/auto.js +39 -17
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -3
  16. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
  17. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +4 -10
  18. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +2 -1
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  20. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -10
  21. package/dist/resources/extensions/gsd/commands/catalog.js +2 -0
  22. package/dist/resources/extensions/gsd/commands-codebase.js +48 -21
  23. package/dist/resources/extensions/gsd/commands-inspect.js +2 -1
  24. package/dist/resources/extensions/gsd/commands-maintenance.js +32 -19
  25. package/dist/resources/extensions/gsd/complexity-classifier.js +8 -4
  26. package/dist/resources/extensions/gsd/custom-verification.js +3 -2
  27. package/dist/resources/extensions/gsd/gsd-db.js +33 -13
  28. package/dist/resources/extensions/gsd/guided-flow.js +19 -9
  29. package/dist/resources/extensions/gsd/init-wizard.js +12 -0
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +11 -9
  31. package/dist/resources/extensions/gsd/md-importer.js +5 -4
  32. package/dist/resources/extensions/gsd/milestone-actions.js +3 -2
  33. package/dist/resources/extensions/gsd/milestone-ids.js +2 -1
  34. package/dist/resources/extensions/gsd/model-router.js +156 -121
  35. package/dist/resources/extensions/gsd/parallel-merge.js +5 -3
  36. package/dist/resources/extensions/gsd/parallel-orchestrator.js +26 -14
  37. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  38. package/dist/resources/extensions/gsd/preferences-validation.js +45 -0
  39. package/dist/resources/extensions/gsd/preferences.js +15 -3
  40. package/dist/resources/extensions/gsd/prompt-loader.js +3 -2
  41. package/dist/resources/extensions/gsd/prompts/rethink.md +1 -1
  42. package/dist/resources/extensions/gsd/rule-registry.js +7 -6
  43. package/dist/resources/extensions/gsd/safe-fs.js +6 -8
  44. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  45. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -2
  46. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  47. package/dist/resources/extensions/gsd/tools/plan-milestone.js +3 -2
  48. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -2
  49. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -1
  50. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +4 -4
  51. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -1
  52. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -1
  53. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -1
  54. package/dist/resources/extensions/gsd/tools/validate-milestone.js +2 -1
  55. package/dist/resources/extensions/gsd/triage-resolution.js +11 -4
  56. package/dist/resources/extensions/gsd/workflow-events.js +2 -1
  57. package/dist/resources/extensions/gsd/workflow-logger.js +37 -4
  58. package/dist/resources/extensions/gsd/workflow-migration.js +14 -12
  59. package/dist/resources/extensions/gsd/workflow-projections.js +2 -2
  60. package/dist/resources/extensions/gsd/workflow-reconcile.js +2 -2
  61. package/dist/resources/extensions/gsd/worktree-manager.js +26 -14
  62. package/dist/resources/extensions/shared/interview-ui.js +3 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  68. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.html +1 -1
  84. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  91. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  92. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  93. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  94. package/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  97. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  99. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/extensions/runner.js +16 -0
  101. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +26 -0
  103. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/lsp/config.js +6 -1
  107. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/lsp/defaults.json +2 -2
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts +2 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js +47 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js.map +1 -0
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  115. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -0
  116. package/packages/pi-coding-agent/src/core/extensions/types.ts +26 -0
  117. package/packages/pi-coding-agent/src/core/lsp/config.ts +7 -1
  118. package/packages/pi-coding-agent/src/core/lsp/defaults.json +2 -2
  119. package/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +70 -0
  120. package/pkg/package.json +1 -1
  121. package/src/resources/extensions/ask-user-questions.ts +7 -3
  122. package/src/resources/extensions/gsd/auto/phases.ts +17 -7
  123. package/src/resources/extensions/gsd/auto-dashboard.ts +22 -8
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -3
  125. package/src/resources/extensions/gsd/auto-model-selection.ts +77 -15
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -4
  127. package/src/resources/extensions/gsd/auto-prompts.ts +37 -20
  128. package/src/resources/extensions/gsd/auto-recovery.ts +38 -18
  129. package/src/resources/extensions/gsd/auto-start.ts +10 -9
  130. package/src/resources/extensions/gsd/auto-timers.ts +12 -5
  131. package/src/resources/extensions/gsd/auto-unit-closeout.ts +6 -2
  132. package/src/resources/extensions/gsd/auto-verification.ts +3 -6
  133. package/src/resources/extensions/gsd/auto-worktree.ts +121 -55
  134. package/src/resources/extensions/gsd/auto.ts +40 -17
  135. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -3
  136. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
  137. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +4 -16
  138. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +2 -1
  139. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  140. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -10
  141. package/src/resources/extensions/gsd/commands/catalog.ts +2 -0
  142. package/src/resources/extensions/gsd/commands-codebase.ts +52 -20
  143. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  144. package/src/resources/extensions/gsd/commands-maintenance.ts +28 -19
  145. package/src/resources/extensions/gsd/complexity-classifier.ts +9 -4
  146. package/src/resources/extensions/gsd/custom-verification.ts +3 -2
  147. package/src/resources/extensions/gsd/gsd-db.ts +12 -14
  148. package/src/resources/extensions/gsd/guided-flow.ts +9 -8
  149. package/src/resources/extensions/gsd/init-wizard.ts +12 -0
  150. package/src/resources/extensions/gsd/markdown-renderer.ts +11 -17
  151. package/src/resources/extensions/gsd/md-importer.ts +5 -4
  152. package/src/resources/extensions/gsd/milestone-actions.ts +3 -2
  153. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  154. package/src/resources/extensions/gsd/model-router.ts +199 -173
  155. package/src/resources/extensions/gsd/parallel-merge.ts +5 -3
  156. package/src/resources/extensions/gsd/parallel-orchestrator.ts +18 -14
  157. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  158. package/src/resources/extensions/gsd/preferences-validation.ts +45 -0
  159. package/src/resources/extensions/gsd/preferences.ts +16 -3
  160. package/src/resources/extensions/gsd/prompt-loader.ts +3 -2
  161. package/src/resources/extensions/gsd/prompts/rethink.md +1 -1
  162. package/src/resources/extensions/gsd/rule-registry.ts +7 -6
  163. package/src/resources/extensions/gsd/safe-fs.ts +6 -5
  164. package/src/resources/extensions/gsd/tests/capability-router.test.ts +347 -0
  165. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +27 -2
  167. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  168. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +1188 -0
  169. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +841 -0
  170. package/src/resources/extensions/gsd/tests/model-router.test.ts +403 -3
  171. package/src/resources/extensions/gsd/tests/preferences.test.ts +62 -0
  172. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +21 -0
  173. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +284 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +120 -0
  175. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +6 -6
  176. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -6
  177. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -6
  178. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -6
  179. package/src/resources/extensions/gsd/tools/plan-milestone.ts +3 -6
  180. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -6
  181. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -3
  182. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +4 -6
  183. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -3
  184. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -3
  185. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -3
  186. package/src/resources/extensions/gsd/tools/validate-milestone.ts +2 -3
  187. package/src/resources/extensions/gsd/triage-resolution.ts +11 -4
  188. package/src/resources/extensions/gsd/types.ts +1 -0
  189. package/src/resources/extensions/gsd/workflow-events.ts +2 -1
  190. package/src/resources/extensions/gsd/workflow-logger.ts +52 -5
  191. package/src/resources/extensions/gsd/workflow-migration.ts +14 -12
  192. package/src/resources/extensions/gsd/workflow-projections.ts +2 -2
  193. package/src/resources/extensions/gsd/workflow-reconcile.ts +2 -2
  194. package/src/resources/extensions/gsd/worktree-manager.ts +16 -14
  195. package/src/resources/extensions/shared/interview-ui.ts +3 -1
  196. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +144 -0
  197. /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_buildManifest.js +0 -0
  198. /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_ssgManifest.js +0 -0
@@ -22,6 +22,7 @@ import { isClosedStatus } from "../status-guards.js";
22
22
  import { renderAllProjections } from "../workflow-projections.js";
23
23
  import { writeManifest } from "../workflow-manifest.js";
24
24
  import { appendEvent } from "../workflow-events.js";
25
+ import { logWarning } from "../workflow-logger.js";
25
26
 
26
27
  export interface ReopenTaskParams {
27
28
  milestoneId: string;
@@ -117,9 +118,7 @@ export async function handleReopenTask(
117
118
  trigger_reason: params.triggerReason,
118
119
  });
119
120
  } catch (hookErr) {
120
- process.stderr.write(
121
- `gsd: reopen-task post-mutation hook warning: ${(hookErr as Error).message}\n`,
122
- );
121
+ logWarning("tool", `reopen-task post-mutation hook warning: ${(hookErr as Error).message}`);
123
122
  }
124
123
 
125
124
  return {
@@ -16,6 +16,7 @@ import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
16
16
  import { renderAllProjections } from "../workflow-projections.js";
17
17
  import { writeManifest } from "../workflow-manifest.js";
18
18
  import { appendEvent } from "../workflow-events.js";
19
+ import { logWarning } from "../workflow-logger.js";
19
20
 
20
21
  export interface ReplanSliceTaskInput {
21
22
  taskId: string;
@@ -226,9 +227,7 @@ export async function handleReplanSlice(
226
227
  trigger_reason: params.triggerReason,
227
228
  });
228
229
  } catch (hookErr) {
229
- process.stderr.write(
230
- `gsd: replan-slice post-mutation hook warning: ${(hookErr as Error).message}\n`,
231
- );
230
+ logWarning("tool", `replan-slice post-mutation hook warning: ${(hookErr as Error).message}`);
232
231
  }
233
232
 
234
233
  return {
@@ -22,6 +22,7 @@ import { saveFile, clearParseCache } from "../files.js";
22
22
  import { invalidateStateCache } from "../state.js";
23
23
  import { VALIDATION_VERDICTS, isValidMilestoneVerdict } from "../verdict-parser.js";
24
24
  import { insertMilestoneValidationGates } from "../milestone-validation-gates.js";
25
+ import { logWarning } from "../workflow-logger.js";
25
26
 
26
27
  export interface ValidateMilestoneParams {
27
28
  milestoneId: string;
@@ -137,9 +138,7 @@ export async function handleValidateMilestone(
137
138
  try {
138
139
  await saveFile(validationPath, validationMd);
139
140
  } catch (renderErr) {
140
- process.stderr.write(
141
- `gsd-db: validate_milestone — disk render failed, rolling back DB row: ${(renderErr as Error).message}\n`,
142
- );
141
+ logWarning("tool", `validate_milestone — disk render failed, rolling back DB row: ${(renderErr as Error).message}`);
143
142
  deleteAssessmentByScope(params.milestoneId, 'milestone-validation');
144
143
  return { error: `disk render failed: ${(renderErr as Error).message}` };
145
144
  }
@@ -148,10 +148,17 @@ export function executeBacktrack(
148
148
  capture: CaptureEntry,
149
149
  ): string | null {
150
150
  try {
151
- // Extract target milestone from capture text or resolution
152
- const targetMatch = (capture.resolution ?? capture.text)
153
- .match(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/);
154
- const targetMilestoneId = targetMatch?.[1] ?? null;
151
+ // Extract target milestone from capture text or resolution.
152
+ // Filter out the current milestone ID to avoid picking it as the backtrack target
153
+ // when the text mentions both current and target milestones (e.g. "backtrack from M004 to M003").
154
+ const sourceText = capture.resolution ?? capture.text;
155
+ const allMatches = [...sourceText.matchAll(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/g)]
156
+ .map(m => m[1])
157
+ .filter(id => id !== currentMilestoneId);
158
+ // Reject ambiguous multi-target strings — if more than one distinct target remains,
159
+ // don't guess; let the user clarify.
160
+ const uniqueTargets = [...new Set(allMatches)];
161
+ const targetMilestoneId = uniqueTargets.length === 1 ? uniqueTargets[0] : null;
155
162
 
156
163
  const ts = new Date().toISOString();
157
164
  const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
@@ -316,6 +316,7 @@ export interface ClassificationResult {
316
316
  tier: ComplexityTier;
317
317
  reason: string;
318
318
  downgraded: boolean;
319
+ taskMetadata?: TaskMetadata;
319
320
  }
320
321
 
321
322
  export interface TaskMetadata {
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { atomicWriteSync } from "./atomic-write.js";
5
+ import { logWarning } from "./workflow-logger.js";
5
6
 
6
7
  // ─── Session ID ───────────────────────────────────────────────────────────
7
8
 
@@ -74,7 +75,7 @@ export function readEvents(logPath: string): WorkflowEvent[] {
74
75
  try {
75
76
  events.push(JSON.parse(line) as WorkflowEvent);
76
77
  } catch {
77
- process.stderr.write(`workflow-events: skipping corrupted event line: ${line.slice(0, 80)}\n`);
78
+ logWarning("event-log", `skipping corrupted event line (${line.length} bytes)`);
78
79
  }
79
80
  }
80
81
 
@@ -2,7 +2,9 @@
2
2
  // Centralized warning/error accumulator for the workflow engine pipeline.
3
3
  // Captures structured entries that the auto-loop can drain after each unit
4
4
  // to surface root causes for stuck loops, silent degradation, and blocked writes.
5
- // All entries are also persisted to .gsd/audit-log.jsonl for post-mortem analysis.
5
+ // Error-severity entries are persisted to .gsd/audit-log.jsonl (sanitized) for
6
+ // post-mortem analysis. Warnings are ephemeral (stderr + buffer only) to avoid
7
+ // log amplification from expected-control-flow catch paths.
6
8
  //
7
9
  // Stderr policy: every logWarning/logError call writes immediately to stderr
8
10
  // for terminal visibility. This is intentional — unlike debug-logger (which is
@@ -33,7 +35,20 @@ export type LogComponent =
33
35
  | "compaction" // Event compaction
34
36
  | "reconcile" // Worktree reconciliation
35
37
  | "db" // Database operations (gsd-db)
36
- | "dispatch"; // Auto-dispatch rule evaluation
38
+ | "dispatch" // Auto-dispatch rule evaluation
39
+ | "recovery" // Auto-recovery and timeout recovery
40
+ | "session" // Session lock and session state I/O
41
+ | "prompt" // Prompt construction and context injection
42
+ | "dashboard" // Auto-dashboard rendering
43
+ | "timer" // Auto-timers (idle watchdog, hard timeout)
44
+ | "worktree" // Worktree lifecycle (create, sync, merge)
45
+ | "command" // Slash command execution and maintenance
46
+ | "parallel" // Parallel orchestrator and merge
47
+ | "fs" // Safe filesystem operations
48
+ | "bootstrap" // Extension bootstrap (system-context, agent-end)
49
+ | "guided" // Guided flow (discuss, plan wizards)
50
+ | "registry" // Rule registry hook state
51
+ | "renderer"; // Markdown renderer and projections
37
52
 
38
53
  export interface LogEntry {
39
54
  ts: string;
@@ -230,15 +245,47 @@ function _push(
230
245
  _buffer.shift();
231
246
  }
232
247
 
233
- // Persist to .gsd/audit-log.jsonl so entries survive context resets
234
- if (_auditBasePath) {
248
+ // Persist errors to .gsd/audit-log.jsonl so they survive context resets.
249
+ // Only error-severity entries are persisted — warnings are ephemeral (stderr + buffer)
250
+ // to avoid log amplification from expected-control-flow catch paths.
251
+ if (_auditBasePath && severity === "error") {
235
252
  try {
236
253
  const auditDir = join(_auditBasePath, ".gsd");
237
254
  mkdirSync(auditDir, { recursive: true });
238
- appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(entry) + "\n", "utf-8");
255
+ const sanitized = _sanitizeForAudit(entry);
256
+ appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(sanitized) + "\n", "utf-8");
239
257
  } catch (auditErr) {
240
258
  // Best-effort — never let audit write failures bubble up
241
259
  process.stderr.write(`[gsd:audit] failed to persist log entry: ${(auditErr as Error).message}\n`);
242
260
  }
243
261
  }
244
262
  }
263
+
264
+ /**
265
+ * Sanitize a log entry before persisting to the audit JSONL file.
266
+ * Strips potentially sensitive context (raw paths, cwd, full error text)
267
+ * to avoid leaking local environment details into durable telemetry.
268
+ */
269
+ function _sanitizeForAudit(entry: LogEntry): LogEntry {
270
+ const sanitized: LogEntry = {
271
+ ts: entry.ts,
272
+ severity: entry.severity,
273
+ component: entry.component,
274
+ // Truncate message to avoid persisting oversized raw error dumps
275
+ message: entry.message.length > 200 ? entry.message.slice(0, 200) + "…[truncated]" : entry.message,
276
+ };
277
+ if (entry.context) {
278
+ // Allowlist: only persist known-safe structured keys
279
+ const SAFE_KEYS = new Set(["fn", "tool", "mid", "sid", "tid", "worktree"]);
280
+ const filtered: Record<string, string> = {};
281
+ for (const [k, v] of Object.entries(entry.context)) {
282
+ if (SAFE_KEYS.has(k)) {
283
+ filtered[k] = v;
284
+ }
285
+ }
286
+ if (Object.keys(filtered).length > 0) {
287
+ sanitized.context = filtered;
288
+ }
289
+ }
290
+ return sanitized;
291
+ }
@@ -7,6 +7,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { _getAdapter, transaction } from "./gsd-db.js";
9
9
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
10
+ import { logWarning } from "./workflow-logger.js";
10
11
 
11
12
  // ─── needsAutoMigration ───────────────────────────────────────────────────
12
13
 
@@ -23,8 +24,8 @@ export function needsAutoMigration(basePath: string): boolean {
23
24
  try {
24
25
  const row = db.prepare("SELECT COUNT(*) as cnt FROM milestones").get();
25
26
  if (row && (row["cnt"] as number) > 0) return false;
26
- } catch {
27
- // Table might not exist yet — that's fine, we can still migrate
27
+ } catch (e) {
28
+ logWarning("migration", `DB probe failed: ${(e as Error).message}`);
28
29
  return false;
29
30
  }
30
31
 
@@ -71,7 +72,7 @@ export function migrateFromMarkdown(basePath: string): void {
71
72
  .filter(e => e.isDirectory())
72
73
  .map(e => e.name);
73
74
  } catch {
74
- process.stderr.write("workflow-migration: failed to read milestones directory\n");
75
+ logWarning("migration", "failed to read milestones directory");
75
76
  return;
76
77
  }
77
78
 
@@ -141,7 +142,7 @@ export function migrateFromMarkdown(basePath: string): void {
141
142
  risk: s.risk || "low",
142
143
  }));
143
144
  } catch (err) {
144
- process.stderr.write(`workflow-migration: failed to parse ROADMAP.md for ${mId}: ${(err as Error).message}\n`);
145
+ logWarning("migration", `failed to parse ROADMAP.md for ${mId}: ${(err as Error).message}`);
145
146
  // Still add milestone with ID as title
146
147
  milestoneInserts.push({ id: mId, title: mId, status: milestoneStatus });
147
148
  }
@@ -191,7 +192,7 @@ export function migrateFromMarkdown(basePath: string): void {
191
192
  });
192
193
  }
193
194
  } catch (err) {
194
- process.stderr.write(`workflow-migration: failed to parse ${slice.id}-PLAN.md for ${mId}: ${(err as Error).message}\n`);
195
+ logWarning("migration", `failed to parse ${slice.id}-PLAN.md for ${mId}: ${(err as Error).message}`);
195
196
  }
196
197
  }
197
198
  }
@@ -206,8 +207,8 @@ export function migrateFromMarkdown(basePath: string): void {
206
207
  process.stderr.write(`workflow-migration: orphaned summary file ${summaryFile} in ${mId} (slice not found in ROADMAP.md), skipping\n`);
207
208
  }
208
209
  }
209
- } catch {
210
- // Non-fatal
210
+ } catch (e) {
211
+ logWarning("migration", `Orphaned summary check failed for ${mId}: ${(e as Error).message}`);
211
212
  }
212
213
  }
213
214
 
@@ -308,17 +309,18 @@ export function validateMigration(basePath: string): { discrepancies: string[] }
308
309
  const planContent = readFileSync(planPath, "utf-8");
309
310
  const plan = parsePlan(planContent);
310
311
  mdTaskCount += plan.tasks.length;
311
- } catch {
312
- // Skip unreadable plan
312
+ } catch (e) {
313
+ logWarning("migration", `Failed to read plan ${slice.id}-PLAN.md: ${(e as Error).message}`);
313
314
  }
314
315
  }
315
316
  }
316
- } catch {
317
- // Skip unreadable roadmap
317
+ } catch (e) {
318
+ logWarning("migration", `Failed to read roadmap for ${mId}: ${(e as Error).message}`);
318
319
  }
319
320
  }
320
321
  }
321
- } catch {
322
+ } catch (e) {
323
+ logWarning("migration", `Validation failed to read markdown: ${(e as Error).message}`);
322
324
  return { discrepancies: ["Failed to read markdown for validation"] };
323
325
  }
324
326
 
@@ -423,7 +423,7 @@ export function regenerateIfMissing(
423
423
  renderSummaryProjection(basePath, milestoneId, sliceId, task.id);
424
424
  regenerated++;
425
425
  } catch (err) {
426
- console.error(`[projections] regenerateIfMissing SUMMARY failed for ${task.id}:`, err);
426
+ logWarning("projection", `regenerateIfMissing SUMMARY failed for ${task.id}: ${(err as Error).message}`);
427
427
  }
428
428
  }
429
429
  }
@@ -452,7 +452,7 @@ export function regenerateIfMissing(
452
452
  }
453
453
  return true;
454
454
  } catch (err) {
455
- console.error(`[projections] regenerateIfMissing ${fileType} failed:`, err);
455
+ logWarning("projection", `regenerateIfMissing ${fileType} failed: ${(err as Error).message}`);
456
456
  return false;
457
457
  }
458
458
  }
@@ -455,8 +455,8 @@ function parseEventBlock(block: string): WorkflowEvent[] {
455
455
  if (paramsMatch) {
456
456
  try {
457
457
  params = JSON.parse(paramsMatch[1]!) as Record<string, unknown>;
458
- } catch {
459
- // Keep empty params on parse error
458
+ } catch (e) {
459
+ logWarning("reconcile", `tool call params parse failed: ${(e as Error).message}`);
460
460
  }
461
461
  i++; // consume params line
462
462
  }
@@ -95,8 +95,8 @@ export function resolveGitDir(basePath: string): string {
95
95
  if (content.startsWith("gitdir: ")) {
96
96
  return resolve(basePath, content.slice(8));
97
97
  }
98
- } catch {
99
- // Not a file or unreadable fall through to default
98
+ } catch (e) {
99
+ logWarning("worktree", `.git file read failed: ${(e as Error).message}`);
100
100
  }
101
101
  return join(basePath, ".git");
102
102
  }
@@ -308,8 +308,9 @@ export function findNestedGitDirs(rootPath: string): string[] {
308
308
  let entries: string[];
309
309
  try {
310
310
  entries = readdirSync(dir);
311
- } catch {
312
- return; // Permission denied, broken symlink, etc.
311
+ } catch (e) {
312
+ logWarning("worktree", `readdirSync failed: ${(e as Error).message}`);
313
+ return;
313
314
  }
314
315
 
315
316
  for (const entry of entries) {
@@ -321,7 +322,8 @@ export function findNestedGitDirs(rootPath: string): string[] {
321
322
  let stat;
322
323
  try {
323
324
  stat = lstatSync(fullPath);
324
- } catch {
325
+ } catch (e) {
326
+ logWarning("worktree", `lstatSync failed for ${fullPath}: ${(e as Error).message}`);
325
327
  continue;
326
328
  }
327
329
  if (!stat.isDirectory()) continue;
@@ -337,8 +339,8 @@ export function findNestedGitDirs(rootPath: string): string[] {
337
339
  // Don't recurse into the nested repo — we found what we need
338
340
  continue;
339
341
  }
340
- } catch {
341
- // No .git here continue scanning
342
+ } catch (e) {
343
+ logWarning("worktree", `existsSync/.git check failed for ${fullPath}: ${(e as Error).message}`);
342
344
  }
343
345
 
344
346
  walk(fullPath, depth + 1);
@@ -374,7 +376,7 @@ export function removeWorktree(
374
376
  if (entry?.path) {
375
377
  wtPath = entry.path;
376
378
  }
377
- } catch { /* fall back to computed path */ }
379
+ } catch (e) { logWarning("worktree", `nativeWorktreeList parse failed: ${(e as Error).message}`); }
378
380
 
379
381
  const resolvedWtPath = existsSync(wtPath) ? realpathSync(wtPath) : wtPath;
380
382
 
@@ -388,7 +390,7 @@ export function removeWorktree(
388
390
  if (!existsSync(wtPath)) {
389
391
  nativeWorktreePrune(basePath);
390
392
  if (deleteBranch) {
391
- try { nativeBranchDelete(basePath, branch, true); } catch { /* branch may not exist */ }
393
+ try { nativeBranchDelete(basePath, branch, true); } catch (e) { logWarning("worktree", `nativeBranchDelete failed: ${(e as Error).message}`); }
392
394
  }
393
395
  return;
394
396
  }
@@ -422,8 +424,8 @@ export function removeWorktree(
422
424
  logWarning("reconcile", `Submodule changes detected — stash failed, changes may be lost during force removal`, { worktree: name, path: resolvedWtPath });
423
425
  }
424
426
  }
425
- } catch {
426
- // submodule status failed proceed with normal removal
427
+ } catch (e) {
428
+ logWarning("worktree", `submodule status check failed: ${(e as Error).message}`);
427
429
  }
428
430
  }
429
431
 
@@ -454,11 +456,11 @@ export function removeWorktree(
454
456
  // Remove worktree: try non-force first when submodules have changes,
455
457
  // falling back to force only after submodule state has been preserved.
456
458
  const useForce = hasSubmoduleChanges ? false : force;
457
- try { nativeWorktreeRemove(basePath, resolvedWtPath, useForce); } catch { /* may fail */ }
459
+ try { nativeWorktreeRemove(basePath, resolvedWtPath, useForce); } catch (e) { logWarning("worktree", `nativeWorktreeRemove failed: ${(e as Error).message}`); }
458
460
 
459
461
  // If the directory is still there (e.g. locked), try harder with force
460
462
  if (existsSync(resolvedWtPath)) {
461
- try { nativeWorktreeRemove(basePath, resolvedWtPath, true); } catch { /* may fail */ }
463
+ try { nativeWorktreeRemove(basePath, resolvedWtPath, true); } catch (e) { logWarning("worktree", `nativeWorktreeRemove (force) failed: ${(e as Error).message}`); }
462
464
  }
463
465
 
464
466
  // (#2821) If the worktree directory STILL exists after both native removal
@@ -488,7 +490,7 @@ export function removeWorktree(
488
490
  nativeWorktreePrune(basePath);
489
491
 
490
492
  if (deleteBranch) {
491
- try { nativeBranchDelete(basePath, branch, true); } catch { /* branch may not exist */ }
493
+ try { nativeBranchDelete(basePath, branch, true); } catch (e) { logWarning("worktree", `final branch delete failed: ${(e as Error).message}`); }
492
494
  }
493
495
  }
494
496
 
@@ -298,7 +298,9 @@ export async function showInterviewRound(
298
298
  // Auto-open the notes field when "None of the above" is selected
299
299
  // so the user can immediately provide a free-text explanation
300
300
  // instead of being trapped in a re-asking loop (bug #2715).
301
- if (!isMultiSelect(currentIdx) && states[currentIdx].cursorIndex === noneOrDoneIdx(currentIdx)) {
301
+ // Only auto-open if the user hasn't already provided notes —
302
+ // otherwise Enter from notes mode loops back here endlessly.
303
+ if (!isMultiSelect(currentIdx) && states[currentIdx].cursorIndex === noneOrDoneIdx(currentIdx) && !states[currentIdx].notes) {
302
304
  states[currentIdx].notesVisible = true;
303
305
  focusNotes = true;
304
306
  loadStateToEditor();
@@ -0,0 +1,144 @@
1
+ // GSD2 — Regression test for interview-ui "None of the above" notes loop
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ /**
5
+ * Regression test for bug #3502:
6
+ *
7
+ * Selecting "None of the above" opens the notes field, but pressing Enter
8
+ * after typing a note called goNextOrSubmit() which saw the cursor still
9
+ * on the "None of the above" slot and re-opened notes — trapping the user
10
+ * in an infinite loop.
11
+ *
12
+ * The fix adds a `!states[currentIdx].notes` guard so auto-open only fires
13
+ * when notes are still empty.
14
+ */
15
+
16
+ import { describe, it } from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { showInterviewRound, type Question, type RoundResult } from "../interview-ui.js";
19
+
20
+ // Raw terminal sequences that matchesKey() recognises
21
+ const ENTER = "\r";
22
+ const DOWN = "\x1b[B";
23
+ const TAB = "\t";
24
+
25
+ /**
26
+ * Drive showInterviewRound with a scripted sequence of key inputs.
27
+ * We mock ctx.ui.custom() to capture the widget, feed it inputs, and
28
+ * resolve when done() is called.
29
+ */
30
+ function runWithInputs(
31
+ questions: Question[],
32
+ inputs: string[],
33
+ ): Promise<RoundResult> {
34
+ return new Promise((resolve, reject) => {
35
+ const timeout = setTimeout(() => reject(new Error("Timed out — likely stuck in infinite loop")), 3000);
36
+
37
+ const mockCtx = {
38
+ ui: {
39
+ custom: (factory: any) => {
40
+ const mockTui = {
41
+ requestRender: () => {},
42
+ };
43
+ const mockTheme = {
44
+ // Minimal theme stubs — render output is not asserted
45
+ fg: (_c: string, t: string) => t,
46
+ bold: (t: string) => t,
47
+ dim: (t: string) => t,
48
+ italic: (t: string) => t,
49
+ strikethrough: (t: string) => t,
50
+ accent: (t: string) => t,
51
+ success: (t: string) => t,
52
+ warning: (t: string) => t,
53
+ error: (t: string) => t,
54
+ info: (t: string) => t,
55
+ muted: (t: string) => t,
56
+ dimmed: (t: string) => t,
57
+ };
58
+ const mockKb = {};
59
+
60
+ const widget = factory(mockTui, mockTheme, mockKb, (result: RoundResult) => {
61
+ clearTimeout(timeout);
62
+ resolve(result);
63
+ });
64
+
65
+ // Feed each input sequentially
66
+ for (const input of inputs) {
67
+ widget.handleInput(input);
68
+ }
69
+ },
70
+ },
71
+ };
72
+
73
+ showInterviewRound(questions, {}, mockCtx as any).catch(reject);
74
+ });
75
+ }
76
+
77
+ describe("interview-ui notes loop regression (#3502)", () => {
78
+ const questions: Question[] = [
79
+ {
80
+ id: "q1",
81
+ header: "Project Type",
82
+ question: "What type of project?",
83
+ options: [
84
+ { label: "Web App", description: "Frontend or full-stack" },
85
+ { label: "CLI Tool", description: "Command-line utility" },
86
+ ],
87
+ },
88
+ ];
89
+
90
+ it("does not loop when Enter is pressed after typing a note on 'None of the above'", async () => {
91
+ // With 2 options, "None of the above" is index 2 (0-based)
92
+ // Cursor starts at 0, so press Down twice to reach it
93
+ const result = await runWithInputs(questions, [
94
+ DOWN, // cursor → index 1 (CLI Tool)
95
+ DOWN, // cursor → index 2 (None of the above)
96
+ ENTER, // commit → auto-opens notes field
97
+ "u", "n", "s", "u", "r", "e", // type "unsure"
98
+ ENTER, // should advance to review, NOT reopen notes
99
+ ENTER, // submit from review screen
100
+ ]);
101
+
102
+ // If we get here, the loop did not occur (timeout would have fired)
103
+ assert.ok(result, "should return a result");
104
+ assert.equal(result.endInterview, false);
105
+
106
+ const answer = result.answers.q1;
107
+ assert.ok(answer, "answer for q1 should exist");
108
+ assert.equal(answer.notes, "unsure", "notes should contain typed text");
109
+ assert.equal(answer.selected, "None of the above");
110
+ });
111
+
112
+ it("still auto-opens notes when selecting 'None of the above' with no prior notes", async () => {
113
+ // Press Down twice to "None of the above", Enter to select
114
+ // Then immediately Enter again (empty notes) — this should re-open notes
115
+ // because the guard only skips when notes are non-empty.
116
+ // Type something on second open, then Enter to proceed.
117
+ const result = await runWithInputs(questions, [
118
+ DOWN, // cursor → 1
119
+ DOWN, // cursor → 2 (None of the above)
120
+ ENTER, // commit → auto-opens notes
121
+ ENTER, // empty notes → goNextOrSubmit → should re-open notes (empty guard)
122
+ "o", "k", // type "ok"
123
+ ENTER, // now notes = "ok" → should advance to review
124
+ ENTER, // submit
125
+ ]);
126
+
127
+ assert.ok(result, "should return a result");
128
+ const answer = result.answers.q1;
129
+ assert.ok(answer, "answer for q1 should exist");
130
+ assert.equal(answer.notes, "ok");
131
+ });
132
+
133
+ it("normal option selection is unaffected", async () => {
134
+ const result = await runWithInputs(questions, [
135
+ ENTER, // select first option (Web App) and advance to review
136
+ ENTER, // submit from review screen
137
+ ]);
138
+
139
+ assert.ok(result, "should return a result");
140
+ const answer = result.answers.q1;
141
+ assert.ok(answer, "answer for q1 should exist");
142
+ assert.equal(answer.selected, "Web App");
143
+ });
144
+ });