@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-plan-checker.md +34 -0
  3. package/agents/gsd-planner.md +2 -0
  4. package/agents/gsd-roadmapper.md +6 -0
  5. package/bin/install.js +199 -365
  6. package/commands/gsd/capture.md +5 -1
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +695 -5
  9. package/gsd-core/bin/lib/adr-parser.cjs +45 -23
  10. package/gsd-core/bin/lib/audit.cjs +2 -2
  11. package/gsd-core/bin/lib/capability-consent.cjs +763 -0
  12. package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
  13. package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
  14. package/gsd-core/bin/lib/capability-loader.cjs +764 -0
  15. package/gsd-core/bin/lib/capability-lock.cjs +553 -0
  16. package/gsd-core/bin/lib/capability-registry.cjs +198 -4
  17. package/gsd-core/bin/lib/capability-source.cjs +1242 -0
  18. package/gsd-core/bin/lib/capability-state.cjs +9 -6
  19. package/gsd-core/bin/lib/capability-trust.cjs +550 -0
  20. package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -5
  22. package/gsd-core/bin/lib/check-command-router.cjs +69 -18
  23. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  24. package/gsd-core/bin/lib/commands.cjs +247 -0
  25. package/gsd-core/bin/lib/config-loader.cjs +98 -84
  26. package/gsd-core/bin/lib/config-schema.cjs +26 -7
  27. package/gsd-core/bin/lib/config.cjs +7 -1
  28. package/gsd-core/bin/lib/decisions.cjs +149 -60
  29. package/gsd-core/bin/lib/frontmatter.cjs +7 -3
  30. package/gsd-core/bin/lib/gap-checker.cjs +126 -11
  31. package/gsd-core/bin/lib/init.cjs +91 -22
  32. package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
  33. package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
  34. package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
  35. package/gsd-core/bin/lib/milestone.cjs +41 -2
  36. package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
  37. package/gsd-core/bin/lib/phase-id.cjs +25 -11
  38. package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
  39. package/gsd-core/bin/lib/phase.cjs +33 -4
  40. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  41. package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
  42. package/gsd-core/bin/lib/project-root.cjs +89 -2
  43. package/gsd-core/bin/lib/resolution.cjs +26 -0
  44. package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
  45. package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
  46. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
  47. package/gsd-core/bin/lib/roadmap.cjs +5 -2
  48. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
  49. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
  50. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
  51. package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
  52. package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
  53. package/gsd-core/bin/lib/semver-compare.cjs +127 -0
  54. package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
  55. package/gsd-core/bin/lib/state-document.cjs +4 -2
  56. package/gsd-core/bin/lib/state.cjs +317 -161
  57. package/gsd-core/bin/lib/surface.cjs +12 -19
  58. package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
  59. package/gsd-core/bin/lib/uat.cjs +39 -26
  60. package/gsd-core/bin/lib/validate.cjs +5 -2
  61. package/gsd-core/bin/lib/verify.cjs +40 -15
  62. package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
  63. package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
  64. package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
  65. package/gsd-core/references/context-budget.md +8 -8
  66. package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
  67. package/gsd-core/references/execute-phase-context-guard.md +16 -0
  68. package/gsd-core/references/execute-phase-wave-guard.md +33 -0
  69. package/gsd-core/references/planner-antipatterns.md +48 -0
  70. package/gsd-core/references/planning-config.md +4 -0
  71. package/gsd-core/references/prohibition-probe.md +15 -9
  72. package/gsd-core/references/scout-codebase.md +2 -2
  73. package/gsd-core/workflows/autonomous.md +33 -33
  74. package/gsd-core/workflows/diagnose-issues.md +6 -1
  75. package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
  76. package/gsd-core/workflows/discuss-phase.md +1 -2
  77. package/gsd-core/workflows/execute-phase.md +12 -12
  78. package/gsd-core/workflows/help/modes/full.md +10 -0
  79. package/gsd-core/workflows/list-seeds.md +63 -0
  80. package/gsd-core/workflows/manager.md +37 -37
  81. package/gsd-core/workflows/pr-branch.md +156 -0
  82. package/gsd-core/workflows/quick.md +6 -1
  83. package/gsd-core/workflows/review.md +10 -2
  84. package/gsd-core/workflows/spec-phase.md +8 -3
  85. package/gsd-core/workflows/verify-phase.md +2 -2
  86. package/package.json +6 -3
  87. package/scripts/gen-capability-matrix.cjs +284 -0
  88. package/scripts/gen-capability-registry.cjs +96 -1853
  89. package/scripts/lint-regression-test-names.allowlist.json +1 -0
  90. package/scripts/lint-resolution-provenance.allowlist.json +1 -0
  91. package/scripts/lint-resolution-provenance.cjs +192 -0
  92. package/scripts/lint-test-file-count.allowlist.json +9 -0
  93. package/scripts/prompt-injection-scan.sh +1 -0
  94. package/scripts/run-tests.cjs +14 -0
  95. package/scripts/sync-manifest-versions.cjs +77 -5
