@opengsd/gsd-core 1.2.0 → 1.3.0-rc.1

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 (92) hide show
  1. package/README.ja-JP.md +6 -5
  2. package/README.ko-KR.md +6 -5
  3. package/README.md +10 -45
  4. package/README.pt-BR.md +5 -4
  5. package/README.zh-CN.md +6 -5
  6. package/agents/gsd-ai-researcher.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +1 -1
  8. package/agents/gsd-doc-writer.md +7 -6
  9. package/agents/gsd-domain-researcher.md +1 -1
  10. package/agents/gsd-eval-planner.md +1 -1
  11. package/agents/gsd-phase-researcher.md +1 -1
  12. package/agents/gsd-ui-researcher.md +1 -1
  13. package/agents/gsd-verifier.md +1 -1
  14. package/assets/gsd-logo-2000-transparent.png +0 -0
  15. package/assets/gsd-logo-2000-transparent.svg +17 -0
  16. package/assets/gsd-logo-2000.png +0 -0
  17. package/assets/gsd-logo-2000.svg +21 -0
  18. package/assets/terminal.svg +68 -0
  19. package/bin/install.js +86 -44
  20. package/commands/gsd/review.md +2 -1
  21. package/get-shit-done/bin/gsd-tools.cjs +37 -1
  22. package/get-shit-done/bin/lib/command-aliases.cjs +16 -0
  23. package/get-shit-done/bin/lib/commands.cjs +109 -5
  24. package/get-shit-done/bin/lib/config-types.cjs +19 -0
  25. package/get-shit-done/bin/lib/core.cjs +455 -63
  26. package/get-shit-done/bin/lib/gap-checker.cjs +56 -7
  27. package/get-shit-done/bin/lib/installer-migration-report.cjs +1 -0
  28. package/get-shit-done/bin/lib/milestone.cjs +59 -1
  29. package/get-shit-done/bin/lib/model-catalog.cjs +17 -0
  30. package/get-shit-done/bin/lib/phase-lifecycle.cjs +0 -1
  31. package/get-shit-done/bin/lib/review-reviewer-selection.cjs +1 -0
  32. package/get-shit-done/bin/lib/roadmap-command-router.cjs +134 -0
  33. package/get-shit-done/bin/lib/roadmap-upgrade.cjs +569 -0
  34. package/get-shit-done/bin/lib/roadmap.cjs +6 -5
  35. package/get-shit-done/bin/lib/semver-compare.cjs +34 -27
  36. package/get-shit-done/bin/lib/shell-command-projection.cjs +35 -0
  37. package/get-shit-done/bin/lib/state-document.cjs +0 -4
  38. package/get-shit-done/bin/lib/state.cjs +41 -6
  39. package/get-shit-done/bin/lib/ui-safety-gate.cjs +111 -0
  40. package/get-shit-done/bin/lib/validate.cjs +43 -17
  41. package/get-shit-done/bin/lib/verify.cjs +107 -4
  42. package/get-shit-done/bin/shared/config-defaults.manifest.json +1 -0
  43. package/get-shit-done/bin/shared/config-schema.manifest.json +12 -1
  44. package/get-shit-done/bin/shared/model-catalog.json +27 -0
  45. package/get-shit-done/references/checkpoints.md +1 -1
  46. package/get-shit-done/references/planner-human-verify-mode.md +1 -1
  47. package/get-shit-done/references/ui-brand.md +4 -2
  48. package/get-shit-done/workflows/audit-fix.md +1 -1
  49. package/get-shit-done/workflows/audit-milestone.md +2 -0
  50. package/get-shit-done/workflows/autonomous.md +7 -5
  51. package/get-shit-done/workflows/cleanup.md +43 -3
  52. package/get-shit-done/workflows/code-review-fix.md +3 -3
  53. package/get-shit-done/workflows/code-review.md +2 -0
  54. package/get-shit-done/workflows/debug.md +6 -1
  55. package/get-shit-done/workflows/diagnose-issues.md +2 -0
  56. package/get-shit-done/workflows/discuss-phase/modes/advisor.md +1 -1
  57. package/get-shit-done/workflows/discuss-phase-assumptions.md +2 -2
  58. package/get-shit-done/workflows/docs-update.md +18 -4
  59. package/get-shit-done/workflows/eval-review.md +1 -1
  60. package/get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  61. package/get-shit-done/workflows/execute-phase.md +21 -11
  62. package/get-shit-done/workflows/execute-plan.md +1 -1
  63. package/get-shit-done/workflows/explore.md +2 -0
  64. package/get-shit-done/workflows/help/modes/default.md +3 -3
  65. package/get-shit-done/workflows/help/modes/full.md +5 -5
  66. package/get-shit-done/workflows/import.md +2 -0
  67. package/get-shit-done/workflows/ingest-docs.md +2 -2
  68. package/get-shit-done/workflows/manager.md +2 -2
  69. package/get-shit-done/workflows/map-codebase.md +2 -0
  70. package/get-shit-done/workflows/new-milestone.md +2 -2
  71. package/get-shit-done/workflows/new-project.md +2 -2
  72. package/get-shit-done/workflows/plan-phase.md +16 -15
  73. package/get-shit-done/workflows/plan-review-convergence.md +3 -3
  74. package/get-shit-done/workflows/quick.md +5 -3
  75. package/get-shit-done/workflows/review.md +109 -1
  76. package/get-shit-done/workflows/scan.md +2 -0
  77. package/get-shit-done/workflows/secure-phase.md +2 -0
  78. package/get-shit-done/workflows/settings-advanced.md +195 -5
  79. package/get-shit-done/workflows/settings.md +15 -1
  80. package/get-shit-done/workflows/ui-phase.md +2 -2
  81. package/get-shit-done/workflows/ui-review.md +1 -1
  82. package/get-shit-done/workflows/validate-phase.md +2 -0
  83. package/get-shit-done/workflows/verify-work.md +2 -2
  84. package/hooks/dist/gsd-worktree-path-guard.js +169 -0
  85. package/hooks/gsd-worktree-path-guard.js +169 -0
  86. package/hooks/managed-hooks-registry.cjs +1 -0
  87. package/package.json +9 -6
  88. package/scripts/build-hooks.js +1 -0
  89. package/scripts/ci-test-scope.cjs +10 -0
  90. package/scripts/run-tests.cjs +25 -1
  91. package/scripts/lint-shared-module-handsync.cjs +0 -388
  92. package/scripts/shared-module-handsync-allowlist.json +0 -183
