@opengsd/gsd-core 1.6.0-rc.1 → 1.6.0-rc.2
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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +92 -332
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +18 -3
- package/gsd-core/bin/lib/adr-parser.cjs +21 -6
- package/gsd-core/bin/lib/capability-registry.cjs +48 -48
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +6 -0
- package/gsd-core/bin/lib/config.cjs +6 -0
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase.cjs +4 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +11 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/planning-config.md +1 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/execute-phase.md +8 -6
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +4 -1
- package/scripts/prompt-injection-scan.sh +1 -0
|
@@ -22,7 +22,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const phaseIdModule = require("./phase-id.cjs");
|
|
25
|
-
const { escapeRegex, phaseMarkdownRegexSource } = phaseIdModule;
|
|
25
|
+
const { escapeRegex, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, stripProjectCodePrefix, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdModule;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
28
|
const { planningDir } = planningWorkspace;
|
|
@@ -170,8 +170,8 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
|
170
170
|
const after = content.slice(offset);
|
|
171
171
|
return before + after.replace(pattern, replacement);
|
|
172
172
|
}
|
|
173
|
-
function findRoadmapPhaseInContent(content, phaseNum) {
|
|
174
|
-
const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
|
|
173
|
+
function findRoadmapPhaseInContent(content, phaseNum, phaseSource) {
|
|
174
|
+
const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
|
|
175
175
|
const headerMatch = content.match(phasePattern);
|
|
176
176
|
if (!headerMatch)
|
|
177
177
|
return null;
|
|
@@ -191,6 +191,21 @@ function findRoadmapPhaseInContent(content, phaseNum) {
|
|
|
191
191
|
section,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
+
function roadmapPhaseLookupSources(phaseNum) {
|
|
195
|
+
const sources = [];
|
|
196
|
+
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
197
|
+
if (exactSource)
|
|
198
|
+
sources.push(exactSource);
|
|
199
|
+
const numericSource = phaseMarkdownRegexSource(phaseNum);
|
|
200
|
+
// Source order matters: the bare numeric source is tried before the
|
|
201
|
+
// prefix-tolerant form so that a canonical bare heading ("Phase 117:") is
|
|
202
|
+
// preferred over a drifted prefixed heading ("Phase MANIFOLD-117:") when
|
|
203
|
+
// both exist in the same ROADMAP. The prefix-tolerant form is the fallback
|
|
204
|
+
// that handles the drifted-only case.
|
|
205
|
+
sources.push(numericSource);
|
|
206
|
+
sources.push(`${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${numericSource}`);
|
|
207
|
+
return [...new Set(sources)];
|
|
208
|
+
}
|
|
194
209
|
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
195
210
|
if (!phaseNum)
|
|
196
211
|
return null;
|
|
@@ -202,10 +217,16 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
202
217
|
if (roadmapRaw === null)
|
|
203
218
|
throw new Error('missing');
|
|
204
219
|
const content = extractCurrentMilestone(roadmapRaw, cwd);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
const fullContent = stripShippedMilestones(roadmapRaw);
|
|
221
|
+
for (const source of roadmapPhaseLookupSources(phaseNum)) {
|
|
222
|
+
const scopedResult = findRoadmapPhaseInContent(content, phaseNum, source);
|
|
223
|
+
if (scopedResult)
|
|
224
|
+
return scopedResult;
|
|
225
|
+
const fullResult = findRoadmapPhaseInContent(fullContent, phaseNum, source);
|
|
226
|
+
if (fullResult)
|
|
227
|
+
return fullResult;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
209
230
|
}
|
|
210
231
|
catch {
|
|
211
232
|
return null;
|
|
@@ -380,7 +401,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
380
401
|
const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
|
|
381
402
|
if (customMatch && normalized.has(customMatch[1].toLowerCase()))
|
|
382
403
|
return true;
|
|
383
|
-
const stripped = dirName
|
|
404
|
+
const stripped = stripProjectCodePrefix(dirName);
|
|
384
405
|
if (stripped !== dirName) {
|
|
385
406
|
const sm = stripped.match(numericRe);
|
|
386
407
|
if (sm && normalized.has(normalizePhaseIdSegments(sm[1]).toLowerCase()))
|
|
@@ -15,7 +15,10 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
15
15
|
const node_child_process_1 = require("node:child_process");
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
17
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
18
20
|
const { planningDir } = planningWorkspace;
|
|
21
|
+
const { stripProjectCodePrefix } = phaseIdMod;
|
|
19
22
|
// ─── Regex helpers ────────────────────────────────────────────────────────────
|
|
20
23
|
// Matches legacy phase headings: ### Phase N: Name (also decimal: Phase 2.1:)
|
|
21
24
|
// Captures: (hashes)(spaces)(phase-number)(rest-of-line)
|
|
@@ -99,7 +102,7 @@ function assignSubIndices(phaseEntries) {
|
|
|
99
102
|
*/
|
|
100
103
|
function extractPhaseNumFromDir(dirName) {
|
|
101
104
|
// Strip optional project_code prefix: "GSD-01-setup" → "01-setup"
|
|
102
|
-
const stripped = dirName
|
|
105
|
+
const stripped = stripProjectCodePrefix(dirName);
|
|
103
106
|
// Matches: digits + optional letter + optional decimal suffix, followed by '-' or end.
|
|
104
107
|
// e.g. "02.1-hotfix" → "02.1", "01-setup" → "01"
|
|
105
108
|
const m = stripped.match(/^(\d+[A-Z]?(?:\.\d+)*)(?:-|$)/i);
|
|
@@ -113,7 +116,7 @@ function extractPhaseNumFromDir(dirName) {
|
|
|
113
116
|
*/
|
|
114
117
|
function buildNewDirName(oldDirName, newId, projectCode) {
|
|
115
118
|
// Strip existing project_code prefix
|
|
116
|
-
const stripped = oldDirName
|
|
119
|
+
const stripped = stripProjectCodePrefix(oldDirName);
|
|
117
120
|
// Extract slug: everything after "NN-" (the old phase num, including decimal like 02.1)
|
|
118
121
|
const slugMatch = stripped.match(/^\d+[A-Z]?(?:\.\d+)*-(.*)/i);
|
|
119
122
|
const slug = slugMatch ? slugMatch[1] : stripped;
|
|
@@ -383,20 +386,30 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
383
386
|
if (gitStatus.trim().length > 0) {
|
|
384
387
|
throw new Error('Working tree is dirty. Commit or stash changes before migrating.');
|
|
385
388
|
}
|
|
386
|
-
// Capture HEAD sha for rollback
|
|
387
|
-
let headSha;
|
|
388
|
-
try {
|
|
389
|
-
headSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd, encoding: 'utf8', windowsHide: true }).trim();
|
|
390
|
-
}
|
|
391
|
-
catch (err) {
|
|
392
|
-
throw new Error(`git rev-parse HEAD failed: ${err.message}`);
|
|
393
|
-
}
|
|
394
389
|
const pDir = planningDir(cwd);
|
|
395
390
|
const phasesDir = node_path_1.default.join(pDir, 'phases');
|
|
396
391
|
const roadmapPath = node_path_1.default.join(pDir, 'ROADMAP.md');
|
|
397
392
|
const configPath = node_path_1.default.join(pDir, 'config.json');
|
|
398
393
|
const renamedDirs = [];
|
|
399
394
|
const editedFiles = [];
|
|
395
|
+
// Surgical, git-independent rollback state (#1542). A `git reset --hard` +
|
|
396
|
+
// `git clean` rollback restores NOTHING for a gitignored `.planning/`
|
|
397
|
+
// (commit_docs:false — the default) and is a whole-repo operation besides.
|
|
398
|
+
// Instead, record the exact renames performed and snapshot each file before
|
|
399
|
+
// rewriting it, then undo precisely those on failure — correct whether
|
|
400
|
+
// `.planning/` is git-tracked or ignored.
|
|
401
|
+
const performedRenames = [];
|
|
402
|
+
const fileBackups = new Map();
|
|
403
|
+
const snapshotFile = (filePath) => {
|
|
404
|
+
if (fileBackups.has(filePath))
|
|
405
|
+
return;
|
|
406
|
+
try {
|
|
407
|
+
fileBackups.set(filePath, { existed: true, content: node_fs_1.default.readFileSync(filePath, 'utf8') });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
fileBackups.set(filePath, { existed: false, content: '' });
|
|
411
|
+
}
|
|
412
|
+
};
|
|
400
413
|
try {
|
|
401
414
|
// 1. Rename phase directories
|
|
402
415
|
for (const phaseEntry of plan.phases) {
|
|
@@ -404,6 +417,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
404
417
|
const newPath = node_path_1.default.join(phasesDir, phaseEntry.newDir);
|
|
405
418
|
if (node_fs_1.default.existsSync(oldPath)) {
|
|
406
419
|
node_fs_1.default.renameSync(oldPath, newPath);
|
|
420
|
+
performedRenames.push({ oldPath, newPath });
|
|
407
421
|
renamedDirs.push(`${phaseEntry.oldDir} → ${phaseEntry.newDir}`);
|
|
408
422
|
}
|
|
409
423
|
}
|
|
@@ -418,6 +432,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
418
432
|
lines[edit.lineIndex] = edit.to;
|
|
419
433
|
}
|
|
420
434
|
}
|
|
435
|
+
snapshotFile(roadmapPath);
|
|
421
436
|
node_fs_1.default.writeFileSync(roadmapPath, lines.join('\n'), 'utf8');
|
|
422
437
|
editedFiles.push('ROADMAP.md');
|
|
423
438
|
}
|
|
@@ -443,6 +458,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
443
458
|
}
|
|
444
459
|
}
|
|
445
460
|
if (changed) {
|
|
461
|
+
snapshotFile(filePath);
|
|
446
462
|
node_fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
447
463
|
editedFiles.push(fileName);
|
|
448
464
|
}
|
|
@@ -454,19 +470,33 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
454
470
|
}
|
|
455
471
|
catch { /* config may not exist yet */ }
|
|
456
472
|
configData['phase_id_convention'] = 'milestone-prefixed';
|
|
473
|
+
snapshotFile(configPath);
|
|
457
474
|
node_fs_1.default.writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n', 'utf8');
|
|
458
475
|
editedFiles.push('config.json');
|
|
459
476
|
}
|
|
460
477
|
catch (err) {
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
478
|
+
// Surgical rollback: reverse the renames (newest first) and restore every
|
|
479
|
+
// file we snapshotted (deleting files that did not previously exist). This
|
|
480
|
+
// actually restores `.planning/` regardless of git tracking — so the
|
|
481
|
+
// "rolled back" claim is truthful — and never touches anything else.
|
|
482
|
+
for (let i = performedRenames.length - 1; i >= 0; i--) {
|
|
483
|
+
const { oldPath, newPath } = performedRenames[i];
|
|
484
|
+
try {
|
|
485
|
+
if (node_fs_1.default.existsSync(newPath))
|
|
486
|
+
node_fs_1.default.renameSync(newPath, oldPath);
|
|
487
|
+
}
|
|
488
|
+
catch { /* best-effort */ }
|
|
465
489
|
}
|
|
466
|
-
|
|
467
|
-
|
|
490
|
+
for (const [filePath, backup] of fileBackups) {
|
|
491
|
+
try {
|
|
492
|
+
if (backup.existed)
|
|
493
|
+
node_fs_1.default.writeFileSync(filePath, backup.content, 'utf8');
|
|
494
|
+
else if (node_fs_1.default.existsSync(filePath))
|
|
495
|
+
node_fs_1.default.unlinkSync(filePath);
|
|
496
|
+
}
|
|
497
|
+
catch { /* best-effort */ }
|
|
468
498
|
}
|
|
469
|
-
throw new Error(`Migration failed
|
|
499
|
+
throw new Error(`Migration failed and rolled back: ${err.message}`);
|
|
470
500
|
}
|
|
471
501
|
return { applied: true, renamedDirs, editedFiles };
|
|
472
502
|
}
|
|
@@ -351,8 +351,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
351
351
|
const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
|
|
352
352
|
const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
|
|
353
353
|
const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
|
|
354
|
-
// Detect phases in summary list without detail sections (malformed ROADMAP)
|
|
355
|
-
|
|
354
|
+
// Detect phases in summary list without detail sections (malformed ROADMAP).
|
|
355
|
+
// The char class must allow `-` (not just `.`) so dash-separated milestone-prefixed
|
|
356
|
+
// IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
|
|
357
|
+
// at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
|
|
358
|
+
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)/gi;
|
|
356
359
|
const checklistPhases = new Set();
|
|
357
360
|
let checklistMatch;
|
|
358
361
|
while ((checklistMatch = checklistPattern.exec(content)) !== null) {
|