@@ -2,6 +2,7 @@
2
2
  "bug-10-semver-policy-consolidation.test.cjs",
3
3
  "bug-130-finishinstall-opencode-testmode.test.cjs",
4
4
  "bug-131-release-tarball-smoke-explicit-home.test.cjs",
5
+ "bug-1367-claude-local-flat-command-layout.test.cjs",
5
6
  "bug-14-progress-auto-flag-dropped.test.cjs",
6
7
  "bug-167-query-meta-command.test.cjs",
7
8
  "bug-17-askuserquestion-option-cap.test.cjs",
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-resolution-provenance.cjs — CI guard for Resolution Provenance contracts.
6
+ *
7
+ * ## Purpose (ADR-1411 P4 / #1417)
8
+ *
9
+ * This is a REGRESSION-LOCK and REGISTRATION RATCHET, NOT a universal static
10
+ * detector (which is intractable given false positives from config-reading
11
+ * helpers that don't consume a `reason`).
12
+ *
13
+ * The guard maintains a REGISTRY of config-interpreting read verbs that MUST
14
+ * carry provenance — each entry names the verb, its source file, and its test
15
+ * file. For every registered verb, the guard asserts that its test file
16
+ * contains BOTH a `configured_empty` assertion AND a `not_configured`
17
+ * assertion, proving that the configured-empty-vs-not-configured contract is
18
+ * explicitly tested (not silently open-to-defaults).
19
+ *
20
+ * ## Registration protocol
21
+ *
22
+ * When adding a NEW config-interpreting read verb:
23
+ * 1. Add an entry to REGISTRY below: { verb, sourceFile, testFile }.
24
+ * 2. Add a `configured_empty` test and a `not_configured` test to testFile.
25
+ * 3. If the test coverage cannot land in the same PR, add the verb to
26
+ * scripts/lint-resolution-provenance.allowlist.json to grandfather it —
27
+ * but the allowlist MUST shrink over time (stale entries fail).
28
+ *
29
+ * See docs/adr/1411-resolution-provenance.md and #1417.
30
+ */
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
35
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
36
+
37
+ const ROOT = path.join(__dirname, '..');
38
+ const ALLOWLIST_PATH = path.join(__dirname, 'lint-resolution-provenance.allowlist.json');
39
+
40
+ /**
41
+ * Registry of config-interpreting read verbs that must carry provenance.
42
+ * Each entry: { verb, sourceFile, testFile }
43
+ *
44
+ * - verb: Short stable name for this verb (used in error messages and the
45
+ * allowlist).
46
+ * - sourceFile: Path (relative to ROOT) to the source implementation.
47
+ * - testFile: Path (relative to ROOT) to the test file that MUST contain
48
+ * both a `configured_empty` assertion and a `not_configured`
49
+ * assertion.
50
+ *
51
+ * Seed: agent-skills (P2/P3 fix, #1415/#1416) is the founding member.
52
+ */
53
+ const REGISTRY = [
54
+ {
55
+ verb: 'agent-skills',
56
+ sourceFile: 'src/init.cts',
57
+ testFile: 'tests/agent-skills.test.cjs',
58
+ },
59
+ ];
60
+
61
+ // Markers that MUST appear in every registered verb's test file.
62
+ const MARKER_CONFIGURED_EMPTY = 'configured_empty';
63
+ const MARKER_NOT_CONFIGURED = 'not_configured';
64
+
65
+ /**
66
+ * Pure check logic — factored out for unit testing without I/O.
67
+ *
68
+ * @param {object} opts
69
+ * @param {Array<{verb: string, sourceFile: string, testFile: string}>} opts.registry
70
+ * The REGISTRY to check (or an injected subset for tests).
71
+ * @param {string[]} opts.allowlist
72
+ * Array of verb names to grandfather (stale entries fail).
73
+ * @param {function(string): string} opts.readFile
74
+ * Reads a file path and returns its content. Injected for testability;
75
+ * callers pass `(p) => fs.readFileSync(p, 'utf8')`.
76
+ * @param {function(string): void} opts.fail
77
+ * Callback invoked with a descriptive failure message.
78
+ * @returns {{ ok: boolean }}
79
+ */
80
+ function checkRegistry({ registry, allowlist, readFile, fail }) {
81
+ const allowlistSet = new Set(allowlist);
82
+ const offenders = []; // verbs that ARE failing (for ratchet: stale check)
83
+ let anyFail = false;
84
+
85
+ for (const entry of registry) {
86
+ const { verb, testFile } = entry;
87
+ const resolvedTestFile = path.isAbsolute(testFile) ? testFile : path.join(ROOT, testFile);
88
+
89
+ // Grandfathered? Check markers anyway to detect when it's been fixed.
90
+ let content;
91
+ try {
92
+ content = readFile(resolvedTestFile);
93
+ } catch (err) {
94
+ fail(
95
+ `[resolution-provenance] Cannot read test file for verb "${verb}" (${testFile}): ${err.message}\n` +
96
+ ` Register the verb's test file correctly, or remove the registry entry.`
97
+ );
98
+ anyFail = true;
99
+ offenders.push(verb);
100
+ continue;
101
+ }
102
+
103
+ const hasConfiguredEmpty = content.includes(MARKER_CONFIGURED_EMPTY);
104
+ const hasNotConfigured = content.includes(MARKER_NOT_CONFIGURED);
105
+
106
+ if (!hasConfiguredEmpty || !hasNotConfigured) {
107
+ offenders.push(verb);
108
+
109
+ if (allowlistSet.has(verb)) {
110
+ // Grandfathered — tolerate but don't report.
111
+ continue;
112
+ }
113
+
114
+ const missing = [];
115
+ if (!hasConfiguredEmpty) missing.push(`\`configured_empty\``);
116
+ if (!hasNotConfigured) missing.push(`\`not_configured\``);
117
+
118
+ fail(
119
+ `[resolution-provenance] verb "${verb}" (${testFile}) is missing contract test marker(s):\n` +
120
+ ` Missing: ${missing.join(', ')}\n` +
121
+ ` Each registered config-interpreting read verb must have both a\n` +
122
+ ` \`configured_empty\` assertion and a \`not_configured\` assertion in its\n` +
123
+ ` test file to prove the configured-empty-vs-not-configured contract is\n` +
124
+ ` tested (ADR-1411 P4 / #1417).\n` +
125
+ ` Add the missing test(s) or grandfather the verb in\n` +
126
+ ` scripts/lint-resolution-provenance.allowlist.json.`
127
+ );
128
+ anyFail = true;
129
+ }
130
+ }
131
+
132
+ // Ratchet: stale allowlist entries (verb is compliant but still grandfathered)
133
+ // must be pruned so the allowlist only ever shrinks.
134
+ const offenderSet = new Set(offenders);
135
+ const staleEntries = [];
136
+ for (const v of allowlistSet) {
137
+ if (!offenderSet.has(v)) {
138
+ staleEntries.push(v);
139
+ }
140
+ }
141
+
142
+ // Also verify stale entries via assertWithinAllowlist for consistent messaging.
143
+ const ratchetFailures = [];
144
+ assertWithinAllowlist({
145
+ label: 'resolution-provenance',
146
+ current: offenders,
147
+ known: allowlist,
148
+ fail: (msg) => ratchetFailures.push(msg),
149
+ pruneHint: 'edit scripts/lint-resolution-provenance.allowlist.json',
150
+ });
151
+
152
+ // Only report stale entries from the ratchet (novel offenders are already
153
+ // reported above with more actionable messages).
154
+ if (staleEntries.length > 0) {
155
+ for (const msg of ratchetFailures) {
156
+ // Only surface the stale-entry message (it contains "stale" or "no longer").
157
+ if (msg.includes('stale') || msg.includes('no longer')) {
158
+ fail(msg);
159
+ anyFail = true;
160
+ }
161
+ }
162
+ }
163
+
164
+ return { ok: !anyFail };
165
+ }
166
+
167
+ function main() {
168
+ const allowlist = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
169
+
170
+ const failures = [];
171
+ const { ok } = checkRegistry({
172
+ registry: REGISTRY,
173
+ allowlist,
174
+ readFile: (filePath) => fs.readFileSync(filePath, 'utf8'),
175
+ fail: (msg) => failures.push(msg),
176
+ });
177
+
178
+ if (!ok) {
179
+ for (const msg of failures) process.stderr.write(`${msg}\n`);
180
+ throw new ExitError(1);
181
+ }
182
+
183
+ console.log(
184
+ `ok lint-resolution-provenance: ${REGISTRY.length} registered verb(s), all carry configured_empty + not_configured contract tests`
185
+ );
186
+ }
187
+
188
+ module.exports = { checkRegistry, REGISTRY };
189
+
190
+ // Only run the CLI check when executed directly, not when imported by tests
191
+ // (keeps the unit tests hermetic — importing checkRegistry must not run main).
192
+ if (require.main === module) runMain(main);
@@ -60,6 +60,7 @@
60
60
  },
61
61
  "phase": {
62
62
  "files": [
63
+ "fix-1437-phase-list-plans.test.cjs",
63
64
  "bug-214-phase-researcher-write-truncation-contract.test.cjs",
64
65
  "phase-dependency-levels.test.cjs",
65
66
  "phase.test.cjs"
@@ -174,6 +175,14 @@
174
175
  "external-job-waiting.test.cjs"
175
176
  ],
176
177
  "issue": "#1165"
178
+ },
179
+ "docs": {
180
+ "files": [
181
+ "docs-parity-live-registry.test.cjs",
182
+ "docs-update.test.cjs",
183
+ "fix-1464-docs-manifest-validation.test.cjs"
184
+ ],
185
+ "issue": "1496"
177
186
  }
178
187
  }
179
188
  }
@@ -78,6 +78,7 @@ ALLOWLIST=(
78
78
  'hooks/gsd-read-injection-scanner.js'
79
79
  'tests/read-injection-scanner.security.test.cjs'
80
80
  'tests/security-prompt-injection.security.test.cjs'
81
+ 'tests/list-seeds.test.cjs'
81
82
  'tests/fixtures/adversarial/security/'
82
83
  'SECURITY.md'
83
84
  # These files contain intentional injection examples / security-model prose
@@ -462,6 +462,20 @@ function main() {
462
462
  delete process.env.GSD_PROJECT;
463
463
  delete process.env.GSD_WORKSTREAM;
464
464
  delete process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
465
+ // Sandbox the overlay home so the loader's global scan ($GSD_HOME/.gsd/capabilities)
466
+ // cannot read a developer's real installed capabilities during tests (ADR-1244 D2).
467
+ // IDEMPOTENT: a nested run-tests spawn (e.g. tests/run-tests-harness.test.cjs)
468
+ // inherits this sandbox via env — it must REUSE it, never mkdtemp a fresh dir per
469
+ // invocation (that churned ~20+ temp dirs per harness run and amplified Docker load).
470
+ {
471
+ const { mkdtempSync } = require('fs');
472
+ const { join: _join, basename: _basename } = require('path');
473
+ const { tmpdir } = require('os');
474
+ const _gh = process.env.GSD_HOME;
475
+ if (!_gh || !_basename(_gh).startsWith('gsd-test-home-')) {
476
+ process.env.GSD_HOME = mkdtempSync(_join(tmpdir(), 'gsd-test-home-'));
477
+ }
478
+ }
465
479
 
466
480
  // Log selected files to stderr for CI / harness-test visibility.
467
481
  // node:test default reporter doesn't echo filenames, so this gives
@@ -68,6 +68,66 @@ function findDrift(opts) {
68
68
  return drift;
69
69
  }
70
70
 
71
+ // ─── ADR-1244 D6: native capability manifests ────────────────────────────────
72
+ //
73
+ // Native capabilities (capabilities/<id>/capability.json) carry a `version`
74
+ // stamped in lockstep with the package version at release. Unlike
75
+ // VERSIONED_MANIFESTS (fixed paths), capabilities are discovered by glob so a
76
+ // new capability is auto-covered without editing this file. The version-sync
77
+ // regression guard (issue #844) treats every swept capability manifest as
78
+ // registered.
79
+
80
+ // Discover capabilities/<id>/capability.json under `root`, sorted for stable
81
+ // staging order. Returns [] when there is no capabilities/ directory.
82
+ function listCapabilityManifests(opts) {
83
+ const root = (opts && opts.root) || ROOT;
84
+ const dir = path.join(root, 'capabilities');
85
+ let entries;
86
+ try {
87
+ entries = fs.readdirSync(dir, { withFileTypes: true });
88
+ } catch {
89
+ return [];
90
+ }
91
+ return entries
92
+ .filter((e) => e.isDirectory())
93
+ // Forward-slash rel paths (NOT path.join) so they match `git ls-files`
94
+ // output, git pathspecs, and the forward-slash VERSIONED_MANIFESTS on every
95
+ // platform — path.join would emit backslashes on Windows and break the
96
+ // issue-844 regression guard's ALLOWED-set comparison.
97
+ .map((e) => 'capabilities/' + e.name + '/capability.json')
98
+ .filter((rel) => fs.existsSync(path.join(root, rel)))
99
+ .sort();
100
+ }
101
+
102
+ // Stamp `version` into each native capability manifest. Returns changed rel paths.
103
+ function syncCapabilityVersions(opts) {
104
+ const root = (opts && opts.root) || ROOT;
105
+ const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
106
+ const changed = [];
107
+ for (const rel of listCapabilityManifests({ root })) {
108
+ const abs = path.join(root, rel);
109
+ const manifest = readJson(abs);
110
+ if (manifest.version !== v) {
111
+ manifest.version = v;
112
+ fs.writeFileSync(abs, JSON.stringify(manifest, null, 2) + '\n');
113
+ changed.push(rel);
114
+ }
115
+ }
116
+ return changed;
117
+ }
118
+
119
+ // Native capability manifests whose version != package version.
120
+ function findCapabilityDrift(opts) {
121
+ const root = (opts && opts.root) || ROOT;
122
+ const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
123
+ const drift = [];
124
+ for (const rel of listCapabilityManifests({ root })) {
125
+ const found = readJson(path.join(root, rel)).version;
126
+ if (found !== v) drift.push({ manifest: rel, found, expected: v });
127
+ }
128
+ return drift;
129
+ }
130
+
71
131
  // Best-effort outside git; fail-closed inside a work tree so a release never
72
132
  // ships a stale manifest that the working-tree test already accepted.
73
133
  function stageManifests(opts) {
@@ -83,21 +143,32 @@ function stageManifests(opts) {
83
143
  console.warn('sync-manifest-versions: not a git work tree; skipping staging.');
84
144
  return;
85
145
  }
146
+ const toStage = [...VERSIONED_MANIFESTS, ...listCapabilityManifests({ root })];
86
147
  try {
87
- execFileSync('git', ['add', '--', ...VERSIONED_MANIFESTS], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
148
+ execFileSync('git', ['add', '--', ...toStage], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
88
149
  } catch (err) {
89
150
  const detail = err && err.stderr ? err.stderr.toString().trim() : (err && err.message) || 'unknown error';
90
151
  throw new Error(`sync-manifest-versions: failed to git-add manifests inside a work tree: ${detail}`);
91
152
  }
92
153
  }
93
154
 
94
- module.exports = { VERSIONED_MANIFESTS, syncManifestVersions, findDrift, getPackageVersion, stageManifests };
155
+ module.exports = {
156
+ VERSIONED_MANIFESTS,
157
+ syncManifestVersions,
158
+ findDrift,
159
+ getPackageVersion,
160
+ stageManifests,
161
+ // ADR-1244 D6: native capability version sweep
162
+ listCapabilityManifests,
163
+ syncCapabilityVersions,
164
+ findCapabilityDrift,
165
+ };
95
166
 
96
167
  if (require.main === module) {
97
168
  const args = process.argv.slice(2);
98
169
  const version = getPackageVersion();
99
170
  if (args.includes('--check')) {
100
- const drift = findDrift({ version });
171
+ const drift = [...findDrift({ version }), ...findCapabilityDrift({ version })];
101
172
  if (drift.length) {
102
173
  for (const d of drift) {
103
174
  console.error('Manifest ' + d.manifest + ' version ' + d.found + ' != package.json ' + d.expected);
@@ -105,10 +176,11 @@ if (require.main === module) {
105
176
  console.error('Run `node scripts/sync-manifest-versions.cjs` to fix.');
106
177
  process.exitCode = 1;
107
178
  } else {
108
- console.log('All ' + VERSIONED_MANIFESTS.length + ' versioned manifests in sync at ' + version + '.');
179
+ const total = VERSIONED_MANIFESTS.length + listCapabilityManifests().length;
180
+ console.log('All ' + total + ' versioned manifests in sync at ' + version + '.');
109
181
  }
110
182
  } else {
111
- const changed = syncManifestVersions({ version });
183
+ const changed = [...syncManifestVersions({ version }), ...syncCapabilityVersions({ version })];
112
184
  if (changed.length) {
113
185
  console.log('Stamped ' + version + ' into: ' + changed.join(', '));
114
186
  } else {