package/bin/install.js CHANGED
@@ -16,6 +16,8 @@ const {
16
16
  projectPersistentPathExportActions,
17
17
  projectShellCommandText,
18
18
  projectCodexHookTomlCommand,
19
+ shellHookOmitsBashRunner,
20
+ buildLocalShellHookCommand,
19
21
  } = require('../get-shit-done/bin/lib/shell-command-projection.cjs');
20
22
 
21
23
  // Bidirectional GSD slash-command namespace transformer (#3583).
@@ -613,9 +615,10 @@ const banner = '\n' +
613
615
  ' ╚██████╔╝███████║██████╔╝\n' +
614
616
  ' ╚═════╝ ╚══════╝╚═════╝' + reset + '\n' +
615
617
  '\n' +
616
- ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
618
+ ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' +
619
+ ' Git. Ship. Done.\n' +
617
620
  ' A meta-prompting, context engineering and spec-driven\n' +
618
- ' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy by TÂCHES.\n';
621
+ ' development workflows for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy.\n';
619
622
 
620
623
  // Pure seam: parse --config-dir / -c from an arbitrary args array.
621
624
  // Returns the path string, '' for an empty equals-form value, or null when the
@@ -1216,7 +1219,7 @@ function buildHookCommand(configDir, hookName, opts) {
1216
1219
  // Windows, so wrapping `.sh` hooks with an explicit `bash.exe` path can
1217
1220
  // trigger `bash.exe: ... cannot execute binary file`. Emit only the quoted
1218
1221
  // script path for Claude on Windows.
1219
- if (platform === 'win32' && runtime === 'claude' && isShellHook) {
1222
+ if (shellHookOmitsBashRunner({ platform, runtime, isShellHook })) {
1220
1223
  if (opts.portableHooks) {
1221
1224
  const portableBaseDir = projectPortableHookBaseDir({
1222
1225
  configDir,
@@ -2811,6 +2814,9 @@ function convertClaudeToCodexMarkdown(content) {
2811
2814
  converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
2812
2815
  converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
2813
2816
  converted = converted.replace(/\.\/\.claude\//g, './.codex/');
2817
+ // Bare ~/.claude without trailing slash (e.g. configDir = ~/.claude)
2818
+ converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
2819
+ converted = converted.replace(/~\/\.claude\b/g, '~/.codex');
2814
2820
  // Bare/project-relative .claude/... references (#2639). Covers strings like
2815
2821
  // "check `.claude/skills/`" where there is no ~/, $HOME/, or ./ anchor.
2816
2822
  // Negative lookbehind prevents double-replacing already-anchored forms and
@@ -6001,10 +6007,10 @@ function writeHermesCategoryDescription(categoryDir) {
6001
6007
  '---',
6002
6008
  'name: gsd',
6003
6009
  `version: ${pkg.version}`,
6004
- 'description: Get Shit Done — disciplined planning, execution, and shipping workflows. Use any gsd-* skill in this category to drive a project through new-project → discuss-phase → plan-phase → execute-phase → ship.',
6010
+ 'description: GSD Core — Git. Ship. Done. Disciplined planning, execution, and shipping workflows. Use any gsd-* skill in this category to drive a project through new-project → discuss-phase → plan-phase → execute-phase → ship.',
6005
6011
  '---',
6006
6012
  '',
6007
- '# Get Shit Done (GSD)',
6013
+ '# GSD Core',
6008
6014
  '',
6009
6015
  'GSD is a structured development workflow. Skills in this category cover',
6010
6016
  'project initialization, phase planning, execution, code review, and shipping.',
@@ -7852,7 +7858,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
7852
7858
  }
7853
7859
  if (fs.existsSync(agentsDir)) {
7854
7860
  for (const file of fs.readdirSync(agentsDir)) {
7855
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
7861
+ if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
7856
7862
  manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
7857
7863
  }
7858
7864
  }
@@ -8785,7 +8791,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
8785
8791
  copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
8786
8792
  restoreUserArtifacts(skillDest, savedGsdArtifacts);
8787
8793
  if (verifyInstalled(skillDest, 'get-shit-done')) {
8788
- console.log(` ${green}✓${reset} Installed get-shit-done`);
8794
+ console.log(` ${green}✓${reset} Installed workflow assets`);
8789
8795
  } else {
8790
8796
  failures.push('get-shit-done');
8791
8797
  }
@@ -9103,42 +9109,48 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9103
9109
  // Report any backed-up local patches
9104
9110
  reportLocalPatches(targetDir, runtime);
9105
9111
 
9106
- // Verify no leaked .claude paths in non-Claude runtimes
9112
+ // Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
9107
9113
  if (runtime !== 'claude') {
9108
9114
  const leakedPaths = [];
9109
- function scanForLeakedPaths(dir) {
9110
- if (!fs.existsSync(dir)) return;
9111
- let entries;
9112
- try {
9113
- entries = fs.readdirSync(dir, { withFileTypes: true });
9114
- } catch (err) {
9115
- if (err.code === 'EPERM' || err.code === 'EACCES') {
9116
- return; // skip inaccessible directories
9115
+ // Only scan files that were written by this install (manifest-tracked).
9116
+ // Scanning the entire targetDir can match user-authored content that
9117
+ // legitimately references ~/.claude (e.g. personal notes), producing
9118
+ // false-positive warnings. Restricting to the manifest avoids that.
9119
+ let manifestFiles = null;
9120
+ try {
9121
+ const manifestPath = path.join(targetDir, MANIFEST_NAME);
9122
+ if (fs.existsSync(manifestPath)) {
9123
+ const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
9124
+ if (manifestData && typeof manifestData.files === 'object') {
9125
+ manifestFiles = Object.keys(manifestData.files);
9117
9126
  }
9118
- throw err;
9119
9127
  }
9120
- for (const entry of entries) {
9121
- const fullPath = path.join(dir, entry.name);
9122
- if (entry.isDirectory()) {
9123
- scanForLeakedPaths(fullPath);
9124
- } else if ((entry.name.endsWith('.md') || entry.name.endsWith('.toml')) && entry.name !== 'CHANGELOG.md') {
9125
- let content;
9126
- try {
9127
- content = fs.readFileSync(fullPath, 'utf8');
9128
- } catch (err) {
9129
- if (err.code === 'EPERM' || err.code === 'EACCES') {
9130
- continue; // skip inaccessible files
9131
- }
9132
- throw err;
9133
- }
9134
- const matches = content.match(/(?:~|\$HOME)\/\.claude\b/g);
9135
- if (matches) {
9136
- leakedPaths.push({ file: fullPath.replace(targetDir + '/', ''), count: matches.length });
9128
+ } catch (_manifestParseErr) {
9129
+ // If we cannot read/parse the manifest, skip the scan entirely to
9130
+ // avoid false positives rather than falling back to a full directory walk.
9131
+ manifestFiles = null;
9132
+ }
9133
+ if (manifestFiles !== null) {
9134
+ for (const relPath of manifestFiles) {
9135
+ const fileName = path.basename(relPath);
9136
+ if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
9137
+ if (fileName === 'CHANGELOG.md') continue;
9138
+ const fullPath = path.join(targetDir, relPath);
9139
+ let content;
9140
+ try {
9141
+ content = fs.readFileSync(fullPath, 'utf8');
9142
+ } catch (err) {
9143
+ if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ENOENT') {
9144
+ continue; // skip inaccessible or missing files
9137
9145
  }
9146
+ throw err;
9147
+ }
9148
+ const matches = content.match(/(?:~|\$HOME)\/\.claude\b/g);
9149
+ if (matches) {
9150
+ leakedPaths.push({ file: relPath, count: matches.length });
9138
9151
  }
9139
9152
  }
9140
9153
  }
9141
- scanForLeakedPaths(targetDir);
9142
9154
  if (leakedPaths.length > 0) {
9143
9155
  const totalLeaks = leakedPaths.reduce((sum, l) => sum + l.count, 0);
9144
9156
  console.warn(`\n ${yellow}⚠${reset} Found ${totalLeaks} unreplaced .claude path reference(s) in ${leakedPaths.length} file(s):`);
@@ -9342,6 +9354,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9342
9354
  }
9343
9355
  console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`);
9344
9356
  console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`);
9357
+ // Re-write the manifest now that .toml agent files exist on disk.
9358
+ // The initial writeManifest call (before Codex config generation) could
9359
+ // not include agents/gsd-*.toml because those files did not yet exist.
9360
+ writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
9345
9361
  } else {
9346
9362
  console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
9347
9363
  }
@@ -9547,7 +9563,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9547
9563
  // Cline uses .clinerules — generate a rules file with GSD system instructions
9548
9564
  const clinerulesDest = path.join(targetDir, '.clinerules');
9549
9565
  const clinerules = [
9550
- '# GSD — Get Shit Done',
9566
+ '# GSD Core — Git. Ship. Done.',
9551
9567
  '',
9552
9568
  '- GSD workflows live in `get-shit-done/workflows/`. Load the relevant workflow when',
9553
9569
  ' the user runs a `/gsd-*` command.',
@@ -9685,14 +9701,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9685
9701
  runtime,
9686
9702
  platform: process.platform,
9687
9703
  });
9688
- const localShellCmd = (hookFile) => localBashRunner === null
9689
- ? null
9690
- : projectShellCommandText({
9691
- runnerToken: localBashRunner,
9692
- argTokens: [`${localPrefix}/hooks/${hookFile}`],
9693
- runtime,
9694
- platform: process.platform,
9695
- });
9704
+ const localShellCmd = (hookFile) => buildLocalShellHookCommand({
9705
+ localPrefix,
9706
+ hookFile,
9707
+ bashRunner: localBashRunner,
9708
+ runtime,
9709
+ platform: process.platform,
9710
+ });
9696
9711
  const statuslineCommand = isGlobal
9697
9712
  ? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
9698
9713
  : localCmd('gsd-statusline.js');
@@ -9930,6 +9945,33 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9930
9945
  console.warn(` ${yellow}⚠${reset} Skipped workflow guard hook — gsd-workflow-guard.js not found at target`);
9931
9946
  }
9932
9947
 
9948
+ // Configure PreToolUse hook for worktree absolute-path safety (#260)
9949
+ // Hard-blocks Edit/Write/MultiEdit tool calls with absolute paths that resolve
9950
+ // outside the current worktree root. Prevents executor agents from
9951
+ // accidentally writing to the main checkout when running in isolation="worktree".
9952
+ const worktreePathGuardCommand = isGlobal
9953
+ ? buildHookCommand(targetDir, 'gsd-worktree-path-guard.js', hookOpts)
9954
+ : localCmd('gsd-worktree-path-guard.js');
9955
+ const hasWorktreePathGuardHook = settings.hooks[preToolEvent].some(entry =>
9956
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-worktree-path-guard'))
9957
+ );
9958
+ const worktreePathGuardFile = path.join(targetDir, 'hooks', 'gsd-worktree-path-guard.js');
9959
+ if (!hasWorktreePathGuardHook && fs.existsSync(worktreePathGuardFile) && worktreePathGuardCommand) {
9960
+ settings.hooks[preToolEvent].push({
9961
+ matcher: 'Write|Edit|MultiEdit',
9962
+ hooks: [
9963
+ {
9964
+ type: 'command',
9965
+ command: worktreePathGuardCommand,
9966
+ timeout: 5
9967
+ }
9968
+ ]
9969
+ });
9970
+ console.log(` ${green}✓${reset} Configured worktree path guard hook`);
9971
+ } else if (!hasWorktreePathGuardHook && !fs.existsSync(worktreePathGuardFile)) {
9972
+ console.warn(` ${yellow}⚠${reset} Skipped worktree path guard hook — gsd-worktree-path-guard.js not found at target`);
9973
+ }
9974
+
9933
9975
  // Configure commit validation hook (Conventional Commits enforcement, opt-in)
9934
9976
  const validateCommitCommand = isGlobal
9935
9977
  ? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:review
3
3
  description: Request cross-AI peer review of phase plans from external AI CLIs
4
- argument-hint: "--phase N [--gemini] [--claude] [--codex] [--opencode] [--qwen] [--cursor] [--all]"
4
+ argument-hint: "--phase N [--gemini] [--claude] [--codex] [--opencode] [--qwen] [--cursor] [--agy] [--all]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -33,6 +33,7 @@ Phase number: extracted from $ARGUMENTS (required)
33
33
  - `--opencode` — Include OpenCode review (uses model from user's OpenCode config)
34
34
  - `--qwen` — Include Qwen Code review (Alibaba Qwen models)
35
35
  - `--cursor` — Include Cursor agent review
36
+ - `--agy` / `--antigravity` — Include Antigravity CLI review
36
37
  - `--all` — Include all available CLIs
37
38
  </context>
38
39
 
@@ -46,6 +46,8 @@
46
46
  * roadmap analyze Full roadmap parse with disk status
47
47
  * roadmap update-plan-progress <N> Update progress table row from disk (PLAN vs SUMMARY counts)
48
48
  * roadmap annotate-dependencies <N> Add wave dependency notes + cross-cutting constraints to ROADMAP.md
49
+ * roadmap validate Validate phase ID convention compliance
50
+ * roadmap upgrade [--apply] --convention milestone-prefixed Migrate phase IDs to M-NN convention
49
51
  *
50
52
  * Requirements Operations:
51
53
  * requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
@@ -368,7 +370,7 @@ async function main() {
368
370
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
369
371
  'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
370
372
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
371
- 'current-timestamp, detect-custom-files, docs-init, extract-messages, find-phase, ' +
373
+ 'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
372
374
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
373
375
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
374
376
  'learnings, list-todos, milestone, phase, phase-plan-index, phases, profile-questionnaire, ' +
@@ -1651,6 +1653,40 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1651
1653
  break;
1652
1654
  }
1653
1655
 
1656
+ case 'effort': {
1657
+ const subcommand = args[1];
1658
+ if (subcommand === 'sync') {
1659
+ const effortSyncArgs = args.slice(2);
1660
+ let dryRun = true;
1661
+ let effortSyncConfigDir;
1662
+ let effortSyncRuntime;
1663
+ for (let i = 0; i < effortSyncArgs.length; i++) {
1664
+ const a = effortSyncArgs[i];
1665
+ if (a === '--apply') { dryRun = false; continue; }
1666
+ if (a === '--dry-run') { dryRun = true; continue; }
1667
+ if (a.startsWith('--config-dir=')) { effortSyncConfigDir = a.slice('--config-dir='.length); continue; }
1668
+ if (a === '--config-dir') {
1669
+ const v = effortSyncArgs[i + 1];
1670
+ if (!v || v.startsWith('--')) error('Missing value for --config-dir', ERROR_REASON.USAGE);
1671
+ effortSyncConfigDir = v; i++; continue;
1672
+ }
1673
+ if (a.startsWith('--runtime=')) { effortSyncRuntime = a.slice('--runtime='.length); continue; }
1674
+ if (a === '--runtime') {
1675
+ const v = effortSyncArgs[i + 1];
1676
+ if (!v || v.startsWith('--')) error('Missing value for --runtime', ERROR_REASON.USAGE);
1677
+ effortSyncRuntime = v; i++; continue;
1678
+ }
1679
+ if (a === '--raw') continue;
1680
+ if (a.startsWith('-')) error(`Unknown flag for effort sync: ${a}`, ERROR_REASON.USAGE);
1681
+ error(`effort sync takes no positional arguments; got: ${a}`, ERROR_REASON.USAGE);
1682
+ }
1683
+ commands.cmdEffortSync(cwd, raw, { dryRun, configDir: effortSyncConfigDir, runtime: effortSyncRuntime });
1684
+ } else {
1685
+ error('Unknown effort subcommand. Available: sync', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1686
+ }
1687
+ break;
1688
+ }
1689
+
1654
1690
  default: {
1655
1691
  // #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
1656
1692
  // above split it so `command` here is the head ("foo"). Use
@@ -540,6 +540,22 @@ const ROADMAP_COMMAND_ALIASES = [
540
540
  ],
541
541
  "subcommand": "annotate-dependencies",
542
542
  "mutation": true
543
+ },
544
+ {
545
+ "canonical": "roadmap.validate",
546
+ "aliases": [
547
+ "roadmap validate"
548
+ ],
549
+ "subcommand": "validate",
550
+ "mutation": false
551
+ },
552
+ {
553
+ "canonical": "roadmap.upgrade",
554
+ "aliases": [
555
+ "roadmap upgrade"
556
+ ],
557
+ "subcommand": "upgrade",
558
+ "mutation": true
543
559
  }
544
560
  ];
545
561
 
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { execGit, platformWriteSync, platformReadSync, platformEnsureDir } = require('./shell-command-projection.cjs');
7
- const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs');
7
+ const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal, extractPhaseToken } = require('./core.cjs');
8
8
  const { renderEffortForRuntime, RUNTIMES_WITH_FAST_MODE } = require('./model-catalog.cjs');
9
9
  const { planningDir, planningPaths } = require('./planning-workspace.cjs');
10
10
  const { extractFrontmatter } = require('./frontmatter.cjs');
@@ -307,6 +307,104 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
307
307
  output(result, raw, effort);
308
308
  }
309
309
 
310
+ /**
311
+ * #488 — Replace or inject the `effort:` value in YAML frontmatter.
312
+ * Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
313
+ */
314
+ function setEffortFrontmatter(content, effortValue) {
315
+ const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
316
+ const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
317
+ const match = fmRe.exec(content);
318
+ if (!match) return content;
319
+ const fmBody = match[1];
320
+ if (/^effort:/m.test(fmBody)) {
321
+ return content.replace(/^(effort:)[ \t]*.*$/m, `$1 ${effortValue}`);
322
+ }
323
+ const openLen = 3 + eol.length;
324
+ const closingStart = match.index + openLen + fmBody.length;
325
+ return content.slice(0, closingStart) + `effort: ${effortValue}${eol}` + content.slice(closingStart);
326
+ }
327
+
328
+ /**
329
+ * #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
330
+ * match the current effort config, without requiring a full reinstall.
331
+ *
332
+ * Uses install-time resolution (readGsdEffectiveEffortConfig + resolveInstallTimeEffort
333
+ * from bin/install.js) rather than the runtime resolver (resolveEffortInternal), because
334
+ * the sync must mirror what install actually wrote: home defaults merged with project config.
335
+ * The runtime resolver (loadConfig) does not merge ~/.gsd/defaults.json when a project
336
+ * .planning/config.json exists, so it would silently ignore home-level effort changes.
337
+ *
338
+ * @param {string} cwd Project working directory (for effort config resolution).
339
+ * @param {boolean} raw JSON output flag.
340
+ * @param {{ dryRun?: boolean, configDir?: string, runtime?: string }} [opts]
341
+ * dryRun — when true (default), report changes without writing; false = apply.
342
+ * configDir — override the agents parent dir (default: runtime global config dir).
343
+ * runtime — override runtime (default: config.runtime || 'claude').
344
+ */
345
+ function cmdEffortSync(cwd, raw, opts) {
346
+ opts = opts || {};
347
+ const dryRun = opts.dryRun !== false;
348
+
349
+ const config = loadConfig(cwd);
350
+ const runtime = opts.runtime || config.runtime || 'claude';
351
+
352
+ if (runtime !== 'claude') {
353
+ output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, reason: `runtime '${runtime}' does not use effort: frontmatter` }, raw, '');
354
+ return;
355
+ }
356
+
357
+ const { getGlobalConfigDir } = require('./runtime-homes.cjs');
358
+ // Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
359
+ // matching the exact logic used when agents were originally installed.
360
+ const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('../../../bin/install.js');
361
+ const effortCfg = readGsdEffectiveEffortConfig(cwd);
362
+
363
+ const agentsDir = path.join(opts.configDir || getGlobalConfigDir(runtime), 'agents');
364
+
365
+ if (!fs.existsSync(agentsDir)) {
366
+ output({ synced: 0, skipped: 0, changes: [], dry_run: dryRun, agents_dir: agentsDir, reason: 'agents directory not found' }, raw, '');
367
+ return;
368
+ }
369
+
370
+ // Skip symlinks — only write regular files to avoid clobbering symlink targets.
371
+ const files = fs.readdirSync(agentsDir).filter(f => {
372
+ if (!f.startsWith('gsd-') || !f.endsWith('.md')) return false;
373
+ try { return fs.lstatSync(path.join(agentsDir, f)).isFile(); } catch { return false; }
374
+ });
375
+ const changes = [];
376
+ let synced = 0;
377
+ let skipped = 0;
378
+
379
+ for (const file of files) {
380
+ const agentName = file.replace(/\.md$/, '');
381
+ const filePath = path.join(agentsDir, file);
382
+ const content = fs.readFileSync(filePath, 'utf8');
383
+
384
+ // Resolve using install-time logic: home defaults merged with project config.
385
+ const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
386
+ const rendered = renderEffortForRuntime(runtime, universalEffort);
387
+ const newEffortValue = rendered.value;
388
+
389
+ const fmMatch = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
390
+ if (!fmMatch) { skipped++; continue; }
391
+
392
+ const effortMatch = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch[1]);
393
+ const currentEffort = effortMatch ? effortMatch[1] : null;
394
+
395
+ if (currentEffort === newEffortValue) { skipped++; continue; }
396
+
397
+ changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
398
+ synced++;
399
+
400
+ if (!dryRun) {
401
+ fs.writeFileSync(filePath, setEffortFrontmatter(content, newEffortValue));
402
+ }
403
+ }
404
+
405
+ output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
406
+ }
407
+
310
408
  function cmdCommit(cwd, message, files, raw, amend, noVerify) {
311
409
  if (!message && !amend) {
312
410
  error('commit message required');
@@ -994,7 +1092,9 @@ function cmdStats(cwd, format, raw) {
994
1092
  const roadmapRaw = platformReadSync(roadmapPath);
995
1093
  if (roadmapRaw === null) throw new Error('roadmap missing');
996
1094
  const roadmapContent = extractCurrentMilestone(roadmapRaw, cwd);
997
- const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
1095
+ // Matches both plain numeric (Phase 1:) and milestone-prefixed (Phase 2-01:) headings.
1096
+ // Also tolerates optional [bracket-token] scope prefix on phase headings.
1097
+ const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:\s*([^\n]+)/gi;
998
1098
  let match;
999
1099
  while ((match = headingPattern.exec(roadmapContent)) !== null) {
1000
1100
  const key = normalizePhaseName(match[1]);
@@ -1017,9 +1117,12 @@ function cmdStats(cwd, format, raw) {
1017
1117
  .sort((a, b) => comparePhaseNum(a, b));
1018
1118
 
1019
1119
  for (const dir of dirs) {
1020
- const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
1021
- const phaseNum = dm ? dm[1] : dir;
1022
- const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
1120
+ // Use extractPhaseToken to correctly parse M-NN-style and code-prefixed dir names.
1121
+ const phaseToken = extractPhaseToken(dir);
1122
+ const phaseNum = phaseToken || dir;
1123
+ // phaseName is everything after the token (strip leading '-')
1124
+ const afterToken = dir.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
1125
+ const phaseName = afterToken ? afterToken.replace(/-/g, ' ') : '';
1023
1126
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
1024
1127
  const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
1025
1128
  const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
@@ -1174,6 +1277,7 @@ module.exports = {
1174
1277
  cmdHistoryDigest,
1175
1278
  cmdResolveModel,
1176
1279
  cmdResolveExecution,
1280
+ cmdEffortSync,
1177
1281
  cmdCommit,
1178
1282
  cmdCommitToSubrepo,
1179
1283
  cmdSummaryExtract,
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * TypeScript type definitions for GSD project config — model_policy block.
4
+ *
5
+ * These types reflect the model_policy config shape consumed by
6
+ * resolveModelPolicy in core.cjs and validated by config-schema.cjs.
7
+ *
8
+ * See feat #49 (model_policy presets) and config-schema.manifest.json.
9
+ * Added under ADR-457: TS sources in src/ compile to CJS artifacts in
10
+ * get-shit-done/bin/lib/ at publish time.
11
+ *
12
+ * Resolution precedence (highest → lowest):
13
+ * 1. model_overrides[agent]
14
+ * 2. model_policy.runtime_tiers[runtime][tier] (Sub-path A)
15
+ * 3. model_policy provider preset + budget (Sub-path B)
16
+ * 4. model_profile_overrides
17
+ * 5. resolve_model_ids / profile fallback
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });