gsd-pi 2.38.0-dev.4d4d14a → 2.38.0-dev.5492881
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/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +538 -469
- package/dist/resources/extensions/gsd/auto-post-unit.js +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +18 -14
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +22 -11
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +342 -304
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -14
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +31 -9
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +106 -31
- package/src/resources/extensions/mcp-client/index.ts +17 -1
|
@@ -123,12 +123,18 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
123
123
|
if (report.fixesApplied.length > 0) {
|
|
124
124
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
125
125
|
}
|
|
126
|
-
// Proactive health tracking
|
|
127
|
-
|
|
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);
|
|
128
134
|
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
129
135
|
// Check if we should escalate to LLM-assisted heal
|
|
130
136
|
if (summary.errors > 0) {
|
|
131
|
-
const unresolvedErrors =
|
|
137
|
+
const unresolvedErrors = milestoneIssues
|
|
132
138
|
.filter(i => i.severity === "error" && !i.fixable)
|
|
133
139
|
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
134
140
|
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
@@ -11,8 +11,15 @@ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksD
|
|
|
11
11
|
import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
|
-
import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
|
|
14
|
+
import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
|
|
15
15
|
import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
|
|
16
|
+
// ─── Preamble Cap ─────────────────────────────────────────────────────────────
|
|
17
|
+
const MAX_PREAMBLE_CHARS = 30_000;
|
|
18
|
+
function capPreamble(preamble) {
|
|
19
|
+
if (preamble.length <= MAX_PREAMBLE_CHARS)
|
|
20
|
+
return preamble;
|
|
21
|
+
return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
|
|
22
|
+
}
|
|
16
23
|
// ─── Executor Constraints ─────────────────────────────────────────────────────
|
|
17
24
|
/**
|
|
18
25
|
* Format executor context constraints for injection into the plan-slice prompt.
|
|
@@ -124,7 +131,6 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
|
|
|
124
131
|
return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
|
|
125
132
|
}
|
|
126
133
|
// For large files, truncate at section boundary
|
|
127
|
-
const { truncateAtSectionBoundary } = await import("./context-budget.js");
|
|
128
134
|
const truncated = truncateAtSectionBoundary(content, threshold).content;
|
|
129
135
|
return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
|
|
130
136
|
}
|
|
@@ -158,7 +164,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
|
|
|
158
164
|
}
|
|
159
165
|
const result = sections.join("\n\n");
|
|
160
166
|
if (budgetChars !== undefined && result.length > budgetChars) {
|
|
161
|
-
const { truncateAtSectionBoundary } = await import("./context-budget.js");
|
|
162
167
|
return truncateAtSectionBoundary(result, budgetChars).content;
|
|
163
168
|
}
|
|
164
169
|
return result;
|
|
@@ -525,7 +530,7 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
|
|
|
525
530
|
if (knowledgeInlineRM)
|
|
526
531
|
inlined.push(knowledgeInlineRM);
|
|
527
532
|
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")}
|
|
533
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
529
534
|
const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
|
|
530
535
|
return loadPrompt("research-milestone", {
|
|
531
536
|
workingDirectory: base,
|
|
@@ -578,7 +583,7 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
|
|
|
578
583
|
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
579
584
|
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
580
585
|
}
|
|
581
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
586
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
582
587
|
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
583
588
|
const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
|
|
584
589
|
const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
|
|
@@ -626,7 +631,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
|
|
|
626
631
|
const overridesInline = formatOverridesSection(activeOverrides);
|
|
627
632
|
if (overridesInline)
|
|
628
633
|
inlined.unshift(overridesInline);
|
|
629
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
634
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
630
635
|
const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
|
|
631
636
|
return loadPrompt("research-slice", {
|
|
632
637
|
workingDirectory: base,
|
|
@@ -672,7 +677,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
672
677
|
const planOverridesInline = formatOverridesSection(planActiveOverrides);
|
|
673
678
|
if (planOverridesInline)
|
|
674
679
|
inlined.unshift(planOverridesInline);
|
|
675
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
680
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
676
681
|
// Build executor context constraints from the budget engine
|
|
677
682
|
const executorContextConstraints = formatExecutorConstraints();
|
|
678
683
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
@@ -760,7 +765,6 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
|
|
|
760
765
|
const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
|
|
761
766
|
let finalCarryForward = carryForwardSection;
|
|
762
767
|
if (carryForwardSection.length > carryForwardBudget) {
|
|
763
|
-
const { truncateAtSectionBoundary } = await import("./context-budget.js");
|
|
764
768
|
finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
|
|
765
769
|
}
|
|
766
770
|
return loadPrompt("execute-task", {
|
|
@@ -819,7 +823,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
|
|
|
819
823
|
const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
|
|
820
824
|
if (completeOverridesInline)
|
|
821
825
|
inlined.unshift(completeOverridesInline);
|
|
822
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
826
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
823
827
|
const sliceRel = relSlicePath(base, mid, sid);
|
|
824
828
|
const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
|
|
825
829
|
const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
|
|
@@ -875,7 +879,7 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
|
|
|
875
879
|
if (contextInline)
|
|
876
880
|
inlined.push(contextInline);
|
|
877
881
|
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")}
|
|
882
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
879
883
|
const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
|
|
880
884
|
return loadPrompt("complete-milestone", {
|
|
881
885
|
workingDirectory: base,
|
|
@@ -942,7 +946,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
|
|
|
942
946
|
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
943
947
|
if (contextInline)
|
|
944
948
|
inlined.push(contextInline);
|
|
945
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
949
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
946
950
|
const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
|
|
947
951
|
const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
|
|
948
952
|
return loadPrompt("validate-milestone", {
|
|
@@ -990,7 +994,7 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
|
|
|
990
994
|
const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
|
|
991
995
|
if (replanOverridesInline)
|
|
992
996
|
inlined.unshift(replanOverridesInline);
|
|
993
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
997
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
994
998
|
const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
|
|
995
999
|
// Build capture context for replan prompt (captures that triggered this replan)
|
|
996
1000
|
let captureContext = "(none)";
|
|
@@ -1030,7 +1034,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
|
|
|
1030
1034
|
const projectInline = await inlineProjectFromDb(base);
|
|
1031
1035
|
if (projectInline)
|
|
1032
1036
|
inlined.push(projectInline);
|
|
1033
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
1037
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
1034
1038
|
const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
|
|
1035
1039
|
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
1036
1040
|
return loadPrompt("run-uat", {
|
|
@@ -1066,7 +1070,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
|
|
|
1066
1070
|
const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
1067
1071
|
if (knowledgeInlineRA)
|
|
1068
1072
|
inlined.push(knowledgeInlineRA);
|
|
1069
|
-
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}
|
|
1073
|
+
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
1070
1074
|
const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
|
|
1071
1075
|
// Build deferred captures context for reassess prompt
|
|
1072
1076
|
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,
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -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}/`)
|
|
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
|
|
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
|
},
|
|
@@ -692,6 +692,10 @@ export function extractUatType(content) {
|
|
|
692
692
|
const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
|
|
693
693
|
if (rawValue.startsWith('artifact-driven'))
|
|
694
694
|
return 'artifact-driven';
|
|
695
|
+
if (rawValue.startsWith('browser-executable'))
|
|
696
|
+
return 'browser-executable';
|
|
697
|
+
if (rawValue.startsWith('runtime-executable'))
|
|
698
|
+
return 'runtime-executable';
|
|
695
699
|
if (rawValue.startsWith('live-runtime'))
|
|
696
700
|
return 'live-runtime';
|
|
697
701
|
if (rawValue.startsWith('human-experience'))
|
|
@@ -14,7 +14,7 @@ import { gsdRoot } from "./paths.js";
|
|
|
14
14
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
16
|
import { detectWorktreeName, SLICE_BRANCH_RE, } from "./worktree.js";
|
|
17
|
-
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges,
|
|
17
|
+
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, } from "./native-git-bridge.js";
|
|
18
18
|
import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
|
|
19
19
|
import { getErrorMessage } from "./error-utils.js";
|
|
20
20
|
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
@@ -261,7 +261,9 @@ export class GitServiceImpl {
|
|
|
261
261
|
}
|
|
262
262
|
this._runtimeFilesCleanedUp = true;
|
|
263
263
|
}
|
|
264
|
-
// Stage everything
|
|
264
|
+
// Stage everything using pathspec exclusions so excluded paths are never
|
|
265
|
+
// hashed by git. The old approach of `git add -A` followed by unstaging
|
|
266
|
+
// hangs indefinitely on repos with large untracked artifact trees (#1605).
|
|
265
267
|
//
|
|
266
268
|
// Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
|
|
267
269
|
// When .gsd/milestones/ files are already tracked in the index (projects
|
|
@@ -271,15 +273,9 @@ export class GitServiceImpl {
|
|
|
271
273
|
// the second half of a milestone's artifacts are never committed (#1326).
|
|
272
274
|
//
|
|
273
275
|
// If .gsd/ IS in .gitignore (the default for external state projects),
|
|
274
|
-
// git add -A already skips it and the
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
for (const exclusion of runtimeExclusions) {
|
|
278
|
-
try {
|
|
279
|
-
nativeResetPaths(this.basePath, [exclusion]);
|
|
280
|
-
}
|
|
281
|
-
catch { /* path not staged — ignore */ }
|
|
282
|
-
}
|
|
276
|
+
// git add -A already skips it and the exclusions are harmless no-ops.
|
|
277
|
+
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
278
|
+
nativeAddAllWithExclusions(this.basePath, allExclusions);
|
|
283
279
|
}
|
|
284
280
|
/** Tracks whether runtime file cleanup has run this session. */
|
|
285
281
|
_runtimeFilesCleanedUp = false;
|
|
@@ -440,6 +436,21 @@ export class GitServiceImpl {
|
|
|
440
436
|
}
|
|
441
437
|
}
|
|
442
438
|
}
|
|
439
|
+
// ─── Draft PR Creation ─────────────────────────────────────────────────────
|
|
440
|
+
/**
|
|
441
|
+
* Create a draft pull request for a completed milestone using `gh pr create`.
|
|
442
|
+
* Returns the PR URL on success, or null on failure.
|
|
443
|
+
* Non-fatal: callers should treat failure as best-effort.
|
|
444
|
+
*/
|
|
445
|
+
export function createDraftPR(basePath, milestoneId, title, body) {
|
|
446
|
+
try {
|
|
447
|
+
const result = execSync(`gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`, { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV });
|
|
448
|
+
return result.trim();
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
443
454
|
// ─── Factory ───────────────────────────────────────────────────────────────
|
|
444
455
|
/** Create a GitServiceImpl with the current effective git preferences. */
|
|
445
456
|
export function createGitService(basePath) {
|
|
@@ -28,6 +28,7 @@ import { showConfirm } from "../shared/mod.js";
|
|
|
28
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
29
|
import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
|
|
30
30
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
31
|
+
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
|
31
32
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
32
33
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, } from "./milestone-ids.js";
|
|
33
34
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
@@ -153,8 +154,32 @@ function parseMilestoneSequenceFromProject(content) {
|
|
|
153
154
|
/**
|
|
154
155
|
* Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
|
|
155
156
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
157
|
+
*
|
|
158
|
+
* When a unitType is provided, resolves the user's model preference for that
|
|
159
|
+
* phase (e.g., models.planning → "plan-milestone") and applies it before
|
|
160
|
+
* dispatching. This ensures guided-flow dispatches respect the same
|
|
161
|
+
* per-phase model preferences that auto-mode uses.
|
|
156
162
|
*/
|
|
157
|
-
function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
163
|
+
async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType) {
|
|
164
|
+
// Apply model preference for this unit type (if configured)
|
|
165
|
+
if (ctx && unitType) {
|
|
166
|
+
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
167
|
+
if (modelConfig) {
|
|
168
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
169
|
+
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
|
170
|
+
for (const modelId of modelsToTry) {
|
|
171
|
+
// Resolve model from available models (same logic as auto-model-selection)
|
|
172
|
+
const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
|
|
173
|
+
if (!model)
|
|
174
|
+
continue;
|
|
175
|
+
const ok = await pi.setModel(model, { persist: false });
|
|
176
|
+
if (ok) {
|
|
177
|
+
debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
158
183
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
159
184
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
160
185
|
pi.sendMessage({
|
|
@@ -163,6 +188,31 @@ function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
|
163
188
|
display: false,
|
|
164
189
|
}, { triggerTurn: true });
|
|
165
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a model ID string to a model object from available models.
|
|
193
|
+
* Handles "provider/model" and bare ID formats.
|
|
194
|
+
*/
|
|
195
|
+
function resolveAvailableModel(modelId, availableModels, currentProvider) {
|
|
196
|
+
const slashIdx = modelId.indexOf("/");
|
|
197
|
+
if (slashIdx !== -1) {
|
|
198
|
+
const maybeProvider = modelId.substring(0, slashIdx);
|
|
199
|
+
const id = modelId.substring(slashIdx + 1);
|
|
200
|
+
const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
|
|
201
|
+
if (knownProviders.has(maybeProvider.toLowerCase())) {
|
|
202
|
+
const match = availableModels.find(m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
|
|
203
|
+
&& m.id.toLowerCase() === id.toLowerCase());
|
|
204
|
+
if (match)
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
// Try matching the full string as a model ID (OpenRouter-style)
|
|
208
|
+
const lower = modelId.toLowerCase();
|
|
209
|
+
return availableModels.find(m => m.id.toLowerCase() === lower
|
|
210
|
+
|| `${m.provider}/${m.id}`.toLowerCase() === lower);
|
|
211
|
+
}
|
|
212
|
+
// Bare ID — prefer current provider, then first available
|
|
213
|
+
const exactProviderMatch = availableModels.find(m => m.id === modelId && m.provider === currentProvider);
|
|
214
|
+
return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
|
|
215
|
+
}
|
|
166
216
|
/**
|
|
167
217
|
* Build the discuss-and-plan prompt for a new milestone.
|
|
168
218
|
* Used by all three "new milestone" paths (first ever, no active, all complete).
|
|
@@ -244,8 +294,8 @@ export async function showHeadlessMilestoneCreation(ctx, pi, basePath, seedConte
|
|
|
244
294
|
const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
|
|
245
295
|
// Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
|
|
246
296
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
247
|
-
// Dispatch
|
|
248
|
-
dispatchWorkflow(pi, prompt);
|
|
297
|
+
// Dispatch — headless milestone creation is a planning activity
|
|
298
|
+
await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
|
|
249
299
|
}
|
|
250
300
|
// ─── Discuss Flow ─────────────────────────────────────────────────────────────
|
|
251
301
|
/**
|
|
@@ -381,23 +431,23 @@ export async function showDiscuss(ctx, pi, basePath) {
|
|
|
381
431
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
382
432
|
: basePrompt;
|
|
383
433
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
384
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
434
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
385
435
|
}
|
|
386
436
|
else if (choice === "discuss_fresh") {
|
|
387
437
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
388
438
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
389
439
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
390
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
440
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
391
441
|
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
392
442
|
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
|
393
|
-
}), "gsd-discuss");
|
|
443
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
394
444
|
}
|
|
395
445
|
else if (choice === "skip_milestone") {
|
|
396
446
|
const milestoneIds = findMilestoneIds(basePath);
|
|
397
447
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
398
448
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
399
449
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
|
|
400
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
450
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
401
451
|
}
|
|
402
452
|
return;
|
|
403
453
|
}
|
|
@@ -484,7 +534,7 @@ export async function showDiscuss(ctx, pi, basePath) {
|
|
|
484
534
|
continue;
|
|
485
535
|
}
|
|
486
536
|
const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
|
|
487
|
-
dispatchWorkflow(pi, prompt, "gsd-discuss");
|
|
537
|
+
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
|
|
488
538
|
// Wait for the discuss session to finish, then loop back to the picker
|
|
489
539
|
await ctx.waitForIdle();
|
|
490
540
|
invalidateAllCaches();
|
|
@@ -611,7 +661,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
|
|
|
611
661
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
612
662
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
613
663
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
614
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
664
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
615
665
|
return true;
|
|
616
666
|
}
|
|
617
667
|
// "back" or null
|
|
@@ -729,7 +779,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
729
779
|
if (isFirst) {
|
|
730
780
|
// First ever — skip wizard, just ask directly
|
|
731
781
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
732
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath));
|
|
782
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
733
783
|
}
|
|
734
784
|
else {
|
|
735
785
|
const choice = await showNextAction(ctx, {
|
|
@@ -747,7 +797,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
747
797
|
});
|
|
748
798
|
if (choice === "new_milestone") {
|
|
749
799
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
750
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
800
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
751
801
|
}
|
|
752
802
|
}
|
|
753
803
|
return;
|
|
@@ -779,7 +829,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
779
829
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
780
830
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
781
831
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
782
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
832
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
783
833
|
}
|
|
784
834
|
else if (choice === "status") {
|
|
785
835
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -825,23 +875,23 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
825
875
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
826
876
|
: basePrompt;
|
|
827
877
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
828
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
878
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
829
879
|
}
|
|
830
880
|
else if (choice === "discuss_fresh") {
|
|
831
881
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
832
882
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
833
883
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
834
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
884
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
835
885
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
836
886
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
837
|
-
}), "gsd-discuss");
|
|
887
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
838
888
|
}
|
|
839
889
|
else if (choice === "skip_milestone") {
|
|
840
890
|
const milestoneIds = findMilestoneIds(basePath);
|
|
841
891
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
842
892
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
843
893
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
844
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
894
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
845
895
|
}
|
|
846
896
|
return;
|
|
847
897
|
}
|
|
@@ -893,24 +943,24 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
893
943
|
inlineTemplate("secrets-manifest", "Secrets Manifest"),
|
|
894
944
|
].join("\n\n---\n\n");
|
|
895
945
|
const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
|
|
896
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
946
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
897
947
|
milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
|
|
898
|
-
}));
|
|
948
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
899
949
|
}
|
|
900
950
|
else if (choice === "discuss") {
|
|
901
951
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
902
952
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
903
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
953
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
904
954
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
905
955
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
906
|
-
}));
|
|
956
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
907
957
|
}
|
|
908
958
|
else if (choice === "skip_milestone") {
|
|
909
959
|
const milestoneIds = findMilestoneIds(basePath);
|
|
910
960
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
911
961
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
912
962
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
913
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
963
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
914
964
|
}
|
|
915
965
|
else if (choice === "discard_milestone") {
|
|
916
966
|
const confirmed = await showConfirm(ctx, {
|
|
@@ -1021,18 +1071,18 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1021
1071
|
inlineTemplate("plan", "Slice Plan"),
|
|
1022
1072
|
inlineTemplate("task-plan", "Task Plan"),
|
|
1023
1073
|
].join("\n\n---\n\n");
|
|
1024
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1074
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1025
1075
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
|
|
1026
|
-
}));
|
|
1076
|
+
}), "gsd-run", ctx, "plan-slice");
|
|
1027
1077
|
}
|
|
1028
1078
|
else if (choice === "discuss") {
|
|
1029
|
-
dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
|
|
1079
|
+
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
|
|
1030
1080
|
}
|
|
1031
1081
|
else if (choice === "research") {
|
|
1032
1082
|
const researchTemplates = inlineTemplate("research", "Research");
|
|
1033
|
-
dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1083
|
+
await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1034
1084
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
|
|
1035
|
-
}));
|
|
1085
|
+
}), "gsd-run", ctx, "research-slice");
|
|
1036
1086
|
}
|
|
1037
1087
|
else if (choice === "status") {
|
|
1038
1088
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1075,9 +1125,9 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1075
1125
|
inlineTemplate("slice-summary", "Slice Summary"),
|
|
1076
1126
|
inlineTemplate("uat", "UAT"),
|
|
1077
1127
|
].join("\n\n---\n\n");
|
|
1078
|
-
dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1128
|
+
await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1079
1129
|
workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
|
|
1080
|
-
}));
|
|
1130
|
+
}), "gsd-run", ctx, "complete-slice");
|
|
1081
1131
|
}
|
|
1082
1132
|
else if (choice === "status") {
|
|
1083
1133
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1138,15 +1188,15 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1138
1188
|
}
|
|
1139
1189
|
if (choice === "execute") {
|
|
1140
1190
|
if (hasInterrupted) {
|
|
1141
|
-
dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1191
|
+
await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1142
1192
|
milestoneId, sliceId,
|
|
1143
|
-
}));
|
|
1193
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1144
1194
|
}
|
|
1145
1195
|
else {
|
|
1146
1196
|
const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1147
|
-
dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1197
|
+
await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1148
1198
|
milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
|
|
1149
|
-
}));
|
|
1199
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1150
1200
|
}
|
|
1151
1201
|
}
|
|
1152
1202
|
else if (choice === "status") {
|
|
@@ -518,6 +518,43 @@ export function nativeAddAll(basePath) {
|
|
|
518
518
|
}
|
|
519
519
|
gitFileExec(basePath, ["add", "-A"]);
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
|
523
|
+
* Excluded paths are never hashed by git, preventing hangs on large
|
|
524
|
+
* untracked artifact trees (57GB+, 11K+ files). See #1605.
|
|
525
|
+
*
|
|
526
|
+
* Falls back to plain `git add -A` when no exclusions are provided.
|
|
527
|
+
* Always uses the CLI path (not libgit2) because libgit2's add_all
|
|
528
|
+
* does not support pathspec exclusion syntax.
|
|
529
|
+
*
|
|
530
|
+
* When excluded paths are already covered by .gitignore, git may exit
|
|
531
|
+
* with code 1 and an "ignored by .gitignore" warning. This is harmless
|
|
532
|
+
* (the staging succeeds for all non-ignored files) and is suppressed.
|
|
533
|
+
*/
|
|
534
|
+
export function nativeAddAllWithExclusions(basePath, exclusions) {
|
|
535
|
+
if (exclusions.length === 0) {
|
|
536
|
+
nativeAddAll(basePath);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const pathspecs = exclusions.map(e => `:!${e}`);
|
|
540
|
+
try {
|
|
541
|
+
execFileSync("git", ["add", "-A", "--", ...pathspecs], {
|
|
542
|
+
cwd: basePath,
|
|
543
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
env: GIT_NO_PROMPT_ENV,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
// git exits 1 when pathspec exclusions reference paths already covered
|
|
550
|
+
// by .gitignore. The staging itself succeeds — only suppress that case.
|
|
551
|
+
const stderr = err?.stderr ?? "";
|
|
552
|
+
if (stderr.includes("ignored by one of your .gitignore files")) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
521
558
|
/**
|
|
522
559
|
* Stage specific files.
|
|
523
560
|
* Native: libgit2 index add.
|