gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc

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 (217) hide show
  1. package/README.md +15 -11
  2. package/dist/app-paths.js +1 -1
  3. package/dist/extension-registry.js +2 -2
  4. package/dist/remote-questions-config.js +2 -2
  5. package/dist/resource-loader.js +34 -1
  6. package/dist/resources/extensions/browser-tools/index.js +3 -1
  7. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  8. package/dist/resources/extensions/env-utils.js +29 -0
  9. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  10. package/dist/resources/extensions/github-sync/cli.js +284 -0
  11. package/dist/resources/extensions/github-sync/index.js +73 -0
  12. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  13. package/dist/resources/extensions/github-sync/sync.js +424 -0
  14. package/dist/resources/extensions/github-sync/templates.js +118 -0
  15. package/dist/resources/extensions/github-sync/types.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  18. package/dist/resources/extensions/gsd/auto-loop.js +636 -594
  19. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  20. package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
  21. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  23. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  24. package/dist/resources/extensions/gsd/auto.js +143 -96
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  27. package/dist/resources/extensions/gsd/commands.js +4 -2
  28. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  29. package/dist/resources/extensions/gsd/detection.js +1 -2
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  31. package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
  32. package/dist/resources/extensions/gsd/doctor.js +20 -1
  33. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +48 -9
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/git-service.js +30 -12
  38. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  39. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  40. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  41. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  42. package/dist/resources/extensions/gsd/index.js +24 -20
  43. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  44. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  46. package/dist/resources/extensions/gsd/paths.js +3 -0
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +22 -11
  51. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  52. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  55. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
  56. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  62. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  63. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  64. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  68. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  69. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  70. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  71. package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
  72. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  73. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  74. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  75. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  76. package/dist/resources/extensions/gsd/state.js +42 -23
  77. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  78. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  79. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  80. package/dist/resources/extensions/mcp-client/index.js +14 -1
  81. package/dist/resources/extensions/remote-questions/status.js +4 -1
  82. package/dist/resources/extensions/remote-questions/store.js +4 -1
  83. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  84. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  85. package/dist/resources/extensions/subagent/isolation.js +2 -1
  86. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  87. package/package.json +1 -1
  88. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  89. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  90. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  91. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  93. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  95. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  97. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  99. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/index.js +1 -1
  101. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  102. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  103. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  104. package/packages/pi-coding-agent/src/index.ts +1 -0
  105. package/src/resources/extensions/browser-tools/index.ts +3 -0
  106. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  107. package/src/resources/extensions/env-utils.ts +31 -0
  108. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  109. package/src/resources/extensions/github-sync/cli.ts +364 -0
  110. package/src/resources/extensions/github-sync/index.ts +93 -0
  111. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  112. package/src/resources/extensions/github-sync/sync.ts +556 -0
  113. package/src/resources/extensions/github-sync/templates.ts +183 -0
  114. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  115. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  116. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  117. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  118. package/src/resources/extensions/github-sync/types.ts +47 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  120. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  121. package/src/resources/extensions/gsd/auto-loop.ts +526 -545
  122. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  123. package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
  124. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  125. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  126. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  127. package/src/resources/extensions/gsd/auto.ts +139 -101
  128. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  129. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  130. package/src/resources/extensions/gsd/commands.ts +5 -3
  131. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  132. package/src/resources/extensions/gsd/detection.ts +2 -2
  133. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  134. package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
  135. package/src/resources/extensions/gsd/doctor.ts +22 -1
  136. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  137. package/src/resources/extensions/gsd/export.ts +1 -1
  138. package/src/resources/extensions/gsd/files.ts +51 -11
  139. package/src/resources/extensions/gsd/forensics.ts +1 -1
  140. package/src/resources/extensions/gsd/git-service.ts +44 -10
  141. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  142. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  143. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  144. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  145. package/src/resources/extensions/gsd/index.ts +24 -17
  146. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  147. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  149. package/src/resources/extensions/gsd/paths.ts +4 -0
  150. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  151. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  152. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  153. package/src/resources/extensions/gsd/preferences.ts +25 -11
  154. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  155. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  156. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  157. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  158. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
  159. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  160. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  161. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  162. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  165. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  166. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  170. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  174. package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
  175. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  176. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  177. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  178. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  179. package/src/resources/extensions/gsd/state.ts +39 -21
  180. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  181. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  182. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  183. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  184. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  186. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  187. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  188. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  189. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  190. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  191. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  193. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  194. package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
  195. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  196. package/src/resources/extensions/gsd/types.ts +18 -1
  197. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  198. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  199. package/src/resources/extensions/mcp-client/index.ts +17 -1
  200. package/src/resources/extensions/remote-questions/status.ts +5 -1
  201. package/src/resources/extensions/remote-questions/store.ts +5 -1
  202. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  203. package/src/resources/extensions/shared/frontmatter.ts +1 -1
  204. package/src/resources/extensions/subagent/isolation.ts +3 -1
  205. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  206. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  207. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  208. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  209. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  210. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  211. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  212. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  213. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  214. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  215. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  216. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  217. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -14,7 +14,7 @@ import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
  import { detectWorktreeName, SLICE_BRANCH_RE, } from "./worktree.js";
