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.
- package/README.md +15 -11
- package/dist/resources/extensions/gsd/auto-prompts.js +171 -4
- package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
- package/dist/resources/extensions/gsd/files.js +42 -7
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +67 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/preferences.js +17 -9
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
- package/dist/resources/extensions/gsd/state.js +41 -22
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/remote-questions/status.js +4 -2
- package/dist/resources/extensions/remote-questions/store.js +4 -2
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +213 -4
- package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
- package/src/resources/extensions/gsd/files.ts +46 -8
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +67 -6
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/preferences.ts +20 -9
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
- package/src/resources/extensions/gsd/state.ts +38 -20
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/types.ts +10 -0
- package/src/resources/extensions/remote-questions/status.ts +4 -2
- package/src/resources/extensions/remote-questions/store.ts +4 -2
- 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.
|
|
27
|
+
## What's New in v2.38
|
|
28
28
|
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
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
|
|
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 {
|
|
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(
|
|
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 =
|
|
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(
|
|
267
|
-
const demo = extractBoldField(
|
|
268
|
-
const mhSection = extractSection(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
100
|
-
return
|
|
112
|
+
// git unavailable, index locked, or repo corrupt — fail 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
12
|
-
if (!existsSync(root))
|
|
11
|
+
if (!existsSync(gsdRoot(basePath)))
|
|
13
12
|
return "none";
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (data.providerIssue)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
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
|
|
57
|
+
return [` ${parts.join(" │ ")}`];
|
|
96
58
|
}
|