merge: sync main into PR #3114 and keep canonical next/profile commands
This commit is contained in:
@@ -26,7 +26,8 @@ const CONFIG_MUTATION_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'co
|
||||
// #2653 — allowlist moved to shared schema module.
|
||||
const CONFIG_SCHEMA_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'config-schema.ts');
|
||||
const CONFIG_GATES_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'config-gates.ts');
|
||||
const QUERY_INDEX_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'index.ts');
|
||||
const QUERY_INDEX_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'registry-assembly.ts');
|
||||
const QUERY_ASSEMBLY_DESCRIPTOR_TS = path.join(__dirname, '..', 'sdk', 'src', 'query', 'registry-assembly-descriptor.ts');
|
||||
|
||||
describe('plan-phase decision-coverage gate (#2492)', () => {
|
||||
const md = fs.readFileSync(PLAN_PHASE, 'utf-8');
|
||||
@@ -168,10 +169,11 @@ describe('SDK wiring for #2492 gates', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('query index.ts registers the new handlers', () => {
|
||||
test('query registry assembly registers the new handlers', () => {
|
||||
const c = fs.readFileSync(QUERY_INDEX_TS, 'utf-8');
|
||||
assert.ok(c.includes('check.decision-coverage-plan'), 'check.decision-coverage-plan handler must be registered');
|
||||
assert.ok(c.includes('check.decision-coverage-verify'), 'check.decision-coverage-verify handler must be registered');
|
||||
assert.ok(c.includes('decisions.parse'), 'decisions.parse handler must be registered');
|
||||
const d = fs.readFileSync(QUERY_ASSEMBLY_DESCRIPTOR_TS, 'utf-8');
|
||||
assert.ok(c.includes('REGISTRY_ASSEMBLY_PLAN'), 'registry assembly must be driven by declarative plan');
|
||||
assert.ok(d.includes('VERIFY_DECISION_STATIC_CATALOG'), 'decision-coverage handlers must be sourced from verify-decision catalog');
|
||||
assert.ok(d.includes("{ kind: 'static', key: 'VERIFY_DECISION_STATIC_CATALOG' }"), 'verify-decision catalog must be present in assembly plan');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
/**
|
||||
* Bug #2524: gsd-sdk query --ws <name> silently ignores the workstream flag.
|
||||
* Tests that --ws is forwarded through the call chain:
|
||||
* cli.ts -> registry.dispatch() -> planningPaths()
|
||||
*
|
||||
* This file is structural/static coverage only (source-file assertions).
|
||||
* Runtime forwarding coverage for the query adapter path lives in:
|
||||
* sdk/src/query/query-cli-adapter.test.ts
|
||||
*
|
||||
* Uses static source-file text assertions (no sdk/dist/ build required in CI).
|
||||
*/
|
||||
@@ -88,12 +90,6 @@ describe('QueryRegistry.dispatch() workstream threading', () => {
|
||||
// ─── Layer 1: CLI forwards args.ws to registry.dispatch() ─────────────────
|
||||
|
||||
describe('CLI forwards --ws to registry.dispatch()', () => {
|
||||
test('cli.ts passes args.ws as the workstream argument to registry.dispatch()', () => {
|
||||
assert.ok(
|
||||
cliTs.includes('registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws)'),
|
||||
'cli.ts must forward args.ws to registry.dispatch() as the workstream argument',
|
||||
);
|
||||
});
|
||||
|
||||
test('cli.ts defines a ws field in ParsedCliArgs', () => {
|
||||
assert.ok(
|
||||
@@ -108,4 +104,5 @@ describe('CLI forwards --ws to registry.dispatch()', () => {
|
||||
'cli.ts query permissive parser must handle the --ws flag',
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -302,6 +302,28 @@ describe('bug #3044: localized doc sets also scrubbed', () => {
|
||||
|
||||
// ─── Replacement commands are documented ────────────────────────────────────
|
||||
|
||||
describe('bug #3094: progress routing does not reference removed /gsd-list-phase-assumptions', () => {
|
||||
test('get-shit-done/workflows/progress.md has no /gsd-list-phase-assumptions token', () => {
|
||||
const content = read('get-shit-done/workflows/progress.md');
|
||||
const tokens = extractSlashCommandTokens(content);
|
||||
assert.equal(
|
||||
tokens.has('/gsd-list-phase-assumptions'),
|
||||
false,
|
||||
'progress.md must not recommend removed /gsd-list-phase-assumptions'
|
||||
);
|
||||
});
|
||||
|
||||
test('progress.md pre-planning guidance uses /gsd-discuss-phase instead', () => {
|
||||
const content = read('get-shit-done/workflows/progress.md');
|
||||
const tokens = extractSlashCommandTokens(content);
|
||||
assert.equal(
|
||||
tokens.has('/gsd-discuss-phase'),
|
||||
true,
|
||||
'progress.md should route pre-planning assumption checks via /gsd-discuss-phase'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replacement commands appear where the deleted ones used to live', () => {
|
||||
test('docs/issue-driven-orchestration.md uses /gsd-workspace --new (not /gsd-new-workspace)', () => {
|
||||
const content = read('docs/issue-driven-orchestration.md');
|
||||
|
||||
57
tests/bug-3043-milestone-complete-scope.test.cjs
Normal file
57
tests/bug-3043-milestone-complete-scope.test.cjs
Normal file
@@ -0,0 +1,57 @@
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
describe('bug #3043: milestone complete respects explicit version scope', () => {
|
||||
test('milestone.complete v3.6 uses v3.6 phases even when STATE milestone is v3.5', () => {
|
||||
const tmpDir = createTempProject('gsd-bug-3043-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '---\nmilestone: v3.5\n---\n');
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n## 🚧 v3.5 Paused\n### Phase 103: old\n### Phase 104: old2\n\n## 🚧 v3.6 Current\n### Phase 108: new\n',
|
||||
);
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n');
|
||||
|
||||
const oldDirA = path.join(tmpDir, '.planning', 'phases', '103.old');
|
||||
const oldDirB = path.join(tmpDir, '.planning', 'phases', '104.old');
|
||||
const newDir = path.join(tmpDir, '.planning', 'phases', '108.new');
|
||||
fs.mkdirSync(oldDirA, { recursive: true });
|
||||
fs.mkdirSync(oldDirB, { recursive: true });
|
||||
fs.mkdirSync(newDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(oldDirA, 'SUMMARY.md'), 'one-liner: old milestone A\n\n## Summary\nold\n');
|
||||
fs.writeFileSync(path.join(oldDirB, 'SUMMARY.md'), 'one-liner: old milestone B\n\n## Summary\nold\n');
|
||||
fs.writeFileSync(path.join(newDir, 'SUMMARY.md'), 'one-liner: new milestone\n\n## Summary\nnew\n');
|
||||
|
||||
const result = runGsdTools(['milestone', 'complete', 'v3.6', '--raw'], tmpDir);
|
||||
assert.equal(result.success, true, result.error || result.output);
|
||||
const payload = JSON.parse(result.output);
|
||||
|
||||
assert.equal(payload.version, 'v3.6');
|
||||
assert.equal(payload.phases, 1, `expected v3.6 to scope to one phase, got ${payload.phases}`);
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('milestone.complete fails when explicit milestone version resolves no phases', () => {
|
||||
const tmpDir = createTempProject('gsd-bug-3043-empty-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '---\nmilestone: v1.0\n---\n');
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n## 🚧 v1.0\n### Phase 1: foundation\n',
|
||||
);
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n');
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
||||
|
||||
const result = runGsdTools(['milestone', 'complete', 'v9.9', '--raw'], tmpDir);
|
||||
assert.equal(result.success, false, 'expected command to fail when no phases match explicit version');
|
||||
assert.match(result.error || '', /no phases|phase/i);
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
62
tests/bug-3072-optional-sketch-findings-guard.test.cjs
Normal file
62
tests/bug-3072-optional-sketch-findings-guard.test.cjs
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..', 'get-shit-done', 'workflows');
|
||||
|
||||
function read(rel) {
|
||||
return fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
}
|
||||
|
||||
function extractFindingsProbesFromBashBlocks(markdown) {
|
||||
const probes = [];
|
||||
const fenceRe = /```bash\n([\s\S]*?)```/g;
|
||||
let fenceMatch;
|
||||
|
||||
while ((fenceMatch = fenceRe.exec(markdown)) !== null) {
|
||||
const block = fenceMatch[1];
|
||||
const baseLine = markdown.slice(0, fenceMatch.index).split('\n').length;
|
||||
const lines = block.split('\n');
|
||||
|
||||
lines.forEach((line, idx) => {
|
||||
if (!line.includes('.claude/skills/')) return;
|
||||
const kind = line.includes('sketch-findings-*/SKILL.md')
|
||||
? 'sketch'
|
||||
: line.includes('spike-findings-*/SKILL.md')
|
||||
? 'spike'
|
||||
: null;
|
||||
if (!kind) return;
|
||||
|
||||
probes.push({
|
||||
lineNumber: baseLine + idx,
|
||||
commandText: line.trim(),
|
||||
kind,
|
||||
hasNonFatalGuard: /\|\|\s*true/.test(line),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return probes;
|
||||
}
|
||||
|
||||
describe('bug #3072: optional sketch/spike findings probes are non-fatal', () => {
|
||||
test('all sketch/spike findings SKILL.md ls probes include || true', () => {
|
||||
const files = ['ui-phase.md', 'plan-phase.md', 'discuss-phase.md', 'new-project.md'];
|
||||
const offenders = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = read(file);
|
||||
const probes = extractFindingsProbesFromBashBlocks(content);
|
||||
for (const probe of probes) {
|
||||
if (!probe.hasNonFatalGuard) {
|
||||
offenders.push(`${file}:${probe.lineNumber} ${probe.commandText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(offenders, [], `missing non-fatal guard on optional findings probe:\n${offenders.join('\n')}`);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Drift guard: every `gsd-sdk query <cmd>` reference in the repo must
|
||||
* resolve to a handler registered in sdk/src/query/index.ts.
|
||||
* resolve to a handler registered in sdk/src/query/registry-assembly.ts.
|
||||
*
|
||||
* The set of commands workflows/agents/commands call must equal the set
|
||||
* the SDK registry exposes. New references with no handler — or handlers
|
||||
@@ -13,7 +13,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'index.ts');
|
||||
const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'registry-assembly.ts');
|
||||
const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs');
|
||||
|
||||
// Prose tokens that repeatedly appear after `gsd-sdk query` in English
|
||||
@@ -43,11 +43,36 @@ function collectRegisteredNames() {
|
||||
const src = fs.readFileSync(REGISTRY_FILE, 'utf8');
|
||||
const names = new Set();
|
||||
|
||||
// Static registrations in index.ts
|
||||
// Static registrations in index.ts (legacy style, may still exist)
|
||||
const re = /registry\.register\(\s*['"]([^'"]+)['"]/g;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) names.add(m[1]);
|
||||
|
||||
// Catalog-based registrations: parse known static catalogs directly.
|
||||
const catalogFileByVar = {
|
||||
FOUNDATION_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-foundation.ts'),
|
||||
STATE_SUPPORT_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-foundation.ts'),
|
||||
MUTATION_SURFACES_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-foundation.ts'),
|
||||
VERIFY_DECISION_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-foundation.ts'),
|
||||
DECISION_ROUTING_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-foundation.ts'),
|
||||
DOMAIN_STATIC_CATALOG: path.join(REPO_ROOT, 'sdk', 'src', 'query', 'command-static-catalog-domain.ts'),
|
||||
};
|
||||
|
||||
for (const [catalogVarName, cf] of Object.entries(catalogFileByVar)) {
|
||||
try {
|
||||
const catSrc = fs.readFileSync(cf, 'utf8');
|
||||
const exportRe = new RegExp(`export const ${catalogVarName}:`, 'm');
|
||||
if (!exportRe.test(catSrc)) continue;
|
||||
const entryRe = /\[\s*['"]([^'"]+)['"]/g;
|
||||
let em;
|
||||
while ((em = entryRe.exec(catSrc)) !== null) {
|
||||
names.add(em[1]);
|
||||
}
|
||||
} catch {
|
||||
// File not found, skip.
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest-generated family aliases registered via loop in index.ts.
|
||||
// Keep this in sync with command-manifest-driven routing seams.
|
||||
try {
|
||||
@@ -159,7 +184,7 @@ describe('gsd-sdk query registry integration', () => {
|
||||
assert.strictEqual(
|
||||
offenders.length, 0,
|
||||
'Referenced `gsd-sdk query <cmd>` tokens with no handler in ' +
|
||||
'sdk/src/query/index.ts. Either register the handler or remove ' +
|
||||
'sdk/src/query/registry-assembly.ts. Either register the handler or remove ' +
|
||||
'the reference.\n\n' + offenders.join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -227,6 +227,43 @@ describe('milestone complete command', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizes stale STATE.md narrative tails after milestone complete (#3088)', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n\n## Current Position\n\nPhase: 03 — EXECUTING\nPlan: 03-02\nStatus: Executing\nLast activity: 2025-01-01 — Running phase\n\n## Operator Next Steps\n\n- Re-run /gsd-complete-milestone v1.0\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(state.includes('Phase: Milestone v1.0 complete'));
|
||||
assert.ok(state.includes('Status: Awaiting next milestone'));
|
||||
assert.ok(!state.includes('Re-run /gsd-complete-milestone'));
|
||||
assert.ok(state.includes('/gsd-new-milestone'));
|
||||
});
|
||||
|
||||
test('appends canonical narrative sections when STATE.md headings are missing (#3088)', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(state.includes('## Current Position'));
|
||||
assert.ok(state.includes('Phase: Milestone v1.0 complete'));
|
||||
assert.ok(state.includes('## Operator Next Steps'));
|
||||
assert.ok(state.includes('/gsd-new-milestone'));
|
||||
});
|
||||
|
||||
test('handles missing ROADMAP.md gracefully', () => {
|
||||
// Only STATE.md — no ROADMAP.md, no REQUIREMENTS.md
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -21,6 +21,24 @@ function getPruneOrphanedWorktrees() {
|
||||
}
|
||||
|
||||
// Create a minimal git repo with an initial commit on main.
|
||||
function canonicalPath(p) {
|
||||
try {
|
||||
return fs.realpathSync.native(path.resolve(p));
|
||||
} catch {
|
||||
return path.resolve(p);
|
||||
}
|
||||
}
|
||||
|
||||
function listedWorktreePaths(repoDir) {
|
||||
const out = execSync('git worktree list --porcelain', { cwd: repoDir, encoding: 'utf8' });
|
||||
return new Set(
|
||||
out
|
||||
.split('\n')
|
||||
.filter((line) => line.startsWith('worktree '))
|
||||
.map((line) => canonicalPath(line.slice('worktree '.length).trim()))
|
||||
);
|
||||
}
|
||||
|
||||
function createGitRepo(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
execSync('git init', { cwd: dir, stdio: 'pipe' });
|
||||
@@ -49,8 +67,8 @@ describe('pruneOrphanedWorktrees', () => {
|
||||
cleanup(tmpBase);
|
||||
});
|
||||
|
||||
// Test 1: removes a worktree whose branch is merged into main
|
||||
test('removes a worktree whose branch is merged into main', () => {
|
||||
// Test 1: keeps a merged worktree (destructive removal disabled by default)
|
||||
test('keeps a worktree whose branch is merged into main', () => {
|
||||
const repoDir = path.join(tmpBase, 'repo');
|
||||
const worktreeDir = path.join(tmpBase, 'wt-merged');
|
||||
|
||||
@@ -72,17 +90,17 @@ describe('pruneOrphanedWorktrees', () => {
|
||||
const pruneOrphanedWorktrees = getPruneOrphanedWorktrees();
|
||||
pruneOrphanedWorktrees(repoDir);
|
||||
|
||||
// Assert: worktree directory no longer exists
|
||||
// Assert: worktree directory still exists
|
||||
assert.ok(
|
||||
!fs.existsSync(worktreeDir),
|
||||
'worktree directory should have been removed but still exists: ' + worktreeDir
|
||||
fs.existsSync(worktreeDir),
|
||||
'merged worktree should not be removed by default: ' + worktreeDir
|
||||
);
|
||||
|
||||
// Assert: git worktree list no longer shows it
|
||||
const listOut = execSync('git worktree list', { cwd: repoDir, encoding: 'utf8' });
|
||||
// Assert: git worktree list still shows it
|
||||
const listed = listedWorktreePaths(repoDir);
|
||||
assert.ok(
|
||||
!listOut.includes(worktreeDir),
|
||||
'git worktree list still references removed worktree:\n' + listOut
|
||||
listed.has(canonicalPath(worktreeDir)),
|
||||
'git worktree list should still reference merged worktree'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -132,11 +150,8 @@ describe('pruneOrphanedWorktrees', () => {
|
||||
const pruneOrphanedWorktrees = getPruneOrphanedWorktrees();
|
||||
const pruned = pruneOrphanedWorktrees(repoDir);
|
||||
|
||||
// process.cwd() must not appear in pruned paths
|
||||
assert.ok(
|
||||
!pruned.includes(process.cwd()),
|
||||
'process.cwd() should never be pruned, but found in: ' + JSON.stringify(pruned)
|
||||
);
|
||||
// No destructive removals are performed by default
|
||||
assert.deepStrictEqual(pruned, []);
|
||||
|
||||
// The main worktree (repoDir) itself must still exist
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user