@opengsd/gsd-core 1.5.0-rc.3 → 1.5.0-rc.4
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-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +36 -57
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +167 -3
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-state.cjs +97 -3
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/config.cjs +80 -24
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +28 -6
- package/gsd-core/bin/lib/install-profiles.cjs +55 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/phase.cjs +28 -12
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +246 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -2
- package/gsd-core/bin/lib/state.cjs +240 -59
- package/gsd-core/bin/lib/verify.cjs +73 -4
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/execute-phase.md +4 -3
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +4 -0
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +2 -0
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/quick.md +3 -2
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +1 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +14 -1
- package/scripts/lint-allow-test-rule-refs.allowlist.json +2 -1
- package/scripts/lint-test-file-count.allowlist.json +6 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +107 -6
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
//
|
|
4
|
+
// gsd-ensure-canonical-path — SessionStart hook (#997)
|
|
5
|
+
//
|
|
6
|
+
// PROBLEM: GSD agents/commands/templates use markdown `@`-file-includes that
|
|
7
|
+
// hardcode the canonical path `@~/.claude/gsd-core/...` (references, workflows,
|
|
8
|
+
// templates, contexts, bin). Markdown @-includes expand `~` but do NOT expand
|
|
9
|
+
// environment variables, so `${CLAUDE_PLUGIN_ROOT}` cannot be used in them.
|
|
10
|
+
// In a classic `bin/install.js` install the canonical path is a real directory
|
|
11
|
+
// holding the bundled tree, so the includes resolve. In a Claude Code
|
|
12
|
+
// *marketplace plugin* install the plugin manager only unpacks the package
|
|
13
|
+
// into the version-pinned plugin cache and never runs `bin/install.js`, so
|
|
14
|
+
// `~/.claude/gsd-core/` is never created and every @-include resolves to
|
|
15
|
+
// nothing — every agent that depends on one fails (e.g. the executor).
|
|
16
|
+
//
|
|
17
|
+
// FIX: On SessionStart, when running under a plugin install (CLAUDE_PLUGIN_ROOT
|
|
18
|
+
// set and a bundled `gsd-core/` tree found beneath it), ensure
|
|
19
|
+
// `~/.claude/gsd-core/` exists and its immutable subdirs (bin, contexts,
|
|
20
|
+
// references, templates, workflows) are symlinked to the plugin's bundled tree.
|
|
21
|
+
// This changes ZERO @-references, is a no-op in classic installs (where each
|
|
22
|
+
// subdir is already a real directory), preserves user-generated files
|
|
23
|
+
// (USER-PROFILE.md, STATE.md, VERSION, …), prunes stale links so it self-heals
|
|
24
|
+
// after `claude plugin update` rotates the version dir, and uses Windows
|
|
25
|
+
// junctions for symlinks on win32.
|
|
26
|
+
//
|
|
27
|
+
// SECURITY: the resolved bundled-tree path and every per-subdir link target are
|
|
28
|
+
// kept strictly inside the resolved plugin root (realpath-normalised, prefix-
|
|
29
|
+
// checked). A real (non-symlink) file or directory already sitting at a managed
|
|
30
|
+
// link target is NEVER clobbered.
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
|
|
38
|
+
// Immutable, bundled subdirectories that the canonical path must expose. These
|
|
39
|
+
// are the directories `@~/.claude/gsd-core/<subdir>/...` includes point into.
|
|
40
|
+
// User-generated artifacts (USER-PROFILE.md, STATE.md, VERSION, config, …) are
|
|
41
|
+
// NOT in this list and are never created, moved, or deleted by this hook.
|
|
42
|
+
const MANAGED_SUBDIRS = ['bin', 'contexts', 'references', 'templates', 'workflows'];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the canonical runtime config dir for the active runtime.
|
|
46
|
+
*
|
|
47
|
+
* Honours CLAUDE_CONFIG_DIR for custom/multi-account setups (mirrors
|
|
48
|
+
* gsd-check-update.js detectConfigDir), else falls back to ~/.claude. The
|
|
49
|
+
* canonical GSD tree always lives at `<configDir>/gsd-core`.
|
|
50
|
+
*/
|
|
51
|
+
function resolveConfigDir(homeDir, env) {
|
|
52
|
+
const envDir = env.CLAUDE_CONFIG_DIR;
|
|
53
|
+
if (envDir && typeof envDir === 'string' && envDir.trim().length > 0) {
|
|
54
|
+
return envDir;
|
|
55
|
+
}
|
|
56
|
+
return path.join(homeDir, '.claude');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Locate the bundled `gsd-core/` tree beneath a plugin root.
|
|
61
|
+
*
|
|
62
|
+
* Claude Code unpacks the package so the bundled tree sits at
|
|
63
|
+
* `<pluginRoot>/gsd-core/`. Returns the absolute, realpath-normalised path to
|
|
64
|
+
* that directory, or null if it is absent / not a directory. Resolving with
|
|
65
|
+
* realpath collapses symlinks/.. so the subsequent containment check is sound.
|
|
66
|
+
*/
|
|
67
|
+
function resolveBundledTree(pluginRoot) {
|
|
68
|
+
if (!pluginRoot || typeof pluginRoot !== 'string' || pluginRoot.trim().length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let root;
|
|
72
|
+
try {
|
|
73
|
+
root = fs.realpathSync(pluginRoot);
|
|
74
|
+
} catch (_) {
|
|
75
|
+
return null; // plugin root does not exist
|
|
76
|
+
}
|
|
77
|
+
const bundled = path.join(root, 'gsd-core');
|
|
78
|
+
let bundledReal;
|
|
79
|
+
try {
|
|
80
|
+
// The bundled tree must be a real directory (or a symlink to one) that
|
|
81
|
+
// resolves to a path inside the plugin root. realpathSync throws ENOENT/
|
|
82
|
+
// ENOTDIR if <pluginRoot>/gsd-core is absent, so no separate existence
|
|
83
|
+
// check is needed. Reject anything that does not resolve to a directory.
|
|
84
|
+
bundledReal = fs.realpathSync(bundled);
|
|
85
|
+
if (!fs.statSync(bundledReal).isDirectory()) return null;
|
|
86
|
+
} catch (_) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// SECURITY: the resolved bundled tree must stay inside the resolved plugin
|
|
90
|
+
// root. A crafted symlink at <pluginRoot>/gsd-core pointing outside the root
|
|
91
|
+
// is rejected — we never link the canonical path at content we do not own.
|
|
92
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
93
|
+
if (bundledReal !== root && !bundledReal.startsWith(rootWithSep)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return bundledReal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The fs.symlinkSync `type` to use for a directory link on a given platform.
|
|
101
|
+
*
|
|
102
|
+
* On Windows, unprivileged users cannot create symlinks but CAN create
|
|
103
|
+
* junctions; 'junction' requires an absolute target (we always pass one). On
|
|
104
|
+
* POSIX a 'dir' symlink is used. Exported so the win32 branch is unit-testable
|
|
105
|
+
* without a Windows host.
|
|
106
|
+
*/
|
|
107
|
+
function dirLinkType(platform) {
|
|
108
|
+
return platform === 'win32' ? 'junction' : 'dir';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a directory symlink (junction on win32) from linkPath -> target.
|
|
113
|
+
* Throws on real failure so the caller records it.
|
|
114
|
+
*/
|
|
115
|
+
function createDirLink(target, linkPath, platform) {
|
|
116
|
+
fs.symlinkSync(target, linkPath, dirLinkType(platform));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Does `linkPath` already correctly point at `expectedTarget`?
|
|
121
|
+
* Used to make the hook idempotent — a correct link is left untouched.
|
|
122
|
+
*/
|
|
123
|
+
function linkPointsAt(linkPath, expectedTarget) {
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.lstatSync(linkPath).isSymbolicLink()) return false;
|
|
126
|
+
const resolved = fs.realpathSync(linkPath);
|
|
127
|
+
return resolved === fs.realpathSync(expectedTarget);
|
|
128
|
+
} catch (_) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensure the canonical `~/.claude/gsd-core/` path exposes the bundled subdirs.
|
|
135
|
+
*
|
|
136
|
+
* Pure, dependency-injected core so tests drive it with a fake home, fake
|
|
137
|
+
* plugin root, and explicit platform. Returns a structured result describing
|
|
138
|
+
* exactly what happened (never throws for ordinary conditions — only truly
|
|
139
|
+
* unexpected I/O errors propagate, and the thin CLI wrapper swallows those so
|
|
140
|
+
* a hook failure never blocks a session).
|
|
141
|
+
*
|
|
142
|
+
* @param {object} opts
|
|
143
|
+
* @param {string} [opts.homeDir] home directory (default os.homedir())
|
|
144
|
+
* @param {string} [opts.pluginRoot] CLAUDE_PLUGIN_ROOT (default from env)
|
|
145
|
+
* @param {string} [opts.platform] process.platform override (tests)
|
|
146
|
+
* @param {object} [opts.env] environment (default process.env)
|
|
147
|
+
* @returns {{status:string, canonicalDir?:string, bundledTree?:string,
|
|
148
|
+
* linked?:string[], prunedStale?:string[], preserved?:string[],
|
|
149
|
+
* skipped?:string[], reason?:string}}
|
|
150
|
+
*/
|
|
151
|
+
function ensureCanonicalPath(opts = {}) {
|
|
152
|
+
const env = opts.env || process.env;
|
|
153
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
154
|
+
const platform = opts.platform || process.platform;
|
|
155
|
+
const pluginRoot = opts.pluginRoot !== undefined ? opts.pluginRoot : env.CLAUDE_PLUGIN_ROOT;
|
|
156
|
+
|
|
157
|
+
// Uniform result contract: every return carries the four action arrays so
|
|
158
|
+
// callers can read result.linked/etc without first switching on status.
|
|
159
|
+
const empty = { linked: [], prunedStale: [], preserved: [], skipped: [] };
|
|
160
|
+
|
|
161
|
+
// No plugin context → classic/npm install or non-plugin runtime. No-op.
|
|
162
|
+
const bundledTree = resolveBundledTree(pluginRoot);
|
|
163
|
+
if (!bundledTree) {
|
|
164
|
+
return { status: 'noop', reason: 'no-plugin-bundle', ...empty };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const configDir = resolveConfigDir(homeDir, env);
|
|
168
|
+
const canonicalDir = path.join(configDir, 'gsd-core');
|
|
169
|
+
|
|
170
|
+
// Inspect the canonical path itself exactly once.
|
|
171
|
+
// - If it is a SYMLINK, the user (or another tool) deliberately pointed the
|
|
172
|
+
// canonical path elsewhere. We must NOT write managed links *through* that
|
|
173
|
+
// symlink into a directory we do not own — bail as a no-op.
|
|
174
|
+
// - If it is a REAL directory with at least one REAL (non-link) managed
|
|
175
|
+
// subdir, this is a classic `bin/install.js` install — leave it alone.
|
|
176
|
+
let canonicalStat = null;
|
|
177
|
+
try { canonicalStat = fs.lstatSync(canonicalDir); } catch (_) { canonicalStat = null; }
|
|
178
|
+
|
|
179
|
+
if (canonicalStat && canonicalStat.isSymbolicLink()) {
|
|
180
|
+
return { status: 'noop', reason: 'canonical-is-symlink', canonicalDir, bundledTree, ...empty };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (canonicalStat && canonicalStat.isDirectory()) {
|
|
184
|
+
for (const sub of MANAGED_SUBDIRS) {
|
|
185
|
+
try {
|
|
186
|
+
const subSt = fs.lstatSync(path.join(canonicalDir, sub));
|
|
187
|
+
if (subSt.isDirectory() && !subSt.isSymbolicLink()) {
|
|
188
|
+
return { status: 'noop', reason: 'classic-install', canonicalDir, bundledTree, ...empty };
|
|
189
|
+
}
|
|
190
|
+
} catch (_) { /* subdir absent — keep checking */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Ensure the canonical directory exists (as a real directory). We never
|
|
195
|
+
// replace an existing real directory; recursive mkdir is a no-op if present.
|
|
196
|
+
try {
|
|
197
|
+
fs.mkdirSync(canonicalDir, { recursive: true });
|
|
198
|
+
} catch (e) {
|
|
199
|
+
return { status: 'error', reason: `mkdir-canonical: ${e.code || e.message}`, canonicalDir, bundledTree, ...empty };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const linked = [];
|
|
203
|
+
const prunedStale = [];
|
|
204
|
+
const preserved = [];
|
|
205
|
+
const skipped = [];
|
|
206
|
+
|
|
207
|
+
// SECURITY: prefix used to confirm every per-subdir link target resolves
|
|
208
|
+
// strictly inside the bundled tree. Defence-in-depth against a tampered
|
|
209
|
+
// bundle that ships an internally-escaping symlink at <bundledTree>/<sub>.
|
|
210
|
+
const bundledWithSep = bundledTree.endsWith(path.sep) ? bundledTree : bundledTree + path.sep;
|
|
211
|
+
|
|
212
|
+
for (const sub of MANAGED_SUBDIRS) {
|
|
213
|
+
const target = path.join(bundledTree, sub);
|
|
214
|
+
// Only expose subdirs the bundle actually ships, AND only when the target
|
|
215
|
+
// resolves to a real directory that stays inside the bundled tree. A
|
|
216
|
+
// subdir whose realpath escapes the bundle (e.g. a planted symlink) is
|
|
217
|
+
// skipped — we never point the canonical path at content outside the
|
|
218
|
+
// validated plugin bundle.
|
|
219
|
+
let targetIsDir = false;
|
|
220
|
+
try {
|
|
221
|
+
const targetReal = fs.realpathSync(target);
|
|
222
|
+
// A NAMED subdir must resolve strictly BELOW the bundled tree root. We do
|
|
223
|
+
// NOT accept targetReal === bundledTree here: a subdir that self-links to
|
|
224
|
+
// the tree root would otherwise be exposed at the wrong level (e.g.
|
|
225
|
+
// `workflows` -> the whole tree), making `@.../workflows/foo` resolve to
|
|
226
|
+
// `<tree>/foo` instead of `<tree>/workflows/foo`.
|
|
227
|
+
targetIsDir = fs.statSync(targetReal).isDirectory()
|
|
228
|
+
&& targetReal.startsWith(bundledWithSep);
|
|
229
|
+
} catch (_) { targetIsDir = false; }
|
|
230
|
+
if (!targetIsDir) {
|
|
231
|
+
skipped.push(sub);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const linkPath = path.join(canonicalDir, sub);
|
|
236
|
+
|
|
237
|
+
// Already a correct link → idempotent no-op.
|
|
238
|
+
if (linkPointsAt(linkPath, target)) {
|
|
239
|
+
linked.push(sub);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let existing = null;
|
|
244
|
+
try { existing = fs.lstatSync(linkPath); } catch (_) { existing = null; }
|
|
245
|
+
|
|
246
|
+
if (existing) {
|
|
247
|
+
// lstat().isSymbolicLink() is true for BOTH POSIX symlinks and Windows
|
|
248
|
+
// junctions, so this single predicate identifies every GSD-managed link.
|
|
249
|
+
if (existing.isSymbolicLink()) {
|
|
250
|
+
// A GSD-managed link that is stale or points elsewhere (e.g. previous
|
|
251
|
+
// plugin version after `claude plugin update`). Prune and recreate.
|
|
252
|
+
try {
|
|
253
|
+
fs.unlinkSync(linkPath);
|
|
254
|
+
prunedStale.push(sub);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
skipped.push(sub);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// A REAL file or directory the user (or a classic install) owns. NEVER
|
|
261
|
+
// clobber it — preserve it untouched. This is the USER-PROFILE.md /
|
|
262
|
+
// partially-real-canonical-dir safety case.
|
|
263
|
+
preserved.push(sub);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
createDirLink(target, linkPath, platform);
|
|
270
|
+
linked.push(sub);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
skipped.push(sub);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
status: 'ensured',
|
|
278
|
+
canonicalDir,
|
|
279
|
+
bundledTree,
|
|
280
|
+
linked,
|
|
281
|
+
prunedStale,
|
|
282
|
+
preserved,
|
|
283
|
+
skipped,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
ensureCanonicalPath,
|
|
289
|
+
resolveBundledTree,
|
|
290
|
+
resolveConfigDir,
|
|
291
|
+
dirLinkType,
|
|
292
|
+
MANAGED_SUBDIRS,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// CLI entry: run on SessionStart. Never block the session — any unexpected
|
|
296
|
+
// failure is swallowed (best-effort self-heal). Emit nothing on stdout to keep
|
|
297
|
+
// the hook silent in normal operation.
|
|
298
|
+
if (require.main === module) {
|
|
299
|
+
try {
|
|
300
|
+
ensureCanonicalPath();
|
|
301
|
+
} catch (_) {
|
|
302
|
+
// Best-effort: a canonical-path failure must never abort a session.
|
|
303
|
+
}
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -312,7 +312,7 @@ function runStatusline() {
|
|
|
312
312
|
const totalCtx = data.context_window?.total_tokens || 1_000_000;
|
|
313
313
|
const acw = parseInt(process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW || '0', 10);
|
|
314
314
|
const AUTO_COMPACT_BUFFER_PCT = acw > 0
|
|
315
|
-
? Math.min(100, (acw / totalCtx) * 100)
|
|
315
|
+
? Math.min(100, Math.max(0, (1 - acw / totalCtx) * 100))
|
|
316
316
|
: 16.5;
|
|
317
317
|
let ctx = '';
|
|
318
318
|
if (remaining != null) {
|
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.5.0-rc.
|
|
3
|
+
"version": "1.5.0-rc.4",
|
|
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",
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
"test:security": "node scripts/run-tests.cjs --suite security",
|
|
115
115
|
"test:slow": "node scripts/run-tests.cjs --suite slow",
|
|
116
116
|
"test:affected": "node scripts/run-affected-tests.cjs",
|
|
117
|
-
"test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
|
|
117
|
+
"test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
|
|
118
118
|
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
|
|
119
|
-
"test:coverage:unit": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit",
|
|
119
|
+
"test:coverage:unit": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && c8 check-coverage --per-file --lines 0 --functions 0 --statements 0 --branches 70 --include 'gsd-core/bin/lib/state.cjs' --include 'gsd-core/bin/lib/phase.cjs' --include 'gsd-core/bin/lib/verify.cjs' --include 'gsd-core/bin/lib/init.cjs' --exclude 'tests/**'",
|
|
120
120
|
"test:coverage:all": "npm run test:coverage",
|
|
121
121
|
"test:mutation": "stryker run",
|
|
122
122
|
"test:mutation:since": "stryker run --incremental --since origin/next"
|
package/scripts/build-hooks.js
CHANGED
|
@@ -26,6 +26,13 @@ const STAGE_DIR = path.join(HOOKS_DIR, `.dist-staging-${process.pid}`);
|
|
|
26
26
|
const HOOKS_TO_COPY = [
|
|
27
27
|
'gsd-check-update-worker.js',
|
|
28
28
|
'gsd-check-update.js',
|
|
29
|
+
// SessionStart canonical-path bootstrap (#997). In a Claude Code marketplace
|
|
30
|
+
// plugin install, ~/.claude/gsd-core is never created, so every
|
|
31
|
+
// `@~/.claude/gsd-core/...` include in agents/commands/templates resolves to
|
|
32
|
+
// nothing. This hook symlinks the canonical path's immutable subdirs to the
|
|
33
|
+
// plugin's bundled gsd-core/ tree; no-op in classic installs. Must ship to
|
|
34
|
+
// dist so the installer copies it into the target hooks/ dir.
|
|
35
|
+
'gsd-ensure-canonical-path.js',
|
|
29
36
|
// Required by gsd-check-update-worker.js at runtime — must ship alongside it
|
|
30
37
|
// so require('./managed-hooks-registry.cjs') resolves in the installed hooks/ dir.
|
|
31
38
|
'managed-hooks-registry.cjs',
|
|
@@ -106,8 +106,14 @@ function parseArgs(argv) {
|
|
|
106
106
|
const r = requireValue(a, i);
|
|
107
107
|
if (!r.ok) return { ok: false, error: r.error };
|
|
108
108
|
if (a === '--type') opts.type = r.value;
|
|
109
|
-
else if (a === '--pr')
|
|
110
|
-
|
|
109
|
+
else if (a === '--pr') {
|
|
110
|
+
// Accept only decimal-integer strings (digits only, no sign, no dot,
|
|
111
|
+
// no hex prefix, no scientific notation). Non-integer input — including
|
|
112
|
+
// empty string and whitespace — is normalized to NaN so the prNaN
|
|
113
|
+
// guard below rejects it with the usage error.
|
|
114
|
+
const trimmed = r.value.trim();
|
|
115
|
+
opts.pr = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
|
|
116
|
+
} else if (a === '--body') opts.body = r.value;
|
|
111
117
|
else if (a === '--repo') opts.repo = r.value;
|
|
112
118
|
i++;
|
|
113
119
|
continue;
|
|
@@ -125,7 +131,15 @@ function main() {
|
|
|
125
131
|
throw new ExitError(2);
|
|
126
132
|
}
|
|
127
133
|
const { opts } = parsed;
|
|
128
|
-
|
|
134
|
+
// opts.pr starts as null (missing flag) and is set by parseArgs to a Number when
|
|
135
|
+
// the raw value is a pure decimal-integer string (digits only), or to NaN for any
|
|
136
|
+
// other input (empty, whitespace, floats, hex, negatives, scientific notation, etc.).
|
|
137
|
+
// Accept integer 0 (the documented pr:0 placeholder); reject a missing flag (null)
|
|
138
|
+
// and any non-decimal-integer value (NaN). The merge/lint gate separately
|
|
139
|
+
// enforces pr > 0 before a fragment can land, so 0 still cannot be merged.
|
|
140
|
+
const prMissing = opts.pr === null;
|
|
141
|
+
const prNaN = typeof opts.pr === 'number' && Number.isNaN(opts.pr);
|
|
142
|
+
if (!opts.type || prMissing || prNaN || !opts.body) {
|
|
129
143
|
throw new ExitError(2, 'usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..."');
|
|
130
144
|
}
|
|
131
145
|
const file = scaffoldFragment(opts);
|
|
@@ -92,9 +92,21 @@ function transformContentToHyphen(src, cmdNames) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function readCmdNames() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
try {
|
|
96
|
+
return fs.readdirSync(COMMANDS_DIR)
|
|
97
|
+
.filter(f => f.endsWith('.md'))
|
|
98
|
+
.map(f => f.replace(/\.md$/, ''));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Only swallow the missing-directory case. Any other error (EACCES, ENOTDIR,
|
|
101
|
+
// etc.) indicates a real misconfiguration and must propagate so callers are
|
|
102
|
+
// not silently handed an empty registry while the real problem goes undetected.
|
|
103
|
+
if (err.code !== 'ENOENT') throw err;
|
|
104
|
+
// COMMANDS_DIR may not exist on installs that use skill-based runtimes or
|
|
105
|
+
// global Claude installs (no local commands/gsd/ directory). Return [] so
|
|
106
|
+
// callers that handle an empty array gracefully (buildPattern returns null,
|
|
107
|
+
// transformContent is a no-op) are not broken by a missing directory.
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
function processFile(file, cmdNames) {
|
|
@@ -546,8 +546,10 @@ function validateFeatureBody(cap) {
|
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
// ADR-857 phase 5e: Closed ConverterName enum — complete set used across 16 runtime descriptors,
|
|
549
|
-
// all exported by bin/install.js
|
|
549
|
+
// all exported by bin/install.js (commands/skills) and src/runtime-artifact-conversion.cts (agents).
|
|
550
|
+
// Any ArtifactKind with a non-null converter must use one of these.
|
|
550
551
|
const VALID_CONVERTER_NAMES = new Set([
|
|
552
|
+
// commands / skills converters (pre-existing)
|
|
551
553
|
'convertClaudeCommandToAntigravitySkill',
|
|
552
554
|
'convertClaudeCommandToAugmentSkill',
|
|
553
555
|
'convertClaudeCommandToClineSkill',
|
|
@@ -563,6 +565,16 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
563
565
|
'convertClaudeCommandToOpencodeSkill',
|
|
564
566
|
'convertClaudeCommandToTraeSkill',
|
|
565
567
|
'convertClaudeCommandToWindsurfSkill',
|
|
568
|
+
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
|
569
|
+
'convertClaudeAgentToCopilotAgent',
|
|
570
|
+
'convertClaudeAgentToAntigravityAgent',
|
|
571
|
+
'convertClaudeAgentToCursorAgent',
|
|
572
|
+
'convertClaudeAgentToWindsurfAgent',
|
|
573
|
+
'convertClaudeAgentToAugmentAgent',
|
|
574
|
+
'convertClaudeAgentToTraeAgent',
|
|
575
|
+
'convertClaudeAgentToCodebuddyAgent',
|
|
576
|
+
'convertClaudeAgentToClineAgent',
|
|
577
|
+
'convertClaudeAgentToCodexAgent',
|
|
566
578
|
]);
|
|
567
579
|
|
|
568
580
|
// C3: Validate role:runtime body
|
|
@@ -2553,6 +2565,7 @@ module.exports = {
|
|
|
2553
2565
|
computeRequiresClosure,
|
|
2554
2566
|
topoSortSteps,
|
|
2555
2567
|
normalizeLineEndings,
|
|
2568
|
+
stripGeneratedComment,
|
|
2556
2569
|
validateConfigSliceEntry,
|
|
2557
2570
|
VALID_CONFIG_SLICE_TYPES,
|
|
2558
2571
|
LOOP_HOST_CONTRACT,
|
|
@@ -215,6 +215,8 @@
|
|
|
215
215
|
"tests/forensics.test.cjs :: source-text-is-the-product",
|
|
216
216
|
"tests/frontmatter-cli.test.cjs :: source-text-is-the-product",
|
|
217
217
|
"tests/gates-taxonomy.test.cjs :: source-text-is-the-product",
|
|
218
|
+
"tests/git-base-branch.test.cjs :: runtime-contract-is-the-product",
|
|
219
|
+
"tests/git-base-branch.test.cjs :: runtime-contract-is-the-product — the workflow .md content IS",
|
|
218
220
|
"tests/gsd-check-update-worker-platform-gate.test.cjs :: structural assertion on spawn-options shape; the behavior",
|
|
219
221
|
"tests/gsd-researcher-app-aware.test.cjs :: source-text-is-the-product",
|
|
220
222
|
"tests/gsd-researcher-flow-diagram.test.cjs :: source-text-is-the-product",
|
|
@@ -289,7 +291,6 @@
|
|
|
289
291
|
"tests/research-agent-profiles.test.cjs :: <runtime-contract-is-the-product> research agent .md content is the governed surface",
|
|
290
292
|
"tests/review-default-reviewers-workflow.test.cjs :: source-text-is-the-product",
|
|
291
293
|
"tests/roadmap.test.cjs :: source-text-is-the-product",
|
|
292
|
-
"tests/roadmapper-granularity.test.cjs :: source-text-is-the-product",
|
|
293
294
|
"tests/run-tests-harness.test.cjs :: run-tests.cjs is a CLI test harness whose only IR is its",
|
|
294
295
|
"tests/runtime-launcher-parity.test.cjs :: structural parity/drift guard — asserts literal presence/absence of the canonical gsd_run launcher and the retired $GSD_SDK / `/gsd-tools` tokens across workflow markdown; there is no typed IR for \"this source file does not contain substring X\".",
|
|
295
296
|
"tests/runtime-name-policy.test.cjs :: runtime-contract-is-the-product — FALLBACK_ALIASES source text IS the",
|