gsd-pi 2.38.0-dev.7209774 → 2.38.0-dev.785052f

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 (81) hide show
  1. package/README.md +15 -11
  2. package/dist/resources/extensions/gsd/auto-prompts.js +171 -4
  3. package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
  4. package/dist/resources/extensions/gsd/files.js +42 -7
  5. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  6. package/dist/resources/extensions/gsd/guided-flow.js +67 -6
  7. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  8. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  9. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  10. package/dist/resources/extensions/gsd/preferences.js +17 -9
  11. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  12. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  13. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  14. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  16. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  18. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  19. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  20. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  21. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  22. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  24. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
  27. package/dist/resources/extensions/gsd/state.js +41 -22
  28. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  29. package/dist/resources/extensions/remote-questions/status.js +4 -2
  30. package/dist/resources/extensions/remote-questions/store.js +4 -2
  31. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  32. package/package.json +1 -1
  33. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  34. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  36. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  37. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  38. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/index.js +1 -1
  40. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  41. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  42. package/packages/pi-coding-agent/src/index.ts +1 -0
  43. package/src/resources/extensions/gsd/auto-prompts.ts +213 -4
  44. package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
  45. package/src/resources/extensions/gsd/files.ts +46 -8
  46. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  47. package/src/resources/extensions/gsd/guided-flow.ts +67 -6
  48. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  49. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  50. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  51. package/src/resources/extensions/gsd/preferences.ts +20 -9
  52. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  53. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  54. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  55. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  56. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  58. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  59. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  60. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  61. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  62. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  63. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  64. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  65. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  66. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  67. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
  68. package/src/resources/extensions/gsd/state.ts +38 -20
  69. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  70. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  71. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  72. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  73. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  74. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  75. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  76. package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
  77. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  78. package/src/resources/extensions/gsd/types.ts +10 -0
  79. package/src/resources/extensions/remote-questions/status.ts +4 -2
  80. package/src/resources/extensions/remote-questions/store.ts +4 -2
  81. package/src/resources/extensions/shared/frontmatter.ts +1 -1
package/README.md CHANGED
@@ -24,21 +24,25 @@ One command. Walk away. Come back to a built project with clean git history.
24
24
 
25
25
  ---
26
26
 
27
- ## What's New in v2.37
27
+ ## What's New in v2.38
28
28
 
29
- - **cmux integration** — sidebar status, progress bars, and notifications for [cmux](https://cmux.com) terminal multiplexer users
30
- - **Redesigned dashboard** — two-column layout with redesigned widget
31
- - **Search budget enforcement** — session-level search budget prevents unbounded native web search
29
+ - **Reactive task execution (ADR-004)** — graph-derived parallel task dispatch within slices. When enabled, GSD derives a dependency graph from IO annotations in task plans and dispatches multiple non-conflicting tasks in parallel via subagents. Backward compatible — disabled by default. Enable with `reactive_execution: true` in preferences.
30
+ - **Anthropic Vertex AI provider** — run Claude models (Opus 4.6, Sonnet 4.6, Haiku 4.5) through Google Vertex AI. Set `ANTHROPIC_VERTEX_PROJECT_ID` to activate.
31
+ - **CI optimization** — GitHub Actions minutes reduced ~60-70% (~10k ~3-4k/month)
32
+ - **Reactive batch verification** — dependency-based carry-forward for verification results across parallel task batches
33
+ - **Backtick file path enforcement** — task plan IO sections now require backtick-wrapped paths for reliable parsing
34
+
35
+ See the full [Changelog](./CHANGELOG.md) for details.
36
+
37
+ ### Previous highlights (v2.34–v2.37)
38
+
39
+ - **cmux integration** — sidebar status, progress bars, and notifications for cmux terminal multiplexer users
40
+ - **Redesigned dashboard** — two-column layout with 4 widget modes (full → small → min → off)
32
41
  - **AGENTS.md support** — deprecated `agent-instructions.md` in favor of standard `AGENTS.md` / `CLAUDE.md`
33
42
  - **AI-powered triage** — automated issue and PR triage via Claude Haiku
34
- - **Auto-generated OpenRouter registry** — model registry built from OpenRouter API for always-current model support
35
- - **Extension manifest system** — user-managed enable/disable for bundled extensions
36
- - **Pipeline simplification (ADR-003)** — merged research into planning, mechanical completion
37
- - **Workflow templates** — right-sized workflows for every task type
38
- - **Health widget** — always-on environment health checks with progress scoring
43
+ - **Auto-generated OpenRouter registry** — model registry built from OpenRouter API
39
44
  - **`/gsd changelog`** — LLM-summarized release notes for any version
40
-
41
- See the full [Changelog](./CHANGELOG.md) for details.
45
+ - **Search budget enforcement** — session-level cap prevents unbounded web search
42
46
 
43
47
  ---
44
48
 
@@ -5,11 +5,12 @@
5
5
  * state, no globals — every dependency is passed as a parameter or imported as a
6
6
  * utility.
7
7
  */
8
- import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, loadActiveOverrides, formatOverridesSection } from "./files.js";
8
+ import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, loadActiveOverrides, formatOverridesSection, parseTaskPlanFile } from "./files.js";
9
9
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
10
10
  import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
