@opengsd/gsd-core 1.5.0-rc.5 → 1.5.0

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 (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +1 -1
  3. package/agents/gsd-executor.md +22 -0
  4. package/bin/install.js +53 -99
  5. package/commands/gsd/autonomous.md +1 -1
  6. package/commands/gsd/execute-phase.md +1 -1
  7. package/commands/gsd/plan-phase.md +1 -1
  8. package/gemini-extension.json +1 -1
  9. package/gsd-core/bin/gsd-tools.cjs +36 -5
  10. package/gsd-core/bin/lib/decisions.cjs +19 -1
  11. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  12. package/gsd-core/bin/lib/phase.cjs +41 -6
  13. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  14. package/gsd-core/bin/lib/prohibition-enforcement.cjs +201 -26
  15. package/gsd-core/bin/lib/roadmap-parser.cjs +25 -20
  16. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
  17. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
  18. package/gsd-core/bin/lib/state.cjs +64 -15
  19. package/gsd-core/bin/lib/teams-status.cjs +74 -0
  20. package/gsd-core/bin/lib/uat.cjs +56 -0
  21. package/gsd-core/bin/lib/worktree-safety.cjs +7 -1
  22. package/gsd-core/references/prohibition-probe.md +48 -19
  23. package/gsd-core/references/worktree-branch-check.md +11 -5
  24. package/gsd-core/workflows/docs-update.md +23 -31
  25. package/gsd-core/workflows/execute-phase.md +2 -1
  26. package/gsd-core/workflows/map-codebase.md +8 -10
  27. package/gsd-core/workflows/plan-phase.md +6 -0
  28. package/gsd-core/workflows/quick.md +23 -2
  29. package/gsd-core/workflows/spec-phase.md +11 -6
  30. package/gsd-core/workflows/verify-phase.md +3 -3
  31. package/hooks/dist/gsd-worktree-path-guard.js +34 -18
  32. package/hooks/gsd-worktree-path-guard.js +34 -18
  33. package/package.json +2 -2
  34. package/scripts/ci-prepare-test-scope.cjs +56 -14
  35. package/scripts/diff-touches-shipped-paths.cjs +5 -11
  36. package/scripts/run-tests.cjs +1 -0
@@ -71,6 +71,17 @@ process.stdin.on('end', () => {
71
71
  process.exit(0); // main repo, submodule, or separate-git-dir — no-op
72
72
  }
73
73
 
74
+ // #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
75
+ // are always on a `worktree-agent-*` branch (the positive allow-list enforced
76
+ // by worktree-branch-check.md, #2924). A manually-created linked worktree (plain
77
+ // non-GSD work, e.g. Claude Code plan-mode) is on the user's own branch, so the
78
+ // guard must be a no-op there. Detached HEAD / error → not GSD-managed → no-op.
79
+ const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
80
+ const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
81
+ if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) {
82
+ process.exit(0); // not a GSD-managed executor worktree — no-op
83
+ }
84
+
74
85
  // Get the raw --show-toplevel output for the worktree (cwd).
75
86
  // We keep it raw (not path.resolve'd) to compare directly with the
76
87
  // file's toplevel — same git binary, same format, no normalization needed.
@@ -110,15 +121,9 @@ process.stdin.on('end', () => {
110
121
 
111
122
  if (!checkDir) {
112
123
  // 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);
124
+ // A path with no existing ancestor is not the #260 main-repo vector;
125
+ // #260 is caught by the different-git-root branch below. Fail open. (#1342)
126
+ process.exit(0);
122
127
  }
123
128
 
124
129
  // Ask git for the toplevel of the file's location.
@@ -130,15 +135,26 @@ process.stdin.on('end', () => {
130
135
  const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
131
136
 
132
137
  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);
138
+ // The target's location is not a git work tree. Two sub-cases:
139
+ // - Inside a .git directory (e.g. /main-repo/.git/config or .git/hooks/*)
140
+ // → an absolute write into a repository's internals; still a #260-class
141
+ // escape (and dangerous) → BLOCK.
142
+ // - Truly outside all git repositories (e.g. ~/.claude/plans/) → not the
143
+ // main-repo vector → fail open. (#1342)
144
+ const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
145
+ if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
146
+ const output = {
147
+ decision: 'block',
148
+ reason:
149
+ `Worktree path guard: '${filePath}' is inside a git internal (.git) directory, ` +
150
+ `not the active worktree at '${wtTopRaw}'. Writing to repository internals via an ` +
151
+ `absolute path is not permitted from an isolated executor worktree. Use a relative path.`,
152
+ };
153
+ process.stdout.write(JSON.stringify(output));
154
+ process.exit(2);
155
+ }
156
+ // Outside all git repositories — fail open (#1342).
157
+ process.exit(0);
142
158
  }
143
159
 
144
160
  const fileTopRaw = fileTopResult.stdout.trim();
@@ -71,6 +71,17 @@ process.stdin.on('end', () => {
71
71
  process.exit(0); // main repo, submodule, or separate-git-dir — no-op
72
72
  }
73
73
 
74
+ // #1342: Only enforce inside a GSD-managed isolated executor worktree. Those
75
+ // are always on a `worktree-agent-*` branch (the positive allow-list enforced
76
+ // by worktree-branch-check.md, #2924). A manually-created linked worktree (plain
77
+ // non-GSD work, e.g. Claude Code plan-mode) is on the user's own branch, so the
78
+ // guard must be a no-op there. Detached HEAD / error → not GSD-managed → no-op.
79
+ const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
80
+ const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
81
+ if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) {
82
+ process.exit(0); // not a GSD-managed executor worktree — no-op
83
+ }
84
+
74
85
  // Get the raw --show-toplevel output for the worktree (cwd).
75
86
  // We keep it raw (not path.resolve'd) to compare directly with the
76
87
  // file's toplevel — same git binary, same format, no normalization needed.
@@ -110,15 +121,9 @@ process.stdin.on('end', () => {
110
121
 
111
122
  if (!checkDir) {
112
123
  // 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);
124
+ // A path with no existing ancestor is not the #260 main-repo vector;
125
+ // #260 is caught by the different-git-root branch below. Fail open. (#1342)
126
+ process.exit(0);
122
127
  }
123
128
 
124
129
  // Ask git for the toplevel of the file's location.
@@ -130,15 +135,26 @@ process.stdin.on('end', () => {
130
135
  const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
131
136
 
132
137
  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);
138
+ // The target's location is not a git work tree. Two sub-cases:
139
+ // - Inside a .git directory (e.g. /main-repo/.git/config or .git/hooks/*)
140
+ // → an absolute write into a repository's internals; still a #260-class
141
+ // escape (and dangerous) → BLOCK.
142
+ // - Truly outside all git repositories (e.g. ~/.claude/plans/) → not the
143
+ // main-repo vector → fail open. (#1342)
144
+ const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
145
+ if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
146
+ const output = {
147
+ decision: 'block',
148
+ reason:
149
+ `Worktree path guard: '${filePath}' is inside a git internal (.git) directory, ` +
150
+ `not the active worktree at '${wtTopRaw}'. Writing to repository internals via an ` +
151
+ `absolute path is not permitted from an isolated executor worktree. Use a relative path.`,
152
+ };
153
+ process.stdout.write(JSON.stringify(output));
154
+ process.exit(2);
155
+ }
156
+ // Outside all git repositories — fail open (#1342).
157
+ process.exit(0);
142
158
  }
143
159
 
144
160
  const fileTopRaw = fileTopResult.stdout.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.5.0-rc.5",
3
+ "version": "1.5.0",
4
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",
@@ -62,7 +62,7 @@
62
62
  "eslint-plugin-no-only-tests": "^3.4.0",
63
63
  "fast-check": "^4.8.0",
64
64
  "globals": "^16.5.0",
65
- "js-yaml": "^4.1.1",
65
+ "js-yaml": "^4.2.0",
66
66
  "typescript": "^6.0.3",
67
67
  "typescript-eslint": "^8.60.0"
68
68
  },
@@ -16,13 +16,37 @@ const path = require('path');
16
16
 
17
17
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
18
18
 
19
- const scope = process.env.TEST_SCOPE || '';
20
- const targeted = process.env.TARGETED_TESTS || '';
21
- const windows = process.env.WINDOWS_TESTS || '';
19
+ // Suite sentinels understood by run-tests.cjs (scripts/run-tests.cjs SUITES).
20
+ // A sentinel resolves to its live file set at run time, so — unlike an explicit
21
+ // filename — it can never reference a since-deleted test file.
22
+ const SUITE_SENTINELS = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
22
23
 
23
- const FALLBACK = 'tests/command-contract.test.cjs tests/commands.test.cjs tests/core.test.cjs tests/package-manifest.test.cjs';
24
+ // Fast smoke set used when scope detection yields no targeted tests. Each entry
25
+ // MUST resolve via run-tests.cjs: an existing repo-relative test file or a
26
+ // SUITE_SENTINELS token. A stale filename here (a test deleted by a refactor)
27
+ // is exactly what broke CI in #1329 — `tests/core.test.cjs`, deleted in #1291,
28
+ // was still listed and crashed every scoped lane that hit this fallback. The
29
+ // parity guard in tests/bug-641-files-from-suite-token.test.cjs fails the
30
+ // moment an entry stops resolving; resolveSelection() filters at write time so
31
+ // a stale entry degrades instead of crashing the lane.
32
+ const FALLBACK = [
33
+ 'tests/command-contract.test.cjs',
34
+ 'tests/commands.test.cjs',
35
+ 'tests/package-manifest.test.cjs',
36
+ ];
24
37
 
25
- function main() {
38
+ // Last-resort selector when every FALLBACK entry has been deleted: the whole
39
+ // unit suite, resolved live by run-tests.cjs (the #408/#641 sentinel path).
40
+ const FALLBACK_SENTINEL = 'unit';
41
+
42
+ // An entry is runnable if it is a suite sentinel or an existing file under root.
43
+ function isResolvable(entry, root) {
44
+ return SUITE_SENTINELS.includes(entry) || fs.existsSync(path.join(root, entry));
45
+ }
46
+
47
+ // Resolve the scoped test selection for the lane. Pure (no I/O beyond the
48
+ // existence probe under `root`) so it can be unit-tested directly.
49
+ function resolveSelection({ scope, targeted, windows, root }) {
26
50
  let selected;
27
51
  if (scope === 'windows') {
28
52
  selected = windows;
@@ -32,20 +56,38 @@ function main() {
32
56
  throw new ExitError(1, `::error::Unknown test scope: ${scope}`);
33
57
  }
34
58
 
35
- // Trim and fall back to default set if empty.
36
- if (!selected.trim()) {
37
- selected = FALLBACK;
59
+ // Detected list passes through verbatim: affected-tests-lib.cjs already filters
60
+ // deleted files, and the list may legitimately carry a suite sentinel.
61
+ const detected = (selected || '').split(/\s+/).filter(Boolean);
62
+ if (detected.length > 0) {
63
+ return detected;
38
64
  }
39
65
 
40
- // Split on whitespace, filter blanks, join with newlines.
41
- const lines = selected.split(/\s+/).filter(Boolean);
42
- const content = lines.join('\n') + '\n';
66
+ // Empty detection → smoke fallback, existence-filtered so a stale entry can
67
+ // never crash the scoped lane (#1329). If nothing survives, use the unit
68
+ // sentinel, which run-tests.cjs always resolves.
69
+ const survivors = FALLBACK.filter((f) => isResolvable(f, root));
70
+ return survivors.length > 0 ? survivors : [FALLBACK_SENTINEL];
71
+ }
43
72
 
44
- const outPath = path.join(process.cwd(), '.ci-selected-tests.txt');
45
- fs.writeFileSync(outPath, content, 'utf-8');
73
+ function main() {
74
+ const root = process.cwd();
75
+ const lines = resolveSelection({
76
+ scope: process.env.TEST_SCOPE || '',
77
+ targeted: process.env.TARGETED_TESTS || '',
78
+ windows: process.env.WINDOWS_TESTS || '',
79
+ root,
80
+ });
81
+
82
+ const content = lines.join('\n') + '\n';
83
+ fs.writeFileSync(path.join(root, '.ci-selected-tests.txt'), content, 'utf-8');
46
84
 
47
85
  process.stdout.write('Scoped tests:\n');
48
86
  process.stdout.write(content);
49
87
  }
50
88
 
51
- runMain(main);
89
+ if (require.main === module) {
90
+ runMain(main);
91
+ }
92
+
93
+ module.exports = { FALLBACK, FALLBACK_SENTINEL, SUITE_SENTINELS, resolveSelection, main };
@@ -12,11 +12,10 @@
12
12
  * - package.json (always included by `npm pack`, regardless of `files`)
13
13
  * - every entry in package.json `files`, treated as either an exact
14
14
  * file match or a directory prefix (matching `npm pack` semantics).
15
- * - CI-gating test paths: `tests/<anything>` plus
16
- * `sdk/src/<anything>/<name>.test.<ts|cjs|mjs|js>` and `.spec.` variants
17
- * — these don't ship in the tarball, but they gate the hotfix-branch
18
- * test job. A test fixture update that aligns with a cherry-picked
19
- * production fix MUST be pickable or CI fails on the hotfix run.
15
+ * - CI-gating test paths: `tests/<anything>` — these don't ship in the
16
+ * tarball, but they gate the hotfix-branch test job. A test fixture
17
+ * update that aligns with a cherry-picked production fix MUST be
18
+ * pickable or CI fails on the hotfix run.
20
19
  * #3621 — root cause of the v1.42.3 hotfix red CI.
21
20
  *
22
21
  * `package-lock.json` is intentionally NOT considered shipped — `npm pack`
@@ -65,12 +64,7 @@ function loadShipPrefixes(pkgPath) {
65
64
  // in hotfix.yml, this lets `test(####):` fixture-alignment commits be
66
65
  // cherry-picked alongside their production counterparts.
67
66
  function isCiGating(diffPath) {
68
- if (diffPath.startsWith('tests/')) return true;
69
- // SDK vitest specs live next to source. Production source ships via
70
- // sdk/dist/ (already in package.json `files`); the test files are what's
71
- // missing from that surface.
72
- if (diffPath.startsWith('sdk/src/') && /\.(test|spec)\.(ts|cjs|mjs|js)$/.test(diffPath)) return true;
73
- return false;
67
+ return diffPath.startsWith('tests/');
74
68
  }
75
69
 
76
70
  function isShipped(diffPath, shipPrefixes) {
@@ -461,6 +461,7 @@ function main() {
461
461
  // them so the local runner matches CI; tests that need them set them explicitly.
462
462
  delete process.env.GSD_PROJECT;
463
463
  delete process.env.GSD_WORKSTREAM;
464
+ delete process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
464
465
 
465
466
  // Log selected files to stderr for CI / harness-test visibility.
466
467
  // node:test default reporter doesn't echo filenames, so this gives