gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.eeb3520

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 (45) hide show
  1. package/dist/cli.js +0 -9
  2. package/dist/extension-discovery.d.ts +3 -5
  3. package/dist/extension-discovery.js +9 -14
  4. package/dist/resources/extensions/browser-tools/package.json +1 -3
  5. package/dist/resources/extensions/cmux/index.js +1 -55
  6. package/dist/resources/extensions/context7/package.json +1 -1
  7. package/dist/resources/extensions/google-search/package.json +1 -3
  8. package/dist/resources/extensions/gsd/auto-loop.js +1 -7
  9. package/dist/resources/extensions/gsd/auto-start.js +1 -6
  10. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -11
  11. package/dist/resources/extensions/gsd/captures.js +1 -9
  12. package/dist/resources/extensions/gsd/commands-handlers.js +3 -16
  13. package/dist/resources/extensions/gsd/commands.js +1 -20
  14. package/dist/resources/extensions/gsd/doctor-checks.js +0 -82
  15. package/dist/resources/extensions/gsd/doctor-environment.js +0 -78
  16. package/dist/resources/extensions/gsd/doctor-format.js +0 -15
  17. package/dist/resources/extensions/gsd/doctor.js +11 -184
  18. package/dist/resources/extensions/gsd/package.json +1 -1
  19. package/dist/resources/extensions/gsd/worktree.js +16 -35
  20. package/dist/resources/extensions/subagent/index.js +3 -12
  21. package/dist/resources/extensions/universal-config/package.json +1 -1
  22. package/package.json +1 -1
  23. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  24. package/packages/pi-coding-agent/dist/core/package-manager.js +4 -8
  25. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  26. package/packages/pi-coding-agent/src/core/package-manager.ts +4 -8
  27. package/src/resources/extensions/cmux/index.ts +1 -57
  28. package/src/resources/extensions/gsd/auto-loop.ts +1 -13
  29. package/src/resources/extensions/gsd/auto-start.ts +1 -7
  30. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -12
  31. package/src/resources/extensions/gsd/captures.ts +1 -10
  32. package/src/resources/extensions/gsd/commands-handlers.ts +2 -17
  33. package/src/resources/extensions/gsd/commands.ts +1 -21
  34. package/src/resources/extensions/gsd/doctor-checks.ts +0 -75
  35. package/src/resources/extensions/gsd/doctor-environment.ts +1 -82
  36. package/src/resources/extensions/gsd/doctor-format.ts +0 -20
  37. package/src/resources/extensions/gsd/doctor-types.ts +1 -16
  38. package/src/resources/extensions/gsd/doctor.ts +13 -177
  39. package/src/resources/extensions/gsd/tests/cmux.test.ts +0 -93
  40. package/src/resources/extensions/gsd/tests/worktree.test.ts +0 -47
  41. package/src/resources/extensions/gsd/worktree.ts +15 -35
  42. package/src/resources/extensions/subagent/index.ts +3 -12
  43. package/dist/welcome-screen.d.ts +0 -12
  44. package/dist/welcome-screen.js +0 -53
  45. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +0 -266
package/dist/cli.js CHANGED
@@ -505,15 +505,6 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
505
505
  session.setScopedModels(scopedModels);
506
506
  }
507
507
  }
508
- // Welcome screen — shown on every fresh interactive session before TUI takes over
509
- {
510
- const { printWelcomeScreen } = await import('./welcome-screen.js');
511
- printWelcomeScreen({
512
- version: process.env.GSD_VERSION || '0.0.0',
513
- modelName: settingsManager.getDefaultModel() || undefined,
514
- provider: settingsManager.getDefaultProvider() || undefined,
515
- });
516
- }
517
508
  const interactiveMode = new InteractiveMode(session);
518
509
  markStartup('InteractiveMode');
519
510
  printStartupTimings();
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * Resolves the entry-point file(s) for a single extension directory.
3
3
  *
