@opengsd/gsd-core 1.9.0 → 1.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +106 -33
- package/bin/install.js +50 -52
- package/gsd-core/bin/gsd-tools.cjs +33 -2
- package/gsd-core/bin/lib/capability-registry.cjs +63 -63
- package/gsd-core/bin/lib/milestone.cjs +31 -4
- package/gsd-core/bin/lib/phase.cjs +4 -1
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/verify.cjs +19 -3
- package/gsd-core/workflows/code-review.md +17 -2
- package/package.json +1 -1
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/validate-registry.cjs +10 -6
- package/vscode/package.json +1 -1
|
@@ -63,6 +63,33 @@ function findProjectRoot(startDir) {
|
|
|
63
63
|
}
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
|
+
// #2843: nearest ancestor (including `from` itself) that contains a `.git`,
|
|
67
|
+
// bounded by `upTo` (exclusive). Returns the git-repo root, or null if none
|
|
68
|
+
// exists before `upTo` / the filesystem root. Used to detect a NESTED child
|
|
69
|
+
// repo whose root is strictly below a candidate ancestor `.planning/` — in
|
|
70
|
+
// that case the caller's repo boundary sits between start and the ancestor,
|
|
71
|
+
// so trusting the ancestor's `.planning/` would silently cross into a
|
|
72
|
+
// different project. (No `git` subprocess — fs walk only, matching
|
|
73
|
+
// isInsideGitRepo's deliberate no-spawn contract.)
|
|
74
|
+
function nearestGitRoot(from, upTo) {
|
|
75
|
+
let d = from;
|
|
76
|
+
while (d !== fsRoot) {
|
|
77
|
+
if (d === upTo)
|
|
78
|
+
break;
|
|
79
|
+
try {
|
|
80
|
+
if (node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
|
|
81
|
+
return d;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
const next = node_path_1.default.dirname(d);
|
|
87
|
+
if (next === d)
|
|
88
|
+
break;
|
|
89
|
+
d = next;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
66
93
|
let dir = resolvedStart;
|
|
67
94
|
let depth = 0;
|
|
68
95
|
while (dir !== fsRoot && depth < FIND_PROJECT_ROOT_MAX_DEPTH) {
|
|
@@ -114,6 +141,18 @@ function findProjectRoot(startDir) {
|
|
|
114
141
|
// claims our startDir — explicit sub_repos config takes precedence over the
|
|
115
142
|
// implicit .git signal. (#1422)
|
|
116
143
|
if (isInsideGitRepo(parent)) {
|
|
144
|
+
// #2843: do NOT cross a git-repo boundary. If the caller is inside its
|
|
145
|
+
// OWN nested repo whose root is strictly below `parent`, trusting
|
|
146
|
+
// `parent`'s .planning/ would silently resolve to a DIFFERENT project.
|
|
147
|
+
// isInsideGitRepo only proved SOME .git exists between start and parent;
|
|
148
|
+
// verify that .git is parent's own (or absent between), not a nested
|
|
149
|
+
// child repo. If nearestGitRoot finds a .git strictly below parent,
|
|
150
|
+
// the boundary is crossed — fall through (do not return parent).
|
|
151
|
+
if (nearestGitRoot(resolvedStart, parent) !== null) {
|
|
152
|
+
dir = parent;
|
|
153
|
+
depth += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
117
156
|
// Lookahead: walk ancestors above `parent` to find a sub_repos claim.
|
|
118
157
|
let ancestor = node_path_1.default.dirname(parent);
|
|
119
158
|
let ancestorDepth = 0;
|
|
@@ -169,6 +208,15 @@ function findProjectRoot(startDir) {
|
|
|
169
208
|
try {
|
|
170
209
|
const candidatePlanning = parent2 + node_path_1.default.sep + '.planning';
|
|
171
210
|
if (node_fs_1.default.existsSync(candidatePlanning) && node_fs_1.default.statSync(candidatePlanning).isDirectory()) {
|
|
211
|
+
// #2843: do not cross a git-repo boundary. If the caller is inside its
|
|
212
|
+
// own nested repo (a .git strictly below parent2), parent2's .planning/
|
|
213
|
+
// belongs to a DIFFERENT project — keep walking is wrong; stop and fall
|
|
214
|
+
// through to the startDir fallback instead of silently resolving to the
|
|
215
|
+
// ancestor project. (Reached only when no .git exists anywhere in the
|
|
216
|
+
// chain per the triage, but guard defensively.)
|
|
217
|
+
if (nearestGitRoot(resolvedStart, parent2) !== null) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
172
220
|
return parent2;
|
|
173
221
|
}
|
|
174
222
|
}
|
|
@@ -161,6 +161,15 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
|
|
|
161
161
|
// is still not read. Recovering it needs a real frontmatter parse, which is
|
|
162
162
|
// deliberately left as a follow-up rather than smuggled in here.
|
|
163
163
|
const mentionedFiles = new Set();
|
|
164
|
+
// #2844: Pattern 1 matches any backticked path-like token. A SUMMARY body is
|
|
165
|
+
// predominantly about what the phase DID, so a backticked path in prose ("Built
|
|
166
|
+
// `src/kept.ts`", a `- \`src/x.ts\`` list item) is a legitimate claim (#2685
|
|
167
|
+
// pins this). The false-positive class #2844 fixes is a path mentioned as a
|
|
168
|
+
// FUTURE/CONDITIONAL deliverable — "next phase will add `shared/types.ts`",
|
|
169
|
+
// "planned", "would", "to be created" — which is NOT a claim about this phase.
|
|
170
|
+
// Exclude those lines rather than requiring an explicit claim verb (which would
|
|
171
|
+
// drop the legitimate "Built …" / list-item forms #2685 protects).
|
|
172
|
+
const isFutureMention = (line) => /\b(?:will(?:\s+(?:add|create|build|land))?(?:[^.])?|(?:next|later|future)\s+phase|planned?|would\s+(?:be|add|create|build)|to\s+be\s+(?:added|created|built)|eventually|not\s+yet)\b/i.test(line);
|
|
164
173
|
const patterns = [
|
|
165
174
|
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
166
175
|
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
|
|
@@ -169,9 +178,16 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
|
|
|
169
178
|
let m;
|
|
170
179
|
while ((m = pattern.exec(content)) !== null) {
|
|
171
180
|
const filePath = m[1];
|
|
172
|
-
if (filePath
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
if (!filePath || !isProbableProjectFile(filePath))
|
|
182
|
+
continue;
|
|
183
|
+
// #2844: skip a backticked path on a future/conditional line — it names a
|
|
184
|
+
// deliverable this phase did NOT produce, so probing it is a false positive.
|
|
185
|
+
const lineStart = content.lastIndexOf('\n', m.index) + 1;
|
|
186
|
+
const lineEnd = content.indexOf('\n', m.index);
|
|
187
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
188
|
+
if (isFutureMention(line))
|
|
189
|
+
continue;
|
|
190
|
+
mentionedFiles.add(filePath);
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
|
@@ -464,7 +464,22 @@ FALLOW_OK=$(FALLOW_TMP=\"${FALLOW_JSON_PATH}.tmp\" node -e \"
|
|
|
464
464
|
if [ \"$FALLOW_OK\" != \"1\" ]; then
|
|
465
465
|
FALLOW_STDERR_SUMMARY=$(head -5 \"$FALLOW_STDERR_TMP\")
|
|
466
466
|
rm -f \"${FALLOW_JSON_PATH}.tmp\" \"$FALLOW_STDERR_TMP\"
|
|
467
|
-
|
|
467
|
+
# #2667: distinguish a hard EXECUTION failure (the binary was found at step 1
|
|
468
|
+
# but would not run) from the binary-missing path (step 2). Exit 124 = timeout,
|
|
469
|
+
# 2 = usage error, 125 = spawn failure (e.g. Windows EINVAL on a .cmd shim —
|
|
470
|
+
# CVE-2024-27980, now mediated by run-with-timeout), 126/127 = not executable /
|
|
471
|
+
# not found. A non-zero exit here with a resolved binary means fallow is
|
|
472
|
+
# installed but did not produce a report — surface that loudly so a Windows
|
|
473
|
+
# user does not mistake it for "fallow absent".
|
|
474
|
+
case \"$FALLOW_EXIT\" in
|
|
475
|
+
124) FALLOW_FAIL_KIND=\"timed out\" ;;
|
|
476
|
+
2) FALLOW_FAIL_KIND=\"usage error\" ;;
|
|
477
|
+
125) FALLOW_FAIL_KIND=\"spawn failure (the binary was found but did not start — e.g. a Windows .cmd shim; run-with-timeout mediates this)\" ;;
|
|
478
|
+
126) FALLOW_FAIL_KIND=\"not executable\" ;;
|
|
479
|
+
127) FALLOW_FAIL_KIND=\"not found\" ;;
|
|
480
|
+
*) FALLOW_FAIL_KIND=\"crashed\" ;;
|
|
481
|
+
esac
|
|
482
|
+
echo \"WARNING: fallow structural pre-pass failed (${FALLOW_FAIL_KIND}, exit ${FALLOW_EXIT}): ${FALLOW_STDERR_SUMMARY}\"
|
|
468
483
|
FALLOW_JSON_PATH=\"\"
|
|
469
484
|
else
|
|
470
485
|
mv \"${FALLOW_JSON_PATH}.tmp\" \"$FALLOW_JSON_PATH\"
|
|
@@ -472,7 +487,7 @@ else
|
|
|
472
487
|
fi
|
|
473
488
|
```
|
|
474
489
|
|
|
475
|
-
On any failure of the structural pre-pass (binary missing, timeout, empty output, or unparseable JSON), the workflow continues with no `<structural_findings>` injection; the reviewer agent receives a normal review request.
|
|
490
|
+
On any failure of the structural pre-pass (binary missing at step 2, or an execution failure here — timeout, spawn failure, crash, empty output, or unparseable JSON), the workflow continues with no `<structural_findings>` injection; the reviewer agent receives a normal review request. The WARNING above names the failure KIND so a hard execution failure (e.g. a Windows `.cmd` spawn failure) is not mistaken for an absent optional dependency.
|
|
476
491
|
|
|
477
492
|
4) Optional MCP bridge path (runtime-dependent):
|
|
478
493
|
- If `FALLOW_MCP=true`, set reviewer input mode to MCP-backed structural findings.
|
package/package.json
CHANGED
package/scripts/gen-registry.cjs
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
|
|
6
|
-
*
|
|
7
|
-
* from
|
|
8
|
-
* Issue #2182.
|
|
5
|
+
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md,
|
|
6
|
+
* docs/registries/eos-registry.md, and docs/registries/reviewer-registry.md
|
|
7
|
+
* from their corresponding source JSON, via registry-schema.cjs#renderMarkdown.
|
|
8
|
+
* Issue #2182 (capability/eos); issue #2904 (reviewer).
|
|
9
|
+
*
|
|
10
|
+
* eos.json and reviewers.json are both OPTIONAL sources (`SOURCES[].optional`)
|
|
11
|
+
* — an absent one is skipped silently. capabilities.json is the primary
|
|
12
|
+
* source and is never optional.
|
|
9
13
|
*
|
|
10
14
|
* NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
|
|
11
15
|
* generates the RUNTIME capability manifest consumed by the host at runtime
|
|
@@ -34,7 +38,8 @@ const { renderMarkdown } = require('./registry-schema.cjs');
|
|
|
34
38
|
|
|
35
39
|
const SOURCES = [
|
|
36
40
|
{ type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
|
|
37
|
-
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
|
|
41
|
+
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md', optional: true },
|
|
42
|
+
{ type: 'reviewer', jsonFile: 'reviewers.json', mdFile: 'reviewer-registry.md', optional: true },
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -57,14 +62,16 @@ function getRegistriesDir() {
|
|
|
57
62
|
* Render the markdown for a single registry type from its committed source
|
|
58
63
|
* JSON.
|
|
59
64
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `capabilities.json` is
|
|
64
|
-
*
|
|
65
|
-
* (
|
|
65
|
+
* Optionality is a per-source data flag (`SOURCES[].optional`), not a
|
|
66
|
+
* hardcoded type literal: `eos.json` (pre-PR2) and `reviewers.json` (issue
|
|
67
|
+
* #2904) are both optional — an absent source JSON returns null and callers
|
|
68
|
+
* treat that as "nothing to do". `capabilities.json` is still the primary
|
|
69
|
+
* registry source and is never optional: a missing `capabilities.json` is
|
|
70
|
+
* ALWAYS an error (never a silent "up to date" pass), mirroring the same
|
|
71
|
+
* `optional` flag in `scripts/validate-registry.cjs`
|
|
72
|
+
* (`optional && !exists → continue`).
|
|
66
73
|
*
|
|
67
|
-
* @param {'capability'|'eos'} type
|
|
74
|
+
* @param {'capability'|'eos'|'reviewer'} type
|
|
68
75
|
* @returns {string|null}
|
|
69
76
|
*/
|
|
70
77
|
function renderFor(type) {
|
|
@@ -73,14 +80,31 @@ function renderFor(type) {
|
|
|
73
80
|
|
|
74
81
|
const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
|
|
75
82
|
if (!fs.existsSync(jsonPath)) {
|
|
76
|
-
if (
|
|
83
|
+
if (source.optional) return null;
|
|
77
84
|
throw new ExitError(
|
|
78
85
|
1,
|
|
79
86
|
`${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
|
|
80
87
|
);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
// A malformed source JSON must surface as an actionable CLI error, not an
|
|
91
|
+
// unhandled SyntaxError with a raw Node stack trace — mirrors
|
|
92
|
+
// scripts/validate-registry.cjs#validateFile's try/catch around JSON.parse.
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new ExitError(1, `${source.jsonFile} is not valid JSON at ${jsonPath}: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Mirrors validate-registry.cjs's explicit non-array rejection: a source
|
|
101
|
+
// JSON that parses to a non-array (object, string, etc.) would otherwise
|
|
102
|
+
// throw an opaque TypeError from `[...entries].sort()` in renderMarkdown,
|
|
103
|
+
// or silently mis-render for an iterable-but-wrong-shape value like a string.
|
|
104
|
+
if (!Array.isArray(entries)) {
|
|
105
|
+
throw new ExitError(1, `${source.jsonFile} must be a JSON array of entries`);
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
|
|
85
109
|
}
|
|
86
110
|
|
|
@@ -91,7 +115,7 @@ function main() {
|
|
|
91
115
|
|
|
92
116
|
for (const { type, mdFile } of SOURCES) {
|
|
93
117
|
const rendered = renderFor(type);
|
|
94
|
-
if (rendered === null) continue; // source JSON absent (eos.json before PR2)
|
|
118
|
+
if (rendered === null) continue; // source JSON absent and optional (eos.json before PR2 / reviewers.json)
|
|
95
119
|
|
|
96
120
|
const mdPath = path.join(registriesDir, mdFile);
|
|
97
121
|
|