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
@@ -16,6 +16,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
16
16
  import { installNotifyInterceptor } from "./notify-interceptor.js";
17
17
  import { initNotificationStore } from "../notification-store.js";
18
18
  import { initNotificationWidget } from "../notification-widget.js";
19
+ import { extractSubagentAgentClasses } from "./subagent-input.js";
19
20
  // Skip the welcome screen on the very first session_start — cli.ts already
20
21
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
21
22
  let isFirstSession = true;
@@ -358,6 +359,7 @@ export function registerHooks(pi, ecosystemHandlers) {
358
359
  const manifest = resolveManifest(activeUnitType);
359
360
  if (manifest) {
360
361
  let planningInput = "";
362
+ let agentClasses;
361
363
  if (isToolCallEventType("write", event)) {
362
364
  planningInput = event.input.path;
363
365
  }
@@ -367,7 +369,11 @@ export function registerHooks(pi, ecosystemHandlers) {
367
369
  else if (isToolCallEventType("bash", event)) {
368
370
  planningInput = event.input.command;
369
371
  }
370
- const planningGuard = shouldBlockPlanningUnit(event.toolName, planningInput, dash.basePath || discussionBasePath, activeUnitType, manifest.tools);
372
+ else if (event.toolName === "subagent" || event.toolName === "task") {
373
+ // Subagent inputs use { agent }, { tasks: [{ agent }] }, or { chain: [{ agent }] }.
374
+ agentClasses = extractSubagentAgentClasses(event.input);
375
+ }
376
+ const planningGuard = shouldBlockPlanningUnit(event.toolName, planningInput, dash.basePath || discussionBasePath, activeUnitType, manifest.tools, agentClasses);
371
377
  if (planningGuard.block)
372
378
  return planningGuard;
373
379
  }
@@ -401,6 +407,18 @@ export function registerHooks(pi, ecosystemHandlers) {
401
407
  return;
402
408
  markToolStart(event.toolCallId, event.toolName);
403
409
  safetyRecordToolCall(event.toolCallId, event.toolName, event.input);
410
+ // Persist immediately at dispatch so a mid-unit re-dispatch — which calls
411
+ // resetEvidence() + loadEvidenceFromDisk() in runUnitPhase — cannot wipe
412
+ // the entry between tool_call and tool_execution_end. Without this, the
413
+ // race window equals the tool's runtime, producing the "no bash calls"
414
+ // false positive when the LLM clearly ran a verification command.
415
+ const callDash = getAutoRuntimeSnapshot();
416
+ if (callDash.basePath && callDash.currentUnit?.type === "execute-task") {
417
+ const { milestone: cMid, slice: cSid, task: cTid } = parseUnitId(callDash.currentUnit.id);
418
+ if (cMid && cSid && cTid) {
419
+ saveEvidenceToDisk(callDash.basePath, cMid, cSid, cTid);
420
+ }
421
+ }
404
422
  // Destructive command classification (warn only, never block)
405
423
  if (isToolCallEventType("bash", event)) {
406
424
  const classification = classifyCommand(event.input.command);
@@ -0,0 +1,22 @@
1
+ export function extractSubagentAgentClasses(input) {
2
+ if (!input || typeof input !== "object")
3
+ return [];
4
+ const record = input;
5
+ const agentClasses = [];
6
+ const addAgentClass = (value) => {
7
+ if (typeof value === "string" && value.trim().length > 0)
8
+ agentClasses.push(value.trim());
9
+ };
10
+ const addFromItems = (value) => {
11
+ if (!Array.isArray(value))
12
+ return;
13
+ for (const item of value) {
14
+ if (item && typeof item === "object")
15
+ addAgentClass(item.agent);
16
+ }
17
+ };
18
+ addAgentClass(record.agent);
19
+ addFromItems(record.tasks);
20
+ addFromItems(record.chain);
21
+ return agentClasses;
22
+ }
@@ -47,6 +47,17 @@ export const BUNDLED_SKILL_TRIGGERS = [
47
47
  { trigger: "HTTP/REST/GraphQL API design — verbs, status codes, pagination, errors, idempotency, versioning", skill: "api-design" },
48
48
  { trigger: "Dependency upgrades — risk-batched, verified between batches, one major per commit", skill: "dependency-upgrade" },
49
49
  { trigger: "Agent-first observability — structured logs, persisted failure state, health surfaces, explicit failure modes", skill: "observability" },
50
+ { trigger: "React/Next.js performance — components, data fetching, bundle optimization, rendering patterns from Vercel Engineering", skill: "react-best-practices" },
51
+ { trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
52
+ { trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
53
+ { trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
54
+ { trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
55
+ { trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
56
+ { trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
57
+ { trigger: "Author or refine a GSD skill — SKILL.md structure, frontmatter, and best practices", skill: "create-skill" },
58
+ { trigger: "Create or debug a GSD extension — tools, commands, event hooks, custom TUI, providers", skill: "create-gsd-extension" },
59
+ { trigger: "Author a YAML workflow definition — steps, triggers, and templates", skill: "create-workflow" },
60
+ { 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" },
50
61
  ];
51
62
  function buildBundledSkillsTable() {
52
63
  const cwd = process.cwd();
@@ -1,6 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { minimatch } from "minimatch";
4
+ import { logWarning } from "../workflow-logger.js";
4
5
  /**
5
6
  * Regex matching milestone CONTEXT.md file names in both legacy M001
6
7
  * and unique M001-abc123 formats. Exported so regex-hardening tests
@@ -458,6 +459,42 @@ export function shouldBlockQueueExecutionInSnapshot(snapshot, toolName, input, q
458
459
  // guards.
459
460
  const PLANNING_WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "notebook_edit"]);
460
461
  const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
462
+ /**
463
+ * Canonical registry for agents that planning-dispatch may consider. Unit
464
+ * manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
465
+ */
466
+ const PLANNING_DISPATCH_AGENT_REGISTRY = {
467
+ scout: { readOnlySpecialist: true },
468
+ planner: { readOnlySpecialist: true },
469
+ reviewer: { readOnlySpecialist: true },
470
+ security: { readOnlySpecialist: true },
471
+ tester: { readOnlySpecialist: true },
472
+ };
473
+ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set(Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
474
+ .filter(([, metadata]) => metadata.readOnlySpecialist)
475
+ .map(([agentId]) => agentId));
476
+ let warnedMissingPlanningDispatchAgentClasses = false;
477
+ function isReadOnlySpecialist(agentId) {
478
+ const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId];
479
+ return metadata?.readOnlySpecialist === true;
480
+ }
481
+ function allowedPlanningDispatchAgentsList() {
482
+ return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
483
+ }
484
+ function warnMissingPlanningDispatchAgentClasses(unitType, mode, toolName) {
485
+ if (warnedMissingPlanningDispatchAgentClasses)
486
+ return;
487
+ warnedMissingPlanningDispatchAgentClasses = true;
488
+ // TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
489
+ const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
490
+ `on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
491
+ console.warn(message);
492
+ logWarning("intercept", message, {
493
+ unitType,
494
+ mode,
495
+ toolName,
496
+ });
497
+ }
461
498
  /**
462
499
  * Read-only / planning-safe tools that any non-"all" mode allows. Mirrors
463
500
  * QUEUE_SAFE_TOOLS / GATE_SAFE_TOOLS but is the inclusive default for
@@ -501,6 +538,10 @@ function blockReason(unitType, mode, what) {
501
538
  * - "read-only" → blocks all writes, bash, and subagent dispatch.
502
539
  * - "planning" → blocks writes to paths outside <basePath>/.gsd/,
503
540
  * bash that isn't read-only, and subagent dispatch.
541
+ * - "planning-dispatch"
542
+ * → like "planning", but permits subagent dispatch only
543
+ * when every forwarded agent class is globally allowed
544
+ * and listed in the policy's allowedSubagents.
504
545
  * - "docs" → like "planning" but also allows writes to paths
505
546
  * matching `allowedPathGlobs` relative to basePath.
506
547
  *
@@ -510,8 +551,13 @@ function blockReason(unitType, mode, what) {
510
551
  * `policy` of null means "no manifest resolved" — pass-through. Callers
511
552
  * that have no active unit (interactive sessions) pass null and this
512
553
  * predicate is a no-op.
554
+ *
555
+ * `agentClasses` is supplied by the tool hook for subagent-shaped calls. If
556
+ * absent, planning-dispatch fails closed so stale callers cannot silently
557
+ * bypass the agent allowlists. An explicitly supplied-but-empty list is
558
+ * allowed through so the downstream tool call can reject the malformed input.
513
559
  */
514
- export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy) {
560
+ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy, agentClasses) {
515
561
  if (!policy)
516
562
  return { block: false };
517
563
  if (policy.mode === "all")
@@ -529,12 +575,48 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
529
575
  // Unknown tool in read-only mode — block by default.
530
576
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
531
577
  }
532
- // planning / docs modes share the same surface for safe tools, bash, and subagent.
578
+ // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
533
579
  if (PLANNING_SAFE_TOOLS.has(tool))
534
580
  return { block: false };
535
581
  if (tool.startsWith("gsd_"))
536
582
  return { block: false };
537
583
  if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
584
+ if (policy.mode === "planning-dispatch") {
585
+ const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
586
+ const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
587
+ const allowed = new Set(allowedSubagents);
588
+ // When agentClasses is undefined, the caller has not been updated to extract
589
+ // agent identities yet. Block and warn so stale callers surface in telemetry
590
+ // instead of silently bypassing the gate.
591
+ if (agentClasses === undefined) {
592
+ warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
593
+ return {
594
+ block: true,
595
+ reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
596
+ };
597
+ }
598
+ // agentClasses was explicitly provided but resolved to an empty list (for
599
+ // example, a bare tool call with no agent field). Pass through; no agents
600
+ // to validate means the downstream tool call itself will fail.
601
+ if (requested.length === 0) {
602
+ return { block: false };
603
+ }
604
+ const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
605
+ if (globallyDisallowed) {
606
+ return {
607
+ block: true,
608
+ reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`),
609
+ };
610
+ }
611
+ const disallowedByPolicy = requested.find(a => !allowed.has(a));
612
+ if (disallowedByPolicy) {
613
+ return {
614
+ block: true,
615
+ reason: blockReason(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`),
616
+ };
617
+ }
618
+ return { block: false };
619
+ }
538
620
  return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
539
621
  }
540
622
  if (tool === "bash") {
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { loadRegistry } from "../workflow-templates.js";
5
5
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
6
- export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|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";
6
+ export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|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";
7
7
  export const TOP_LEVEL_SUBCOMMANDS = [
8
8
  { cmd: "help", desc: "Categorized command reference with descriptions" },
9
9
  { cmd: "next", desc: "Explicit step mode (same as /gsd)" },
@@ -71,6 +71,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
71
71
  { cmd: "add-tests", desc: "Generate tests for completed slices" },
72
72
  { cmd: "scan", desc: "Rapid codebase assessment — lightweight alternative to full map (--focus tech|arch|quality|concerns|tech+arch)" },
73
73
  { cmd: "language", desc: "Set or clear the global response language (e.g. /gsd language Chinese)" },
74
+ { cmd: "worktree", desc: "Manage worktrees from the TUI (list, merge, clean, remove)" },
74
75
  ];
75
76
  const NESTED_COMPLETIONS = {
76
77
  auto: [
@@ -286,6 +287,12 @@ const NESTED_COMPLETIONS = {
286
287
  { cmd: "off", desc: "Clear the language preference (revert to default)" },
287
288
  { cmd: "clear", desc: "Alias for off — clear the language preference" },
288
289
  ],
290
+ worktree: [
291
+ { cmd: "list", desc: "Show all worktrees with status" },
292
+ { cmd: "merge", desc: "Merge a worktree into main and clean up" },
293
+ { cmd: "clean", desc: "Remove all merged/empty worktrees" },
294
+ { cmd: "remove", desc: "Remove a worktree (--force to skip safety checks)" },
295
+ ],
289
296
  };
290
297
  function filterOptions(partial, options, prefix = "") {
291
298
  const normalizedPrefix = prefix ? `${prefix} ` : "";
@@ -124,6 +124,7 @@ export function showHelp(ctx, args = "") {
124
124
  " /gsd forensics Examine execution logs and post-mortem analysis",
125
125
  " /gsd export Export milestone/slice results [--json|--markdown|--html] [--all]",
126
126
  " /gsd cleanup Remove merged branches or snapshots [branches|snapshots]",
127
+ " /gsd worktree Manage worktrees from the TUI [list|merge|clean|remove]",
127
128
  " /gsd migrate Migrate .planning/ (v1) to .gsd/ (v2) format",
128
129
  " /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
129
130
  " /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
@@ -259,5 +259,13 @@ Examples:
259
259
  await handleScan(trimmed.replace(/^scan\s*/, "").trim(), ctx, pi);
260
260
  return true;
261
261
  }
262
+ if (trimmed === "worktree" ||
263
+ trimmed.startsWith("worktree ") ||
264
+ trimmed === "wt" ||
265
+ trimmed.startsWith("wt ")) {
266
+ const { handleWorktree } = await import("../../commands-worktree.js");
267
+ await handleWorktree(trimmed.replace(/^(worktree|wt)\s*/, "").trim(), ctx);
268
+ return true;
269
+ }
262
270
  return false;
263
271
  }
@@ -6,6 +6,7 @@
6
6
  import { AuthStorage } from "@gsd/pi-coding-agent";
7
7
  import { existsSync, mkdirSync } from "node:fs";
8
8
  import { join, dirname } from "node:path";
9
+ import { getHomeDir } from "./home-dir.js";
9
10
  /**
10
11
  * Tool API key configurations.
11
12
  * This is the source of truth for tool credentials - used by both the config wizard
@@ -29,7 +30,7 @@ function getStoredToolKey(auth, providerId) {
29
30
  */
30
31
  export function loadToolApiKeys() {
31
32
  try {
32
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
33
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
33
34
  if (!existsSync(authPath))
34
35
  return;
35
36
  const auth = AuthStorage.create(authPath);
@@ -45,7 +46,7 @@ export function loadToolApiKeys() {
45
46
  }
46
47
  }
47
48
  export function getConfigAuthStorage() {
48
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
49
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
49
50
  mkdirSync(dirname(authPath), { recursive: true });
50
51
  return AuthStorage.create(authPath);
51
52
  }
@@ -10,7 +10,50 @@ import { dirname, join, resolve } from "node:path";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { lockSync, unlockSync } from "proper-lockfile";
13
- import semver from "semver";
13
+ /**
14
+ * Strict numeric comparison of two npm-style version strings.
15
+ *
16
+ * Returns true when `a` is strictly greater than `b`. Compares the dotted
17
+ * release components numerically (so `1.10.0` > `1.9.0`) and treats any
18
+ * prerelease suffix (`-beta.1`, `-rc.2`) as less than the equivalent
19
+ * release version (`1.0.0` > `1.0.0-beta.1`). Sufficient for npm package
20
+ * version comparison in the extension installer; we don't need the full
21
+ * semver range/intersect machinery here.
22
+ *
23
+ * Replaces the earlier `import semver from "semver"` — that import broke
24
+ * `tsc -p tsconfig.json` whenever `@types/semver` failed to install
25
+ * (Issue #4946) because the file is pulled in transitively despite being
26
+ * under the `src/resources` exclude.
27
+ */
28
+ export function isVersionGreater(a, b) {
29
+ const split = (v) => {
30
+ const dash = v.indexOf("-");
31
+ const release = (dash === -1 ? v : v.slice(0, dash))
32
+ .split(".")
33
+ .map(part => Number.parseInt(part, 10) || 0);
34
+ const pre = dash === -1 ? null : v.slice(dash + 1);
35
+ return { release, pre };
36
+ };
37
+ const sa = split(a);
38
+ const sb = split(b);
39
+ const len = Math.max(sa.release.length, sb.release.length);
40
+ for (let i = 0; i < len; i++) {
41
+ const ai = sa.release[i] ?? 0;
42
+ const bi = sb.release[i] ?? 0;
43
+ if (ai !== bi)
44
+ return ai > bi;
45
+ }
46
+ // Release components equal — a release version beats any prerelease,
47
+ // and prerelease strings are compared lexicographically (good enough
48
+ // for `beta.1` vs `beta.2`, the only realistic case here).
49
+ if (sa.pre === null && sb.pre !== null)
50
+ return true;
51
+ if (sa.pre !== null && sb.pre === null)
52
+ return false;
53
+ if (sa.pre !== null && sb.pre !== null)
54
+ return sa.pre > sb.pre;
55
+ return false;
56
+ }
14
57
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
58
  // ─── Registry I/O ───────────────────────────────────────────────────────────
16
59
  function getRegistryPath() {
@@ -413,7 +456,7 @@ async function updateSingleExtension(id, registry, ctx) {
413
456
  ctx.ui.notify(`Could not fetch latest version for "${id}".`, "warning");
414
457
  return;
415
458
  }
416
- if (semver.gt(latest, current)) {
459
+ if (isVersionGreater(latest, current)) {
417
460
  ctx.ui.notify(`Updating "${id}": v${current} → v${latest}...`, "info");
418
461
  await handleInstall(packageName, ctx);
419
462
  }
@@ -464,7 +507,7 @@ async function updateAllExtensions(registry, ctx) {
464
507
  skipped++;
465
508
  continue;
466
509
  }
467
- if (semver.gt(latest, current)) {
510
+ if (isVersionGreater(latest, current)) {
468
511
  ctx.ui.notify(` ${entry.id}: v${current} → v${latest} (updating)`, "info");
469
512
  await handleInstall(packageName, ctx);
470
513
  updated++;
@@ -9,6 +9,7 @@ import { join, resolve as resolvePath, sep } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { deriveState } from "./state.js";
11
11
  import { gsdRoot } from "./paths.js";
12
+ import { getHomeDir } from "./home-dir.js";
12
13
  import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
13
14
  import { appendOverride, appendKnowledge } from "./files.js";
14
15
  import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
@@ -60,7 +61,7 @@ async function fetchLatestVersionForCommand() {
60
61
  }
61
62
  }
62
63
  export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
63
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
64
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
64
65
  const workflow = readFileSync(workflowPath, "utf-8");
65
66
  const prompt = loadPrompt("doctor-heal", {
66
67
  doctorSummary: reportText,
@@ -210,7 +211,7 @@ export async function handleTriage(ctx, pi, basePath) {
210
211
  currentPlan: currentPlan || "(no active slice plan)",
211
212
  roadmapContext: roadmapContext || "(no active roadmap)",
212
213
  });
213
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
214
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
214
215
  const workflow = readFileSync(workflowPath, "utf-8");
215
216
  pi.sendMessage({
216
217
  customType: "gsd-triage",