@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +199 -365
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +695 -5
- package/gsd-core/bin/lib/adr-parser.cjs +45 -23
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +98 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +7 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +33 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +40 -15
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +4 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +12 -12
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +6 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/run-tests.cjs +14 -0
- 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
|
-
|
|
157
|
-
|
|
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
|
}
|