@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.
- package/README.ja-JP.md +6 -5
- package/README.ko-KR.md +6 -5
- package/README.md +10 -45
- package/README.pt-BR.md +5 -4
- package/README.zh-CN.md +6 -5
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-doc-writer.md +7 -6
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-planner.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +1 -1
- package/assets/gsd-logo-2000-transparent.png +0 -0
- package/assets/gsd-logo-2000-transparent.svg +17 -0
- package/assets/gsd-logo-2000.png +0 -0
- package/assets/gsd-logo-2000.svg +21 -0
- package/assets/terminal.svg +68 -0
- package/bin/install.js +86 -44
- package/commands/gsd/review.md +2 -1
- package/get-shit-done/bin/gsd-tools.cjs +37 -1
- package/get-shit-done/bin/lib/command-aliases.cjs +16 -0
- package/get-shit-done/bin/lib/commands.cjs +109 -5
- package/get-shit-done/bin/lib/config-types.cjs +19 -0
- package/get-shit-done/bin/lib/core.cjs +455 -63
- package/get-shit-done/bin/lib/gap-checker.cjs +56 -7
- package/get-shit-done/bin/lib/installer-migration-report.cjs +1 -0
- package/get-shit-done/bin/lib/milestone.cjs +59 -1
- package/get-shit-done/bin/lib/model-catalog.cjs +17 -0
- package/get-shit-done/bin/lib/phase-lifecycle.cjs +0 -1
- package/get-shit-done/bin/lib/review-reviewer-selection.cjs +1 -0
- package/get-shit-done/bin/lib/roadmap-command-router.cjs +134 -0
- package/get-shit-done/bin/lib/roadmap-upgrade.cjs +569 -0
- package/get-shit-done/bin/lib/roadmap.cjs +6 -5
- package/get-shit-done/bin/lib/semver-compare.cjs +34 -27
- package/get-shit-done/bin/lib/shell-command-projection.cjs +35 -0
- package/get-shit-done/bin/lib/state-document.cjs +0 -4
- package/get-shit-done/bin/lib/state.cjs +41 -6
- package/get-shit-done/bin/lib/ui-safety-gate.cjs +111 -0
- package/get-shit-done/bin/lib/validate.cjs +43 -17
- package/get-shit-done/bin/lib/verify.cjs +107 -4
- package/get-shit-done/bin/shared/config-defaults.manifest.json +1 -0
- package/get-shit-done/bin/shared/config-schema.manifest.json +12 -1
- package/get-shit-done/bin/shared/model-catalog.json +27 -0
- package/get-shit-done/references/checkpoints.md +1 -1
- package/get-shit-done/references/planner-human-verify-mode.md +1 -1
- package/get-shit-done/references/ui-brand.md +4 -2
- package/get-shit-done/workflows/audit-fix.md +1 -1
- package/get-shit-done/workflows/audit-milestone.md +2 -0
- package/get-shit-done/workflows/autonomous.md +7 -5
- package/get-shit-done/workflows/cleanup.md +43 -3
- package/get-shit-done/workflows/code-review-fix.md +3 -3
- package/get-shit-done/workflows/code-review.md +2 -0
- package/get-shit-done/workflows/debug.md +6 -1
- package/get-shit-done/workflows/diagnose-issues.md +2 -0
- package/get-shit-done/workflows/discuss-phase/modes/advisor.md +1 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/docs-update.md +18 -4
- package/get-shit-done/workflows/eval-review.md +1 -1
- package/get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/get-shit-done/workflows/execute-phase.md +21 -11
- package/get-shit-done/workflows/execute-plan.md +1 -1
- package/get-shit-done/workflows/explore.md +2 -0
- package/get-shit-done/workflows/help/modes/default.md +3 -3
- package/get-shit-done/workflows/help/modes/full.md +5 -5
- package/get-shit-done/workflows/import.md +2 -0
- package/get-shit-done/workflows/ingest-docs.md +2 -2
- package/get-shit-done/workflows/manager.md +2 -2
- package/get-shit-done/workflows/map-codebase.md +2 -0
- package/get-shit-done/workflows/new-milestone.md +2 -2
- package/get-shit-done/workflows/new-project.md +2 -2
- package/get-shit-done/workflows/plan-phase.md +16 -15
- package/get-shit-done/workflows/plan-review-convergence.md +3 -3
- package/get-shit-done/workflows/quick.md +5 -3
- package/get-shit-done/workflows/review.md +109 -1
- package/get-shit-done/workflows/scan.md +2 -0
- package/get-shit-done/workflows/secure-phase.md +2 -0
- package/get-shit-done/workflows/settings-advanced.md +195 -5
- package/get-shit-done/workflows/settings.md +15 -1
- package/get-shit-done/workflows/ui-phase.md +2 -2
- package/get-shit-done/workflows/ui-review.md +1 -1
- package/get-shit-done/workflows/validate-phase.md +2 -0
- package/get-shit-done/workflows/verify-work.md +2 -2
- package/hooks/dist/gsd-worktree-path-guard.js +169 -0
- package/hooks/gsd-worktree-path-guard.js +169 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +9 -6
- package/scripts/build-hooks.js +1 -0
- package/scripts/ci-test-scope.cjs +10 -0
- package/scripts/run-tests.cjs +25 -1
- package/scripts/lint-shared-module-handsync.cjs +0 -388
- 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
|
-
'
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
'#
|
|
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
|
|
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
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
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
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
if (
|
|
9136
|
-
|
|
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 —
|
|
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) =>
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
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)
|
package/commands/gsd/review.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
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 });
|