Four bugfixes for open issues: 1. Worktree created from integration branch, not main (#606) - createAutoWorktree reads integration branch from META.json - mergeMilestoneToMain merges to integration branch, not hardcoded main - createWorktree accepts optional startPoint parameter 2. Resolve project root from worktree paths in all commands (#608, #602) - Add resolveProjectRoot() to detect .gsd/worktrees/ in cwd - All GSD commands use projectRoot() instead of raw process.cwd() - Fixes stale cwd after milestone completion (#608) - Fixes discuss/status basepath disagreement (#602) 3. Milestone merge skipped in branch isolation mode (#603) - Add branch-mode fallback when isInAutoWorktree() is false - Detects milestone/* branch and performs squash-merge - Uses same mergeMilestoneToMain flow as worktree mode 4. Remote questions onboarding missing .js module (#592) - Extract saveRemoteQuestionsConfig into compiled src/ helper - Avoids cross-boundary import from compiled JS to raw .ts
This commit is contained in:
parent
7e25e6d427
commit
15be720fbf
5 changed files with 113 additions and 18 deletions
|
|
@ -747,7 +747,7 @@ async function runRemoteQuestionsStep(
|
|||
})
|
||||
if (p.isCancel(channelId) || !channelId) return null
|
||||
|
||||
const { saveRemoteQuestionsConfig } = await import('./resources/extensions/remote-questions/remote-command.js')
|
||||
const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js')
|
||||
saveRemoteQuestionsConfig('slack', (channelId as string).trim())
|
||||
p.log.success(`Slack channel: ${pc.green((channelId as string).trim())}`)
|
||||
return 'Slack'
|
||||
|
|
@ -852,7 +852,7 @@ async function runDiscordChannelStep(p: ClackModule, pc: PicoModule, token: stri
|
|||
}
|
||||
|
||||
// Save remote questions config
|
||||
const { saveRemoteQuestionsConfig } = await import('./resources/extensions/remote-questions/remote-command.js')
|
||||
const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js')
|
||||
saveRemoteQuestionsConfig('discord', channelId)
|
||||
const channelName = channels.find(ch => ch.id === channelId)?.name
|
||||
p.log.success(`Discord channel: ${pc.green(channelName ? `#${channelName}` : channelId)}`)
|
||||
|
|
|
|||
40
src/remote-questions-config.ts
Normal file
40
src/remote-questions-config.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Remote Questions Config Helper
|
||||
*
|
||||
* Extracted from remote-questions extension so onboarding.ts can import
|
||||
* it without crossing the compiled/uncompiled boundary. The extension
|
||||
* files in src/resources/ are shipped as raw .ts and loaded via jiti,
|
||||
* but onboarding.ts is compiled by tsc — dynamic imports from compiled
|
||||
* JS to uncompiled .ts fail at runtime (#592).
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { getGlobalGSDPreferencesPath } from "./resources/extensions/gsd/preferences.js";
|
||||
|
||||
export function saveRemoteQuestionsConfig(channel: "slack" | "discord", channelId: string): void {
|
||||
const prefsPath = getGlobalGSDPreferencesPath();
|
||||
const block = [
|
||||
"remote_questions:",
|
||||
` channel: ${channel}`,
|
||||
` channel_id: "${channelId}"`,
|
||||
" timeout_minutes: 5",
|
||||
" poll_interval_seconds: 5",
|
||||
].join("\n");
|
||||
|
||||
const content = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
let next = content;
|
||||
|
||||
if (fmMatch) {
|
||||
let frontmatter = fmMatch[1];
|
||||
const regex = /remote_questions:[\s\S]*?(?=\n[a-zA-Z_]|\n---|$)/;
|
||||
frontmatter = regex.test(frontmatter) ? frontmatter.replace(regex, block) : `${frontmatter.trimEnd()}\n${block}`;
|
||||
next = `---\n${frontmatter}\n---${content.slice(fmMatch[0].length)}`;
|
||||
} else {
|
||||
next = `---\n${block}\n---\n\n${content}`;
|
||||
}
|
||||
|
||||
mkdirSync(dirname(prefsPath), { recursive: true });
|
||||
writeFileSync(prefsPath, next, "utf-8");
|
||||
}
|
||||
|
|
@ -92,6 +92,7 @@ import {
|
|||
getAutoWorktreePath,
|
||||
getAutoWorktreeOriginalBase,
|
||||
mergeMilestoneToMain,
|
||||
autoWorktreeBranch,
|
||||
} from "./auto-worktree.js";
|
||||
import { pruneQueueOrder } from "./queue-order.js";
|
||||
import { showNextAction } from "../shared/next-action-ui.js";
|
||||
|
|
@ -1404,6 +1405,32 @@ async function dispatchNextUnit(
|
|||
try { process.chdir(basePath); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
} else if (currentMilestoneId && !isInAutoWorktree(basePath)) {
|
||||
// Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
|
||||
// Squash-merge back to the integration branch (or main) before stopping.
|
||||
try {
|
||||
const currentBranch = getCurrentBranch(basePath);
|
||||
const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
|
||||
if (currentBranch === milestoneBranch) {
|
||||
const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
|
||||
if (roadmapPath) {
|
||||
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
||||
// mergeMilestoneToMain handles: auto-commit, checkout integration branch,
|
||||
// squash merge, commit, optional push, branch deletion.
|
||||
const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
|
||||
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
||||
ctx.ui.notify(
|
||||
`Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.ui.notify(
|
||||
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
}
|
||||
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
||||
await stopAuto(ctx, pi);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { deriveState } from "./state.js";
|
|||
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
||||
import { showQueue, showDiscuss } from "./guided-flow.js";
|
||||
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
||||
import { resolveProjectRoot } from "./worktree.js";
|
||||
import {
|
||||
getGlobalGSDPreferencesPath,
|
||||
getLegacyGlobalGSDPreferencesPath,
|
||||
|
|
@ -56,6 +57,11 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|||
);
|
||||
}
|
||||
|
||||
/** Resolve the effective project root, accounting for worktree paths. */
|
||||
function projectRoot(): string {
|
||||
return resolveProjectRoot(process.cwd());
|
||||
}
|
||||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("gsd", {
|
||||
description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer|knowledge",
|
||||
|
|
@ -169,24 +175,24 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
|
||||
if (trimmed === "next" || trimmed.startsWith("next ")) {
|
||||
if (trimmed.includes("--dry-run")) {
|
||||
await handleDryRun(ctx, process.cwd());
|
||||
await handleDryRun(ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
const verboseMode = trimmed.includes("--verbose");
|
||||
await startAuto(ctx, pi, process.cwd(), verboseMode, { step: true });
|
||||
await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "auto" || trimmed.startsWith("auto ")) {
|
||||
const verboseMode = trimmed.includes("--verbose");
|
||||
await startAuto(ctx, pi, process.cwd(), verboseMode);
|
||||
await startAuto(ctx, pi, projectRoot(), verboseMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "stop") {
|
||||
if (!isAutoActive() && !isAutoPaused()) {
|
||||
// Not running in this process — check for a remote auto-mode session
|
||||
const result = stopAutoRemote(process.cwd());
|
||||
const result = stopAutoRemote(projectRoot());
|
||||
if (result.found) {
|
||||
ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
|
||||
} else if (result.error) {
|
||||
|
|
@ -214,42 +220,42 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
}
|
||||
|
||||
if (trimmed === "history" || trimmed.startsWith("history ")) {
|
||||
await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, process.cwd());
|
||||
await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "undo" || trimmed.startsWith("undo ")) {
|
||||
await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, process.cwd());
|
||||
await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("skip ")) {
|
||||
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, process.cwd());
|
||||
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "export" || trimmed.startsWith("export ")) {
|
||||
await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, process.cwd());
|
||||
await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "cleanup branches") {
|
||||
await handleCleanupBranches(ctx, process.cwd());
|
||||
await handleCleanupBranches(ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "cleanup snapshots") {
|
||||
await handleCleanupSnapshots(ctx, process.cwd());
|
||||
await handleCleanupSnapshots(ctx, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "queue") {
|
||||
await showQueue(ctx, pi, process.cwd());
|
||||
await showQueue(ctx, pi, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "discuss") {
|
||||
await showDiscuss(ctx, pi, process.cwd());
|
||||
await showDiscuss(ctx, pi, projectRoot());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +301,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
|
||||
if (trimmed === "") {
|
||||
// Bare /gsd defaults to step mode
|
||||
await startAuto(ctx, pi, process.cwd(), false, { step: true });
|
||||
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +314,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
}
|
||||
|
||||
async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
|
||||
const basePath = process.cwd();
|
||||
const basePath = projectRoot();
|
||||
const state = await deriveState(basePath);
|
||||
|
||||
if (state.registry.length === 0) {
|
||||
|
|
@ -392,9 +398,9 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
|
|||
const parts = trimmed ? trimmed.split(/\s+/) : [];
|
||||
const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
|
||||
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
||||
const scope = await selectDoctorScope(process.cwd(), requestedScope);
|
||||
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
||||
const effectiveScope = mode === "audit" ? requestedScope : scope;
|
||||
const report = await runGSDDoctor(process.cwd(), {
|
||||
const report = await runGSDDoctor(projectRoot(), {
|
||||
fix: mode === "fix" || mode === "heal",
|
||||
scope: effectiveScope,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,6 +76,28 @@ export function detectWorktreeName(basePath: string): string | null {
|
|||
return name || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the project root from a path that may be inside a worktree.
|
||||
* If the path contains `/.gsd/worktrees/<name>/`, returns the portion
|
||||
* before `/.gsd/`. Otherwise returns the input unchanged.
|
||||
*
|
||||
* Use this in commands that call `process.cwd()` to ensure they always
|
||||
* operate against the real project root, not a worktree subdirectory.
|
||||
*/
|
||||
export function resolveProjectRoot(basePath: string): string {
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const marker = "/.gsd/worktrees/";
|
||||
const idx = normalizedPath.indexOf(marker);
|
||||
if (idx === -1) return basePath;
|
||||
// Return the original path up to the .gsd/ marker (un-normalized)
|
||||
// Account for potential OS-specific separators
|
||||
const sep = basePath.includes("\\") ? "\\" : "/";
|
||||
const markerOs = `${sep}.gsd${sep}worktrees${sep}`;
|
||||
const idxOs = basePath.indexOf(markerOs);
|
||||
if (idxOs !== -1) return basePath.slice(0, idxOs);
|
||||
return basePath.slice(0, idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slice branch name, namespaced by worktree when inside one.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue