gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.8a893322c

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 (147) hide show
  1. package/README.md +1 -0
  2. package/dist/bundled-resource-path.d.ts +7 -0
  3. package/dist/bundled-resource-path.js +34 -2
  4. package/dist/claude-cli-check.js +18 -6
  5. package/dist/headless-query.js +21 -6
  6. package/dist/loader.js +2 -3
  7. package/dist/resource-loader.js +2 -8
  8. package/dist/resources/.managed-resources-content-hash +1 -1
  9. package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
  10. package/dist/resources/extensions/google-search/index.js +2 -6
  11. package/dist/resources/extensions/gsd/auto/phases.js +3 -11
  12. package/dist/resources/extensions/gsd/auto/session.js +2 -6
  13. package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
  14. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
  15. package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
  16. package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -1
  18. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -0
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
  21. package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
  22. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  23. package/dist/resources/extensions/gsd/commands/handlers/ops.js +8 -0
  24. package/dist/resources/extensions/gsd/commands-config.js +3 -2
  25. package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
  26. package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
  27. package/dist/resources/extensions/gsd/commands-worktree.js +309 -0
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  29. package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
  30. package/dist/resources/extensions/gsd/forensics.js +8 -6
  31. package/dist/resources/extensions/gsd/guided-flow.js +2 -1
  32. package/dist/resources/extensions/gsd/home-dir.js +16 -0
  33. package/dist/resources/extensions/gsd/key-manager.js +2 -1
  34. package/dist/resources/extensions/gsd/migrate/command.js +3 -2
  35. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  38. package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  39. package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
  40. package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
  41. package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
  42. package/dist/resources/extensions/gsd/worktree-root.js +124 -0
  43. package/dist/resources/extensions/gsd/worktree.js +4 -115
  44. package/dist/resources/extensions/mcp-client/index.js +0 -6
  45. package/dist/resources/extensions/ollama/index.js +15 -2
  46. package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
  47. package/dist/resources/extensions/ollama/ollama-client.js +40 -4
  48. package/dist/resources/extensions/subagent/index.js +324 -178
  49. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  50. package/dist/web/standalone/.next/BUILD_ID +1 -1
  51. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  52. package/dist/web/standalone/.next/build-manifest.json +2 -2
  53. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  54. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.html +1 -1
  71. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  78. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  80. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  81. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  82. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  83. package/dist/welcome-screen.js +27 -1
  84. package/dist/worktree-cli.d.ts +1 -0
  85. package/dist/worktree-cli.js +9 -3
  86. package/package.json +1 -3
  87. package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
  88. package/packages/native/tsconfig.tsbuildinfo +1 -1
  89. package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
  90. package/src/resources/extensions/google-search/index.ts +2 -9
  91. package/src/resources/extensions/gsd/auto/phases.ts +3 -11
  92. package/src/resources/extensions/gsd/auto/session.ts +2 -6
  93. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
  94. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
  95. package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
  96. package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
  97. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +19 -0
  98. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
  99. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -0
  100. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
  101. package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
  102. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  103. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  104. package/src/resources/extensions/gsd/commands-config.ts +3 -2
  105. package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
  106. package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
  107. package/src/resources/extensions/gsd/commands-worktree.ts +383 -0
  108. package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  109. package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
  110. package/src/resources/extensions/gsd/forensics.ts +10 -5
  111. package/src/resources/extensions/gsd/guided-flow.ts +2 -1
  112. package/src/resources/extensions/gsd/home-dir.ts +19 -0
  113. package/src/resources/extensions/gsd/journal.ts +4 -1
  114. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  115. package/src/resources/extensions/gsd/migrate/command.ts +3 -2
  116. package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  117. package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  118. package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  119. package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  120. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
  121. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +50 -27
  122. package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
  123. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +48 -0
  124. package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +25 -65
  125. package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
  126. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
  127. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
  128. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +34 -0
  129. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
  130. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
  131. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
  132. package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
  133. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
  134. package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
  135. package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
  136. package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
  137. package/src/resources/extensions/gsd/worktree-root.ts +144 -0
  138. package/src/resources/extensions/gsd/worktree.ts +8 -119
  139. package/src/resources/extensions/mcp-client/index.ts +0 -7
  140. package/src/resources/extensions/ollama/index.ts +16 -2
  141. package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
  142. package/src/resources/extensions/ollama/ollama-client.ts +41 -4
  143. package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
  144. package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
  145. package/src/resources/extensions/subagent/index.ts +165 -7
  146. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_buildManifest.js +0 -0
  147. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_ssgManifest.js +0 -0
