gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7

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 (88) hide show
  1. package/dist/cli.js +2 -7
  2. package/dist/resource-loader.d.ts +1 -1
  3. package/dist/resource-loader.js +1 -13
  4. package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
  5. package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
  6. package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
  7. package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
  8. package/dist/resources/extensions/bg-shell/types.js +2 -0
  9. package/dist/resources/extensions/context7/index.js +0 -5
  10. package/dist/resources/extensions/get-secrets-from-user.js +30 -2
  11. package/dist/resources/extensions/google-search/index.js +0 -5
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
  13. package/dist/resources/extensions/gsd/auto-loop.js +1 -10
  14. package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
  15. package/dist/resources/extensions/gsd/auto-start.js +2 -35
  16. package/dist/resources/extensions/gsd/auto.js +4 -59
  17. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  18. package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
  19. package/dist/resources/extensions/gsd/files.js +1 -9
  20. package/dist/resources/extensions/gsd/gitignore.js +7 -54
  21. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  22. package/dist/resources/extensions/gsd/health-widget.js +46 -97
  23. package/dist/resources/extensions/gsd/index.js +1 -10
  24. package/dist/resources/extensions/gsd/migrate-external.js +2 -55
  25. package/dist/resources/extensions/gsd/paths.js +7 -74
  26. package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
  27. package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  30. package/dist/resources/extensions/gsd/session-lock.js +2 -26
  31. package/dist/resources/extensions/gsd/templates/plan.md +0 -8
  32. package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
  33. package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
  34. package/dist/resources/extensions/shared/mod.js +1 -1
  35. package/dist/resources/extensions/shared/sanitize.js +0 -30
  36. package/dist/resources/extensions/shared/wizard-ui.js +478 -0
  37. package/dist/resources/extensions/subagent/index.js +14 -6
  38. package/package.json +1 -2
  39. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
  41. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  42. package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
  43. package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
  44. package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
  45. package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
  46. package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
  47. package/src/resources/extensions/bg-shell/types.ts +12 -0
  48. package/src/resources/extensions/context7/index.ts +0 -7
  49. package/src/resources/extensions/get-secrets-from-user.ts +35 -2
  50. package/src/resources/extensions/google-search/index.ts +0 -7
  51. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
  52. package/src/resources/extensions/gsd/auto-loop.ts +1 -11
  53. package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
  54. package/src/resources/extensions/gsd/auto-start.ts +2 -42
  55. package/src/resources/extensions/gsd/auto.ts +3 -61
  56. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  57. package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
  58. package/src/resources/extensions/gsd/files.ts +1 -10
  59. package/src/resources/extensions/gsd/gitignore.ts +7 -54
  60. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  61. package/src/resources/extensions/gsd/health-widget.ts +59 -103
  62. package/src/resources/extensions/gsd/index.ts +1 -10
  63. package/src/resources/extensions/gsd/migrate-external.ts +2 -47
  64. package/src/resources/extensions/gsd/paths.ts +7 -73
  65. package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
  66. package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
  67. package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  68. package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  69. package/src/resources/extensions/gsd/session-lock.ts +2 -29
  70. package/src/resources/extensions/gsd/templates/plan.md +0 -8
  71. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
  72. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
  73. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
  74. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
  75. package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
  76. package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
  77. package/src/resources/extensions/shared/mod.ts +1 -1
  78. package/src/resources/extensions/shared/sanitize.ts +0 -36
  79. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  80. package/src/resources/extensions/subagent/index.ts +12 -6
  81. package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
  82. package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
  83. package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
  84. package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
  85. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
  86. package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
  87. package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
  88. package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
