Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/resources/extensions/gsd/auto-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,12 @@ export async function bootstrapAutoSession(
s.currentMilestoneId = null;
}

if (!hasSurvivorBranch && !deepProjectStagePending) {
// Custom workflow runs do not require an active milestone. They manage
// progression from GRAPH.yaml/runDir state instead of milestone planning
// state, so guided-flow discussion must not preempt them.
const isCustomWorkflowRun = !!s.activeEngineId && s.activeEngineId !== "dev";

if (!hasSurvivorBranch && !deepProjectStagePending && !isCustomWorkflowRun) {
// No active work — start a new milestone via discuss flow
if (!state.activeMilestone || state.phase === "complete") {
// Guard against recursive dialog loop (#1348):
Expand Down Expand Up @@ -1099,8 +1104,8 @@ export async function bootstrapAutoSession(
}
}

// Unreachable safety check
if (!state.activeMilestone && !deepProjectStagePending) {
// Unreachable safety check (also guarded by isCustomWorkflowRun above)
if (!state.activeMilestone && !deepProjectStagePending && !isCustomWorkflowRun) {
const { showSmartEntry } = await import("./guided-flow.js");
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
return releaseLockAndReturn();
Expand Down
76 changes: 76 additions & 0 deletions src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ function makeRepo(): string {
return base;
}

function makeLightRepo(): string {
const base = join(tmpdir(), `gsd-custom-workflow-bootstrap-${randomUUID()}`);
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base });
execFileSync("git", ["config", "user.name", "Test"], { cwd: base });
writeFileSync(join(base, "README.md"), "# test\n");
execFileSync("git", ["add", "-A"], { cwd: base, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "init"], { cwd: base, stdio: "ignore" });
return base;
}

function makeCtx(sessionId = "test-session") {
const model = { provider: "claude-code", id: "claude-sonnet-4-6", contextWindow: 128000 };
return {
Expand Down Expand Up @@ -311,6 +323,70 @@ test("deep project setup: bootstrap can start auto-mode without an active milest
}
});

test("custom workflow bootstrap: no active milestone does not enter guided discuss", async () => {
const base = makeLightRepo();
const previousCwd = process.cwd();
const messages: unknown[] = [];
try {
process.chdir(base);
const s = new AutoSession();
s.activeEngineId = "custom";
s.activeRunDir = join(base, ".gsd", "workflow-runs", "test", "run");

const ready = await bootstrapAutoSession(
s,
makeCtx(`custom-${randomUUID()}`) as any,
{
...makePi(messages),
getThinkingLevel: () => "medium",
} as any,
base,
false,
false,
{
shouldUseWorktreeIsolation: () => false,
registerSigtermHandler: () => {},
registerAutoWorkerForSession: () => {},
lockBase: () => base,
buildLifecycle: () => ({
adoptSessionRoot: (sessionBase: string, originalBase?: string) => {
s.basePath = sessionBase;
if (originalBase !== undefined) {
s.originalBasePath = originalBase;
} else if (!s.originalBasePath) {
s.originalBasePath = sessionBase;
}
},
}) as any,
},
{
classification: "none",
lock: null,
pausedSession: null,
state: null,
recovery: null,
recoveryPrompt: null,
recoveryToolCallCount: 0,
artifactSatisfied: false,
hasResumableDiskState: false,
isBootstrapCrash: false,
},
);

assert.equal(ready, true);
assert.equal(s.active, true);
assert.equal(s.currentMilestoneId, null);
assert.deepEqual(messages, [], "custom workflow bootstrap must not dispatch guided discuss");
} finally {
process.chdir(previousCwd);
try {
const { closeDatabase } = await import("../gsd-db.ts");
closeDatabase();
} catch {}
rmSync(base, { recursive: true, force: true });
}
});

test("deep project setup: pre-dispatch can run before the first milestone exists", async () => {
const base = makeBase();
try {
Expand Down
Loading