@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// GSD Worktree Path Guard — PreToolUse hook
|
|
4
|
+
// Blocks Edit/Write/MultiEdit tool calls that target absolute paths outside the worktree root.
|
|
5
|
+
//
|
|
6
|
+
// Problem: gsd-executor agents spawned with isolation="worktree" sometimes issue
|
|
7
|
+
// Edit/Write calls with absolute paths rooted at the MAIN repository instead of
|
|
8
|
+
// the worktree (issue #260). The prose guard in agents/gsd-executor.md step 0b
|
|
9
|
+
// is never enforced because the model under load skips it.
|
|
10
|
+
//
|
|
11
|
+
// This hook enforces the constraint at the tooling layer, making it HARD-BLOCKING.
|
|
12
|
+
//
|
|
13
|
+
// Triggers on: Edit, Write, and MultiEdit tool calls
|
|
14
|
+
// Action: BLOCK (exit 2) if file_path is absolute and outside the worktree root
|
|
15
|
+
// No-op: relative paths, non-worktree CWDs, hook errors (silent fail)
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 };
|
|
22
|
+
|
|
23
|
+
function git(args, cwd) {
|
|
24
|
+
return spawnSync('git', args, { ...SPAWNOPT, cwd });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Walk up from `start` to find the nearest existing directory.
|
|
28
|
+
// Returns null if we reach the filesystem root without finding one.
|
|
29
|
+
function nearestExistingDir(start) {
|
|
30
|
+
let dir = start;
|
|
31
|
+
let prev;
|
|
32
|
+
do {
|
|
33
|
+
prev = dir;
|
|
34
|
+
try { fs.accessSync(dir, fs.constants.F_OK); return dir; } catch { /* keep walking */ }
|
|
35
|
+
dir = path.dirname(dir);
|
|
36
|
+
} while (dir !== prev);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
44
|
+
process.stdin.on('end', () => {
|
|
45
|
+
clearTimeout(stdinTimeout);
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(input);
|
|
48
|
+
const toolName = data.tool_name;
|
|
49
|
+
|
|
50
|
+
// Only guard Edit, Write, and MultiEdit tool calls
|
|
51
|
+
if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'MultiEdit') {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cwd = data.cwd || process.cwd();
|
|
56
|
+
|
|
57
|
+
// Detect whether CWD is inside a linked git worktree by inspecting
|
|
58
|
+
// the git-dir path. In a linked worktree, git rev-parse --git-dir
|
|
59
|
+
// returns a path containing .git/worktrees/ as a component.
|
|
60
|
+
// In the main repo or a submodule it returns .git (or a path without /worktrees/).
|
|
61
|
+
// This approach works even when cwd is a subdirectory of the worktree.
|
|
62
|
+
const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
|
|
63
|
+
if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
|
|
64
|
+
process.exit(0); // not a git repo — pass through
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gitDir = gitDirResult.stdout.trim();
|
|
68
|
+
// A linked worktree's --git-dir contains .git/worktrees/ as a path component
|
|
69
|
+
const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
|
|
70
|
+
if (!isLinkedWorktree) {
|
|
71
|
+
process.exit(0); // main repo, submodule, or separate-git-dir — no-op
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get the raw --show-toplevel output for the worktree (cwd).
|
|
75
|
+
// We keep it raw (not path.resolve'd) to compare directly with the
|
|
76
|
+
// file's toplevel — same git binary, same format, no normalization needed.
|
|
77
|
+
const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
|
|
78
|
+
if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
|
|
79
|
+
process.exit(0); // can't determine root — fail open
|
|
80
|
+
}
|
|
81
|
+
const wtTopRaw = wtTopResult.stdout.trim();
|
|
82
|
+
|
|
83
|
+
const rawFilePath = data.tool_input?.file_path || '';
|
|
84
|
+
if (!rawFilePath) {
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Relative paths are always safe — they resolve relative to CWD inside the worktree
|
|
89
|
+
if (!path.isAbsolute(rawFilePath)) {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Normalise .. traversal so /worktree/src/../../../main/file
|
|
94
|
+
// resolves to its true location before we check containment.
|
|
95
|
+
const filePath = path.resolve(rawFilePath);
|
|
96
|
+
|
|
97
|
+
// Find the nearest existing ancestor of filePath so we can ask git
|
|
98
|
+
// for its toplevel. The file itself may not exist yet (Write creates
|
|
99
|
+
// new files), but at least one ancestor directory must exist.
|
|
100
|
+
// We check the file itself first in case it already exists.
|
|
101
|
+
const checkDir = nearestExistingDir(
|
|
102
|
+
(() => {
|
|
103
|
+
try {
|
|
104
|
+
return fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
|
|
105
|
+
} catch {
|
|
106
|
+
return path.dirname(filePath);
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!checkDir) {
|
|
112
|
+
// Walked to root without finding any directory — path is synthetic.
|
|
113
|
+
// Block conservatively.
|
|
114
|
+
const output = {
|
|
115
|
+
decision: 'block',
|
|
116
|
+
reason:
|
|
117
|
+
`Worktree path guard: '${filePath}' has no existing ancestor directory — ` +
|
|
118
|
+
`cannot verify it is inside the worktree '${wtTopRaw}'. Use a relative path instead.`,
|
|
119
|
+
};
|
|
120
|
+
process.stdout.write(JSON.stringify(output));
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ask git for the toplevel of the file's location.
|
|
125
|
+
// Comparing two raw git --show-toplevel outputs avoids every
|
|
126
|
+
// platform-specific path normalisation pitfall (Windows 8.3 short names,
|
|
127
|
+
// case differences between realpathSync and path.resolve, forward- vs
|
|
128
|
+
// back-slash inconsistencies) — both values come from the same git binary
|
|
129
|
+
// in the same format by definition.
|
|
130
|
+
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
|
|
131
|
+
|
|
132
|
+
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
|
|
133
|
+
// checkDir is not inside any git repo → cannot be inside the worktree.
|
|
134
|
+
const output = {
|
|
135
|
+
decision: 'block',
|
|
136
|
+
reason:
|
|
137
|
+
`Worktree path guard: '${filePath}' is not inside any git repository — ` +
|
|
138
|
+
`it cannot be inside the worktree at '${wtTopRaw}'. Use a relative path instead.`,
|
|
139
|
+
};
|
|
140
|
+
process.stdout.write(JSON.stringify(output));
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fileTopRaw = fileTopResult.stdout.trim();
|
|
145
|
+
|
|
146
|
+
// Same git toplevel → file is inside the worktree → allow
|
|
147
|
+
if (fileTopRaw === wtTopRaw) {
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// BLOCK: file resolves to a different git root than the active worktree
|
|
152
|
+
const output = {
|
|
153
|
+
decision: 'block',
|
|
154
|
+
reason:
|
|
155
|
+
`Worktree path guard: '${filePath}' resolves to git root '${fileTopRaw}' which ` +
|
|
156
|
+
`differs from the active worktree root '${wtTopRaw}'. This likely means an ` +
|
|
157
|
+
`absolute path was derived from the orchestrator's main repository instead of ` +
|
|
158
|
+
`the active worktree. To fix: use a relative path, or re-derive the base ` +
|
|
159
|
+
`directory with \`git rev-parse --show-toplevel\` from within the worktree ` +
|
|
160
|
+
`(hook cwd: '${cwd}').`,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.stdout.write(JSON.stringify(output));
|
|
164
|
+
process.exit(2);
|
|
165
|
+
} catch {
|
|
166
|
+
// Silent fail — never block valid tool calls due to hook errors
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// GSD Worktree Path Guard — PreToolUse hook
|
|
4
|
+
// Blocks Edit/Write/MultiEdit tool calls that target absolute paths outside the worktree root.
|
|
5
|
+
//
|
|
6
|
+
// Problem: gsd-executor agents spawned with isolation="worktree" sometimes issue
|
|
7
|
+
// Edit/Write calls with absolute paths rooted at the MAIN repository instead of
|
|
8
|
+
// the worktree (issue #260). The prose guard in agents/gsd-executor.md step 0b
|
|
9
|
+
// is never enforced because the model under load skips it.
|
|
10
|
+
//
|
|
11
|
+
// This hook enforces the constraint at the tooling layer, making it HARD-BLOCKING.
|
|
12
|
+
//
|
|
13
|
+
// Triggers on: Edit, Write, and MultiEdit tool calls
|
|
14
|
+
// Action: BLOCK (exit 2) if file_path is absolute and outside the worktree root
|
|
15
|
+
// No-op: relative paths, non-worktree CWDs, hook errors (silent fail)
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 };
|
|
22
|
+
|
|
23
|
+
function git(args, cwd) {
|
|
24
|
+
return spawnSync('git', args, { ...SPAWNOPT, cwd });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Walk up from `start` to find the nearest existing directory.
|
|
28
|
+
// Returns null if we reach the filesystem root without finding one.
|
|
29
|
+
function nearestExistingDir(start) {
|
|
30
|
+
let dir = start;
|
|
31
|
+
let prev;
|
|
32
|
+
do {
|
|
33
|
+
prev = dir;
|
|
34
|
+
try { fs.accessSync(dir, fs.constants.F_OK); return dir; } catch { /* keep walking */ }
|
|
35
|
+
dir = path.dirname(dir);
|
|
36
|
+
} while (dir !== prev);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
44
|
+
process.stdin.on('end', () => {
|
|
45
|
+
clearTimeout(stdinTimeout);
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(input);
|
|
48
|
+
const toolName = data.tool_name;
|
|
49
|
+
|
|
50
|
+
// Only guard Edit, Write, and MultiEdit tool calls
|
|
51
|
+
if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'MultiEdit') {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cwd = data.cwd || process.cwd();
|
|
56
|
+
|
|
57
|
+
// Detect whether CWD is inside a linked git worktree by inspecting
|
|
58
|
+
// the git-dir path. In a linked worktree, git rev-parse --git-dir
|
|
59
|
+
// returns a path containing .git/worktrees/ as a component.
|
|
60
|
+
// In the main repo or a submodule it returns .git (or a path without /worktrees/).
|
|
61
|
+
// This approach works even when cwd is a subdirectory of the worktree.
|
|
62
|
+
const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
|
|
63
|
+
if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
|
|
64
|
+
process.exit(0); // not a git repo — pass through
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gitDir = gitDirResult.stdout.trim();
|
|
68
|
+
// A linked worktree's --git-dir contains .git/worktrees/ as a path component
|
|
69
|
+
const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
|
|
70
|
+
if (!isLinkedWorktree) {
|
|
71
|
+
process.exit(0); // main repo, submodule, or separate-git-dir — no-op
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get the raw --show-toplevel output for the worktree (cwd).
|
|
75
|
+
// We keep it raw (not path.resolve'd) to compare directly with the
|
|
76
|
+
// file's toplevel — same git binary, same format, no normalization needed.
|
|
77
|
+
const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
|
|
78
|
+
if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
|
|
79
|
+
process.exit(0); // can't determine root — fail open
|
|
80
|
+
}
|
|
81
|
+
const wtTopRaw = wtTopResult.stdout.trim();
|
|
82
|
+
|
|
83
|
+
const rawFilePath = data.tool_input?.file_path || '';
|
|
84
|
+
if (!rawFilePath) {
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Relative paths are always safe — they resolve relative to CWD inside the worktree
|
|
89
|
+
if (!path.isAbsolute(rawFilePath)) {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Normalise .. traversal so /worktree/src/../../../main/file
|
|
94
|
+
// resolves to its true location before we check containment.
|
|
95
|
+
const filePath = path.resolve(rawFilePath);
|
|
96
|
+
|
|
97
|
+
// Find the nearest existing ancestor of filePath so we can ask git
|
|
98
|
+
// for its toplevel. The file itself may not exist yet (Write creates
|
|
99
|
+
// new files), but at least one ancestor directory must exist.
|
|
100
|
+
// We check the file itself first in case it already exists.
|
|
101
|
+
const checkDir = nearestExistingDir(
|
|
102
|
+
(() => {
|
|
103
|
+
try {
|
|
104
|
+
return fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
|
|
105
|
+
} catch {
|
|
106
|
+
return path.dirname(filePath);
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!checkDir) {
|
|
112
|
+
// Walked to root without finding any directory — path is synthetic.
|
|
113
|
+
// Block conservatively.
|
|
114
|
+
const output = {
|
|
115
|
+
decision: 'block',
|
|
116
|
+
reason:
|
|
117
|
+
`Worktree path guard: '${filePath}' has no existing ancestor directory — ` +
|
|
118
|
+
`cannot verify it is inside the worktree '${wtTopRaw}'. Use a relative path instead.`,
|
|
119
|
+
};
|
|
120
|
+
process.stdout.write(JSON.stringify(output));
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ask git for the toplevel of the file's location.
|
|
125
|
+
// Comparing two raw git --show-toplevel outputs avoids every
|
|
126
|
+
// platform-specific path normalisation pitfall (Windows 8.3 short names,
|
|
127
|
+
// case differences between realpathSync and path.resolve, forward- vs
|
|
128
|
+
// back-slash inconsistencies) — both values come from the same git binary
|
|
129
|
+
// in the same format by definition.
|
|
130
|
+
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
|
|
131
|
+
|
|
132
|
+
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
|
|
133
|
+
// checkDir is not inside any git repo → cannot be inside the worktree.
|
|
134
|
+
const output = {
|
|
135
|
+
decision: 'block',
|
|
136
|
+
reason:
|
|
137
|
+
`Worktree path guard: '${filePath}' is not inside any git repository — ` +
|
|
138
|
+
`it cannot be inside the worktree at '${wtTopRaw}'. Use a relative path instead.`,
|
|
139
|
+
};
|
|
140
|
+
process.stdout.write(JSON.stringify(output));
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fileTopRaw = fileTopResult.stdout.trim();
|
|
145
|
+
|
|
146
|
+
// Same git toplevel → file is inside the worktree → allow
|
|
147
|
+
if (fileTopRaw === wtTopRaw) {
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// BLOCK: file resolves to a different git root than the active worktree
|
|
152
|
+
const output = {
|
|
153
|
+
decision: 'block',
|
|
154
|
+
reason:
|
|
155
|
+
`Worktree path guard: '${filePath}' resolves to git root '${fileTopRaw}' which ` +
|
|
156
|
+
`differs from the active worktree root '${wtTopRaw}'. This likely means an ` +
|
|
157
|
+
`absolute path was derived from the orchestrator's main repository instead of ` +
|
|
158
|
+
`the active worktree. To fix: use a relative path, or re-derive the base ` +
|
|
159
|
+
`directory with \`git rev-parse --show-toplevel\` from within the worktree ` +
|
|
160
|
+
`(hook cwd: '${cwd}').`,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.stdout.write(JSON.stringify(output));
|
|
164
|
+
process.exit(2);
|
|
165
|
+
} catch {
|
|
166
|
+
// Silent fail — never block valid tool calls due to hook errors
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.0-rc.1",
|
|
4
|
+
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gsd-core": "bin/install.js",
|
|
7
7
|
"gsd-tools": "get-shit-done/bin/gsd-tools.cjs"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"commands",
|
|
12
12
|
"get-shit-done",
|
|
13
|
+
"assets",
|
|
13
14
|
"agents",
|
|
14
15
|
"hooks",
|
|
15
16
|
"scripts"
|
|
@@ -69,12 +70,14 @@
|
|
|
69
70
|
"check:alias-drift": "node scripts/check-alias-drift.cjs",
|
|
70
71
|
"check:identity-drift": "node scripts/lint-package-identity-drift.cjs",
|
|
71
72
|
"check:integrity": "node scripts/check-npm-integrity.cjs",
|
|
72
|
-
"build": "npm run generate:identity && npm run build:hooks",
|
|
73
|
+
"build": "npm run generate:identity && npm run build:lib && npm run build:hooks",
|
|
73
74
|
"build:hooks": "node scripts/build-hooks.js",
|
|
75
|
+
"build:lib": "tsc -p tsconfig.build.json",
|
|
74
76
|
"generate:identity": "node scripts/generate-package-identity.cjs",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"pretest
|
|
77
|
+
"prepack": "npm run build:lib",
|
|
78
|
+
"prepublishOnly": "npm run build:lib && npm run build:hooks",
|
|
79
|
+
"pretest": "npm run build:lib && npm run lint:skill-deps",
|
|
80
|
+
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
78
81
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
79
82
|
"lint:fix": "eslint . --fix",
|
|
80
83
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
package/scripts/build-hooks.js
CHANGED
|
@@ -33,6 +33,7 @@ const HOOKS_TO_COPY = [
|
|
|
33
33
|
'gsd-statusline.js',
|
|
34
34
|
'gsd-update-banner.js',
|
|
35
35
|
'gsd-workflow-guard.js',
|
|
36
|
+
'gsd-worktree-path-guard.js',
|
|
36
37
|
// Community hooks (bash, opt-in via .planning/config.json hooks.community)
|
|
37
38
|
'gsd-session-state.sh',
|
|
38
39
|
'gsd-validate-commit.sh',
|
|
@@ -42,6 +42,16 @@ const RULES = [
|
|
|
42
42
|
'tests/bug-3588-npm-audit-clean.test.cjs',
|
|
43
43
|
],
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: 'TS runtime sources (ADR-457 build-at-publish)',
|
|
47
|
+
// src/*.cts compiles into get-shit-done/bin/lib/*.cjs; a source-only edit must
|
|
48
|
+
// still trigger the migrated module's tests (otherwise CI silently skips them).
|
|
49
|
+
match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
|
|
50
|
+
tests: [
|
|
51
|
+
'tests/semver-compare.test.cjs',
|
|
52
|
+
'tests/bug-10-semver-policy-consolidation.test.cjs',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
45
55
|
{
|
|
46
56
|
name: 'installer and package layout',
|
|
47
57
|
match: path => path.startsWith('bin/') ||
|
package/scripts/run-tests.cjs
CHANGED
|
@@ -20,11 +20,32 @@
|
|
|
20
20
|
// See docs/TESTING-SUITES.md for full grouping policy.
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
|
-
const { readdirSync } = require('fs');
|
|
23
|
+
const { readdirSync, existsSync } = require('fs');
|
|
24
24
|
const { join } = require('path');
|
|
25
25
|
const { execFileSync } = require('child_process');
|
|
26
26
|
|
|
27
27
|
const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
|
|
28
|
+
|
|
29
|
+
// ADR-457 build-at-publish: get-shit-done/bin/lib/*.cjs is generated from
|
|
30
|
+
// src/*.cts and gitignored, so on a clean checkout (fresh CI, before any build)
|
|
31
|
+
// the artifact is absent — yet test files require it. This is the universal
|
|
32
|
+
// chokepoint every test path funnels through (test:unit, --files-from, direct
|
|
33
|
+
// invocation), so build the artifact here if missing. It is a no-op once built
|
|
34
|
+
// (dev, pretest, a prior run in the same job), which keeps the harness test's
|
|
35
|
+
// spawned invocations side-effect-free. Paths resolve from __dirname (not cwd),
|
|
36
|
+
// so it works regardless of GSD_TEST_DIR / temp-dir cwd. NOTE: the sentinel is
|
|
37
|
+
// the pilot module; revisit (or switch to an unconditional quiet build) as more
|
|
38
|
+
// modules migrate into src/.
|
|
39
|
+
function ensureBuiltArtifacts() {
|
|
40
|
+
const root = join(__dirname, '..');
|
|
41
|
+
const sentinel = join(root, 'get-shit-done', 'bin', 'lib', 'semver-compare.cjs');
|
|
42
|
+
if (existsSync(sentinel)) return;
|
|
43
|
+
const tscBin = require.resolve('typescript/bin/tsc');
|
|
44
|
+
execFileSync(process.execPath, [tscBin, '-p', join(root, 'tsconfig.build.json')], {
|
|
45
|
+
cwd: root,
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
28
49
|
const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
|
|
29
50
|
|
|
30
51
|
function parseArgs(argv) {
|
|
@@ -203,6 +224,9 @@ function main() {
|
|
|
203
224
|
process.exit(0);
|
|
204
225
|
}
|
|
205
226
|
|
|
227
|
+
// Build the gitignored bin/lib artifact if absent, before any test requires it.
|
|
228
|
+
ensureBuiltArtifacts();
|
|
229
|
+
|
|
206
230
|
// Log selected files to stderr for CI / harness-test visibility.
|
|
207
231
|
// node:test default reporter doesn't echo filenames, so this gives
|
|
208
232
|
// operators a single stable line they can grep.
|