merge: sync main into PR #3114 and keep canonical next/profile commands
This commit is contained in:
@@ -792,19 +792,13 @@ function parseWorktreePorcelain(porcelain) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove linked git worktrees whose branch has already been merged into the
|
||||
* current HEAD of the main worktree. Also runs `git worktree prune` to clear
|
||||
* any stale references left by manually-deleted worktree directories.
|
||||
* Clear stale worktree metadata references via `git worktree prune`.
|
||||
*
|
||||
* Safe guards:
|
||||
* - Never removes the main worktree (first entry in --porcelain output).
|
||||
* - Never removes the worktree at process.cwd().
|
||||
* - Never removes a worktree whose branch has unmerged commits.
|
||||
* - Skips detached-HEAD worktrees (no branch name).
|
||||
* Destructive linked-worktree removal is disabled by default for safety.
|
||||
*
|
||||
* @param {string} repoRoot - absolute path to the main (or any) worktree of
|
||||
* the repository; used as `cwd` for git commands.
|
||||
* @returns {string[]} list of worktree paths that were removed
|
||||
* @returns {string[]} list of worktree paths that were removed (always empty)
|
||||
*/
|
||||
function pruneOrphanedWorktrees(repoRoot) {
|
||||
const pruned = [];
|
||||
@@ -821,37 +815,14 @@ function pruneOrphanedWorktrees(repoRoot) {
|
||||
return pruned;
|
||||
}
|
||||
|
||||
// 2. First entry is the main worktree — never touch it
|
||||
const mainWorktreePath = worktrees[0].path;
|
||||
|
||||
// 3. Check each non-main worktree
|
||||
for (let i = 1; i < worktrees.length; i++) {
|
||||
const { path: wtPath, branch } = worktrees[i];
|
||||
|
||||
// Never remove the worktree for the current process directory
|
||||
if (wtPath === cwd || cwd.startsWith(wtPath + path.sep)) continue;
|
||||
|
||||
// Check if the branch is fully merged into HEAD (main)
|
||||
// git merge-base --is-ancestor <branch> HEAD exits 0 when merged
|
||||
const ancestorCheck = execGit(repoRoot, [
|
||||
'merge-base', '--is-ancestor', branch, 'HEAD',
|
||||
]);
|
||||
|
||||
if (ancestorCheck.exitCode !== 0) {
|
||||
// Not yet merged — leave it alone
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove the worktree and delete the branch
|
||||
const removeResult = execGit(repoRoot, ['worktree', 'remove', '--force', wtPath]);
|
||||
if (removeResult.exitCode === 0) {
|
||||
execGit(repoRoot, ['branch', '-D', branch]);
|
||||
pruned.push(wtPath);
|
||||
}
|
||||
}
|
||||
// Destructive removal of linked worktrees is intentionally disabled.
|
||||
// Keep metadata cleanup only (git worktree prune), which clears stale refs
|
||||
// for manually-deleted directories without removing active sibling worktrees.
|
||||
void cwd;
|
||||
void worktrees;
|
||||
} catch { /* never crash the caller */ }
|
||||
|
||||
// 4. Always run prune to clear stale references (e.g. manually-deleted dirs)
|
||||
// Always run prune to clear stale references (e.g. manually-deleted dirs)
|
||||
execGit(repoRoot, ['worktree', 'prune']);
|
||||
|
||||
return pruned;
|
||||
@@ -1895,10 +1866,62 @@ function getMilestoneInfo(cwd) {
|
||||
* to the current milestone based on ROADMAP.md phase headings.
|
||||
* If no ROADMAP exists or no phases are listed, returns a pass-all filter.
|
||||
*/
|
||||
function getMilestonePhaseFilter(cwd) {
|
||||
function getMilestonePhaseFilter(cwd, versionOverride) {
|
||||
const milestonePhaseNums = new Set();
|
||||
let missingExplicitVersion = false;
|
||||
try {
|
||||
const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
||||
|
||||
if (versionOverride) {
|
||||
const escapedVersion = escapeRegex(versionOverride);
|
||||
const sectionPattern = new RegExp(`(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`, 'mi');
|
||||
const sectionMatch = roadmapContent.match(sectionPattern);
|
||||
if (!sectionMatch) {
|
||||
// Only treat this as an error case when the roadmap is milestone-versioned.
|
||||
// Older/flat roadmap formats without vX.Y milestone headings should keep
|
||||
// legacy pass-through behavior for milestone.complete.
|
||||
const hasVersionedMilestones = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
||||
if (hasVersionedMilestones) {
|
||||
roadmap = '';
|
||||
missingExplicitVersion = true;
|
||||
}
|
||||
} else {
|
||||
const sectionStart = sectionMatch.index;
|
||||
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
|
||||
const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
|
||||
const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
||||
|
||||
let sectionEnd = roadmapContent.length;
|
||||
let fenceChar = null;
|
||||
let fenceLen = 0;
|
||||
let charOffset = 0;
|
||||
for (const line of restContent.split('\n')) {
|
||||
const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
||||
if (fenceMatch) {
|
||||
const char = fenceMatch[1][0];
|
||||
const len = fenceMatch[1].length;
|
||||
const trailing = fenceMatch[2] || '';
|
||||
if (!fenceChar) {
|
||||
fenceChar = char;
|
||||
fenceLen = len;
|
||||
} else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
|
||||
fenceChar = null;
|
||||
fenceLen = 0;
|
||||
}
|
||||
} else if (!fenceChar && nextMilestonePattern.test(line)) {
|
||||
sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
|
||||
break;
|
||||
}
|
||||
charOffset += line.length + 1;
|
||||
}
|
||||
|
||||
const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
|
||||
roadmap = currentSection;
|
||||
}
|
||||
}
|
||||
|
||||
// Match both numeric phases (Phase 1:) and custom IDs (Phase PROJ-42:)
|
||||
const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
||||
let m;
|
||||
@@ -1910,6 +1933,7 @@ function getMilestonePhaseFilter(cwd) {
|
||||
if (milestonePhaseNums.size === 0) {
|
||||
const passAll = () => true;
|
||||
passAll.phaseCount = 0;
|
||||
passAll.missingExplicitVersion = missingExplicitVersion;
|
||||
return passAll;
|
||||
}
|
||||
|
||||
@@ -1927,6 +1951,7 @@ function getMilestonePhaseFilter(cwd) {
|
||||
return false;
|
||||
}
|
||||
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
||||
isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
|
||||
return isDirInMilestone;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
// Scope stats and accomplishments to only the phases belonging to the
|
||||
// current milestone's ROADMAP. Uses the shared filter from core.cjs
|
||||
// (same logic used by cmdPhasesList and other callers).
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
|
||||
if (isDirInMilestone.missingExplicitVersion) {
|
||||
error(`no phases found for milestone ${version} in ROADMAP.md`);
|
||||
}
|
||||
|
||||
// Gather stats from phases (scoped to current milestone only)
|
||||
let phaseCount = 0;
|
||||
@@ -196,7 +199,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
|
||||
}
|
||||
|
||||
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
||||
// Update STATE.md — keep frontmatter/body semantically aligned after closure
|
||||
if (fs.existsSync(statePath)) {
|
||||
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
@@ -205,6 +208,31 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
||||
`${version} milestone completed and archived`);
|
||||
|
||||
// Reset Current Position narrative so resume/progress flows do not keep
|
||||
// pointing at closed-phase execution instructions.
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const closedPositionBody =
|
||||
`\nPhase: Milestone ${version} complete\n` +
|
||||
`Plan: —\n` +
|
||||
`Status: Awaiting next milestone\n` +
|
||||
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
|
||||
if (positionPattern.test(stateContent)) {
|
||||
stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
|
||||
} else {
|
||||
stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
|
||||
}
|
||||
|
||||
// Normalize operator-next-step tails that can become stale after close.
|
||||
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
if (operatorPattern.test(stateContent)) {
|
||||
stateContent = stateContent.replace(
|
||||
operatorPattern,
|
||||
`$1\n- Start the next milestone with /gsd-new-milestone\n\n`,
|
||||
);
|
||||
} else {
|
||||
stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with /gsd-new-milestone\n`;
|
||||
}
|
||||
|
||||
writeStateMd(statePath, stateContent, cwd);
|
||||
}
|
||||
|
||||
|
||||
@@ -244,8 +244,8 @@ For each CONTEXT.md read: extract `<decisions>` (locked preferences), `<specific
|
||||
|
||||
**Spike/sketch findings:** Check for project-local skills:
|
||||
```bash
|
||||
SPIKE_FINDINGS=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_FINDINGS=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
SKETCH_FINDINGS=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
RAW_SPIKES=$(ls .planning/spikes/MANIFEST.md 2>/dev/null)
|
||||
RAW_SKETCHES=$(ls .planning/sketches/MANIFEST.md 2>/dev/null)
|
||||
```
|
||||
|
||||
@@ -253,10 +253,10 @@ Check for existing spike and sketch work that should inform project setup:
|
||||
|
||||
```bash
|
||||
# Check for spike findings skill (project-local)
|
||||
SPIKE_SKILL=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_SKILL=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
|
||||
# Check for sketch findings skill (project-local)
|
||||
SKETCH_SKILL=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_SKILL=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
|
||||
# Check for raw spikes/sketches in .planning/
|
||||
HAS_SPIKES=$(ls .planning/spikes/MANIFEST.md 2>/dev/null)
|
||||
|
||||
@@ -668,8 +668,8 @@ REVIEWS_PATH=$(_gsd_field "$INIT" reviews_path)
|
||||
PATTERNS_PATH=$(_gsd_field "$INIT" patterns_path)
|
||||
|
||||
# Detect spike/sketch findings skills (project-local)
|
||||
SPIKE_FINDINGS_PATH=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_FINDINGS_PATH=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
```
|
||||
|
||||
## 7.5. Verify Nyquist Artifacts
|
||||
|
||||
@@ -275,7 +275,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" ||
|
||||
**Also available:**
|
||||
- `/gsd-ui-phase {phase}` — generate UI design contract (recommended for frontend phases)
|
||||
- `/gsd-plan-phase {phase}` — skip discussion, plan directly
|
||||
- `/gsd-list-phase-assumptions {phase}` — see Claude's assumptions
|
||||
- `/gsd-discuss-phase {phase}` — include assumptions check before planning
|
||||
|
||||
---
|
||||
```
|
||||
@@ -297,7 +297,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" ||
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-plan-phase {phase} ${GSD_WS}` — skip discussion, plan directly
|
||||
- `/gsd-list-phase-assumptions {phase} ${GSD_WS}` — see Claude's assumptions
|
||||
- `/gsd-discuss-phase {phase} ${GSD_WS}` — include assumptions check before planning
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded
|
||||
|
||||
Detect sketch findings:
|
||||
```bash
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
```
|
||||
|
||||
Resolve UI agent models:
|
||||
|
||||
Reference in New Issue
Block a user