4
- * 1. If the directory contains a package.json with a `pi` manifest object,
5
- * the manifest is authoritative:
6
- * - `pi.extensions` array resolve each entry relative to the directory.
7
- * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
8
- * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
4
+ * 1. If the directory contains a package.json with a `pi.extensions` array,
5
+ * each entry is resolved relative to the directory and returned (if it exists).
6
+ * 2. Otherwise falls back to `index.ts` `index.js`.
9
7
  */
10
8
  export declare function resolveExtensionEntries(dir: string): string[];
11
9
  /**
@@ -6,29 +6,24 @@ function isExtensionFile(name) {
6
6
  /**
7
7
  * Resolves the entry-point file(s) for a single extension directory.
8
8
  *
9
- * 1. If the directory contains a package.json with a `pi` manifest object,
10
- * the manifest is authoritative:
11
- * - `pi.extensions` array resolve each entry relative to the directory.
12
- * - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
13
- * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
9
+ * 1. If the directory contains a package.json with a `pi.extensions` array,
10
+ * each entry is resolved relative to the directory and returned (if it exists).
11
+ * 2. Otherwise falls back to `index.ts` `index.js`.
14
12
  */
15
13
  export function resolveExtensionEntries(dir) {
16
14
  const packageJsonPath = join(dir, 'package.json');
17
15
  if (existsSync(packageJsonPath)) {
18
16
  try {
19
17
  const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
20
- if (pkg?.pi && typeof pkg.pi === 'object') {
21
- // When a pi manifest exists, it is authoritative — don't fall through
22
- // to index.ts/index.js auto-detection. This allows library directories
23
- // (like cmux) to opt out by declaring "pi": {} with no extensions.
24
- const declared = pkg.pi.extensions;
25
- if (!Array.isArray(declared) || declared.length === 0) {
26
- return [];
27
- }
28
- return declared
18
+ const declared = pkg?.pi?.extensions;
19
+ if (Array.isArray(declared)) {
20
+ const resolved = declared
29
21
  .filter((entry) => typeof entry === 'string')
30
22
  .map((entry) => resolve(dir, entry))
31
23
  .filter((entry) => existsSync(entry));
24
+ if (resolved.length > 0) {
25
+ return resolved;
26
+ }
32
27
  }
33
28
  }
34
29
  catch {
@@ -7,9 +7,7 @@
7
7
  "test": "node --test tests/*.test.mjs"
8
8
  },
9
9
  "pi": {
10
- "extensions": [
11
- "./index.js"
12
- ]
10
+ "extensions": ["./index.ts"]
13
11
  },
14
12
  "peerDependencies": {
15
13
  "playwright": ">=1.40.0",
@@ -237,14 +237,11 @@ export class CmuxClient {
237
237
  return extractSurfaceIds(parsed);
238
238
  }
239
239
  async createSplit(direction) {
240
- return this.createSplitFrom(this.config.surfaceId, direction);
241
- }
242
- async createSplitFrom(sourceSurfaceId, direction) {
243
240
  if (!this.config.splits)
244
241
  return null;
245
242
  const before = new Set(await this.listSurfaceIds());
246
243
  const args = ["new-split", direction];
247
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
244
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
248
245
  await this.runAsync(scopedArgs);
249
246
  const after = await this.listSurfaceIds();
250
247
  for (const id of after) {
@@ -253,57 +250,6 @@ export class CmuxClient {
253
250
  }
254
251
  return null;
255
252
  }
256
- /**
257
- * Create a grid of surfaces for parallel agent execution.
258
- *
259
- * Layout strategy (gsd stays in the original surface):
260
- * 1 agent: [gsd | A]
261
- * 2 agents: [gsd | A]
262
- * [ | B]
263
- * 3 agents: [gsd | A]
264
- * [ C | B]
265
- * 4 agents: [gsd | A]
266
- * [ C | B] (D splits from B downward)
267
- * [ | D]
268
- *
269
- * Returns surface IDs in order, or empty array on failure.
270
- */
271
- async createGridLayout(count) {
272
- if (!this.config.splits || count <= 0)
273
- return [];
274
- const surfaces = [];
275
- // First split: create right column from the gsd surface
276
- const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
277
- if (!rightCol)
278
- return [];
279
- surfaces.push(rightCol);
280
- if (count === 1)
281
- return surfaces;
282
- // Second split: split right column down → bottom-right
283
- const bottomRight = await this.createSplitFrom(rightCol, "down");
284
- if (!bottomRight)
285
- return surfaces;
286
- surfaces.push(bottomRight);
287
- if (count === 2)
288
- return surfaces;
289
- // Third split: split gsd surface down → bottom-left
290
- const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
291
- if (!bottomLeft)
292
- return surfaces;
293
- surfaces.push(bottomLeft);
294
- if (count === 3)
295
- return surfaces;
296
- // Fourth+: split subsequent surfaces down from the last created
297
- let lastSurface = bottomRight;
298
- for (let i = 3; i < count; i++) {
299
- const next = await this.createSplitFrom(lastSurface, "down");
300
- if (!next)
301
- break;
302
- surfaces.push(next);
303
- lastSurface = next;
304
- }
305
- return surfaces;
306
- }
307
253
  async sendSurface(surfaceId, text) {
308
254
  const payload = text.endsWith("\n") ? text : `${text}\n`;
309
255
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "pi": {
7
7
  "extensions": [
8
- "./index.js"
8
+ "./index.ts"
9
9
  ]
10
10
  }
11
11
  }
@@ -4,8 +4,6 @@
4
4
  "version": "1.0.0",
5
5
  "type": "module",
6
6
  "pi": {
7
- "extensions": [
8
- "./index.js"
9
- ]
7
+ "extensions": ["./index.ts"]
10
8
  }
11
9
  }
@@ -373,7 +373,7 @@ export async function autoLoop(ctx, pi, s, deps) {
373
373
  await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
374
374
  }
375
375
  const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
376
- if (incomplete.length === 0 && state.registry.length > 0) {
376
+ if (incomplete.length === 0) {
377
377
  // All milestones complete — merge milestone branch before stopping
378
378
  if (s.currentMilestoneId) {
379
379
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -382,12 +382,6 @@ export async function autoLoop(ctx, pi, s, deps) {
382
382
  deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
383
383
  await deps.stopAuto(ctx, pi, "All milestones complete");
384
384
  }
385
- else if (incomplete.length === 0 && state.registry.length === 0) {
386
- // Empty registry — no milestones visible, likely a path resolution bug
387
- const diag = `basePath=${s.basePath}, phase=${state.phase}`;
388
- ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
389
- await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
390
- }
391
385
  else if (state.phase === "blocked") {
392
386
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
393
387
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -303,16 +303,11 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
303
303
  // ── Auto-worktree setup ──
304
304
  s.originalBasePath = base;
305
305
  const isUnderGsdWorktrees = (p) => {
306
- // Direct layout: /.gsd/worktrees/
307
306
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
308
307
  if (p.includes(marker))
309
308
  return true;
310
309
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
311
- if (p.endsWith(worktreesSuffix))
312
- return true;
313
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
314
- const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`);
315
- return symlinkRe.test(p);
310
+ return p.endsWith(worktreesSuffix);
316
311
  };
317
312
  if (s.currentMilestoneId &&
318
313
  shouldUseWorktreeIsolation() &&
@@ -115,17 +115,10 @@ export function checkResourcesStale(versionOnStart) {
115
115
  * Returns the corrected base path.
116
116
  */
117
117
  export function escapeStaleWorktree(base) {
118
- // Direct layout: /.gsd/worktrees/
119
- const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
120
- let idx = base.indexOf(directMarker);
121
- if (idx === -1) {
122
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
123
- const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`);
124
- const match = base.match(symlinkRe);
125
- if (!match || match.index === undefined)
126
- return base;
127
- idx = match.index;
128
- }
118
+ const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
119
+ const idx = base.indexOf(marker);
120
+ if (idx === -1)
121
+ return base;
129
122
  // base is inside .gsd/worktrees/<something> — extract the project root
130
123
  const projectRoot = base.slice(0, idx);
131
124
  try {
@@ -30,16 +30,8 @@ const VALID_CLASSIFICATIONS = [
30
30
  */
31
31
  export function resolveCapturesPath(basePath) {
32
32
  const resolved = resolve(basePath);
33
- // Direct layout: /.gsd/worktrees/
34
33
  const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
35
- let idx = resolved.indexOf(worktreeMarker);
36
- if (idx === -1) {
37
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
38
- const symlinkRe = new RegExp(`\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`);
39
- const match = resolved.match(symlinkRe);
40
- if (match && match.index !== undefined)
41
- idx = match.index;
42
- }
34
+ const idx = resolved.indexOf(worktreeMarker);
43
35
  if (idx !== -1) {
44
36
  // basePath is inside a worktree — resolve to project root
45
37
  const projectRoot = resolved.slice(0, idx);
@@ -10,7 +10,7 @@ import { deriveState } from "./state.js";
10
10
  import { gsdRoot } from "./paths.js";
11
11
  import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
12
12
  import { appendOverride, appendKnowledge } from "./files.js";
13
- import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
13
+ import { formatDoctorIssuesForPrompt, formatDoctorReport, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
14
14
  import { isAutoActive } from "./auto.js";
15
15
  import { projectRoot } from "./commands.js";
16
16
  import { loadPrompt } from "./prompt-loader.js";
@@ -28,28 +28,15 @@ export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
28
28
  }
29
29
  export async function handleDoctor(args, ctx, pi) {
30
30
  const trimmed = args.trim();
31
- // Extract flags before positional parsing
32
- const jsonMode = trimmed.includes("--json");
33
- const dryRun = trimmed.includes("--dry-run");
34
- const includeBuild = trimmed.includes("--build");
35
- const includeTests = trimmed.includes("--test");
36
- const stripped = trimmed.replace(/--json|--dry-run|--build|--test/g, "").trim();
37
- const parts = stripped ? stripped.split(/\s+/) : [];
31
+ const parts = trimmed ? trimmed.split(/\s+/) : [];
38
32
  const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
39
33
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
40
34
  const scope = await selectDoctorScope(projectRoot(), requestedScope);
41
35
  const effectiveScope = mode === "audit" ? requestedScope : scope;
42
36
  const report = await runGSDDoctor(projectRoot(), {
43
- fix: mode === "fix" || mode === "heal" || dryRun,
44
- dryRun,
37
+ fix: mode === "fix" || mode === "heal",
45
38
  scope: effectiveScope,
46
- includeBuild,
47
- includeTests,
48
39
  });
49
- if (jsonMode) {
50
- ctx.ui.notify(formatDoctorReportJson(report), "info");
51
- return;
52
- }
53
40
  const reportText = formatDoctorReport(report, {
54
41
  scope: effectiveScope,
55
42
  includeWarnings: mode === "audit",
@@ -135,7 +135,7 @@ async function guardRemoteSession(ctx, pi) {
135
135
  }
136
136
  export function registerGSDCommand(pi) {
137
137
  pi.registerCommand("gsd", {
138
- description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|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",
138
+ description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
139
139
  getArgumentCompletions: (prefix) => {
140
140
  const subcommands = [
141
141
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -186,11 +186,7 @@ export function registerGSDCommand(pi) {
186
186
  { cmd: "templates", desc: "List available workflow templates" },
187
187
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
188
188
  ];
189
- const hasTrailingSpace = prefix.endsWith(" ");
190
189
  const parts = prefix.trim().split(/\s+/);
191
- if (hasTrailingSpace && parts.length >= 1) {
192
- parts.push("");
193
- }
194
190
  if (parts.length <= 1) {
195
191
  return subcommands
196
192
  .filter((item) => item.cmd.startsWith(parts[0] ?? ""))
@@ -472,10 +468,6 @@ export function registerGSDCommand(pi) {
472
468
  { cmd: "fix", desc: "Auto-fix detected issues" },
473
469
  { cmd: "heal", desc: "AI-driven deep healing" },
474
470
  { cmd: "audit", desc: "Run health audit without fixing" },
475
- { cmd: "--dry-run", desc: "Show what --fix would change without applying" },
476
- { cmd: "--json", desc: "Output report as JSON (CI/tooling friendly)" },
477
- { cmd: "--build", desc: "Include slow build health check (npm run build)" },
478
- { cmd: "--test", desc: "Include slow test health check (npm test)" },
479
471
  ];
480
472
  if (parts.length <= 2) {
481
473
  return modes
@@ -499,17 +491,6 @@ export function registerGSDCommand(pi) {
499
491
  .filter((p) => p.cmd.startsWith(phasePrefix))
500
492
  .map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
501
493
  }
502
- if (parts[0] === "rate" && parts.length <= 2) {
503
- const tierPrefix = parts[1] ?? "";
504
- const tiers = [
505
- { cmd: "over", desc: "Model was overqualified for this task" },
506
- { cmd: "ok", desc: "Model was appropriate for this task" },
507
- { cmd: "under", desc: "Model was underqualified for this task" },
508
- ];
509
- return tiers
510
- .filter((t) => t.cmd.startsWith(tierPrefix))
511
- .map((t) => ({ value: `rate ${t.cmd}`, label: t.cmd, description: t.desc }));
512
- }
513
494
  return [];
514
495
  },
515
496
  async handler(args, ctx) {
@@ -618,88 +618,6 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
618
618
  catch {
619
619
  // Non-fatal — external state check failed
620
620
  }
621
- // ── Metrics ledger integrity ───────────────────────────────────────────
622
- try {
623
- const metricsPath = join(root, "metrics.json");
624
- if (existsSync(metricsPath)) {
625
- try {
626
- const raw = readFileSync(metricsPath, "utf-8");
627
- const ledger = JSON.parse(raw);
628
- if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
629
- issues.push({
630
- severity: "warning",
631
- code: "metrics_ledger_corrupt",
632
- scope: "project",
633
- unitId: "project",
634
- message: "metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
635
- file: ".gsd/metrics.json",
636
- fixable: false,
637
- });
638
- }
639
- }
640
- catch {
641
- issues.push({
642
- severity: "warning",
643
- code: "metrics_ledger_corrupt",
644
- scope: "project",
645
- unitId: "project",
646
- message: "metrics.json is not valid JSON — metrics data may be corrupt",
647
- file: ".gsd/metrics.json",
648
- fixable: false,
649
- });
650
- }
651
- }
652
- }
653
- catch {
654
- // Non-fatal — metrics check failed
655
- }
656
- // ── Large planning file detection ──────────────────────────────────────
657
- // Files over 100KB can cause LLM context pressure. Report the worst offenders.
658
- try {
659
- const MAX_FILE_BYTES = 100 * 1024; // 100KB
660
- const milestonesPath = milestonesDir(basePath);
661
- if (existsSync(milestonesPath)) {
662
- const largeFiles = [];
663
- function scanForLargeFiles(dir, depth = 0) {
664
- if (depth > 6)
665
- return;
666
- try {
667
- for (const entry of readdirSync(dir)) {
668
- const full = join(dir, entry);
669
- try {
670
- const s = statSync(full);
671
- if (s.isDirectory()) {
672
- scanForLargeFiles(full, depth + 1);
673
- continue;
674
- }
675
- if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
676
- largeFiles.push({ path: full.replace(basePath + "/", ""), sizeKB: Math.round(s.size / 1024) });
677
- }
678
- }
679
- catch { /* skip entry */ }
680
- }
681
- }
682
- catch { /* skip dir */ }
683
- }
684
- scanForLargeFiles(milestonesPath);
685
- if (largeFiles.length > 0) {
686
- largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
687
- const worst = largeFiles[0];
688
- issues.push({
689
- severity: "warning",
690
- code: "large_planning_file",
691
- scope: "project",
692
- unitId: "project",
693
- message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
694
- file: worst.path,
695
- fixable: false,
696
- });
697
- }
698
- }
699
- }
700
- catch {
701
- // Non-fatal — large file scan failed
702
- }
703
621
  }
704
622
  /**
705
623
  * Build STATE.md markdown content from derived state.
@@ -355,63 +355,6 @@ function checkGitRemote(basePath) {
355
355
  }
356
356
  return { name: "git_remote", status: "ok", message: "Git remote reachable" };
357
357
  }
358
- /**
359
- * Check if the project build passes (opt-in slow check, use --build flag).
360
- * Runs npm run build and reports failure as env_build.
361
- */
362
- function checkBuildHealth(basePath) {
363
- const pkgPath = join(basePath, "package.json");
364
- if (!existsSync(pkgPath))
365
- return null;
366
- try {
367
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
368
- const buildScript = pkg.scripts?.build;
369
- if (!buildScript)
370
- return null;
371
- const result = tryExec("npm run build 2>&1", basePath);
372
- if (result === null) {
373
- return {
374
- name: "build",
375
- status: "error",
376
- message: "Build failed — npm run build exited non-zero",
377
- detail: "Fix build errors before dispatching work",
378
- };
379
- }
380
- return { name: "build", status: "ok", message: "Build passes" };
381
- }
382
- catch {
383
- return null;
384
- }
385
- }
386
- /**
387
- * Check if tests pass (opt-in slow check, use --test flag).
388
- * Runs npm test and reports failures as env_test.
389
- */
390
- function checkTestHealth(basePath) {
391
- const pkgPath = join(basePath, "package.json");
392
- if (!existsSync(pkgPath))
393
- return null;
394
- try {
395
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
396
- const testScript = pkg.scripts?.test;
397
- // Skip if no test script or the default placeholder
398
- if (!testScript || testScript.includes("no test specified"))
399
- return null;
400
- const result = tryExec("npm test 2>&1", basePath);
401
- if (result === null) {
402
- return {
403
- name: "test",
404
- status: "warning",
405
- message: "Tests failing — npm test exited non-zero",
406
- detail: "Fix failing tests before shipping",
407
- };
408
- }
409
- return { name: "test", status: "ok", message: "Tests pass" };
410
- }
411
- catch {
412
- return null;
413
- }
414
- }
415
358
  // ── Public API ─────────────────────────────────────────────────────────────
416
359
  /**
417
360
  * Run all environment health checks. Returns structured results for
@@ -451,24 +394,6 @@ export function runFullEnvironmentChecks(basePath) {
451
394
  results.push(remoteCheck);
452
395
  return results;
453
396
  }
454
- /**
455
- * Run slow opt-in checks (build and/or test).
456
- * These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
457
- */
458
- export function runSlowEnvironmentChecks(basePath, options) {
459
- const results = [];
460
- if (options?.includeBuild) {
461
- const buildCheck = checkBuildHealth(basePath);
462
- if (buildCheck)
463
- results.push(buildCheck);
464
- }
465
- if (options?.includeTests) {
466
- const testCheck = checkTestHealth(basePath);
467
- if (testCheck)
468
- results.push(testCheck);
469
- }
470
- return results;
471
- }
472
397
  /**
473
398
  * Convert environment check results to DoctorIssue format for the doctor pipeline.
474
399
  */
@@ -492,9 +417,6 @@ export async function checkEnvironmentHealth(basePath, issues, options) {
492
417
  const results = options?.includeRemote
493
418
  ? runFullEnvironmentChecks(basePath)
494
419
  : runEnvironmentChecks(basePath);
495
- if (options?.includeBuild || options?.includeTests) {
496
- results.push(...runSlowEnvironmentChecks(basePath, options));
497
- }
498
420
  issues.push(...environmentResultsToDoctorIssues(results));
499
421
  }
500
422
  /**
@@ -69,18 +69,3 @@ export function formatDoctorIssuesForPrompt(issues) {
69
69
  return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
70
70
  }).join("\n");
71
71
  }
72
- /**
73
- * Serialize a doctor report to JSON — suitable for CI/tooling integration.
74
- * Usage: /gsd doctor --json
75
- */
76
- export function formatDoctorReportJson(report) {
77
- return JSON.stringify({
78
- ok: report.ok,
79
- basePath: report.basePath,
80
- generatedAt: new Date().toISOString(),
81
- summary: summarizeDoctorIssues(report.issues),
82
- issues: report.issues,
83
- fixesApplied: report.fixesApplied,
84
- ...(report.timing ? { timing: report.timing } : {}),
85
- }, null, 2);
86
- }