17
- import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAll, nativeResetPaths, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
17
+ import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
18
18
  import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
19
19
  import { getErrorMessage } from "./error-utils.js";
20
20
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -36,12 +36,19 @@ export function buildTaskCommitMessage(ctx) {
36
36
  : description;
37
37
  const subject = `${type}(${scope}): ${truncated}`;
38
38
  // Build body with key files if available
39
+ const bodyParts = [];
39
40
  if (ctx.keyFiles && ctx.keyFiles.length > 0) {
40
41
  const fileLines = ctx.keyFiles
41
42
  .slice(0, 8) // cap at 8 files to keep commit concise
42
43
  .map(f => `- ${f}`)
43
44
  .join("\n");
44
- return `${subject}\n\n${fileLines}`;
45
+ bodyParts.push(fileLines);
46
+ }
47
+ if (ctx.issueNumber) {
48
+ bodyParts.push(`Resolves #${ctx.issueNumber}`);
49
+ }
50
+ if (bodyParts.length > 0) {
51
+ return `${subject}\n\n${bodyParts.join("\n\n")}`;
45
52
  }
46
53
  return subject;
47
54
  }
@@ -254,7 +261,9 @@ export class GitServiceImpl {
254
261
  }
255
262
  this._runtimeFilesCleanedUp = true;
256
263
  }
257
- // Stage everything, then unstage excluded paths.
264
+ // Stage everything using pathspec exclusions so excluded paths are never
265
+ // hashed by git. The old approach of `git add -A` followed by unstaging
266
+ // hangs indefinitely on repos with large untracked artifact trees (#1605).
258
267
  //
259
268
  // Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
260
269
  // When .gsd/milestones/ files are already tracked in the index (projects
@@ -264,15 +273,9 @@ export class GitServiceImpl {
264
273
  // the second half of a milestone's artifacts are never committed (#1326).
265
274
  //
266
275
  // If .gsd/ IS in .gitignore (the default for external state projects),
267
- // git add -A already skips it and the reset is a harmless no-op.
268
- nativeAddAll(this.basePath);
269
- const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
270
- for (const exclusion of runtimeExclusions) {
271
- try {
272
- nativeResetPaths(this.basePath, [exclusion]);
273
- }
274
- catch { /* path not staged — ignore */ }
275
- }
276
+ // git add -A already skips it and the exclusions are harmless no-ops.
277
+ const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
278
+ nativeAddAllWithExclusions(this.basePath, allExclusions);
276
279
  }
277
280
  /** Tracks whether runtime file cleanup has run this session. */
278
281
  _runtimeFilesCleanedUp = false;
@@ -433,6 +436,21 @@ export class GitServiceImpl {
433
436
  }
434
437
  }
435
438
  }
439
+ // ─── Draft PR Creation ─────────────────────────────────────────────────────
440
+ /**
441
+ * Create a draft pull request for a completed milestone using `gh pr create`.
442
+ * Returns the PR URL on success, or null on failure.
443
+ * Non-fatal: callers should treat failure as best-effort.
444
+ */
445
+ export function createDraftPR(basePath, milestoneId, title, body) {
446
+ try {
447
+ const result = execSync(`gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`, { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV });
448
+ return result.trim();
449
+ }
450
+ catch {
451
+ return null;
452
+ }
453
+ }
436
454
  // ─── Factory ───────────────────────────────────────────────────────────────
437
455
  /** Create a GitServiceImpl with the current effective git preferences. */
