gsd-pi 2.38.0-dev.63ad7e5 → 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 (143) hide show
  1. package/README.md +15 -11
  2. package/dist/resource-loader.js +34 -1
  3. package/dist/resources/extensions/browser-tools/index.js +3 -1
  4. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  5. package/dist/resources/extensions/github-sync/cli.js +284 -0
  6. package/dist/resources/extensions/github-sync/index.js +73 -0
  7. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  8. package/dist/resources/extensions/github-sync/sync.js +424 -0
  9. package/dist/resources/extensions/github-sync/templates.js +118 -0
  10. package/dist/resources/extensions/github-sync/types.js +7 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  12. package/dist/resources/extensions/gsd/auto-loop.js +593 -516
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  14. package/dist/resources/extensions/gsd/auto-prompts.js +197 -19
  15. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  16. package/dist/resources/extensions/gsd/commands.js +2 -1
  17. package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
  18. package/dist/resources/extensions/gsd/doctor.js +20 -1
  19. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  20. package/dist/resources/extensions/gsd/files.js +46 -7
  21. package/dist/resources/extensions/gsd/git-service.js +30 -12
  22. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  23. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  24. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  25. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  26. package/dist/resources/extensions/gsd/index.js +22 -19
  27. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  28. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  29. package/dist/resources/extensions/gsd/paths.js +3 -0
  30. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  31. package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
  32. package/dist/resources/extensions/gsd/preferences.js +20 -9
  33. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  34. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  47. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  48. package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -1
  49. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  50. package/dist/resources/extensions/gsd/state.js +41 -22
  51. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  52. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  53. package/dist/resources/extensions/mcp-client/index.js +14 -1
  54. package/dist/resources/extensions/remote-questions/status.js +4 -2
  55. package/dist/resources/extensions/remote-questions/store.js +4 -2
  56. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  57. package/package.json +1 -1
  58. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  59. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  60. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  61. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  63. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  65. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  67. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/index.js +1 -1
  71. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  72. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  73. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  74. package/packages/pi-coding-agent/src/index.ts +1 -0
  75. package/src/resources/extensions/browser-tools/index.ts +3 -0
  76. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  77. package/src/resources/extensions/github-sync/cli.ts +364 -0
  78. package/src/resources/extensions/github-sync/index.ts +93 -0
  79. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  80. package/src/resources/extensions/github-sync/sync.ts +556 -0
  81. package/src/resources/extensions/github-sync/templates.ts +183 -0
  82. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  83. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  84. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  85. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  86. package/src/resources/extensions/github-sync/types.ts +47 -0
  87. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  88. package/src/resources/extensions/gsd/auto-loop.ts +472 -434
  89. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  90. package/src/resources/extensions/gsd/auto-prompts.ts +242 -19
  91. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  92. package/src/resources/extensions/gsd/commands.ts +2 -2
  93. package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
  94. package/src/resources/extensions/gsd/doctor.ts +22 -1
  95. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  96. package/src/resources/extensions/gsd/files.ts +49 -9
  97. package/src/resources/extensions/gsd/git-service.ts +44 -10
  98. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  99. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  100. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  101. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  102. package/src/resources/extensions/gsd/index.ts +21 -16
  103. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  104. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  105. package/src/resources/extensions/gsd/paths.ts +4 -0
  106. package/src/resources/extensions/gsd/preferences-types.ts +4 -0
  107. package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
  108. package/src/resources/extensions/gsd/preferences.ts +23 -9
  109. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  110. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  111. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  112. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -1
  113. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  114. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  115. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  116. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  117. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  118. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  119. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  120. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  121. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  122. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  123. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  124. package/src/resources/extensions/gsd/prompts/run-uat.md +3 -1
  125. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  126. package/src/resources/extensions/gsd/state.ts +38 -20
  127. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  128. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  129. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
  130. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  131. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  132. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  133. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  134. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  135. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  136. package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
  137. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  138. package/src/resources/extensions/gsd/types.ts +18 -0
  139. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  140. package/src/resources/extensions/mcp-client/index.ts +17 -1
  141. package/src/resources/extensions/remote-questions/status.ts +4 -2
  142. package/src/resources/extensions/remote-questions/store.ts +4 -2
  143. package/src/resources/extensions/shared/frontmatter.ts +1 -1