package/dist/cli.js CHANGED
@@ -327,10 +327,7 @@ if (isPrintMode) {
327
327
  markStartup('createAgentSession');
328
328
  if (extensionsResult.errors.length > 0) {
329
329
  for (const err of extensionsResult.errors) {
330
- // Downgrade conflicts with built-in tools to warnings (#1347)
331
- const isSuperseded = err.error.includes("supersedes");
332
- const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
333
- process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
330
+ process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
334
331
  }
335
332
  }
336
333
  // Apply --model override if specified
@@ -459,9 +456,7 @@ const { session, extensionsResult } = await createAgentSession({
459
456
  markStartup('createAgentSession');
460
457
  if (extensionsResult.errors.length > 0) {
461
458
  for (const err of extensionsResult.errors) {
462
- const isSuperseded = err.error.includes("supersedes");
463
- const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
464
- process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
459
+ process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
465
460
  }
466
461
  }
467
462
  // Restore scoped models from settings on startup.
@@ -9,7 +9,7 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
9
9
  * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
10
10
  * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
11
11
  * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
12
- * - GSD-WORKFLOW.md ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
12
+ * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
13
13
  *
14
14
  * Skips the copy when the managed-resources.json version matches the current
15
15
  * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
@@ -18,9 +18,6 @@ import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegi
18
18
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
19
  const distResources = join(packageRoot, 'dist', 'resources');
20
20
  const srcResources = join(packageRoot, 'src', 'resources');
21
- // Use dist/resources only if it has the full expected structure.
22
- // A partial build (tsc without copy-resources) creates dist/resources/extensions/
23
- // but not agents/ or skills/, causing initResources to sync from an incomplete source.
24
21
  const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
25
22
  ? distResources
26
23
  : srcResources;
@@ -223,7 +220,7 @@ function copyDirRecursive(src, dest) {
223
220
  * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
224
221
  * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
225
222
  * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
226
- * - GSD-WORKFLOW.md ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
223
+ * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
227
224
  *
228
225
  * Skips the copy when the managed-resources.json version matches the current
229
226
  * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
@@ -250,15 +247,6 @@ export function initResources(agentDir) {
250
247
  syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'));
251
248
  syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
252
249
  syncResourceDir(join(resourcesDir, 'skills'), join(agentDir, 'skills'));
253
- // Sync GSD-WORKFLOW.md to agentDir as a fallback for when GSD_WORKFLOW_PATH
254
- // env var is not set (e.g. fork/dev builds, alternative entry points).
255
- const workflowSrc = join(resourcesDir, 'GSD-WORKFLOW.md');
256
- if (existsSync(workflowSrc)) {
257
- try {
258
- copyFileSync(workflowSrc, join(agentDir, 'GSD-WORKFLOW.md'));
259
- }
260
- catch { /* non-fatal */ }
261
- }
262
250
  // Ensure all newly copied files are owner-writable so the next run can
263
251
  // overwrite them (covers extensions, agents, and skills in one walk).
264
252
  makeTreeWritable(agentDir);
@@ -52,12 +52,14 @@ export function createAwaitTool(getManager) {
52
52
  const running = watched.filter((j) => j.status === "running");
53
53
  if (running.length === 0) {
54
54
  const result = formatResults(watched);
55
+ manager.acknowledgeDeliveries(watched.map((j) => j.id));
55
56
  return { content: [{ type: "text", text: result }], details: undefined };
56
57
  }
57
58
  // Wait for at least one to complete
58
59
  await Promise.race(running.map((j) => j.promise));
59
60
  // Collect all completed results (more may have finished while waiting)
60
61
  const completed = watched.filter((j) => j.status !== "running");
62
+ manager.acknowledgeDeliveries(completed.map((j) => j.id));
61
63
  const stillRunning = watched.filter((j) => j.status === "running");
62
64
  let result = formatResults(completed);
63
65
  if (stillRunning.length > 0) {
@@ -101,6 +101,12 @@ export class AsyncJobManager {
101
101
  getAllJobs() {
102
102
  return [...this.jobs.values()];
103
103
  }
104
+ /**
105
+ * No-op. Retained for API compatibility with await_job tool.
106
+ */
107
+ acknowledgeDeliveries(_jobIds) {
108
+ // Delivery is fire-once; no retries to cancel.
109
+ }
104
110
  /**
105
111
  * Cleanup all timers and resources.
106
112
  */
@@ -2,7 +2,7 @@
2
2
  * Output analysis, digest generation, highlights extraction, and output retrieval.
3
3
  */
4
4
  import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, } from "@gsd/pi-coding-agent";
5
- import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, } from "./types.js";
5
+ import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, LINE_DEDUP_MAX, } from "./types.js";
6
6
  import { addEvent, pushAlert } from "./process-manager.js";
7
7
  import { transitionToReady } from "./readiness-detector.js";
8
8
  import { formatUptime, formatTimeAgo } from "./utilities.js";
@@ -78,6 +78,24 @@ export function analyzeLine(bg, line, stream) {
78
78
  pushAlert(bg, "recovered — errors cleared");
79
79
  }
80
80
  }
81
+ // Dedup tracking — evict oldest entry when map exceeds LINE_DEDUP_MAX (LRU via Map insertion order)
82
+ bg.totalRawLines++;
83
+ const lineHash = line.trim().slice(0, 100);
84
+ const existing = bg.lineDedup.get(lineHash);
85
+ if (existing !== undefined) {
86
+ // Re-insert to update insertion order (move to tail = most recent)
87
+ bg.lineDedup.delete(lineHash);
88
+ bg.lineDedup.set(lineHash, existing + 1);
89
+ }
90
+ else {
91
+ if (bg.lineDedup.size >= LINE_DEDUP_MAX) {
92
+ // Evict oldest entry (Map iteration order = insertion order = LRU at head)
93
+ const oldest = bg.lineDedup.keys().next().value;
94
+ if (oldest !== undefined)
95
+ bg.lineDedup.delete(oldest);
96
+ }
97
+ bg.lineDedup.set(lineHash, 1);
98
+ }
81
99
  }
82
100
  // ── Digest Generation ──────────────────────────────────────────────────────
83
101
  export function generateDigest(bg, mutate = false) {
@@ -135,8 +135,12 @@ export function startProcess(opts) {
135
135
  group: opts.group || null,
136
136
  lastErrorCount: 0,
137
137
  lastWarningCount: 0,
138
+ commandHistory: [],
139
+ lineDedup: new Map(),
140
+ totalRawLines: 0,
138
141
  stdoutLineCount: 0,
139
142
  stderrLineCount: 0,
143
+ envKeys: Object.keys(opts.env || {}),
140
144
  restartCount: 0,
141
145
  startConfig: {
142
146
  command,
@@ -5,6 +5,8 @@
5
5
  export const MAX_BUFFER_LINES = 5000;
6
6
  export const MAX_EVENTS = 200;
7
7
  export const DEAD_PROCESS_TTL = 10 * 60 * 1000;
8
+ /** Maximum unique entries in the per-process lineDedup Map before LRU eviction. */
9
+ export const LINE_DEDUP_MAX = 500;
8
10
  export const PORT_PROBE_TIMEOUT = 500;
9
11
  export const READY_POLL_INTERVAL = 250;
10
12
  export const DEFAULT_READY_TIMEOUT = 30000;
@@ -327,11 +327,6 @@ export default function (pi) {
327
327
  return new Text(text, 0, 0);
328
328
  },
329
329
  });
330
- // ── Session cleanup ─────────────────────────────────────────────────────
331
- pi.on("session_shutdown", async () => {
332
- searchCache.clear();
333
- docCache.clear();
334
- });
335
330
  // ── Startup notification ─────────────────────────────────────────────────
336
331
  pi.on("session_start", async (_event, ctx) => {
337
332
  if (!getApiKey()) {
@@ -8,9 +8,9 @@
8
8
  import { readFile, writeFile } from "node:fs/promises";
9
9
  import { existsSync, statSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
- import { Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
11
+ import { CURSOR_MARKER, Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
12
12
  import { Type } from "@sinclair/typebox";
13
- import { makeUI, maskEditorLine } from "./shared/mod.js";
13
+ import { makeUI } from "./shared/mod.js";
14
14
  import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
15
15
  import { resolveMilestoneFile } from "./gsd/paths.js";
16
16
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -21,6 +21,34 @@ function maskPreview(value) {
21
21
  return "*".repeat(value.length);
22
22
  return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
23
23
  }
24
+ /**
25
+ * Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
26
+ */
27
+ function maskEditorLine(line) {
28
+ // Keep border / metadata lines readable.
29
+ if (line.startsWith("─")) {
30
+ return line;
31
+ }
32
+ let output = "";
33
+ let i = 0;
34
+ while (i < line.length) {
35
+ if (line.startsWith(CURSOR_MARKER, i)) {
36
+ output += CURSOR_MARKER;
37
+ i += CURSOR_MARKER.length;
38
+ continue;
39
+ }
40
+ const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
41
+ if (ansiMatch) {
42
+ output += ansiMatch[0];
43
+ i += ansiMatch[0].length;
44
+ continue;
45
+ }
46
+ const ch = line[i];
47
+ output += ch === " " ? " " : "*";
48
+ i += 1;
49
+ }
50
+ return output;
51
+ }
24
52
  function shellEscapeSingle(value) {
25
53
  return `'${value.replace(/'/g, `'\\''`)}'`;
26
54
  }
@@ -326,11 +326,6 @@ export default function (pi) {
326
326
  return new Text(text, 0, 0);
327
327
  },
328
328
  });
329
- // ── Session cleanup ─────────────────────────────────────────────────────
330
- pi.on("session_shutdown", async () => {
331
- resultCache.clear();
332
- client = null;
333
- });
334
329
  // ── Startup notification ─────────────────────────────────────────────────
335
330
  pi.on("session_start", async (_event, ctx) => {
336
331
  if (process.env.GEMINI_API_KEY)
@@ -8,7 +8,7 @@
8
8
  * data structure that is inspectable, testable per-rule, and extensible
9
9
  * without modifying orchestration code.
10
10
  */
11
- import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
11
+ import { loadFile, loadActiveOverrides } from "./files.js";
12
12
  import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
@@ -274,28 +274,6 @@ const DISPATCH_RULES = [
274
274
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
275
275
  if (state.phase !== "validating-milestone")
276
276
  return null;
277
- // Safety guard (#1368): verify all roadmap slices have SUMMARY files before
278
- // allowing milestone validation. If any slice lacks a summary, the milestone
279
- // is not genuinely complete — something skipped earlier slices.
280
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
281
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
282
- if (roadmapContent) {
283
- const roadmap = parseRoadmap(roadmapContent);
284
- const missingSlices = [];
285
- for (const slice of roadmap.slices) {
286
- const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
287
- if (!summaryPath || !existsSync(summaryPath)) {
288
- missingSlices.push(slice.id);
289
- }
290
- }
291
- if (missingSlices.length > 0) {
292
- return {
293
- action: "stop",
294
- reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
295
- level: "error",
296
- };
297
- }
298
- }
299
277
  // Skip preference: write a minimal pass-through VALIDATION file
300
278
  if (prefs?.phases?.skip_milestone_validation) {
301
279
  const mDir = resolveMilestonePath(basePath, mid);
@@ -330,26 +308,6 @@ const DISPATCH_RULES = [
330
308
  match: async ({ state, mid, midTitle, basePath }) => {
331
309
  if (state.phase !== "completing-milestone")
332
310
  return null;
333
- // Safety guard (#1368): verify all roadmap slices have SUMMARY files.
334
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
335
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
336
- if (roadmapContent) {
337
- const roadmap = parseRoadmap(roadmapContent);
338
- const missingSlices = [];
339
- for (const slice of roadmap.slices) {
340
- const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
341
- if (!summaryPath || !existsSync(summaryPath)) {
342
- missingSlices.push(slice.id);
343
- }
344
- }
345
- if (missingSlices.length > 0) {
346
- return {
347
- action: "stop",
348
- reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
349
- level: "error",
350
- };
351
- }
352
- }
353
311
  return {
354
312
  action: "dispatch",
355
313
  unitType: "complete-milestone",
@@ -161,15 +161,6 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
161
161
  const unitPromise = new Promise((resolve) => {
162
162
  s.pendingResolve = resolve;
163
163
  });
164
- // Ensure cwd matches basePath before dispatch (#1389).
165
- // async_bash and background jobs can drift cwd away from the worktree.
166
- // Realigning here prevents commits from landing on the wrong branch.
167
- try {
168
- if (process.cwd() !== s.basePath) {
169
- process.chdir(s.basePath);
170
- }
171
- }
172
- catch { /* non-fatal — chdir may fail if dir was removed */ }
173
164
  // ── Send the prompt ──
174
165
  debugLog("runUnit", { phase: "send-message", unitType, unitId });
175
166
  pi.sendMessage({ customType: "gsd-auto", content: prompt, display: s.verbose }, { triggerTurn: true });
@@ -507,7 +498,7 @@ export async function autoLoop(ctx, pi, s, deps) {
507
498
  }
508
499
  // Secrets re-check gate
509
500
  try {
510
- const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
501
+ const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
511
502
  if (manifestStatus && manifestStatus.pending.length > 0) {
512
503
  const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
513
504
  if (result &&
@@ -6,13 +6,11 @@
6
6
  * Pure functions that receive all needed state as parameters — no module-level
7
7
  * globals or AutoContext dependency.
8
8
  */
9
- import { parseUnitId } from "./unit-id.js";
10
9
  import { clearUnitRuntimeRecord } from "./unit-runtime.js";
11
10
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
12
11
  import { isValidationTerminal } from "./state.js";
13
12
  import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
14
13
  import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
15
- import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
16
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
17
15
  import { dirname, join } from "node:path";
18
16
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
@@ -434,39 +432,6 @@ export async function selfHealRuntimeRecords(base, ctx) {
434
432
  const now = Date.now();
435
433
  for (const record of records) {
436
434
  const { unitType, unitId } = record;
437
- // Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
438
- // If a complete-slice was interrupted after writing artifacts but before
439
- // flipping the roadmap checkbox, the verification fails and the dispatch
440
- // loop relaunches the same unit forever. Auto-fix the checkbox.
441
- if (unitType === "complete-slice") {
442
- const { milestone: mid, slice: sid } = parseUnitId(unitId);
443
- if (mid && sid) {
444
- const dir = resolveSlicePath(base, mid, sid);
445
- if (dir) {
446
- const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
447
- const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
448
- if (existsSync(summaryPath) && existsSync(uatPath)) {
449
- const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
450
- if (roadmapFile && existsSync(roadmapFile)) {
451
- try {
452
- const roadmapContent = readFileSync(roadmapFile, "utf-8");
453
- const roadmap = parseRoadmap(roadmapContent);
454
- const slice = (roadmap.slices ?? []).find(s => s.id === sid);
455
- if (slice && !slice.done) {
456
- // Auto-fix: flip the checkbox using shared utility
457
- if (markSliceDoneInRoadmap(base, mid, sid)) {
458
- ctx.ui.notify(`Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`, "info");
459
- }
460
- }
461
- }
462
- catch {
463
- // Roadmap parse failure — don't block self-heal
464
- }
465
- }
466
- }
467
- }
468
- }
469
- }
470
435
  // Clear stale dispatched records (dispatched > 1h ago, process crashed)
471
436
  const age = now - (record.startedAt ?? 0);
472
437
  if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
@@ -11,8 +11,6 @@
11
11
  import { deriveState } from "./state.js";
12
12
  import { loadFile, getManifestStatus } from "./files.js";
13
13
  import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
14
- import { ensureGsdSymlink } from "./repo-identity.js";
15
- import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
16
14
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
17
15
  import { gsdRoot, resolveMilestoneFile } from "./paths.js";
18
16
  import { invalidateAllCaches } from "./cache.js";
@@ -44,12 +42,6 @@ import { sep as pathSep } from "node:path";
44
42
  * Returns false if the bootstrap aborted (e.g., guided flow returned,
45
43
  * concurrent session detected). Returns true when ready to dispatch.
46
44
  */
47
- /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
48
- * Prevents the recursive dialog loop described in #1348 where
49
- * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
50
- * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
51
- let _consecutiveCompleteBootstraps = 0;
52
- const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
53
45
  export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
54
46
  const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
55
47
  const lockResult = acquireSessionLock(base);
@@ -68,19 +60,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
68
60
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
69
61
  nativeInit(base, mainBranch);
70
62
  }
71
- // Migrate legacy in-project .gsd/ to external state directory.
72
- // Migration MUST run before ensureGitignore to avoid adding ".gsd" to
73
- // .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
74
- recoverFailedMigration(base);
75
- const migration = migrateToExternalState(base);
76
- if (migration.error) {
77
- ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
78
- }
79
- // Ensure symlink exists (handles fresh projects and post-migration)
80
- ensureGsdSymlink(base);
81
- // Ensure .gitignore has baseline patterns.
82
- // ensureGitignore checks for git-tracked .gsd/ files and skips the
83
- // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
63
+ // Ensure .gitignore has baseline patterns
84
64
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
85
65
  const commitDocs = gitPrefs?.commit_docs;
86
66
  const manageGitignore = gitPrefs?.manage_gitignore;
@@ -206,16 +186,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
206
186
  if (!hasSurvivorBranch) {
207
187
  // No active work — start a new milestone via discuss flow
208
188
  if (!state.activeMilestone || state.phase === "complete") {
209
- // Guard against recursive dialog loop (#1348):
210
- // If we've entered this branch multiple times in quick succession,
211
- // the discuss workflow isn't producing a milestone. Break the cycle.
212
- _consecutiveCompleteBootstraps++;
213
- if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
214
- _consecutiveCompleteBootstraps = 0;
215
- ctx.ui.notify("All milestones are complete and the discussion didn't produce a new one. " +
216
- "Run /gsd to start a new milestone manually.", "warning");
217
- return releaseLockAndReturn();
218
- }
219
189
  const { showSmartEntry } = await import("./guided-flow.js");
220
190
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
221
191
  invalidateAllCaches();
@@ -223,7 +193,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
223
193
  if (postState.activeMilestone &&
224
194
  postState.phase !== "complete" &&
225
195
  postState.phase !== "pre-planning") {
226
- _consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
227
196
  state = postState;
228
197
  }
229
198
  else if (postState.activeMilestone &&
@@ -268,8 +237,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
268
237
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
269
238
  return releaseLockAndReturn();
270
239
  }
271
- // Successfully resolved an active milestone — reset the re-entry guard
272
- _consecutiveCompleteBootstraps = 0;
273
240
  // ── Initialize session state ──
274
241
  s.active = true;
275
242
  s.stepMode = requestedStepMode;
@@ -378,7 +345,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
378
345
  // Secrets collection gate
379
346
  const mid = state.activeMilestone.id;
380
347
  try {
381
- const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base);
348
+ const manifestStatus = await getManifestStatus(base, mid);
382
349
  if (manifestStatus && manifestStatus.pending.length > 0) {
383
350
  const result = await collectSecretsFromManifest(base, mid, ctx);
384
351
  if (result &&
@@ -36,7 +36,7 @@ import { clearSkillSnapshot } from "./skill-discovery.js";
36
36
  import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
37
37
  import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
38
38
  import { join } from "node:path";
39
- import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
39
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
40
40
  import { atomicWriteSync } from "./atomic-write.js";
41
41
  import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
42
42
  import { GitServiceImpl } from "./git-service.js";
@@ -325,13 +325,6 @@ export async function stopAuto(ctx, pi, reason) {
325
325
  resetHookState();
326
326
  if (s.basePath)
327
327
  clearPersistedHookState(s.basePath);
328
- // Remove paused-session metadata if present (#1383)
329
- try {
330
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
331
- if (existsSync(pausedPath))
332
- unlinkSync(pausedPath);
333
- }
334
- catch { /* non-fatal */ }
335
328
  s.active = false;
336
329
  s.paused = false;
337
330
  s.stepMode = false;
@@ -376,28 +369,10 @@ export async function pauseAuto(ctx, _pi) {
376
369
  return;
377
370
  clearUnitTimeout();
378
371
  s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
379
- // Persist paused-session metadata so resume survives /exit (#1383).
380
- // The fresh-start bootstrap checks for this file and restores worktree context.
381
- try {
382
- const pausedMeta = {
383
- milestoneId: s.currentMilestoneId,
384
- worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
385
- originalBasePath: s.originalBasePath,
386
- stepMode: s.stepMode,
387
- pausedAt: new Date().toISOString(),
388
- sessionFile: s.pausedSessionFile,
389
- };
390
- const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
391
- mkdirSync(runtimeDir, { recursive: true });
392
- writeFileSync(join(runtimeDir, "paused-session.json"), JSON.stringify(pausedMeta, null, 2), "utf-8");
393
- }
394
- catch {
395
- // Non-fatal — resume will still work via full bootstrap, just without worktree context
396
- }
397
- if (lockBase()) {
398
- releaseSessionLock(lockBase());
372
+ if (lockBase())
399
373
  clearLock(lockBase());
400
- }
374
+ if (lockBase())
375
+ releaseSessionLock(lockBase());
401
376
  deregisterSigtermHandler();
402
377
  s.active = false;
403
378
  s.paused = true;
@@ -545,30 +520,6 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
545
520
  // Escape stale worktree cwd from a previous milestone (#608).
546
521
  base = escapeStaleWorktree(base);
547
522
  // If resuming from paused state, just re-activate and dispatch next unit.
548
- // Check persisted paused-session first (#1383) — survives /exit.
549
- if (!s.paused) {
550
- try {
551
- const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
552
- if (existsSync(pausedPath)) {
553
- const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
554
- if (meta.milestoneId) {
555
- s.currentMilestoneId = meta.milestoneId;
556
- s.originalBasePath = meta.originalBasePath || base;
557
- s.stepMode = meta.stepMode ?? requestedStepMode;
558
- s.paused = true;
559
- // Clean up the persisted file — we're consuming it
560
- try {
561
- unlinkSync(pausedPath);
562
- }
563
- catch { /* non-fatal */ }
564
- ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, "info");
565
- }
566
- }
567
- }
568
- catch {
569
- // Malformed or missing — proceed with fresh bootstrap
570
- }
571
- }
572
523
  if (s.paused) {
573
524
  const resumeLock = acquireSessionLock(base);
574
525
  if (!resumeLock.acquired) {
@@ -803,12 +754,6 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
803
754
  }, hookHardTimeoutMs);
804
755
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
805
756
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
806
- // Ensure cwd matches basePath before hook dispatch (#1389)
807
- try {
808
- if (process.cwd() !== s.basePath)
809
- process.chdir(s.basePath);
810
- }
811
- catch { }
812
757
  debugLog("dispatchHookUnit", {
813
758
  phase: "send-message",
814
759
  promptLength: hookPrompt.length,
@@ -15,7 +15,7 @@ import { isAutoActive } from "./auto.js";
15
15
  import { projectRoot } from "./commands.js";
16
16
  import { loadPrompt } from "./prompt-loader.js";
17
17
  export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
18
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
18
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
19
19
  const workflow = readFileSync(workflowPath, "utf-8");
20
20
  const prompt = loadPrompt("doctor-heal", {
21
21
  doctorSummary: reportText,
@@ -144,7 +144,7 @@ export async function handleTriage(ctx, pi, basePath) {
144
144
  currentPlan: currentPlan || "(no active slice plan)",
145
145
  roadmapContext: roadmapContext || "(no active roadmap)",
146
146
  });
147
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
147
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
148
148
  const workflow = readFileSync(workflowPath, "utf-8");
149
149
  pi.sendMessage({
150
150
  customType: "gsd-triage",
@@ -148,36 +148,27 @@ function checkPortConflicts(basePath) {
148
148
  // Try to detect ports from package.json scripts
149
149
  const portsToCheck = new Set();
150
150
  const pkgPath = join(basePath, "package.json");
151
- if (!existsSync(pkgPath)) {
152
- // No package.json — this isn't a Node.js project. Skip port checks
153
- // entirely to avoid false positives from system services (e.g., macOS
154
- // AirPlay Receiver on port 5000). (#1381)
155
- return [];
156
- }
157
- try {
158
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
159
- const scripts = pkg.scripts ?? {};
160
- const scriptText = Object.values(scripts).join(" ");
161
- // Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
162
- const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
163
- for (const m of portMatches) {
164
- const port = parseInt(m[1], 10);
165
- if (port >= 1024 && port <= 65535)
166
- portsToCheck.add(port);
151
+ if (existsSync(pkgPath)) {
152
+ try {
153
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
154
+ const scripts = pkg.scripts ?? {};
155
+ const scriptText = Object.values(scripts).join(" ");
156
+ // Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
157
+ const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
158
+ for (const m of portMatches) {
159
+ const port = parseInt(m[1], 10);
160
+ if (port >= 1024 && port <= 65535)
161
+ portsToCheck.add(port);
162
+ }
163
+ }
164
+ catch {
165
+ // parse failed use defaults
167
166
  }
168
167
  }
169
- catch {
170
- // parse failed — skip port checks rather than using defaults
171
- return [];
172
- }
173
- // If no ports found in scripts, check common defaults.
174
- // Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
168
+ // If no ports found in scripts, check common defaults
175
169
  if (portsToCheck.size === 0) {
176
- for (const p of DEFAULT_DEV_PORTS) {
177
- if (p === 5000 && process.platform === "darwin")
178
- continue;
170
+ for (const p of DEFAULT_DEV_PORTS)
179
171
  portsToCheck.add(p);
180
- }
181
172
  }
182
173
  for (const port of portsToCheck) {
183
174
  const result = tryExec(`lsof -i :${port} -sTCP:LISTEN -t`, basePath);