438
456
  export function createGitService(basePath) {
@@ -6,9 +6,11 @@
6
6
  * Both idempotent — non-destructive if already present.
7
7
  */
8
8
  import { join } from "node:path";
9
+ import { execFileSync } from "node:child_process";
9
10
  import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
10
11
  import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
11
12
  import { gsdRoot } from "./paths.js";
13
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
12
14
  /**
13
15
  * GSD runtime patterns for git index cleanup.
14
16
  * With external state (symlink), these are a no-op in most cases,
@@ -93,11 +95,22 @@ export function hasGitTrackedGsdFiles(basePath) {
93
95
  // Check if git tracks any files under .gsd/
94
96
  try {
95
97
  const tracked = nativeLsFiles(basePath, ".gsd");
96
- return tracked.length > 0;
98
+ if (tracked.length > 0)
99
+ return true;
100
+ // nativeLsFiles swallows git failures and returns []. An empty result
101
+ // could mean "nothing tracked" OR "git failed silently". Verify git is
102
+ // reachable before trusting the empty result — if it isn't, fail safe
103
+ // by assuming files ARE tracked to prevent data loss.
104
+ execFileSync("git", ["rev-parse", "--git-dir"], {
105
+ cwd: basePath,
106
+ stdio: "pipe",
107
+ env: GIT_NO_PROMPT_ENV,
108
+ });
109
+ return false;
97
110
  }
98
111
  catch {
99
- // Not a git repo or git not available — safe to proceed
100
- return false;
112
+ // git unavailable, index locked, or repo corruptfail safe
113
+ return true;
101
114
  }
102
115
  }
103
116
  /**
@@ -8,6 +8,7 @@
8
8
  import { showNextAction } from "../shared/mod.js";
9
9
  import { loadFile, parseRoadmap } from "./files.js";
10
10
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
11
+ import { buildSkillActivationBlock } from "./auto-prompts.js";
11
12
  import { deriveState } from "./state.js";
12
13
  import { invalidateAllCaches } from "./cache.js";
13
14
  import { startAuto } from "./auto.js";
@@ -28,6 +29,7 @@ import { showConfirm } from "../shared/mod.js";
28
29
  import { debugLog } from "./debug-logger.js";
29
30
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
30
31
  import { parkMilestone, discardMilestone } from "./milestone-actions.js";
32
+ import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
31
33
  // ─── Re-exports (preserve public API for existing importers) ────────────────
32
34
  export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, } from "./milestone-ids.js";
33
35
  export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
@@ -153,8 +155,32 @@ function parseMilestoneSequenceFromProject(content) {
153
155
  /**
154
156
  * Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
155
157
  * This is the only way the wizard triggers work — everything else is the LLM's job.
158
+ *
159
+ * When a unitType is provided, resolves the user's model preference for that
160
+ * phase (e.g., models.planning → "plan-milestone") and applies it before
161
+ * dispatching. This ensures guided-flow dispatches respect the same
162
+ * per-phase model preferences that auto-mode uses.
156
163
  */
157
- function dispatchWorkflow(pi, note, customType = "gsd-run") {
164
+ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType) {
165
+ // Apply model preference for this unit type (if configured)
166
+ if (ctx && unitType) {
167
+ const modelConfig = resolveModelWithFallbacksForUnit(unitType);
168
+ if (modelConfig) {
169
+ const availableModels = ctx.modelRegistry.getAvailable();
170
+ const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
171
+ for (const modelId of modelsToTry) {
172
+ // Resolve model from available models (same logic as auto-model-selection)
173
+ const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
174
+ if (!model)
175
+ continue;
176
+ const ok = await pi.setModel(model, { persist: false });
177
+ if (ok) {
178
+ debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ }
158
184
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
159
185
  const workflow = readFileSync(workflowPath, "utf-8");
160
186
  pi.sendMessage({
@@ -163,6 +189,31 @@ function dispatchWorkflow(pi, note, customType = "gsd-run") {
163
189
  display: false,
164
190
  }, { triggerTurn: true });
165
191
  }
192
+ /**
193
+ * Resolve a model ID string to a model object from available models.
194
+ * Handles "provider/model" and bare ID formats.
195
+ */
196
+ function resolveAvailableModel(modelId, availableModels, currentProvider) {
197
+ const slashIdx = modelId.indexOf("/");
198
+ if (slashIdx !== -1) {
199
+ const maybeProvider = modelId.substring(0, slashIdx);
200
+ const id = modelId.substring(slashIdx + 1);
201
+ const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
202
+ if (knownProviders.has(maybeProvider.toLowerCase())) {
203
+ const match = availableModels.find(m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
204
+ && m.id.toLowerCase() === id.toLowerCase());
205
+ if (match)
206
+ return match;
207
+ }
208
+ // Try matching the full string as a model ID (OpenRouter-style)
209
+ const lower = modelId.toLowerCase();
210
+ return availableModels.find(m => m.id.toLowerCase() === lower
211
+ || `${m.provider}/${m.id}`.toLowerCase() === lower);
212
+ }
213
+ // Bare ID — prefer current provider, then first available
214
+ const exactProviderMatch = availableModels.find(m => m.id === modelId && m.provider === currentProvider);
215
+ return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
216
+ }
166
217
  /**
167
218
  * Build the discuss-and-plan prompt for a new milestone.
168
219
  * Used by all three "new milestone" paths (first ever, no active, all complete).
@@ -244,8 +295,8 @@ export async function showHeadlessMilestoneCreation(ctx, pi, basePath, seedConte
244
295
  const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
245
296
  // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
246
297
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
247
- // Dispatch
248
- dispatchWorkflow(pi, prompt);
298
+ // Dispatch — headless milestone creation is a planning activity
299
+ await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
249
300
  }
250
301
  // ─── Discuss Flow ─────────────────────────────────────────────────────────────
251
302
  /**
@@ -381,23 +432,23 @@ export async function showDiscuss(ctx, pi, basePath) {
381
432
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
382
433
  : basePrompt;
383
434
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
384
- dispatchWorkflow(pi, seed, "gsd-discuss");
435
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
385
436
  }
386
437
  else if (choice === "discuss_fresh") {
387
438
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
388
439
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
389
440
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
390
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
441
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
391
442
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
392
443
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
393
- }), "gsd-discuss");
444
+ }), "gsd-discuss", ctx, "plan-milestone");
394
445
  }
395
446
  else if (choice === "skip_milestone") {
396
447
  const milestoneIds = findMilestoneIds(basePath);
397
448
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
398
449
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
399
450
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
400
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
451
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
401
452
  }
402
453
  return;
403
454
  }
@@ -484,7 +535,7 @@ export async function showDiscuss(ctx, pi, basePath) {
484
535
  continue;
485
536
  }
486
537
  const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
487
- dispatchWorkflow(pi, prompt, "gsd-discuss");
538
+ await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
488
539
  // Wait for the discuss session to finish, then loop back to the picker
489
540
  await ctx.waitForIdle();
490
541
  invalidateAllCaches();
@@ -611,7 +662,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
611
662
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
612
663
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
613
664
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
614
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
665
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
615
666
  return true;
616
667
  }
617
668
  // "back" or null
@@ -729,7 +780,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
729
780
  if (isFirst) {
730
781
  // First ever — skip wizard, just ask directly
731
782
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
732
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath));
783
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "plan-milestone");
733
784
  }
734
785
  else {
735
786
  const choice = await showNextAction(ctx, {
@@ -747,7 +798,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
747
798
  });
748
799
  if (choice === "new_milestone") {
749
800
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
750
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
801
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
751
802
  }
752
803
  }
753
804
  return;
@@ -779,7 +830,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
779
830
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
780
831
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
781
832
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
782
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
833
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
783
834
  }
784
835
  else if (choice === "status") {
785
836
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -825,23 +876,23 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
825
876
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
826
877
  : basePrompt;
827
878
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
828
- dispatchWorkflow(pi, seed, "gsd-discuss");
879
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
829
880
  }
830
881
  else if (choice === "discuss_fresh") {
831
882
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
832
883
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
833
884
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
834
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
885
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
835
886
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
836
887
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
837
- }), "gsd-discuss");
888
+ }), "gsd-discuss", ctx, "plan-milestone");
838
889
  }
839
890
  else if (choice === "skip_milestone") {
840
891
  const milestoneIds = findMilestoneIds(basePath);
841
892
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
842
893
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
843
894
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
844
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
895
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
845
896
  }
846
897
  return;
847
898
  }
@@ -893,24 +944,33 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
893
944
  inlineTemplate("secrets-manifest", "Secrets Manifest"),
894
945
  ].join("\n\n---\n\n");
895
946
  const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
896
- dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
897
- milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
898
- }));
947
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
948
+ milestoneId,
949
+ milestoneTitle,
950
+ secretsOutputPath,
951
+ inlinedTemplates: planMilestoneTemplates,
952
+ skillActivation: buildSkillActivationBlock({
953
+ base: basePath,
954
+ milestoneId,
955
+ milestoneTitle,
956
+ extraContext: [planMilestoneTemplates],
957
+ }),
958
+ }), "gsd-run", ctx, "plan-milestone");
899
959
  }
900
960
  else if (choice === "discuss") {
901
961
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
902
962
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
903
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
963
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
904
964
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
905
965
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
906
- }));
966
+ }), "gsd-run", ctx, "plan-milestone");
907
967
  }
908
968
  else if (choice === "skip_milestone") {
909
969
  const milestoneIds = findMilestoneIds(basePath);
910
970
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
911
971
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
912
972
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
913
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
973
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
914
974
  }
915
975
  else if (choice === "discard_milestone") {
916
976
  const confirmed = await showConfirm(ctx, {
@@ -1021,18 +1081,38 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1021
1081
  inlineTemplate("plan", "Slice Plan"),
1022
1082
  inlineTemplate("task-plan", "Task Plan"),
1023
1083
  ].join("\n\n---\n\n");
1024
- dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1025
- milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
1026
- }));
1084
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1085
+ milestoneId,
1086
+ sliceId,
1087
+ sliceTitle,
1088
+ inlinedTemplates: planSliceTemplates,
1089
+ skillActivation: buildSkillActivationBlock({
1090
+ base: basePath,
1091
+ milestoneId,
1092
+ sliceId,
1093
+ sliceTitle,
1094
+ extraContext: [planSliceTemplates],
1095
+ }),
1096
+ }), "gsd-run", ctx, "plan-slice");
1027
1097
  }
1028
1098
  else if (choice === "discuss") {
1029
- dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
1099
+ await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
1030
1100
  }
1031
1101
  else if (choice === "research") {
1032
1102
  const researchTemplates = inlineTemplate("research", "Research");
1033
- dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1034
- milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
1035
- }));
1103
+ await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1104
+ milestoneId,
1105
+ sliceId,
1106
+ sliceTitle,
1107
+ inlinedTemplates: researchTemplates,
1108
+ skillActivation: buildSkillActivationBlock({
1109
+ base: basePath,
1110
+ milestoneId,
1111
+ sliceId,
1112
+ sliceTitle,
1113
+ extraContext: [researchTemplates],
1114
+ }),
1115
+ }), "gsd-run", ctx, "research-slice");
1036
1116
  }
1037
1117
  else if (choice === "status") {
1038
1118
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -1075,9 +1155,20 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1075
1155
  inlineTemplate("slice-summary", "Slice Summary"),
1076
1156
  inlineTemplate("uat", "UAT"),
1077
1157
  ].join("\n\n---\n\n");
1078
- dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1079
- workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
1080
- }));
1158
+ await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1159
+ workingDirectory: basePath,
1160
+ milestoneId,
1161
+ sliceId,
1162
+ sliceTitle,
1163
+ inlinedTemplates: completeSliceTemplates,
1164
+ skillActivation: buildSkillActivationBlock({
1165
+ base: basePath,
1166
+ milestoneId,
1167
+ sliceId,
1168
+ sliceTitle,
1169
+ extraContext: [completeSliceTemplates],
1170
+ }),
1171
+ }), "gsd-run", ctx, "complete-slice");
1081
1172
  }
1082
1173
  else if (choice === "status") {
1083
1174
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -1138,15 +1229,35 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1138
1229
  }
1139
1230
  if (choice === "execute") {
1140
1231
  if (hasInterrupted) {
1141
- dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1142
- milestoneId, sliceId,
1143
- }));
1232
+ await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1233
+ milestoneId,
1234
+ sliceId,
1235
+ skillActivation: buildSkillActivationBlock({
1236
+ base: basePath,
1237
+ milestoneId,
1238
+ sliceId,
1239
+ taskId,
1240
+ taskTitle,
1241
+ }),
1242
+ }), "gsd-run", ctx, "execute-task");
1144
1243
  }
1145
1244
  else {
1146
1245
  const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
1147
- dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1148
- milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
1149
- }));
1246
+ await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1247
+ milestoneId,
1248
+ sliceId,
1249
+ taskId,
1250
+ taskTitle,
1251
+ inlinedTemplates: executeTaskTemplates,
1252
+ skillActivation: buildSkillActivationBlock({
1253
+ base: basePath,
1254
+ milestoneId,
1255
+ sliceId,
1256
+ taskId,
1257
+ taskTitle,
1258
+ extraContext: [executeTaskTemplates],
1259
+ }),
1260
+ }), "gsd-run", ctx, "execute-task");
1150
1261
  }
1151
1262
  }
1152
1263
  else if (choice === "status") {
@@ -4,65 +4,18 @@
4
4
  * Separates project-state detection and line rendering from the widget's
5
5
  * runtime integrations so the regressions can be tested directly.
6
6
  */
7
- import { existsSync, readdirSync } from "node:fs";
7
+ import { existsSync } from "node:fs";
8
+ import { detectProjectState } from "./detection.js";
8
9
  import { gsdRoot } from "./paths.js";
9
- import { join } from "node:path";
10
10
  export function detectHealthWidgetProjectState(basePath) {
11
- const root = gsdRoot(basePath);
12
- if (!existsSync(root))
11
+ if (!existsSync(gsdRoot(basePath)))
13
12
  return "none";
14
- // Lightweight milestone count avoids the full detectProjectState() scan
15
- // (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
16
- try {
17
- const milestonesDir = join(root, "milestones");
18
- if (existsSync(milestonesDir)) {
19
- const entries = readdirSync(milestonesDir, { withFileTypes: true });
20
- if (entries.some(e => e.isDirectory()))
21
- return "active";
22
- }
23
- }
24
- catch { /* non-fatal */ }
25
- return "initialized";
13
+ const { state } = detectProjectState(basePath);
14
+ return state === "v2-gsd" ? "active" : "initialized";
26
15
  }
27
16
  function formatCost(n) {
28
17
  return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
29
18
  }
30
- function formatProgress(progress) {
31
- if (!progress)
32
- return null;
33
- const parts = [];
34
- parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
35
- if (progress.slices)
36
- parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
37
- if (progress.tasks)
38
- parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
39
- return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
40
- }
41
- function formatEnvironmentSummary(errorCount, warningCount) {
42
- if (errorCount <= 0 && warningCount <= 0)
43
- return null;
44
- const parts = [];
45
- if (errorCount > 0)
46
- parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
47
- if (warningCount > 0)
48
- parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
49
- return `Env: ${parts.join(", ")}`;
50
- }
51
- function formatBudgetSummary(data) {
52
- if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
53
- const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
54
- return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
55
- }
56
- if (data.budgetSpent > 0) {
57
- return `Spent: ${formatCost(data.budgetSpent)}`;
58
- }
59
- return null;
60
- }
61
- function buildExecutionHeadline(data) {
62
- const status = data.executionStatus ?? "Active project";
63
- const target = data.executionTarget ?? data.blocker ?? "loading status…";
64
- return ` GSD ${status}${target ? ` - ${target}` : ""}`;
65
- }
66
19
  /**
67
20
  * Build compact health lines for the widget.
68
21
  * Returns a string array suitable for setWidget().
@@ -74,23 +27,32 @@ export function buildHealthLines(data) {
74
27
  if (data.projectState === "initialized") {
75
28
  return [" GSD Project initialized — run /gsd to continue setup"];
76
29
  }
77
- const lines = [buildExecutionHeadline(data)];
78
- const details = [];
79
- const progress = formatProgress(data.progress);
80
- if (progress)
81
- details.push(progress);
82
- if (data.providerIssue)
83
- details.push(data.providerIssue);
84
- const environment = formatEnvironmentSummary(data.environmentErrorCount, data.environmentWarningCount);
85
- if (environment)
86
- details.push(environment);
87
- const budget = formatBudgetSummary(data);
88
- if (budget)
89
- details.push(budget);
90
- if (data.eta)
91
- details.push(data.eta);
92
- if (details.length > 0) {
93
- lines.push(` ${details.join(" │ ")}`);
30
+ const parts = [];
31
+ const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
32
+ if (totalIssues === 0) {
33
+ parts.push("● System OK");
34
+ }
35
+ else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("✗")) {
36
+ parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
37
+ }
38
+ else {
39
+ parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
40
+ }
41
+ if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
42
+ const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
43
+ parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
44
+ }
45
+ else if (data.budgetSpent > 0) {
46
+ parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
47
+ }
48
+ if (data.providerIssue) {
49
+ parts.push(data.providerIssue);
50
+ }
51
+ if (data.environmentErrorCount > 0) {
52
+ parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
53
+ }
54
+ else if (data.environmentWarningCount > 0) {
55
+ parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
94
56
  }
95
- return lines;
57
+ return [` ${parts.join(" │ ")}`];
96
58
  }