@@ -72,11 +72,21 @@ export async function postUnitPreVerification(pctx, opts) {
72
72
  const summaryContent = await loadFile(summaryPath);
73
73
  if (summaryContent) {
74
74
  const summary = parseSummary(summaryContent);
75
+ // Look up GitHub issue number for commit linking
76
+ let ghIssueNumber;
77
+ try {
78
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
79
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
80
+ }
81
+ catch {
82
+ // GitHub sync not available — skip
83
+ }
75
84
  taskContext = {
76
85
  taskId: `${sid}/${tid}`,
77
86
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
78
87
  oneLiner: summary.oneLiner || undefined,
79
88
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
89
+ issueNumber: ghIssueNumber,
80
90
  };
81
91
  }
82
92
  }
@@ -94,6 +104,14 @@ export async function postUnitPreVerification(pctx, opts) {
94
104
  catch (e) {
95
105
  debugLog("postUnit", { phase: "auto-commit", error: String(e) });
96
106
  }
107
+ // GitHub sync (non-blocking, opt-in)
108
+ try {
109
+ const { runGitHubSync } = await import("../github-sync/sync.js");
110
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
111
+ }
112
+ catch (e) {
113
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
114
+ }
97
115
  // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
98
116
  if (!opts?.skipDoctor)
99
117
  try {
@@ -105,12 +123,18 @@ export async function postUnitPreVerification(pctx, opts) {
105
123
  if (report.fixesApplied.length > 0) {
106
124
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
107
125
  }
108
- // Proactive health tracking
109
- const summary = summarizeDoctorIssues(report.issues);
126
+ // Proactive health tracking — filter to current milestone to avoid
127
+ // cross-milestone stale errors inflating the escalation counter
128
+ const currentMilestoneId = s.currentUnit.id.split("/")[0];
129
+ const milestoneIssues = currentMilestoneId
130
+ ? report.issues.filter(i => i.unitId === currentMilestoneId ||
131
+ i.unitId.startsWith(`${currentMilestoneId}/`))
132
+ : report.issues;
133
+ const summary = summarizeDoctorIssues(milestoneIssues);
110
134
  recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
111
135
  // Check if we should escalate to LLM-assisted heal
112
136
  if (summary.errors > 0) {
113
- const unresolvedErrors = report.issues
137
+ const unresolvedErrors = milestoneIssues
114
138
  .filter(i => i.severity === "error" && !i.fixable)
115
139
  .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
116
140
  const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
@@ -123,6 +147,7 @@ export async function postUnitPreVerification(pctx, opts) {
123
147
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
124
148
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
125
149
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
150
+ return "dispatched";
126
151
  }
127
152
  catch (e) {
128
153
  debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
@@ -5,14 +5,22 @@
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
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
11
- import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
12
- import { join } from "node:path";
10
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
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
- import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
15
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
15
16
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
17
+ // ─── Preamble Cap ─────────────────────────────────────────────────────────────
18
+ const MAX_PREAMBLE_CHARS = 30_000;
19
+ function capPreamble(preamble) {
20
+ if (preamble.length <= MAX_PREAMBLE_CHARS)
21
+ return preamble;
22
+ return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
23
+ }
16
24
  // ─── Executor Constraints ─────────────────────────────────────────────────────
17
25
  /**
18
26
  * Format executor context constraints for injection into the plan-slice prompt.
@@ -124,7 +132,6 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
124
132
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
125
133
  }
126
134
  // For large files, truncate at section boundary
127
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
128
135
  const truncated = truncateAtSectionBoundary(content, threshold).content;
129
136
  return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
130
137
  }
@@ -158,7 +165,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
158
165
  }
159
166
  const result = sections.join("\n\n");
160
167
  if (budgetChars !== undefined && result.length > budgetChars) {
161
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
162
168
  return truncateAtSectionBoundary(result, budgetChars).content;
163
169
  }
164
170
  return result;
@@ -245,7 +251,129 @@ export async function inlineProjectFromDb(base) {
245
251
  }
246
252
  return inlineGsdRootFile(base, "project.md", "Project");
247
253
  }
248
- // ─── 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
+ }
249
377
  /**
250
378
  * Build the skill discovery template variables for research prompts.
251
379
  * Returns { skillDiscoveryMode, skillDiscoveryInstructions } for template substitution.
@@ -525,7 +653,7 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
525
653
  if (knowledgeInlineRM)
526
654
  inlined.push(knowledgeInlineRM);
527
655
  inlined.push(inlineTemplate("research", "Research"));
528
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
656
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
529
657
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
530
658
  return loadPrompt("research-milestone", {
531
659
  workingDirectory: base,
@@ -534,6 +662,12 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
534
662
  contextPath: contextRel,
535
663
  outputPath: join(base, outputRelPath),
536
664
  inlinedContext,
665
+ skillActivation: buildSkillActivationBlock({
666
+ base,
667
+ milestoneId: mid,
668
+ milestoneTitle: midTitle,
669
+ extraContext: [inlinedContext],
670
+ }),
537
671
  ...buildSkillDiscoveryVars(),
538
672
  });
539
673
  }
@@ -578,7 +712,7 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
578
712
  inlined.push(inlineTemplate("plan", "Slice Plan"));
579
713
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
580
714
  }
581
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
715
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
582
716
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
583
717
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
584
718
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -593,6 +727,12 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
593
727
  secretsOutputPath,
594
728
  inlinedContext,
595
729
  sourceFilePaths: buildSourceFilePaths(base, mid),
730
+ skillActivation: buildSkillActivationBlock({
731
+ base,
732
+ milestoneId: mid,
733
+ milestoneTitle: midTitle,
734
+ extraContext: [inlinedContext],
735
+ }),
596
736
  ...buildSkillDiscoveryVars(),
597
737
  });
598
738
  }
@@ -626,7 +766,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
626
766
  const overridesInline = formatOverridesSection(activeOverrides);
627
767
  if (overridesInline)
628
768
  inlined.unshift(overridesInline);
629
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
769
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
630
770
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
631
771
  return loadPrompt("research-slice", {
632
772
  workingDirectory: base,
@@ -638,6 +778,13 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
638
778
  outputPath: join(base, outputRelPath),
639
779
  inlinedContext,
640
780
  dependencySummaries: depContent,
781
+ skillActivation: buildSkillActivationBlock({
782
+ base,
783
+ milestoneId: mid,
784
+ sliceId: sid,
785
+ sliceTitle: sTitle,
786
+ extraContext: [inlinedContext, depContent],
787
+ }),
641
788
  ...buildSkillDiscoveryVars(),
642
789
  });
643
790
  }
@@ -672,7 +819,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
672
819
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
673
820
  if (planOverridesInline)
674
821
  inlined.unshift(planOverridesInline);
675
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
822
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
676
823
  // Build executor context constraints from the budget engine
677
824
  const executorContextConstraints = formatExecutorConstraints();
678
825
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
@@ -693,6 +840,13 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
693
840
  sourceFilePaths: buildSourceFilePaths(base, mid, sid),
694
841
  executorContextConstraints,
695
842
  commitInstruction,
843
+ skillActivation: buildSkillActivationBlock({
844
+ base,
845
+ milestoneId: mid,
846
+ sliceId: sid,
847
+ sliceTitle: sTitle,
848
+ extraContext: [inlinedContext, depContent],
849
+ }),
696
850
  });
697
851
  }
698
852
  export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
@@ -760,11 +914,17 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
760
914
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
761
915
  let finalCarryForward = carryForwardSection;
762
916
  if (carryForwardSection.length > carryForwardBudget) {
763
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
764
917
  finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
765
918
  }
919
+ // Inline RUNTIME.md if present
920
+ const runtimePath = resolveRuntimeFile(base);
921
+ const runtimeContent = existsSync(runtimePath) ? await loadFile(runtimePath) : null;
922
+ const runtimeContext = runtimeContent
923
+ ? `### Runtime Context\nSource: \`.gsd/RUNTIME.md\`\n\n${runtimeContent.trim()}`
924
+ : "";
766
925
  return loadPrompt("execute-task", {
767
926
  overridesSection,
927
+ runtimeContext,
768
928
  workingDirectory: base,
769
929
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
770
930
  planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
@@ -778,6 +938,16 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
778
938
  taskSummaryPath,
779
939
  inlinedTemplates,
780
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
+ }),
781
951
  });
782
952
  }
783
953
  export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base, level) {
@@ -819,7 +989,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
819
989
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
820
990
  if (completeOverridesInline)
821
991
  inlined.unshift(completeOverridesInline);
822
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
992
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
823
993
  const sliceRel = relSlicePath(base, mid, sid);
824
994
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
825
995
  const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
@@ -875,7 +1045,7 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
875
1045
  if (contextInline)
876
1046
  inlined.push(contextInline);
877
1047
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
878
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1048
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
879
1049
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
880
1050
  return loadPrompt("complete-milestone", {
881
1051
  workingDirectory: base,
@@ -942,7 +1112,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
942
1112
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
943
1113
  if (contextInline)
944
1114
  inlined.push(contextInline);
945
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1115
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
946
1116
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
947
1117
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
948
1118
  return loadPrompt("validate-milestone", {
@@ -990,7 +1160,7 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
990
1160
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
991
1161
  if (replanOverridesInline)
992
1162
  inlined.unshift(replanOverridesInline);
993
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1163
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
994
1164
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
995
1165
  // Build capture context for replan prompt (captures that triggered this replan)
996
1166
  let captureContext = "(none)";
@@ -1015,6 +1185,14 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
1015
1185
  inlinedContext,
1016
1186
  replanPath,
1017
1187
  captureContext,
1188
+ skillActivation: buildSkillActivationBlock({
1189
+ base,
1190
+ milestoneId: mid,
1191
+ milestoneTitle: midTitle,
1192
+ sliceId: sid,
1193
+ sliceTitle: sTitle,
1194
+ extraContext: [inlinedContext, captureContext],
1195
+ }),
1018
1196
  });
1019
1197
  }
1020
1198
  export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base) {
@@ -1030,7 +1208,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
1030
1208
  const projectInline = await inlineProjectFromDb(base);
1031
1209
  if (projectInline)
1032
1210
  inlined.push(projectInline);
1033
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1211
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1034
1212
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1035
1213
  const uatType = extractUatType(uatContent) ?? "human-experience";
1036
1214
  return loadPrompt("run-uat", {
@@ -1066,7 +1244,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1066
1244
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1067
1245
  if (knowledgeInlineRA)
1068
1246
  inlined.push(knowledgeInlineRA);
1069
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1247
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1070
1248
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1071
1249
  // Build deferred captures context for reassess prompt
1072
1250
  let deferredCaptures = "(none)";
@@ -15,10 +15,10 @@ import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { createWorktree, removeWorktree, worktreePath, } from "./worktree-manager.js";
17
17
  import { detectWorktreeName, nudgeGitBranchCache, } from "./worktree.js";
18
- import { MergeConflictError, readIntegrationBranch } from "./git-service.js";
18
+ import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
19
19
  import { parseRoadmap } from "./files.js";
20
20
  import { loadEffectiveGSDPreferences } from "./preferences.js";
21
- import { nativeGetCurrentBranch, nativeWorkingTreeStatus, nativeAddAll, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeCheckoutTheirs, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchExists, } from "./native-git-bridge.js";
21
+ import { nativeGetCurrentBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeCheckoutTheirs, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchExists, } from "./native-git-bridge.js";
22
22
  // ─── Module State ──────────────────────────────────────────────────────────
23
23
  /** Original project root before chdir into auto-worktree. */
24
24
  let originalBase = null;
@@ -656,7 +656,7 @@ function autoCommitDirtyState(cwd) {
656
656
  const status = nativeWorkingTreeStatus(cwd);
657
657
  if (!status)
658
658
  return false;
659
- nativeAddAll(cwd);
659
+ nativeAddAllWithExclusions(cwd, RUNTIME_EXCLUSION_PATHS);
660
660
  const result = nativeCommit(cwd, "chore: auto-commit before milestone merge");
661
661
  return result !== null;
662
662
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * One command, one wizard. Routes to smart entry or status.
5
5
  */
6
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
6
7
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import { join } from "node:path";
@@ -529,7 +530,7 @@ export async function handleGSDCommand(args, ctx, pi) {
529
530
  return;
530
531
  }
531
532
  if (trimmed === "widget" || trimmed.startsWith("widget ")) {
532
- const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
533
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await importExtensionModule(import.meta.url, "./auto-dashboard.js");
533
534
  const arg = trimmed.replace(/^widget\s*/, "").trim();
534
535
  if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
535
536
  setWidgetMode(arg);
@@ -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)) {
@@ -257,10 +257,23 @@ async function markSliceDoneInRoadmap(basePath, milestoneId, sliceId, fixesAppli
257
257
  fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
258
258
  }
259
259
  }
260
+ async function markSliceUndoneInRoadmap(basePath, milestoneId, sliceId, fixesApplied) {
261
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
262
+ if (!roadmapPath)
263
+ return;
264
+ const content = await loadFile(roadmapPath);
265
+ if (!content)
266
+ return;
267
+ const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"), `$1[ ] **${sliceId}:`);
268
+ if (updated !== content) {
269
+ await saveFile(roadmapPath, updated);
270
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
271
+ }
272
+ }
260
273
  function matchesScope(unitId, scope) {
261
274
  if (!scope)
262
275
  return true;
263
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
276
+ return unitId === scope || unitId.startsWith(`${scope}/`);
264
277
  }
265
278
  function auditRequirements(content) {
266
279
  if (!content)
@@ -828,6 +841,12 @@ export async function runGSDDoctor(basePath, options) {
828
841
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
829
842
  fixable: true,
830
843
  });
844
+ if (!allTasksDone) {
845
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
846
+ if (shouldFix("slice_checked_missing_summary")) {
847
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
848
+ }
849
+ }
831
850
  }
832
851
  if (slice.done && !hasSliceUat) {
833
852
  issues.push({
@@ -1,9 +1,10 @@
1
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
1
2
  export function registerExitCommand(pi, deps = {}) {
2
3
  pi.registerCommand("exit", {
3
4
  description: "Exit GSD gracefully",
4
5
  handler: async (_args, ctx) => {
5
6
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
6
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
7
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule(import.meta.url, "./auto.js")).stopAuto;
7
8
  await stopAuto(ctx, pi, "Graceful exit");
8
9
  ctx.shutdown();
9
10
  },
@@ -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 });
@@ -692,6 +727,10 @@ export function extractUatType(content) {
692
727
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
693
728
  if (rawValue.startsWith('artifact-driven'))
694
729
  return 'artifact-driven';
730
+ if (rawValue.startsWith('browser-executable'))
731
+ return 'browser-executable';
732
+ if (rawValue.startsWith('runtime-executable'))
733
+ return 'runtime-executable';
695
734
  if (rawValue.startsWith('live-runtime'))
696
735
  return 'live-runtime';
697
736
  if (rawValue.startsWith('human-experience'))