@opengsd/gsd-core 1.4.0-rc.1 → 1.4.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 +23 -0
- package/GEMINI.md +53 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.md +1 -1
- package/agents/gsd-framework-selector.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-pattern-mapper.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- 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-user-profiler.md +1 -1
- package/bin/install.js +1911 -354
- package/commands/gsd/autonomous.md +2 -0
- package/commands/gsd/execute-phase.md +2 -0
- package/commands/gsd/plan-phase.md +2 -0
- package/commands/gsd/progress.md +1 -0
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/update.md +3 -2
- package/gemini-extension.json +6 -0
- package/gsd-core/bin/check-latest-version.cjs +58 -4
- package/gsd-core/bin/lib/install-profiles.cjs +58 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
- package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
- package/gsd-core/bin/lib/shell-command-projection.cjs +6 -0
- package/gsd-core/bin/lib/surface.cjs +54 -11
- package/gsd-core/workflows/help/modes/full.md +2 -1
- package/gsd-core/workflows/review.md +2 -2
- package/gsd-core/workflows/update.md +32 -5
- package/hooks/dist/gsd-config-reload.js +133 -0
- package/hooks/dist/gsd-cursor-post-tool.js +75 -0
- package/hooks/dist/gsd-cursor-session-start.js +52 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-config-reload.js +133 -0
- package/hooks/gsd-cursor-post-tool.js +75 -0
- package/hooks/gsd-cursor-session-start.js +52 -0
- package/hooks/hooks.json +69 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +5 -1
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/cli.cjs +53 -10
- package/scripts/ci-test-scope.cjs +120 -18
- package/scripts/issue-dedupe.cjs +278 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-notes/discord-release-summary.cjs +373 -0
- package/scripts/research-profiles.cjs +3 -3
- package/scripts/sync-manifest-versions.cjs +119 -0
|
@@ -26,6 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
};
|
|
27
27
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
28
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
29
30
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
32
|
const installProfiles = require("./install-profiles.cjs");
|
|
@@ -33,7 +34,7 @@ const { readActiveProfile, resolveProfile, loadSkillsManifest, } = installProfil
|
|
|
33
34
|
const clusters_cjs_1 = require("./clusters.cjs");
|
|
34
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
36
|
const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
|
|
36
|
-
const { findInstallSourceRoot } = runtimeArtifactLayout;
|
|
37
|
+
const { findInstallSourceRoot, getInstallExports } = runtimeArtifactLayout;
|
|
37
38
|
const SURFACE_FILE_NAME = '.gsd-surface.json';
|
|
38
39
|
/**
|
|
39
40
|
* Read the surface state from a runtime config directory.
|
|
@@ -210,8 +211,29 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap) {
|
|
|
210
211
|
}
|
|
211
212
|
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
|
212
213
|
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap);
|
|
214
|
+
// Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
|
|
215
|
+
// so SKILL.md bodies reference the install target (pathPrefix), not the
|
|
216
|
+
// converter's default ~/.claude paths (#813). Computed lazily so command-only
|
|
217
|
+
// runtimes do not trigger the install.js require.
|
|
218
|
+
let pathPrefix = null;
|
|
213
219
|
for (const kind of layout.kinds) {
|
|
214
220
|
const staged = kind.stage(resolved);
|
|
221
|
+
if (kind.kind === 'skills') {
|
|
222
|
+
const installExports = getInstallExports();
|
|
223
|
+
if (pathPrefix === null) {
|
|
224
|
+
const scope = layout.scope ?? 'global';
|
|
225
|
+
const resolvedTarget = node_path_1.default.resolve(layout.configDir).replace(/\\/g, '/');
|
|
226
|
+
const homeDir = node_os_1.default.homedir().replace(/\\/g, '/');
|
|
227
|
+
pathPrefix = installExports.computePathPrefix({
|
|
228
|
+
isGlobal: scope === 'global',
|
|
229
|
+
isOpencode: layout.runtime === 'opencode',
|
|
230
|
+
isWindowsHost: process.platform === 'win32',
|
|
231
|
+
resolvedTarget,
|
|
232
|
+
homeDir,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
installExports.applyRuntimeContentRewritesInPlace(staged, layout.runtime, pathPrefix);
|
|
236
|
+
}
|
|
215
237
|
const dest = node_path_1.default.join(layout.configDir, kind.destSubpath);
|
|
216
238
|
_syncGsdDir(staged, dest, kind, skillManifest);
|
|
217
239
|
}
|
|
@@ -335,20 +357,41 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
|
|
|
335
357
|
pruneSkillDirs(destDir, stagedDirs, kindPrefix, manifest);
|
|
336
358
|
}
|
|
337
359
|
else {
|
|
338
|
-
// commands / agents kind:
|
|
339
|
-
|
|
340
|
-
//
|
|
360
|
+
// commands / agents kind: mirror installRuntimeArtifacts (_copyStaged /
|
|
361
|
+
// _removeGsdEntries in bin/install.js) so surface produces the SAME files as a
|
|
362
|
+
// fresh install (#816). Flat command dirs (opencode/cursor/augment/kilo) take
|
|
363
|
+
// the gsd- prefix on copy; namespaced command dirs (commands/gsd) and agents
|
|
364
|
+
// keep their staged names. Copying staged names verbatim previously diverged
|
|
365
|
+
// from install and orphaned the installed gsd-*.md files, and the unscoped
|
|
366
|
+
// prune deleted user-owned command files.
|
|
367
|
+
//
|
|
368
|
+
// NOTE: the destName rule below intentionally mirrors bin/install.js
|
|
369
|
+
// `_copyStaged` (the `namespacedByDir` decision). Keep them in sync.
|
|
370
|
+
const destLast = (typeof kind === 'object' && kind !== null && kind.destSubpath)
|
|
371
|
+
? node_path_1.default.basename(kind.destSubpath)
|
|
372
|
+
: '';
|
|
373
|
+
const prefixStem = kindPrefix ? kindPrefix.replace(/-$/, '') : '';
|
|
374
|
+
const namespacedByDir = kindName === 'commands' && destLast === prefixStem;
|
|
375
|
+
const stagedFiles = node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
|
376
|
+
const stagedDestNames = new Set();
|
|
341
377
|
for (const file of stagedFiles) {
|
|
342
|
-
|
|
378
|
+
const destName = (kindName === 'agents' || namespacedByDir)
|
|
379
|
+
? file
|
|
380
|
+
: `${kindPrefix}${file.slice(0, -3)}.md`;
|
|
381
|
+
node_fs_1.default.copyFileSync(node_path_1.default.join(stagedDir, file), node_path_1.default.join(destDir, destName));
|
|
382
|
+
stagedDestNames.add(destName);
|
|
343
383
|
}
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
384
|
+
// Prune stale GSD-owned files not in the staged set, preserving user-owned files
|
|
385
|
+
// (mirrors install's prefix-scoped _removeGsdEntries):
|
|
386
|
+
// - agents: only gsd-* are GSD-owned
|
|
387
|
+
// - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
|
|
388
|
+
// - namespaced command dirs: the whole dir is GSD-owned
|
|
389
|
+
for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
|
349
390
|
if (kindName === 'agents' && !file.startsWith('gsd-'))
|
|
350
391
|
continue;
|
|
351
|
-
if (!
|
|
392
|
+
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix))
|
|
393
|
+
continue;
|
|
394
|
+
if (!stagedDestNames.has(file)) {
|
|
352
395
|
try {
|
|
353
396
|
node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
|
|
354
397
|
}
|
|
@@ -550,11 +550,12 @@ Usage: `/gsd:help --full`
|
|
|
550
550
|
Usage: `/gsd:help debug`
|
|
551
551
|
Usage: `/gsd:help --brief debug`
|
|
552
552
|
|
|
553
|
-
**`/gsd:update [--sync] [--reapply]`**
|
|
553
|
+
**`/gsd:update [--sync] [--reapply] [--next | --rc]`**
|
|
554
554
|
Update GSD to latest version with changelog preview.
|
|
555
555
|
|
|
556
556
|
- `--sync` — sync managed GSD skills across runtime roots (replaces the former `gsd-sync-skills`)
|
|
557
557
|
- `--reapply` — reapply local modifications after an update (replaces the former `gsd-reapply-patches`)
|
|
558
|
+
- `--next` (alias `--rc`) — install/refresh from the `@next` RC dist-tag instead of `@latest` (ADR #660); omit for the stable channel
|
|
558
559
|
|
|
559
560
|
- Shows installed vs latest version comparison
|
|
560
561
|
- Displays changelog entries for versions you've missed
|
|
@@ -248,9 +248,9 @@ fi
|
|
|
248
248
|
**Codex:**
|
|
249
249
|
```bash
|
|
250
250
|
if [ -n "$CODEX_MODEL" ] && [ "$CODEX_MODEL" != "null" ]; then
|
|
251
|
-
cat /tmp/gsd-review-prompt-{phase}.md | codex exec --model "$CODEX_MODEL" --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
|
|
251
|
+
cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --model "$CODEX_MODEL" --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
|
|
252
252
|
else
|
|
253
|
-
cat /tmp/gsd-review-prompt-{phase}.md | codex exec --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
|
|
253
|
+
cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
|
|
254
254
|
fi
|
|
255
255
|
```
|
|
256
256
|
|
|
@@ -75,6 +75,25 @@ If multiple runtime installs are detected and the invoking runtime cannot be det
|
|
|
75
75
|
**If VERSION file missing (version resolves to `0.0.0`):** report the installed version as Unknown and proceed to install (treated as `0.0.0` for comparison).
|
|
76
76
|
</step>
|
|
77
77
|
|
|
78
|
+
<step name="parse_update_channel">
|
|
79
|
+
Determine the release channel from `$ARGUMENTS`. This selects which npm dist-tag the entire update flow targets — `latest` (stable) by default, or `next` (the RC channel established by ADR #660) when the user opts in with `--next`/`--rc`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
case " $ARGUMENTS " in
|
|
83
|
+
*" --next "*|*" --rc "*)
|
|
84
|
+
TAG="next"
|
|
85
|
+
CHANNEL_LABEL="next (RC)"
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
TAG="latest"
|
|
89
|
+
CHANNEL_LABEL="latest (stable)"
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`TAG` is restricted to `latest`/`next` by `check-latest-version.cjs` (it rejects any other value with exit 2), so no arbitrary dist-tag can leak through. Omitting `--next`/`--rc` reproduces the prior behavior exactly: `TAG=latest`.
|
|
95
|
+
</step>
|
|
96
|
+
|
|
78
97
|
<step name="check_latest_version">
|
|
79
98
|
Check npm for latest version via the deterministic script. **Do NOT run `npm view` or `npm search` directly** — the package name must come from the script, not from a free choice at execution time. (#2992: LLM-driven prescriptions of npm package names produced wrong-package queries; moving the package name into a script constant closes that gap.)
|
|
80
99
|
|
|
@@ -91,7 +110,7 @@ if [ -z "$GSD_DIR" ]; then
|
|
|
91
110
|
LATEST_VERSION=""
|
|
92
111
|
LATEST_REASON="no_install_detected"
|
|
93
112
|
else
|
|
94
|
-
LATEST_RESULT="$(node "$GSD_DIR/gsd-core/bin/check-latest-version.cjs" --json 2>/dev/null)"
|
|
113
|
+
LATEST_RESULT="$(node "$GSD_DIR/gsd-core/bin/check-latest-version.cjs" --json --tag "$TAG" 2>/dev/null)"
|
|
95
114
|
LATEST_STATUS=$?
|
|
96
115
|
# #2993 CR: when node is missing or the script doesn't exist, LATEST_RESULT
|
|
97
116
|
# is empty and piping it to `jq` produces a parse error on stderr while
|
|
@@ -114,7 +133,7 @@ fi
|
|
|
114
133
|
```text
|
|
115
134
|
Couldn't check for updates (reason: {LATEST_REASON}, exit: {LATEST_STATUS}).
|
|
116
135
|
|
|
117
|
-
To update manually: `npx -y --package=@opengsd/gsd-core@
|
|
136
|
+
To update manually: `npx -y --package=@opengsd/gsd-core@{TAG} -- gsd-core --global`
|
|
118
137
|
```
|
|
119
138
|
|
|
120
139
|
Exit.
|
|
@@ -123,6 +142,14 @@ Exit.
|
|
|
123
142
|
<step name="compare_versions">
|
|
124
143
|
Compare installed vs latest:
|
|
125
144
|
|
|
145
|
+
**Only when `TAG=next`** (the user passed `--next`/`--rc`), prepend a channel banner so they know they are leaving the stable line — add this line immediately after the `**Latest:**` line in whichever output block renders:
|
|
146
|
+
|
|
147
|
+
**Channel:** {CHANNEL_LABEL}
|
|
148
|
+
|
|
149
|
+
On the default stable channel (`TAG=latest`), do NOT add a channel line — the output must match the prior stable behavior exactly.
|
|
150
|
+
|
|
151
|
+
When `TAG=next`, the "latest" value is the release candidate published under `@next` (e.g. `1.4.0-rc.1`). Apply standard semver precedence for prereleases (`1.4.0-rc.1` is newer than `1.3.1` but older than the final `1.4.0`). Do NOT treat an `-rc.N` suffix as a dev install or as "behind" — offer it as an available update.
|
|
152
|
+
|
|
126
153
|
**If installed == latest:**
|
|
127
154
|
```
|
|
128
155
|
## GSD Update
|
|
@@ -327,17 +354,17 @@ RUNTIME_FLAG="--$TARGET_RUNTIME"
|
|
|
327
354
|
|
|
328
355
|
**If LOCAL install:**
|
|
329
356
|
```bash
|
|
330
|
-
npx -y --package=@opengsd/gsd-core@
|
|
357
|
+
npx -y --package=@opengsd/gsd-core@"$TAG" -- gsd-core "$RUNTIME_FLAG" --local
|
|
331
358
|
```
|
|
332
359
|
|
|
333
360
|
**If GLOBAL install:**
|
|
334
361
|
```bash
|
|
335
|
-
npx -y --package=@opengsd/gsd-core@
|
|
362
|
+
npx -y --package=@opengsd/gsd-core@"$TAG" -- gsd-core "$RUNTIME_FLAG" --global
|
|
336
363
|
```
|
|
337
364
|
|
|
338
365
|
**If UNKNOWN install:**
|
|
339
366
|
```bash
|
|
340
|
-
npx -y --package=@opengsd/gsd-core@
|
|
367
|
+
npx -y --package=@opengsd/gsd-core@"$TAG" -- gsd-core --claude --global
|
|
341
368
|
```
|
|
342
369
|
|
|
343
370
|
Capture output. If install fails, show error and exit.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// gsd-config-reload.js — FileChanged hook: hot-reload GSD config context
|
|
4
|
+
// Fires when .planning/config.json is modified, created, or deleted.
|
|
5
|
+
//
|
|
6
|
+
// When the user edits .planning/config.json mid-session, this hook reads the
|
|
7
|
+
// updated config and injects a summary as additionalContext so the agent knows
|
|
8
|
+
// the new configuration without requiring a session restart.
|
|
9
|
+
//
|
|
10
|
+
// Input (from Claude Code):
|
|
11
|
+
// { session_id, cwd, hook_event_name: "FileChanged",
|
|
12
|
+
// file_path: "/abs/path/.planning/config.json", event: "change"|"add"|"unlink" }
|
|
13
|
+
//
|
|
14
|
+
// Output:
|
|
15
|
+
// { hookSpecificOutput: { hookEventName: "FileChanged", additionalContext: "..." } }
|
|
16
|
+
// or exits 0 silently (if config absent, unreadable, or event is "unlink").
|
|
17
|
+
//
|
|
18
|
+
// Enabled for all Claude Code installs. This hook is always-on — it is a
|
|
19
|
+
// no-op when .planning/config.json is absent (ENOENT → exit 0).
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
// Timeout guard: if stdin does not close within 8s exit silently rather than
|
|
26
|
+
// hanging until Claude Code kills the process and reports "hook error".
|
|
27
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 8000);
|
|
28
|
+
process.stdin.setEncoding('utf8');
|
|
29
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
30
|
+
process.stdin.on('end', () => {
|
|
31
|
+
clearTimeout(stdinTimeout);
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(input);
|
|
34
|
+
const event = data.event; // "change" | "add" | "unlink"
|
|
35
|
+
const filePath = data.file_path || '';
|
|
36
|
+
const cwd = data.cwd || process.cwd();
|
|
37
|
+
|
|
38
|
+
// Only handle the GSD planning config — verify both basename and that the
|
|
39
|
+
// resolved path is .planning/config.json relative to cwd. The hook
|
|
40
|
+
// matcher ('config.json') fires on any watched config.json; this guard
|
|
41
|
+
// ensures an unrelated config.json in node_modules/ or elsewhere does not
|
|
42
|
+
// inject spurious additionalContext.
|
|
43
|
+
const basename = path.basename(filePath);
|
|
44
|
+
if (basename !== 'config.json') {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
const expectedPath = path.resolve(cwd, '.planning', 'config.json');
|
|
48
|
+
if (path.resolve(filePath) !== expectedPath) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// On unlink (deletion) emit a brief notice and exit
|
|
53
|
+
if (event === 'unlink') {
|
|
54
|
+
process.stdout.write(JSON.stringify({
|
|
55
|
+
hookSpecificOutput: {
|
|
56
|
+
hookEventName: 'FileChanged',
|
|
57
|
+
additionalContext:
|
|
58
|
+
'GSD config (.planning/config.json) was deleted. ' +
|
|
59
|
+
'Falling back to built-in defaults for this session.',
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Read the updated config file
|
|
66
|
+
let config;
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
69
|
+
config = JSON.parse(raw);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e && e.code === 'ENOENT') process.exit(0);
|
|
72
|
+
// Malformed JSON — inform the agent without crashing
|
|
73
|
+
process.stdout.write(JSON.stringify({
|
|
74
|
+
hookSpecificOutput: {
|
|
75
|
+
hookEventName: 'FileChanged',
|
|
76
|
+
additionalContext:
|
|
77
|
+
'GSD config (.planning/config.json) was modified but could not be parsed. ' +
|
|
78
|
+
'Check the file for JSON syntax errors.',
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build a concise summary of key config fields the agent cares about
|
|
85
|
+
const lines = ['GSD config reloaded (.planning/config.json updated):'];
|
|
86
|
+
|
|
87
|
+
if (config.runtime) lines.push(` runtime: ${config.runtime}`);
|
|
88
|
+
if (config.mode) lines.push(` mode: ${config.mode}`);
|
|
89
|
+
|
|
90
|
+
// hooks section (opt-in toggles agents act on)
|
|
91
|
+
if (config.hooks && typeof config.hooks === 'object') {
|
|
92
|
+
const hookKeys = Object.entries(config.hooks)
|
|
93
|
+
.filter(([, v]) => v !== undefined)
|
|
94
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
95
|
+
.join(', ');
|
|
96
|
+
if (hookKeys) lines.push(` hooks: { ${hookKeys} }`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// workflow section (key toggles)
|
|
100
|
+
if (config.workflow && typeof config.workflow === 'object') {
|
|
101
|
+
const wfKeys = Object.entries(config.workflow)
|
|
102
|
+
.filter(([, v]) => v !== undefined)
|
|
103
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
104
|
+
.join(', ');
|
|
105
|
+
if (wfKeys) lines.push(` workflow: { ${wfKeys} }`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// model overrides (agents use these)
|
|
109
|
+
if (config.models && typeof config.models === 'object') {
|
|
110
|
+
const modelKeys = Object.entries(config.models)
|
|
111
|
+
.filter(([, v]) => v !== undefined)
|
|
112
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
113
|
+
.join(', ');
|
|
114
|
+
if (modelKeys) lines.push(` models: { ${modelKeys} }`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (lines.length === 1) {
|
|
118
|
+
// No notable fields — still confirm the reload happened
|
|
119
|
+
lines.push(' (no notable keys changed)');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const additionalContext = lines.join('\n');
|
|
123
|
+
process.stdout.write(JSON.stringify({
|
|
124
|
+
hookSpecificOutput: {
|
|
125
|
+
hookEventName: 'FileChanged',
|
|
126
|
+
additionalContext,
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Silent fail — never block the session on a config reload error
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// gsd-cursor-post-tool.js — Cursor postToolUse hook (issue #777)
|
|
4
|
+
//
|
|
5
|
+
// Cursor invokes this script after each tool call completes.
|
|
6
|
+
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
|
|
7
|
+
//
|
|
8
|
+
// Input schema (cursor postToolUse):
|
|
9
|
+
// { tool_name, tool_input, tool_output, duration,
|
|
10
|
+
// conversation_id, generation_id, model, hook_event_name,
|
|
11
|
+
// cursor_version, workspace_roots, user_email, transcript_path }
|
|
12
|
+
//
|
|
13
|
+
// Output schema (cursor postToolUse):
|
|
14
|
+
// { additional_context?: string } ← injected as context after the tool use
|
|
15
|
+
//
|
|
16
|
+
// Behaviour:
|
|
17
|
+
// - After a write-class tool that targets .planning/, reminds the agent
|
|
18
|
+
// to keep STATE.md current.
|
|
19
|
+
// - Fails open: any error silently exits 0.
|
|
20
|
+
//
|
|
21
|
+
// Cursor docs: https://cursor.com/docs/hooks
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const WRITE_TOOL_RE = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
|
|
26
|
+
const PATH_KEY_RE = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
|
|
27
|
+
const PLANNING_PATH_RE = /(^|[\\/])\.planning([\\/]|$)/;
|
|
28
|
+
|
|
29
|
+
let raw = '';
|
|
30
|
+
const stdinTimeout = setTimeout(() => {
|
|
31
|
+
// Timeout guard: exit silently rather than hanging.
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}, 10000);
|
|
34
|
+
|
|
35
|
+
process.stdin.setEncoding('utf8');
|
|
36
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
37
|
+
process.stdin.on('end', () => {
|
|
38
|
+
clearTimeout(stdinTimeout);
|
|
39
|
+
try {
|
|
40
|
+
let input;
|
|
41
|
+
try { input = JSON.parse(raw || '{}'); } catch { process.stdout.write(JSON.stringify({})); return; }
|
|
42
|
+
|
|
43
|
+
const toolName = String(
|
|
44
|
+
input.tool_name || input.toolName || ''
|
|
45
|
+
).toLowerCase();
|
|
46
|
+
|
|
47
|
+
const isWrite = WRITE_TOOL_RE.test(toolName);
|
|
48
|
+
if (!isWrite) { process.stdout.write(JSON.stringify({})); return; }
|
|
49
|
+
|
|
50
|
+
// Collect only PATH-bearing field values (not free-form content).
|
|
51
|
+
const paths = [];
|
|
52
|
+
const walk = (v, depth) => {
|
|
53
|
+
if (depth > 5 || paths.length > 64) return;
|
|
54
|
+
if (Array.isArray(v)) { for (const x of v) walk(x, depth + 1); return; }
|
|
55
|
+
if (v && typeof v === 'object') {
|
|
56
|
+
for (const k of Object.keys(v)) {
|
|
57
|
+
const val = v[k];
|
|
58
|
+
if (typeof val === 'string' && PATH_KEY_RE.test(k)) paths.push(val);
|
|
59
|
+
else walk(val, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(input.tool_input || input.toolInput || {}, 0);
|
|
64
|
+
|
|
65
|
+
if (paths.some((p) => PLANNING_PATH_RE.test(p))) {
|
|
66
|
+
process.stdout.write(JSON.stringify({
|
|
67
|
+
additional_context:
|
|
68
|
+
'GSD: .planning/ artifact updated — ensure STATE.md reflects the latest phase and progress.',
|
|
69
|
+
}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
} catch { /* fall through to empty response */ }
|
|
73
|
+
|
|
74
|
+
process.stdout.write(JSON.stringify({}));
|
|
75
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// gsd-cursor-session-start.js — Cursor sessionStart hook (issue #777)
|
|
4
|
+
//
|
|
5
|
+
// Cursor invokes this script at the start of each agent session.
|
|
6
|
+
// Protocol: JSON from Cursor on stdin; JSON response on stdout.
|
|
7
|
+
//
|
|
8
|
+
// Input schema (cursor sessionStart):
|
|
9
|
+
// { session_id, is_background_agent, composer_mode, conversation_id,
|
|
10
|
+
// generation_id, model, hook_event_name, cursor_version,
|
|
11
|
+
// workspace_roots, user_email, transcript_path }
|
|
12
|
+
//
|
|
13
|
+
// Output schema (cursor sessionStart):
|
|
14
|
+
// { additional_context?: string } ← injected into the session as context
|
|
15
|
+
//
|
|
16
|
+
// Behaviour:
|
|
17
|
+
// - If .planning/STATE.md is present, injects a brief state reminder.
|
|
18
|
+
// - If absent, nudges the user toward /gsd:new-project.
|
|
19
|
+
// - Fails open: any error silently exits 0 so a hook bug never wedges Cursor.
|
|
20
|
+
//
|
|
21
|
+
// Cursor docs: https://cursor.com/docs/hooks
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const MSG_PRESENT =
|
|
29
|
+
'GSD: .planning/STATE.md is present — review the current phase and any blockers before acting.';
|
|
30
|
+
const MSG_ABSENT =
|
|
31
|
+
'GSD: no .planning/ workflow found — run /gsd:new-project to start a tracked workflow.';
|
|
32
|
+
|
|
33
|
+
let raw = '';
|
|
34
|
+
const stdinTimeout = setTimeout(() => {
|
|
35
|
+
// Timeout guard: exit silently rather than hanging.
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}, 10000);
|
|
38
|
+
|
|
39
|
+
process.stdin.setEncoding('utf8');
|
|
40
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
41
|
+
process.stdin.on('end', () => {
|
|
42
|
+
clearTimeout(stdinTimeout);
|
|
43
|
+
try {
|
|
44
|
+
const statePath = path.join(process.cwd(), '.planning', 'STATE.md');
|
|
45
|
+
const statePresent = fs.existsSync(statePath);
|
|
46
|
+
const msg = statePresent ? MSG_PRESENT : MSG_ABSENT;
|
|
47
|
+
process.stdout.write(JSON.stringify({ additional_context: msg }));
|
|
48
|
+
} catch {
|
|
49
|
+
// Fail open — never block a Cursor session because of a GSD hook error.
|
|
50
|
+
process.stdout.write(JSON.stringify({}));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
const MANAGED_HOOKS = [
|
|
19
19
|
'gsd-check-update-worker.js',
|
|
20
20
|
'gsd-check-update.js',
|
|
21
|
+
'gsd-config-reload.js',
|
|
21
22
|
'gsd-context-monitor.js',
|
|
23
|
+
'gsd-cursor-post-tool.js',
|
|
24
|
+
'gsd-cursor-session-start.js',
|
|
22
25
|
'gsd-graphify-update.sh',
|
|
23
26
|
'gsd-phase-boundary.sh',
|
|
24
27
|
'gsd-prompt-guard.js',
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// gsd-config-reload.js — FileChanged hook: hot-reload GSD config context
|
|
4
|
+
// Fires when .planning/config.json is modified, created, or deleted.
|
|
5
|
+
//
|
|
6
|
+
// When the user edits .planning/config.json mid-session, this hook reads the
|
|
7
|
+
// updated config and injects a summary as additionalContext so the agent knows
|
|
8
|
+
// the new configuration without requiring a session restart.
|
|
9
|
+
//
|
|
10
|
+
// Input (from Claude Code):
|
|
11
|
+
// { session_id, cwd, hook_event_name: "FileChanged",
|
|
12
|
+
// file_path: "/abs/path/.planning/config.json", event: "change"|"add"|"unlink" }
|
|
13
|
+
//
|
|
14
|
+
// Output:
|
|
15
|
+
// { hookSpecificOutput: { hookEventName: "FileChanged", additionalContext: "..." } }
|
|
16
|
+
// or exits 0 silently (if config absent, unreadable, or event is "unlink").
|
|
17
|
+
//
|
|
18
|
+
// Enabled for all Claude Code installs. This hook is always-on — it is a
|
|
19
|
+
// no-op when .planning/config.json is absent (ENOENT → exit 0).
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
// Timeout guard: if stdin does not close within 8s exit silently rather than
|
|
26
|
+
// hanging until Claude Code kills the process and reports "hook error".
|
|
27
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 8000);
|
|
28
|
+
process.stdin.setEncoding('utf8');
|
|
29
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
30
|
+
process.stdin.on('end', () => {
|
|
31
|
+
clearTimeout(stdinTimeout);
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(input);
|
|
34
|
+
const event = data.event; // "change" | "add" | "unlink"
|
|
35
|
+
const filePath = data.file_path || '';
|
|
36
|
+
const cwd = data.cwd || process.cwd();
|
|
37
|
+
|
|
38
|
+
// Only handle the GSD planning config — verify both basename and that the
|
|
39
|
+
// resolved path is .planning/config.json relative to cwd. The hook
|
|
40
|
+
// matcher ('config.json') fires on any watched config.json; this guard
|
|
41
|
+
// ensures an unrelated config.json in node_modules/ or elsewhere does not
|
|
42
|
+
// inject spurious additionalContext.
|
|
43
|
+
const basename = path.basename(filePath);
|
|
44
|
+
if (basename !== 'config.json') {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
const expectedPath = path.resolve(cwd, '.planning', 'config.json');
|
|
48
|
+
if (path.resolve(filePath) !== expectedPath) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// On unlink (deletion) emit a brief notice and exit
|
|
53
|
+
if (event === 'unlink') {
|
|
54
|
+
process.stdout.write(JSON.stringify({
|
|
55
|
+
hookSpecificOutput: {
|
|
56
|
+
hookEventName: 'FileChanged',
|
|
57
|
+
additionalContext:
|
|
58
|
+
'GSD config (.planning/config.json) was deleted. ' +
|
|
59
|
+
'Falling back to built-in defaults for this session.',
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Read the updated config file
|
|
66
|
+
let config;
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
69
|
+
config = JSON.parse(raw);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e && e.code === 'ENOENT') process.exit(0);
|
|
72
|
+
// Malformed JSON — inform the agent without crashing
|
|
73
|
+
process.stdout.write(JSON.stringify({
|
|
74
|
+
hookSpecificOutput: {
|
|
75
|
+
hookEventName: 'FileChanged',
|
|
76
|
+
additionalContext:
|
|
77
|
+
'GSD config (.planning/config.json) was modified but could not be parsed. ' +
|
|
78
|
+
'Check the file for JSON syntax errors.',
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build a concise summary of key config fields the agent cares about
|
|
85
|
+
const lines = ['GSD config reloaded (.planning/config.json updated):'];
|
|
86
|
+
|
|
87
|
+
if (config.runtime) lines.push(` runtime: ${config.runtime}`);
|
|
88
|
+
if (config.mode) lines.push(` mode: ${config.mode}`);
|
|
89
|
+
|
|
90
|
+
// hooks section (opt-in toggles agents act on)
|
|
91
|
+
if (config.hooks && typeof config.hooks === 'object') {
|
|
92
|
+
const hookKeys = Object.entries(config.hooks)
|
|
93
|
+
.filter(([, v]) => v !== undefined)
|
|
94
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
95
|
+
.join(', ');
|
|
96
|
+
if (hookKeys) lines.push(` hooks: { ${hookKeys} }`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// workflow section (key toggles)
|
|
100
|
+
if (config.workflow && typeof config.workflow === 'object') {
|
|
101
|
+
const wfKeys = Object.entries(config.workflow)
|
|
102
|
+
.filter(([, v]) => v !== undefined)
|
|
103
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
104
|
+
.join(', ');
|
|
105
|
+
if (wfKeys) lines.push(` workflow: { ${wfKeys} }`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// model overrides (agents use these)
|
|
109
|
+
if (config.models && typeof config.models === 'object') {
|
|
110
|
+
const modelKeys = Object.entries(config.models)
|
|
111
|
+
.filter(([, v]) => v !== undefined)
|
|
112
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
113
|
+
.join(', ');
|
|
114
|
+
if (modelKeys) lines.push(` models: { ${modelKeys} }`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (lines.length === 1) {
|
|
118
|
+
// No notable fields — still confirm the reload happened
|
|
119
|
+
lines.push(' (no notable keys changed)');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const additionalContext = lines.join('\n');
|
|
123
|
+
process.stdout.write(JSON.stringify({
|
|
124
|
+
hookSpecificOutput: {
|
|
125
|
+
hookEventName: 'FileChanged',
|
|
126
|
+
additionalContext,
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Silent fail — never block the session on a config reload error
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
});
|