11
- import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
12
- import { join } from "node:path";
11
+ import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences, resolveAllSkillReferences } from "./preferences.js";
12
+ import { getLoadedSkills } from "@gsd/pi-coding-agent";
13
+ import { join, basename } from "node:path";
13
14
  import { existsSync } from "node:fs";
14
15
  import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
15
16
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
@@ -250,7 +251,129 @@ export async function inlineProjectFromDb(base) {
250
251
  }
251
252
  return inlineGsdRootFile(base, "project.md", "Project");
252
253
  }
253
- // ─── Skill Discovery ──────────────────────────────────────────────────────
254
+ // ─── Skill Activation & Discovery ─────────────────────────────────────────
255
+ function normalizeSkillReference(ref) {
256
+ const normalized = ref.replace(/\\/g, "/").trim();
257
+ const base = basename(normalized).replace(/\.md$/i, "");
258
+ const name = /^SKILL$/i.test(base)
259
+ ? basename(normalized.replace(/\/SKILL(?:\.md)?$/i, ""))
260
+ : base;
261
+ return name.trim().toLowerCase();
262
+ }
263
+ function tokenizeSkillContext(...parts) {
264
+ const tokens = new Set();
265
+ const addVariants = (raw) => {
266
+ const value = raw.trim().toLowerCase();
267
+ if (!value || value.length < 2)
268
+ return;
269
+ tokens.add(value);
270
+ tokens.add(value.replace(/[-_]+/g, " "));
271
+ tokens.add(value.replace(/\s+/g, "-"));
272
+ tokens.add(value.replace(/\s+/g, ""));
273
+ };
274
+ for (const part of parts) {
275
+ if (!part)
276
+ continue;
277
+ const text = part.toLowerCase();
278
+ const phraseMatches = text.match(/[a-z0-9][a-z0-9+.#/_-]{1,}/g) ?? [];
279
+ for (const match of phraseMatches) {
280
+ addVariants(match);
281
+ for (const piece of match.split(/[^a-z0-9+.#]+/g)) {
282
+ if (piece.length >= 3)
283
+ addVariants(piece);
284
+ }
285
+ }
286
+ }
287
+ return tokens;
288
+ }
289
+ function skillMatchesContext(skill, contextTokens) {
290
+ const haystacks = [
291
+ skill.name.toLowerCase(),
292
+ skill.name.toLowerCase().replace(/[-_]+/g, " "),
293
+ skill.description.toLowerCase(),
294
+ ];
295
+ return [...contextTokens].some(token => token.length >= 3 && haystacks.some(haystack => haystack.includes(token)));
296
+ }
297
+ function resolvePreferenceSkillNames(refs, base) {
298
+ if (refs.length === 0)
299
+ return [];
300
+ const prefs = { always_use_skills: refs };
301
+ const report = resolveAllSkillReferences(prefs, base);
302
+ return refs.map(ref => {
303
+ const resolution = report.resolutions.get(ref);
304
+ return normalizeSkillReference(resolution?.resolvedPath ?? ref);
305
+ }).filter(Boolean);
306
+ }
307
+ function ruleMatchesContext(when, contextTokens) {
308
+ const whenTokens = tokenizeSkillContext(when);
309
+ return [...whenTokens].some(token => contextTokens.has(token) || [...contextTokens].some(ctx => ctx.includes(token) || token.includes(ctx)));
310
+ }
311
+ function resolveSkillRuleMatches(prefs, contextTokens, base) {
312
+ if (!prefs?.skill_rules?.length)
313
+ return { include: [], avoid: [] };
314
+ const include = [];
315
+ const avoid = [];
316
+ for (const rule of prefs.skill_rules) {
317
+ if (!ruleMatchesContext(rule.when, contextTokens))
318
+ continue;
319
+ include.push(...resolvePreferenceSkillNames([...(rule.use ?? []), ...(rule.prefer ?? [])], base));
320
+ avoid.push(...resolvePreferenceSkillNames(rule.avoid ?? [], base));
321
+ }
322
+ return { include, avoid };
323
+ }
324
+ function resolvePreferredSkillNames(prefs, visibleSkills, contextTokens, base) {
325
+ if (!prefs?.prefer_skills?.length)
326
+ return [];
327
+ const preferred = new Set(resolvePreferenceSkillNames(prefs.prefer_skills, base));
328
+ return visibleSkills
329
+ .filter(skill => preferred.has(normalizeSkillReference(skill.name)) && skillMatchesContext(skill, contextTokens))
330
+ .map(skill => normalizeSkillReference(skill.name));
331
+ }
332
+ function formatSkillActivationBlock(skillNames) {
333
+ if (skillNames.length === 0)
334
+ return "";
335
+ const calls = skillNames.map(name => `Call Skill('${name}')`).join('. ');
336
+ return `<skill_activation>${calls}.</skill_activation>`;
337
+ }
338
+ export function buildSkillActivationBlock(params) {
339
+ const prefs = params.preferences ?? loadEffectiveGSDPreferences()?.preferences;
340
+ const contextTokens = tokenizeSkillContext(params.milestoneId, params.milestoneTitle, params.sliceId, params.sliceTitle, params.taskId, params.taskTitle, ...(params.extraContext ?? []), params.taskPlanContent ?? undefined);
341
+ const visibleSkills = getLoadedSkills().filter(skill => !skill.disableModelInvocation);
342
+ const installedNames = new Set(visibleSkills.map(skill => normalizeSkillReference(skill.name)));
343
+ const avoided = new Set(resolvePreferenceSkillNames(prefs?.avoid_skills ?? [], params.base));
344
+ const matched = new Set();
345
+ for (const name of resolvePreferenceSkillNames(prefs?.always_use_skills ?? [], params.base)) {
346
+ matched.add(name);
347
+ }
348
+ const ruleMatches = resolveSkillRuleMatches(prefs, contextTokens, params.base);
349
+ for (const name of ruleMatches.include)
350
+ matched.add(name);
351
+ for (const name of ruleMatches.avoid)
352
+ avoided.add(name);
353
+ for (const name of resolvePreferredSkillNames(prefs, visibleSkills, contextTokens, params.base)) {
354
+ matched.add(name);
355
+ }
356
+ if (params.taskPlanContent) {
357
+ try {
358
+ const taskPlan = parseTaskPlanFile(params.taskPlanContent);
359
+ for (const skillName of taskPlan.frontmatter.skills_used) {
360
+ matched.add(normalizeSkillReference(skillName));
361
+ }
362
+ }
363
+ catch {
364
+ // Non-fatal — malformed task plan should not break prompt construction
365
+ }
366
+ }
367
+ for (const skill of visibleSkills) {
368
+ if (skillMatchesContext(skill, contextTokens)) {
369
+ matched.add(normalizeSkillReference(skill.name));
370
+ }
371
+ }
372
+ const ordered = [...matched]
373
+ .filter(name => installedNames.has(name) && !avoided.has(name))
374
+ .sort();
375
+ return formatSkillActivationBlock(ordered);
376
+ }
254
377
  /**
255
378
  * Build the skill discovery template variables for research prompts.
256
379
  * Returns { skillDiscoveryMode, skillDiscoveryInstructions } for template substitution.
@@ -539,6 +662,12 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
539
662
  contextPath: contextRel,
540
663
  outputPath: join(base, outputRelPath),
541
664
  inlinedContext,
665
+ skillActivation: buildSkillActivationBlock({
666
+ base,
667
+ milestoneId: mid,
668
+ milestoneTitle: midTitle,
669
+ extraContext: [inlinedContext],
670
+ }),
542
671
  ...buildSkillDiscoveryVars(),
543
672
  });
544
673
  }
@@ -598,6 +727,12 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
598
727
  secretsOutputPath,
599
728
  inlinedContext,
600
729
  sourceFilePaths: buildSourceFilePaths(base, mid),
730
+ skillActivation: buildSkillActivationBlock({
731
+ base,
732
+ milestoneId: mid,
733
+ milestoneTitle: midTitle,
734
+ extraContext: [inlinedContext],
735
+ }),
601
736
  ...buildSkillDiscoveryVars(),
602
737
  });
603
738
  }
@@ -643,6 +778,13 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
643
778
  outputPath: join(base, outputRelPath),
644
779
  inlinedContext,
645
780
  dependencySummaries: depContent,
781
+ skillActivation: buildSkillActivationBlock({
782
+ base,
783
+ milestoneId: mid,
784
+ sliceId: sid,
785
+ sliceTitle: sTitle,
786
+ extraContext: [inlinedContext, depContent],
787
+ }),
646
788
  ...buildSkillDiscoveryVars(),
647
789
  });
648
790
  }
@@ -698,6 +840,13 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
698
840
  sourceFilePaths: buildSourceFilePaths(base, mid, sid),
699
841
  executorContextConstraints,
700
842
  commitInstruction,
843
+ skillActivation: buildSkillActivationBlock({
844
+ base,
845
+ milestoneId: mid,
846
+ sliceId: sid,
847
+ sliceTitle: sTitle,
848
+ extraContext: [inlinedContext, depContent],
849
+ }),
701
850
  });
702
851
  }
703
852
  export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
@@ -789,6 +938,16 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
789
938
  taskSummaryPath,
790
939
  inlinedTemplates,
791
940
  verificationBudget,
941
+ skillActivation: buildSkillActivationBlock({
942
+ base,
943
+ milestoneId: mid,
944
+ sliceId: sid,
945
+ sliceTitle: sTitle,
946
+ taskId: tid,
947
+ taskTitle: tTitle,
948
+ taskPlanContent,
949
+ extraContext: [taskPlanInline, slicePlanExcerpt, finalCarryForward, resumeSection],
950
+ }),
792
951
  });
793
952
  }
794
953
  export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base, level) {
@@ -1026,6 +1185,14 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
1026
1185
  inlinedContext,
1027
1186
  replanPath,
1028
1187
  captureContext,
1188
+ skillActivation: buildSkillActivationBlock({
1189
+ base,
1190
+ milestoneId: mid,
1191
+ milestoneTitle: midTitle,
1192
+ sliceId: sid,
1193
+ sliceTitle: sTitle,
1194
+ extraContext: [inlinedContext, captureContext],
1195
+ }),
1029
1196
  });
1030
1197
  }
1031
1198
  export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base) {
@@ -98,6 +98,9 @@ function collectConfiguredModelProviders() {
98
98
  }
99
99
  function resolveKey(providerId) {
100
100
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
101
+ if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
102
+ return { found: true, source: "env", backedOff: false };
103
+ }
101
104
  // Check auth.json
102
105
  const authPath = getAuthPath();
103
106
  if (existsSync(authPath)) {
@@ -222,13 +222,48 @@ export function formatSecretsManifest(manifest) {
222
222
  return lines.join('\n') + '\n';
223
223
  }
224
224
  // ─── Slice Plan Parser ─────────────────────────────────────────────────────
225
+ function normalizeTaskPlanFrontmatter(frontmatter) {
226
+ const estimatedStepsRaw = frontmatter.estimated_steps;
227
+ const estimatedFilesRaw = frontmatter.estimated_files;
228
+ const skillsUsedRaw = frontmatter.skills_used;
229
+ const parseOptionalNumber = (value) => {
230
+ if (typeof value === 'number' && Number.isFinite(value))
231
+ return value;
232
+ if (typeof value === 'string' && value.trim()) {
233
+ const parsed = parseInt(value, 10);
234
+ if (Number.isFinite(parsed))
235
+ return parsed;
236
+ }
237
+ return undefined;
238
+ };
239
+ const estimated_steps = parseOptionalNumber(estimatedStepsRaw);
240
+ const estimated_files = parseOptionalNumber(estimatedFilesRaw);
241
+ const skills_used = Array.isArray(skillsUsedRaw)
242
+ ? skillsUsedRaw.map(v => String(v).trim()).filter(Boolean)
243
+ : typeof skillsUsedRaw === 'string' && skillsUsedRaw.trim()
244
+ ? [skillsUsedRaw.trim()]
245
+ : [];
246
+ return {
247
+ ...(estimated_steps !== undefined ? { estimated_steps } : {}),
248
+ ...(estimated_files !== undefined ? { estimated_files } : {}),
249
+ skills_used,
250
+ };
251
+ }
252
+ export function parseTaskPlanFile(content) {
253
+ const [fmLines] = splitFrontmatter(content);
254
+ const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
255
+ return {
256
+ frontmatter: normalizeTaskPlanFrontmatter(fm),
257
+ };
258
+ }
225
259
  export function parsePlan(content) {
226
260
  return cachedParse(content, 'plan', _parsePlanImpl);
227
261
  }
228
262
  function _parsePlanImpl(content) {
229
263
  const stopTimer = debugTime("parse-plan");
264
+ const [, body] = splitFrontmatter(content);
230
265
  // Try native parser first for better performance
231
- const nativeResult = nativeParsePlanFile(content);
266
+ const nativeResult = nativeParsePlanFile(body);
232
267
  if (nativeResult) {
233
268
  stopTimer({ native: true });
234
269
  return {
@@ -249,7 +284,7 @@ function _parsePlanImpl(content) {
249
284
  filesLikelyTouched: nativeResult.filesLikelyTouched,
250
285
  };
251
286
  }
252
- const lines = content.split('\n');
287
+ const lines = body.split('\n');
253
288
  const h1 = lines.find(l => l.startsWith('# '));
254
289
  let id = '';
255
290
  let title = '';
@@ -263,11 +298,11 @@ function _parsePlanImpl(content) {
263
298
  title = h1.slice(2).trim();
264
299
  }
265
300
  }
266
- const goal = extractBoldField(content, 'Goal') || '';
267
- const demo = extractBoldField(content, 'Demo') || '';
268
- const mhSection = extractSection(content, 'Must-Haves');
301
+ const goal = extractBoldField(body, 'Goal') || '';
302
+ const demo = extractBoldField(body, 'Demo') || '';
303
+ const mhSection = extractSection(body, 'Must-Haves');
269
304
  const mustHaves = mhSection ? parseBullets(mhSection) : [];
270
- const tasksSection = extractSection(content, 'Tasks');
305
+ const tasksSection = extractSection(body, 'Tasks');
271
306
  const tasks = [];
272
307
  if (tasksSection) {
273
308
  const taskLines = tasksSection.split('\n');
@@ -315,7 +350,7 @@ function _parsePlanImpl(content) {
315
350
  if (currentTask)
316
351
  tasks.push(currentTask);
317
352
  }
318
- const filesSection = extractSection(content, 'Files Likely Touched');
353
+ const filesSection = extractSection(body, 'Files Likely Touched');
319
354
  const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
320
355
  const result = { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
321
356
  stopTimer({ tasks: tasks.length });
@@ -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";
@@ -944,7 +945,16 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
944
945
  ].join("\n\n---\n\n");
945
946
  const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
946
947
  await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
947
- milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
948
+ milestoneId,
949
+ milestoneTitle,
950
+ secretsOutputPath,
951
+ inlinedTemplates: planMilestoneTemplates,
952
+ skillActivation: buildSkillActivationBlock({
953
+ base: basePath,
954
+ milestoneId,
955
+ milestoneTitle,
956
+ extraContext: [planMilestoneTemplates],
957
+ }),
948
958
  }), "gsd-run", ctx, "plan-milestone");
949
959
  }
950
960
  else if (choice === "discuss") {
@@ -1072,7 +1082,17 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1072
1082
  inlineTemplate("task-plan", "Task Plan"),
1073
1083
  ].join("\n\n---\n\n");
1074
1084
  await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1075
- milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
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
+ }),
1076
1096
  }), "gsd-run", ctx, "plan-slice");
1077
1097
  }
1078
1098
  else if (choice === "discuss") {
@@ -1081,7 +1101,17 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1081
1101
  else if (choice === "research") {
1082
1102
  const researchTemplates = inlineTemplate("research", "Research");
1083
1103
  await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1084
- milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
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
+ }),
1085
1115
  }), "gsd-run", ctx, "research-slice");
1086
1116
  }
1087
1117
  else if (choice === "status") {
@@ -1126,7 +1156,18 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1126
1156
  inlineTemplate("uat", "UAT"),
1127
1157
  ].join("\n\n---\n\n");
1128
1158
  await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1129
- workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
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
+ }),
1130
1171
  }), "gsd-run", ctx, "complete-slice");
1131
1172
  }
1132
1173
  else if (choice === "status") {
@@ -1189,13 +1230,33 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1189
1230
  if (choice === "execute") {
1190
1231
  if (hasInterrupted) {
1191
1232
  await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1192
- milestoneId, sliceId,
1233
+ milestoneId,
1234
+ sliceId,
1235
+ skillActivation: buildSkillActivationBlock({
1236
+ base: basePath,
1237
+ milestoneId,
1238
+ sliceId,
1239
+ taskId,
1240
+ taskTitle,
1241
+ }),
1193
1242
  }), "gsd-run", ctx, "execute-task");
1194
1243
  }
1195
1244
  else {
1196
1245
  const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
1197
1246
  await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1198
- milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
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
+ }),
1199
1260
  }), "gsd-run", ctx, "execute-task");
1200
1261
  }
1201
1262
  }
@@ -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
  }