@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
@@ -17,6 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.canonicalizeRuntimeName = canonicalizeRuntimeName;
19
19
  exports.resolveRuntimeNameFromCandidates = resolveRuntimeNameFromCandidates;
20
+ exports.getDirName = getDirName;
20
21
  const node_fs_1 = __importDefault(require("node:fs"));
21
22
  const node_path_1 = __importDefault(require("node:path"));
22
23
  const FALLBACK_ALIASES = {
@@ -95,3 +96,46 @@ function resolveRuntimeNameFromCandidates(...candidates) {
95
96
  }
96
97
  return null;
97
98
  }
99
+ /**
100
+ * Map a canonical runtime id to its on-disk local config directory name
101
+ * (e.g. `cursor` -> `.cursor`, `windsurf` -> `.devin`). Unknown/empty inputs
102
+ * fall back to `.claude`.
103
+ *
104
+ * Pure runtime-identity projection. Relocated from `bin/install.js` per
105
+ * ADR-1508 (epic #1507, #1510 Phase 1) so the Runtime Artifact Conversion
106
+ * Module's rewrite engine can consume it without importing the installer.
107
+ * `bin/install.js` re-exports this same function for back-compat.
108
+ */
109
+ function getDirName(runtime) {
110
+ if (runtime === 'copilot')
111
+ return '.github';
112
+ if (runtime === 'opencode')
113
+ return '.opencode';
114
+ if (runtime === 'gemini')
115
+ return '.gemini';
116
+ if (runtime === 'kilo')
117
+ return '.kilo';
118
+ if (runtime === 'codex')
119
+ return '.codex';
120
+ if (runtime === 'antigravity')
121
+ return '.agents';
122
+ if (runtime === 'cursor')
123
+ return '.cursor';
124
+ if (runtime === 'windsurf')
125
+ return '.devin';
126
+ if (runtime === 'augment')
127
+ return '.augment';
128
+ if (runtime === 'trae')
129
+ return '.trae';
130
+ if (runtime === 'qwen')
131
+ return '.qwen';
132
+ if (runtime === 'hermes')
133
+ return '.hermes';
134
+ if (runtime === 'kimi')
135
+ return '.kimi-code';
136
+ if (runtime === 'codebuddy')
137
+ return '.codebuddy';
138
+ if (runtime === 'cline')
139
+ return '.cline';
140
+ return '.claude';
141
+ }
@@ -14,6 +14,7 @@ exports.toNumericTuple = toNumericTuple;
14
14
  exports.compareSemverCore = compareSemverCore;
15
15
  exports.isSemverNewer = isSemverNewer;
16
16
  exports.isStableTripletSemver = isStableTripletSemver;
17
+ exports.semverSatisfies = semverSatisfies;
17
18
  function toNumericTuple(input) {
18
19
  const cleaned = String(input == null ? '' : input).trim().replace(/^v/, '');
19
20
  const base = cleaned.replace(/[-+].*$/, '');
@@ -40,3 +41,129 @@ function isSemverNewer(a, b) {
40
41
  function isStableTripletSemver(v) {
41
42
  return /^\d+\.\d+\.\d+$/.test(String(v || '').replace(/^v/, ''));
42
43
  }
44
+ function compareTuples(a, b) {
45
+ if (a[0] !== b[0])
46
+ return a[0] > b[0] ? 1 : -1;
47
+ if (a[1] !== b[1])
48
+ return a[1] > b[1] ? 1 : -1;
49
+ if (a[2] !== b[2])
50
+ return a[2] > b[2] ? 1 : -1;
51
+ return 0;
52
+ }
53
+ // Parse a version-ish token into a tuple + how many leading numeric parts were
54
+ // specified (0 = bare wildcard "*"/"x", 1 = "1", 2 = "1.2", 3 = "1.2.3").
55
+ // Returns null if the token is not a parseable partial/full version.
56
+ function parseVersionToken(token) {
57
+ const clean = token.trim().replace(/^v/, '').replace(/[-+].*$/, '');
58
+ if (clean === '' || clean === '*' || clean === 'x' || clean === 'X')
59
+ return { tuple: [0, 0, 0], specified: 0 };
60
+ const parts = clean.split('.');
61
+ if (parts.length > 3)
62
+ return null;
63
+ const nums = [];
64
+ let sawWildcard = false;
65
+ for (const p of parts) {
66
+ if (p === 'x' || p === 'X' || p === '*') {
67
+ sawWildcard = true;
68
+ continue;
69
+ }
70
+ // A concrete segment after a wildcard ("1.x.2", "1.*.2") is malformed → fail closed.
71
+ if (sawWildcard)
72
+ return null;
73
+ if (!/^\d+$/.test(p))
74
+ return null;
75
+ nums.push(Number.parseInt(p, 10));
76
+ }
77
+ if (nums.length === 0)
78
+ return { tuple: [0, 0, 0], specified: 0 };
79
+ return { tuple: [nums[0] || 0, nums[1] || 0, nums[2] || 0], specified: nums.length };
80
+ }
81
+ // Expand a single comparator into primitive (op, tuple) constraints, or null if
82
+ // unparseable (→ fail closed).
83
+ function expandComparator(c) {
84
+ const trimmed = c.trim();
85
+ if (trimmed === '' || trimmed === '*' || trimmed === 'x' || trimmed === 'X')
86
+ return [{ op: '>=', t: [0, 0, 0] }];
87
+ const m = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(trimmed);
88
+ if (!m)
89
+ return null;
90
+ const op = m[1] || '';
91
+ const pv = parseVersionToken(m[2]);
92
+ if (!pv)
93
+ return null;
94
+ const { tuple, specified } = pv;
95
+ const [maj, min, pat] = tuple;
96
+ if (op === '^') {
97
+ let upper;
98
+ if (maj > 0)
99
+ upper = [maj + 1, 0, 0];
100
+ else if (min > 0)
101
+ upper = [0, min + 1, 0];
102
+ else
103
+ upper = [0, 0, pat + 1];
104
+ return [{ op: '>=', t: tuple }, { op: '<', t: upper }];
105
+ }
106
+ if (op === '~') {
107
+ const upper = specified >= 2 ? [maj, min + 1, 0] : [maj + 1, 0, 0];
108
+ return [{ op: '>=', t: tuple }, { op: '<', t: upper }];
109
+ }
110
+ if (op === '' || op === '=') {
111
+ if (specified === 0)
112
+ return [{ op: '>=', t: [0, 0, 0] }]; // "*" → any
113
+ if (specified === 3)
114
+ return [{ op: '=', t: tuple }];
115
+ const upper = specified === 1 ? [maj + 1, 0, 0] : [maj, min + 1, 0];
116
+ return [{ op: '>=', t: tuple }, { op: '<', t: upper }];
117
+ }
118
+ // >= <= > < with an explicit version
119
+ if (specified === 0)
120
+ return null; // e.g. ">=*" is meaningless → fail closed
121
+ return [{ op: op, t: tuple }];
122
+ }
123
+ function satisfiesPrimitive(v, prim) {
124
+ const cmp = compareTuples(v, prim.t);
125
+ switch (prim.op) {
126
+ case '>=': return cmp >= 0;
127
+ case '<=': return cmp <= 0;
128
+ case '>': return cmp > 0;
129
+ case '<': return cmp < 0;
130
+ case '=': return cmp === 0;
131
+ default: return false;
132
+ }
133
+ }
134
+ // One whitespace-separated comparator set (ANDed). Fail closed if any comparator
135
+ // is unparseable.
136
+ function satisfiesSet(v, set) {
137
+ const trimmed = set.trim();
138
+ if (trimmed === '')
139
+ return false;
140
+ const comparators = trimmed.split(/\s+/).filter(Boolean);
141
+ if (comparators.length === 0)
142
+ return false;
143
+ for (const c of comparators) {
144
+ const prims = expandComparator(c);
145
+ if (prims === null)
146
+ return false; // unparseable → fail closed
147
+ for (const prim of prims) {
148
+ if (!satisfiesPrimitive(v, prim))
149
+ return false;
150
+ }
151
+ }
152
+ return true;
153
+ }
154
+ /**
155
+ * Does `version` satisfy the semver `range`? OR-composed across `||`, AND-composed
156
+ * across whitespace. Fail-closed: an empty range, or any comparator this minimal
157
+ * implementation cannot parse, returns false. Comparison is on the numeric
158
+ * major.minor.patch core (prerelease tags are stripped, per `toNumericTuple`).
159
+ */
160
+ function semverSatisfies(version, range) {
161
+ const r = String(range == null ? '' : range).trim();
162
+ if (r === '')
163
+ return false;
164
+ const v = toNumericTuple(version);
165
+ const orSets = r.split('||').map((s) => s.trim()).filter((s) => s.length > 0);
166
+ if (orSets.length === 0)
167
+ return false;
168
+ return orSets.some((set) => satisfiesSet(v, set));
169
+ }
@@ -510,13 +510,49 @@ function normalizeContent(filePath, content, opts = {}) {
510
510
  }
511
511
  return { content: normalized, encoding };
512
512
  }
513
+ // Rename errnos that are transient on Windows: a concurrent reader (or an AV
514
+ // scanner / indexer) holding the target open makes renameSync fail briefly.
515
+ // Same idiom as capability-ledger.cts / capability-consent.cts.
516
+ const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
517
+ const RENAME_MAX_ATTEMPTS = 3;
518
+ const RENAME_RETRY_BACKOFF_MS = 50;
519
+ /** Synchronous best-effort backoff sleep (Atomics.wait — same idiom as io.cts). */
520
+ let _renameSleepBuf = null;
521
+ function renameBackoff() {
522
+ if (_renameSleepBuf === null)
523
+ _renameSleepBuf = new Int32Array(new SharedArrayBuffer(4));
524
+ Atomics.wait(_renameSleepBuf, 0, 0, RENAME_RETRY_BACKOFF_MS);
525
+ }
526
+ /**
527
+ * Atomic publish with bounded retry on transient Windows lock errnos.
528
+ * Returns null on success, or the final error if every attempt failed.
529
+ */
530
+ function atomicRenameWithRetry(tmpPath, filePath) {
531
+ let renameErr = null;
532
+ for (let attempt = 1; attempt <= RENAME_MAX_ATTEMPTS; attempt++) {
533
+ try {
534
+ node_fs_1.default.renameSync(tmpPath, filePath);
535
+ return null;
536
+ }
537
+ catch (err) {
538
+ renameErr = err;
539
+ if (attempt < RENAME_MAX_ATTEMPTS && RENAME_RETRY_ERRNOS.has(renameErr.code ?? '')) {
540
+ renameBackoff();
541
+ continue;
542
+ }
543
+ break;
544
+ }
545
+ }
546
+ return renameErr;
547
+ }
513
548
  function platformWriteSync(filePath, content, opts = {}) {
514
549
  const { content: normalized, encoding } = normalizeContent(filePath, content, opts);
515
550
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
516
551
  const tmpPath = filePath + '.tmp.' + process.pid;
552
+ // Step 1: write the sibling tmp file. If THIS fails, nothing was published, so a
553
+ // direct fallback write cannot truncate a concurrent reader of an existing file.
517
554
  try {
518
555
  node_fs_1.default.writeFileSync(tmpPath, normalized, encoding);
519
- node_fs_1.default.renameSync(tmpPath, filePath);
520
556
  }
521
557
  catch {
522
558
  try {
@@ -524,7 +560,25 @@ function platformWriteSync(filePath, content, opts = {}) {
524
560
  }
525
561
  catch { /* already gone */ }
526
562
  node_fs_1.default.writeFileSync(filePath, normalized, encoding);
563
+ return;
564
+ }
565
+ // Step 2: atomic publish, retrying transient Windows locks.
566
+ const renameErr = atomicRenameWithRetry(tmpPath, filePath);
567
+ if (renameErr === null)
568
+ return;
569
+ try {
570
+ node_fs_1.default.unlinkSync(tmpPath);
571
+ }
572
+ catch { /* already gone */ }
573
+ if (RENAME_RETRY_ERRNOS.has(renameErr.code ?? '')) {
574
+ // A live reader still holds the target open after every retry. A non-atomic
575
+ // direct write here would truncate that reader (the exact corruption this seam
576
+ // exists to prevent), so surface the error instead of falling back.
577
+ throw renameErr;
527
578
  }
579
+ // Atomic publish is genuinely impossible here (e.g. EXDEV cross-device move):
580
+ // fall back to a direct write to preserve write availability.
581
+ node_fs_1.default.writeFileSync(filePath, normalized, encoding);
528
582
  }
529
583
  function platformReadSync(filePath, opts = {}) {
530
584
  const encoding = opts.encoding ?? 'utf-8';
@@ -153,8 +153,10 @@ function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
153
153
  return false;
154
154
  const existing = existingProgress;
155
155
  const derived = derivedProgress;
156
- return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
157
- existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
156
+ // total_phases is intentionally excluded from the ratchet: it must always
157
+ // take the freshly derived value so it can correct downward (#1446).
158
+ // Only completed_phases, total_plans, and completed_plans keep ratchet behaviour.
159
+ return (existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
158
160
  existingProgressExceedsDerived(existing, derived, 'total_plans') ||
159
161
  existingProgressExceedsDerived(existing, derived, 'completed_plans'));
160
162
  }