@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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-roadmapper.md +6 -0
  3. package/bin/install.js +92 -332
  4. package/commands/gsd/capture.md +5 -1
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +18 -3
  7. package/gsd-core/bin/lib/adr-parser.cjs +21 -6
  8. package/gsd-core/bin/lib/capability-registry.cjs +48 -48
  9. package/gsd-core/bin/lib/commands.cjs +247 -0
  10. package/gsd-core/bin/lib/config-loader.cjs +6 -0
  11. package/gsd-core/bin/lib/config.cjs +6 -0
  12. package/gsd-core/bin/lib/frontmatter.cjs +7 -3
  13. package/gsd-core/bin/lib/phase-id.cjs +25 -11
  14. package/gsd-core/bin/lib/phase.cjs +4 -4
  15. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  16. package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
  17. package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
  18. package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
  19. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
  20. package/gsd-core/bin/lib/roadmap.cjs +5 -2
  21. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
  22. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
  23. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
  24. package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
  25. package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
  26. package/gsd-core/bin/lib/surface.cjs +12 -19
  27. package/gsd-core/bin/lib/validate.cjs +5 -2
  28. package/gsd-core/bin/lib/verify.cjs +11 -2
  29. package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
  30. package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
  31. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  32. package/gsd-core/references/context-budget.md +8 -8
  33. package/gsd-core/references/execute-phase-context-guard.md +16 -0
  34. package/gsd-core/references/planning-config.md +1 -0
  35. package/gsd-core/references/prohibition-probe.md +15 -9
  36. package/gsd-core/workflows/autonomous.md +33 -33
  37. package/gsd-core/workflows/diagnose-issues.md +6 -1
  38. package/gsd-core/workflows/execute-phase.md +8 -6
  39. package/gsd-core/workflows/help/modes/full.md +10 -0
  40. package/gsd-core/workflows/list-seeds.md +63 -0
  41. package/gsd-core/workflows/manager.md +37 -37
  42. package/gsd-core/workflows/pr-branch.md +156 -0
  43. package/gsd-core/workflows/quick.md +6 -1
  44. package/gsd-core/workflows/review.md +10 -2
  45. package/gsd-core/workflows/spec-phase.md +8 -3
  46. package/gsd-core/workflows/verify-phase.md +2 -2
  47. package/package.json +4 -1
  48. 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 scopedResult = findRoadmapPhaseInContent(content, phaseNum);
206
- if (scopedResult)
207
- return scopedResult;
208
- return findRoadmapPhaseInContent(stripShippedMilestones(roadmapRaw), phaseNum);
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.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
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.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
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.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
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
- // Rollback via git reset --hard + git clean
462
- try {
463
- (0, node_child_process_1.execSync)(`git reset --hard ${headSha}`, { cwd, stdio: 'pipe', windowsHide: true });
464
- (0, node_child_process_1.execSync)('git clean -fd .planning/phases/', { cwd, stdio: 'pipe', windowsHide: true });
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
- catch {
467
- // Swallow rollback errors — surface original error
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 (rolled back to ${headSha}): ${err.message}`);
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
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
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) {