@@ -46,6 +46,11 @@ import {
46
46
  resolveGitHeadPath,
47
47
  nudgeGitBranchCache,
48
48
  } from "./worktree.js";
49
+ import {
50
+ isGsdWorktreePath,
51
+ normalizeWorktreePathForCompare,
52
+ resolveWorktreeProjectRoot,
53
+ } from "./worktree-root.js";
49
54
  import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
50
55
  import { debugLog } from "./debug-logger.js";
51
56
  import { logWarning, logError } from "./workflow-logger.js";
@@ -1202,6 +1207,8 @@ export function createAutoWorktree(
1202
1207
  basePath: string,
1203
1208
  milestoneId: string,
1204
1209
  ): string {
1210
+ basePath = resolveWorktreeProjectRoot(basePath);
1211
+
1205
1212
  // Check if repo has commits — git worktree requires a valid HEAD
1206
1213
  try {
1207
1214
  execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: basePath, stdio: "pipe" });
@@ -1361,6 +1368,8 @@ export function teardownAutoWorktree(
1361
1368
  milestoneId: string,
1362
1369
  opts: { preserveBranch?: boolean } = {},
1363
1370
  ): void {
1371
+ originalBasePath = resolveWorktreeProjectRoot(originalBasePath);
1372
+
1364
1373
  const branch = autoWorktreeBranch(milestoneId);
1365
1374
  const { preserveBranch = false } = opts;
1366
1375
  const previousCwd = process.cwd();
@@ -1412,16 +1421,28 @@ export function teardownAutoWorktree(
1412
1421
 
1413
1422
  /**
1414
1423
  * Detect if the process is currently inside an auto-worktree.
1415
- * Checks both module state and git branch prefix.
1424
+ * Uses the current directory structure plus git branch prefix so detection
1425
+ * still works after process restart when module state has been reset.
1416
1426
  */
1417
1427
  export function isInAutoWorktree(basePath: string): boolean {
1418
- if (!originalBase) return false;
1419
1428
  const cwd = process.cwd();
1420
- const resolvedBase = existsSync(basePath) ? realpathSync(basePath) : basePath;
1421
- const wtDir = join(resolvedBase, ".gsd", "worktrees");
1422
- if (!cwd.startsWith(wtDir)) return false;
1423
- const branch = nativeGetCurrentBranch(cwd);
1424
- return branch.startsWith("milestone/");
1429
+ if (!isGsdWorktreePath(cwd)) return false;
1430
+
1431
+ const projectRoot = resolveWorktreeProjectRoot(basePath, originalBase);
1432
+ const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
1433
+ if (
1434
+ normalizeWorktreePathForCompare(projectRoot) !==
1435
+ normalizeWorktreePathForCompare(cwdProjectRoot)
1436
+ ) {
1437
+ return false;
1438
+ }
1439
+
1440
+ try {
1441
+ const branch = nativeGetCurrentBranch(cwd);
1442
+ return branch.startsWith("milestone/");
1443
+ } catch {
1444
+ return false;
1445
+ }
1425
1446
  }
1426
1447
 
1427
1448
  /**
@@ -1436,6 +1457,8 @@ export function getAutoWorktreePath(
1436
1457
  basePath: string,
1437
1458
  milestoneId: string,
1438
1459
  ): string | null {
1460
+ basePath = resolveWorktreeProjectRoot(basePath);
1461
+
1439
1462
  const p = worktreePath(basePath, milestoneId);
1440
1463
  if (!existsSync(p)) return null;
1441
1464
 
@@ -1464,6 +1487,8 @@ export function enterAutoWorktree(
1464
1487
  basePath: string,
1465
1488
  milestoneId: string,
1466
1489
  ): string {
1490
+ basePath = resolveWorktreeProjectRoot(basePath);
1491
+
1467
1492
  const p = worktreePath(basePath, milestoneId);
1468
1493
  if (!existsSync(p)) {
1469
1494
  throw new GSDError(
@@ -1520,6 +1545,10 @@ export function getAutoWorktreeOriginalBase(): string | null {
1520
1545
  return originalBase;
1521
1546
  }
1522
1547
 
1548
+ export function _resetAutoWorktreeOriginalBaseForTests(): void {
1549
+ originalBase = null;
1550
+ }
1551
+
1523
1552
  export function getActiveAutoWorktreeContext(): {
1524
1553
  originalBase: string;
1525
1554
  worktreeName: string;
@@ -1527,11 +1556,14 @@ export function getActiveAutoWorktreeContext(): {
1527
1556
  } | null {
1528
1557
  if (!originalBase) return null;
1529
1558
  const cwd = process.cwd();
1530
- const resolvedBase = existsSync(originalBase)
1531
- ? realpathSync(originalBase)
1532
- : originalBase;
1533
- const wtDir = join(resolvedBase, ".gsd", "worktrees");
1534
- if (!cwd.startsWith(wtDir)) return null;
1559
+ if (!isGsdWorktreePath(cwd)) return null;
1560
+ const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
1561
+ if (
1562
+ normalizeWorktreePathForCompare(cwdProjectRoot) !==
1563
+ normalizeWorktreePathForCompare(originalBase)
1564
+ ) {
1565
+ return null;
1566
+ }
1535
1567
  const worktreeName = detectWorktreeName(cwd);
1536
1568
  if (!worktreeName) return null;
1537
1569
  const branch = nativeGetCurrentBranch(cwd);
@@ -22,6 +22,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
22
22
  import { installNotifyInterceptor } from "./notify-interceptor.js";
23
23
  import { initNotificationStore } from "../notification-store.js";
24
24
  import { initNotificationWidget } from "../notification-widget.js";
25
+ import { extractSubagentAgentClasses } from "./subagent-input.js";
25
26
 
26
27
  // Skip the welcome screen on the very first session_start — cli.ts already
27
28
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
@@ -390,12 +391,16 @@ export function registerHooks(
390
391
  const manifest = resolveManifest(activeUnitType);
391
392
  if (manifest) {
392
393
  let planningInput = "";
394
+ let agentClasses: string[] | undefined;
393
395
  if (isToolCallEventType("write", event)) {
394
396
  planningInput = event.input.path;
395
397
  } else if (isToolCallEventType("edit", event)) {
396
398
  planningInput = event.input.path;
397
399
  } else if (isToolCallEventType("bash", event)) {
398
400
  planningInput = event.input.command;
401
+ } else if (event.toolName === "subagent" || event.toolName === "task") {
402
+ // Subagent inputs use { agent }, { tasks: [{ agent }] }, or { chain: [{ agent }] }.
403
+ agentClasses = extractSubagentAgentClasses((event as { input?: unknown }).input);
399
404
  }
400
405
  const planningGuard = shouldBlockPlanningUnit(
401
406
  event.toolName,
@@ -403,6 +408,7 @@ export function registerHooks(
403
408
  dash.basePath || discussionBasePath,
404
409
  activeUnitType,
405
410
  manifest.tools,
411
+ agentClasses,
406
412
  );
407
413
  if (planningGuard.block) return planningGuard;
408
414
  }
@@ -445,6 +451,19 @@ export function registerHooks(
445
451
  markToolStart(event.toolCallId, event.toolName);
446
452
  safetyRecordToolCall(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
447
453
 
454
+ // Persist immediately at dispatch so a mid-unit re-dispatch — which calls
455
+ // resetEvidence() + loadEvidenceFromDisk() in runUnitPhase — cannot wipe
456
+ // the entry between tool_call and tool_execution_end. Without this, the
457
+ // race window equals the tool's runtime, producing the "no bash calls"
458
+ // false positive when the LLM clearly ran a verification command.
459
+ const callDash = getAutoRuntimeSnapshot();
460
+ if (callDash.basePath && callDash.currentUnit?.type === "execute-task") {
461
+ const { milestone: cMid, slice: cSid, task: cTid } = parseUnitId(callDash.currentUnit.id);
462
+ if (cMid && cSid && cTid) {
463
+ saveEvidenceToDisk(callDash.basePath, cMid, cSid, cTid);
464
+ }
465
+ }
466
+
448
467
  // Destructive command classification (warn only, never block)
449
468
  if (isToolCallEventType("bash", event)) {
450
469
  const classification = classifyCommand(event.input.command);
@@ -0,0 +1,20 @@
1
+ export function extractSubagentAgentClasses(input: unknown): string[] {
2
+ if (!input || typeof input !== "object") return [];
3
+
4
+ const record = input as Record<string, unknown>;
5
+ const agentClasses: string[] = [];
6
+ const addAgentClass = (value: unknown): void => {
7
+ if (typeof value === "string" && value.trim().length > 0) agentClasses.push(value.trim());
8
+ };
9
+ const addFromItems = (value: unknown): void => {
10
+ if (!Array.isArray(value)) return;
11
+ for (const item of value) {
12
+ if (item && typeof item === "object") addAgentClass((item as Record<string, unknown>).agent);
13
+ }
14
+ };
15
+
16
+ addAgentClass(record.agent);
17
+ addFromItems(record.tasks);
18
+ addFromItems(record.chain);
19
+ return agentClasses;
20
+ }
@@ -52,6 +52,17 @@ export const BUNDLED_SKILL_TRIGGERS: Array<{ trigger: string; skill: string }> =
52
52
  { trigger: "HTTP/REST/GraphQL API design — verbs, status codes, pagination, errors, idempotency, versioning", skill: "api-design" },
53
53
  { trigger: "Dependency upgrades — risk-batched, verified between batches, one major per commit", skill: "dependency-upgrade" },
54
54
  { trigger: "Agent-first observability — structured logs, persisted failure state, health surfaces, explicit failure modes", skill: "observability" },
55
+ { trigger: "React/Next.js performance — components, data fetching, bundle optimization, rendering patterns from Vercel Engineering", skill: "react-best-practices" },
56
+ { trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
57
+ { trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
58
+ { trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
59
+ { trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
60
+ { trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
61
+ { trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
62
+ { trigger: "Author or refine a GSD skill — SKILL.md structure, frontmatter, and best practices", skill: "create-skill" },
63
+ { trigger: "Create or debug a GSD extension — tools, commands, event hooks, custom TUI, providers", skill: "create-gsd-extension" },
64
+ { trigger: "Author a YAML workflow definition — steps, triggers, and templates", skill: "create-workflow" },
65
+ { trigger: "Deep code optimization audit — perf anti-patterns, memory leaks, algorithmic complexity, bundle size, I/O, caching, dead code (parallel pattern-based hunt)", skill: "code-optimizer" },
55
66
  ];
56
67
 
57
68
  function buildBundledSkillsTable(): string {
@@ -4,6 +4,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { minimatch } from "minimatch";
5
5
 
6
6
  import type { ToolsPolicy } from "../unit-context-manifest.js";
7
+ import { logWarning } from "../workflow-logger.js";
7
8
 
8
9
  /**
9
10
  * Regex matching milestone CONTEXT.md file names in both legacy M001
@@ -546,6 +547,49 @@ export function shouldBlockQueueExecutionInSnapshot(
546
547
  const PLANNING_WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "notebook_edit"]);
547
548
  const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
548
549
 
550
+ /**
551
+ * Canonical registry for agents that planning-dispatch may consider. Unit
552
+ * manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
553
+ */
554
+ const PLANNING_DISPATCH_AGENT_REGISTRY = {
555
+ scout: { readOnlySpecialist: true },
556
+ planner: { readOnlySpecialist: true },
557
+ reviewer: { readOnlySpecialist: true },
558
+ security: { readOnlySpecialist: true },
559
+ tester: { readOnlySpecialist: true },
560
+ } as const satisfies Record<string, { readonly readOnlySpecialist: boolean }>;
561
+
562
+ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set<string>(
563
+ Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
564
+ .filter(([, metadata]) => metadata.readOnlySpecialist)
565
+ .map(([agentId]) => agentId),
566
+ );
567
+
568
+ let warnedMissingPlanningDispatchAgentClasses = false;
569
+
570
+ function isReadOnlySpecialist(agentId: string): boolean {
571
+ const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId as keyof typeof PLANNING_DISPATCH_AGENT_REGISTRY];
572
+ return metadata?.readOnlySpecialist === true;
573
+ }
574
+
575
+ function allowedPlanningDispatchAgentsList(): string {
576
+ return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
577
+ }
578
+
579
+ function warnMissingPlanningDispatchAgentClasses(unitType: string, mode: string, toolName: string): void {
580
+ if (warnedMissingPlanningDispatchAgentClasses) return;
581
+ warnedMissingPlanningDispatchAgentClasses = true;
582
+ // TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
583
+ const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
584
+ `on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
585
+ console.warn(message);
586
+ logWarning("intercept", message, {
587
+ unitType,
588
+ mode,
589
+ toolName,
590
+ });
591
+ }
592
+
549
593
  /**
550
594
  * Read-only / planning-safe tools that any non-"all" mode allows. Mirrors
551
595
  * QUEUE_SAFE_TOOLS / GATE_SAFE_TOOLS but is the inclusive default for
@@ -592,6 +636,10 @@ function blockReason(unitType: string, mode: string, what: string): string {
592
636
  * - "read-only" → blocks all writes, bash, and subagent dispatch.
593
637
  * - "planning" → blocks writes to paths outside <basePath>/.gsd/,
594
638
  * bash that isn't read-only, and subagent dispatch.
639
+ * - "planning-dispatch"
640
+ * → like "planning", but permits subagent dispatch only
641
+ * when every forwarded agent class is globally allowed
642
+ * and listed in the policy's allowedSubagents.
595
643
  * - "docs" → like "planning" but also allows writes to paths
596
644
  * matching `allowedPathGlobs` relative to basePath.
597
645
  *
@@ -601,6 +649,11 @@ function blockReason(unitType: string, mode: string, what: string): string {
601
649
  * `policy` of null means "no manifest resolved" — pass-through. Callers
602
650
  * that have no active unit (interactive sessions) pass null and this
603
651
  * predicate is a no-op.
652
+ *
653
+ * `agentClasses` is supplied by the tool hook for subagent-shaped calls. If
654
+ * absent, planning-dispatch fails closed so stale callers cannot silently
655
+ * bypass the agent allowlists. An explicitly supplied-but-empty list is
656
+ * allowed through so the downstream tool call can reject the malformed input.
604
657
  */
605
658
  export function shouldBlockPlanningUnit(
606
659
  toolName: string,
@@ -608,6 +661,7 @@ export function shouldBlockPlanningUnit(
608
661
  basePath: string,
609
662
  unitType: string,
610
663
  policy: ToolsPolicy | null | undefined,
664
+ agentClasses?: readonly string[],
611
665
  ): { block: boolean; reason?: string } {
612
666
  if (!policy) return { block: false };
613
667
  if (policy.mode === "all") return { block: false };
@@ -625,11 +679,59 @@ export function shouldBlockPlanningUnit(
625
679
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
626
680
  }
627
681
 
628
- // planning / docs modes share the same surface for safe tools, bash, and subagent.
682
+ // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
629
683
  if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
630
684
  if (tool.startsWith("gsd_")) return { block: false };
631
685
 
632
686
  if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
687
+ if (policy.mode === "planning-dispatch") {
688
+ const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
689
+ const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
690
+ const allowed = new Set(allowedSubagents);
691
+ // When agentClasses is undefined, the caller has not been updated to extract
692
+ // agent identities yet. Block and warn so stale callers surface in telemetry
693
+ // instead of silently bypassing the gate.
694
+ if (agentClasses === undefined) {
695
+ warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
696
+ return {
697
+ block: true,
698
+ reason: blockReason(
699
+ unitType,
700
+ policy.mode,
701
+ `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`,
702
+ ),
703
+ };
704
+ }
705
+ // agentClasses was explicitly provided but resolved to an empty list (for
706
+ // example, a bare tool call with no agent field). Pass through; no agents
707
+ // to validate means the downstream tool call itself will fail.
708
+ if (requested.length === 0) {
709
+ return { block: false };
710
+ }
711
+ const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
712
+ if (globallyDisallowed) {
713
+ return {
714
+ block: true,
715
+ reason: blockReason(
716
+ unitType,
717
+ policy.mode,
718
+ `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`,
719
+ ),
720
+ };
721
+ }
722
+ const disallowedByPolicy = requested.find(a => !allowed.has(a));
723
+ if (disallowedByPolicy) {
724
+ return {
725
+ block: true,
726
+ reason: blockReason(
727
+ unitType,
728
+ policy.mode,
729
+ `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`,
730
+ ),
731
+ };
732
+ }
733
+ return { block: false };
734
+ }
633
735
  return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
634
736
  }
635
737
 
@@ -14,7 +14,7 @@ export interface GsdCommandDefinition {
14
14
  type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
15
15
 
16
16
  export const GSD_COMMAND_DESCRIPTION =
17
- "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language";
17
+ "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language|worktree";
18
18
 
19
19
  export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
20
20
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -83,6 +83,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
83
83
  { cmd: "add-tests", desc: "Generate tests for completed slices" },
84
84
  { cmd: "scan", desc: "Rapid codebase assessment — lightweight alternative to full map (--focus tech|arch|quality|concerns|tech+arch)" },
85
85
  { cmd: "language", desc: "Set or clear the global response language (e.g. /gsd language Chinese)" },
86
+ { cmd: "worktree", desc: "Manage worktrees from the TUI (list, merge, clean, remove)" },
86
87
  ];
87
88
 
88
89
  const NESTED_COMPLETIONS: CompletionMap = {
@@ -299,6 +300,12 @@ const NESTED_COMPLETIONS: CompletionMap = {
299
300
  { cmd: "off", desc: "Clear the language preference (revert to default)" },
300
301
  { cmd: "clear", desc: "Alias for off — clear the language preference" },
301
302
  ],
303
+ worktree: [
304
+ { cmd: "list", desc: "Show all worktrees with status" },
305
+ { cmd: "merge", desc: "Merge a worktree into main and clean up" },
306
+ { cmd: "clean", desc: "Remove all merged/empty worktrees" },
307
+ { cmd: "remove", desc: "Remove a worktree (--force to skip safety checks)" },
308
+ ],
302
309
  };
303
310
 
304
311
  function filterOptions(
@@ -130,6 +130,7 @@ export function showHelp(ctx: ExtensionCommandContext, args = ""): void {
130
130
  " /gsd forensics Examine execution logs and post-mortem analysis",
131
131
  " /gsd export Export milestone/slice results [--json|--markdown|--html] [--all]",
132
132
  " /gsd cleanup Remove merged branches or snapshots [branches|snapshots]",
133
+ " /gsd worktree Manage worktrees from the TUI [list|merge|clean|remove]",
133
134
  " /gsd migrate Migrate .planning/ (v1) to .gsd/ (v2) format",
134
135
  " /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
135
136
  " /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
@@ -262,5 +262,15 @@ Examples:
262
262
  await handleScan(trimmed.replace(/^scan\s*/, "").trim(), ctx, pi);
263
263
  return true;
264
264
  }
265
+ if (
266
+ trimmed === "worktree" ||
267
+ trimmed.startsWith("worktree ") ||
268
+ trimmed === "wt" ||
269
+ trimmed.startsWith("wt ")
270
+ ) {
271
+ const { handleWorktree } = await import("../../commands-worktree.js");
272
+ await handleWorktree(trimmed.replace(/^(worktree|wt)\s*/, "").trim(), ctx);
273
+ return true;
274
+ }
265
275
  return false;
266
276
  }
@@ -8,6 +8,7 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
8
8
  import { AuthStorage } from "@gsd/pi-coding-agent";
9
9
  import { existsSync, mkdirSync } from "node:fs";
10
10
  import { join, dirname } from "node:path";
11
+ import { getHomeDir } from "./home-dir.js";
11
12
 
12
13
  /**
13
14
  * Tool API key configurations.
@@ -34,7 +35,7 @@ function getStoredToolKey(auth: AuthStorage, providerId: string): string | undef
34
35
  */
35
36
  export function loadToolApiKeys(): void {
36
37
  try {
37
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
38
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
38
39
  if (!existsSync(authPath)) return;
39
40
 
40
41
  const auth = AuthStorage.create(authPath);
@@ -50,7 +51,7 @@ export function loadToolApiKeys(): void {
50
51
  }
51
52
 
52
53
  export function getConfigAuthStorage(): AuthStorage {
53
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
54
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
54
55
  mkdirSync(dirname(authPath), { recursive: true });
55
56
  return AuthStorage.create(authPath);
56
57
  }
@@ -12,7 +12,47 @@ import { dirname, join, resolve } from "node:path";
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { lockSync, unlockSync } from "proper-lockfile";
15
- import semver from "semver";
15
+
16
+ /**
17
+ * Strict numeric comparison of two npm-style version strings.
18
+ *
19
+ * Returns true when `a` is strictly greater than `b`. Compares the dotted
20
+ * release components numerically (so `1.10.0` > `1.9.0`) and treats any
21
+ * prerelease suffix (`-beta.1`, `-rc.2`) as less than the equivalent
22
+ * release version (`1.0.0` > `1.0.0-beta.1`). Sufficient for npm package
23
+ * version comparison in the extension installer; we don't need the full
24
+ * semver range/intersect machinery here.
25
+ *
26
+ * Replaces the earlier `import semver from "semver"` — that import broke
27
+ * `tsc -p tsconfig.json` whenever `@types/semver` failed to install
28
+ * (Issue #4946) because the file is pulled in transitively despite being
29
+ * under the `src/resources` exclude.
30
+ */
31
+ export function isVersionGreater(a: string, b: string): boolean {
32
+ const split = (v: string): { release: number[]; pre: string | null } => {
33
+ const dash = v.indexOf("-");
34
+ const release = (dash === -1 ? v : v.slice(0, dash))
35
+ .split(".")
36
+ .map(part => Number.parseInt(part, 10) || 0);
37
+ const pre = dash === -1 ? null : v.slice(dash + 1);
38
+ return { release, pre };
39
+ };
40
+ const sa = split(a);
41
+ const sb = split(b);
42
+ const len = Math.max(sa.release.length, sb.release.length);
43
+ for (let i = 0; i < len; i++) {
44
+ const ai = sa.release[i] ?? 0;
45
+ const bi = sb.release[i] ?? 0;
46
+ if (ai !== bi) return ai > bi;
47
+ }
48
+ // Release components equal — a release version beats any prerelease,
49
+ // and prerelease strings are compared lexicographically (good enough
50
+ // for `beta.1` vs `beta.2`, the only realistic case here).
51
+ if (sa.pre === null && sb.pre !== null) return true;
52
+ if (sa.pre !== null && sb.pre === null) return false;
53
+ if (sa.pre !== null && sb.pre !== null) return sa.pre > sb.pre;
54
+ return false;
55
+ }
16
56
 
17
57
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
18
58
 
@@ -538,7 +578,7 @@ async function updateSingleExtension(
538
578
  return;
539
579
  }
540
580
 
541
- if (semver.gt(latest, current)) {
581
+ if (isVersionGreater(latest, current)) {
542
582
  ctx.ui.notify(`Updating "${id}": v${current} → v${latest}...`, "info");
543
583
  await handleInstall(packageName, ctx);
544
584
  } else {
@@ -599,7 +639,7 @@ async function updateAllExtensions(
599
639
  continue;
600
640
  }
601
641
 
602
- if (semver.gt(latest, current)) {
642
+ if (isVersionGreater(latest, current)) {
603
643
  ctx.ui.notify(` ${entry.id}: v${current} → v${latest} (updating)`, "info");
604
644
  await handleInstall(packageName, ctx);
605
645
  updated++;
@@ -11,6 +11,7 @@ import { join, resolve as resolvePath, sep } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { deriveState } from "./state.js";
13
13
  import { gsdRoot } from "./paths.js";
14
+ import { getHomeDir } from "./home-dir.js";
14
15
  import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
15
16
  import { appendOverride, appendKnowledge } from "./files.js";
16
17
  import {
@@ -68,7 +69,7 @@ async function fetchLatestVersionForCommand(): Promise<string | null> {
68
69
  }
69
70
 
70
71
  export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
71
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
72
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
72
73
  const workflow = readFileSync(workflowPath, "utf-8");
73
74
  const prompt = loadPrompt("doctor-heal", {
74
75
  doctorSummary: reportText,
@@ -255,7 +256,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
255
256
  roadmapContext: roadmapContext || "(no active roadmap)",
256
257
  });
257
258
 
258
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
259
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
259
260
  const workflow = readFileSync(workflowPath, "utf-8");
260
261
 
261
262
  pi.sendMessage(