@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0
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-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-statusline.js +164 -14
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// gsd-windsurf-pre-write.js — Windsurf/Cascade pre_write_code hook (ADR-1239 / #2100)
|
|
4
|
+
//
|
|
5
|
+
// Cascade (Windsurf's agent) invokes this script before each file-write tool
|
|
6
|
+
// call executes, via the workspace/global hooks.json hook bus.
|
|
7
|
+
//
|
|
8
|
+
// Input schema (Cascade pre_write_code envelope, JSON on stdin):
|
|
9
|
+
// { agent_action_name: 'pre_write_code', trajectory_id, execution_id,
|
|
10
|
+
// timestamp, model_name,
|
|
11
|
+
// tool_info: { file_path, edits: [{ old_string, new_string }] } }
|
|
12
|
+
//
|
|
13
|
+
// Decision protocol — DISTINCT from Cursor's stdout-JSON form:
|
|
14
|
+
// - exit 0 -> allow the write to proceed (no stdout contract)
|
|
15
|
+
// - exit 2 -> BLOCK the write; the printed stderr text is the reason shown
|
|
16
|
+
// to the agent/user
|
|
17
|
+
//
|
|
18
|
+
// Behaviour: reimplements the core containment check from
|
|
19
|
+
// hooks/gsd-worktree-path-guard.js — block a write whose file_path resolves
|
|
20
|
+
// (via `git rev-parse --show-toplevel`) to a DIFFERENT git root than the
|
|
21
|
+
// current working directory, or lands inside a `.git/` internals directory.
|
|
22
|
+
// Fails OPEN on any error, timeout, non-git cwd, or missing git binary — a
|
|
23
|
+
// hook bug must never wedge Cascade.
|
|
24
|
+
//
|
|
25
|
+
// Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt ,
|
|
26
|
+
// https://docs.devin.ai/desktop/cascade/hooks
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const { spawnSync } = require('child_process');
|
|
33
|
+
|
|
34
|
+
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
|
|
35
|
+
|
|
36
|
+
function git(args, cwd) {
|
|
37
|
+
return spawnSync('git', args, { ...SPAWNOPT, cwd });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Walk up from `start` to find the nearest existing DIRECTORY (not merely an
|
|
41
|
+
// existing filesystem entry) — a linked git worktree's `.git` is a plain FILE
|
|
42
|
+
// (a `gitdir:` pointer), not a directory, so a plain existence check would
|
|
43
|
+
// hand spawnSync an invalid `cwd` and silently fail the git calls below.
|
|
44
|
+
// Returns null if we reach the filesystem root without finding one.
|
|
45
|
+
function nearestExistingDir(start) {
|
|
46
|
+
let dir = start;
|
|
47
|
+
let prev;
|
|
48
|
+
do {
|
|
49
|
+
prev = dir;
|
|
50
|
+
try { if (fs.statSync(dir).isDirectory()) return dir; } catch { /* keep walking */ }
|
|
51
|
+
dir = path.dirname(dir);
|
|
52
|
+
} while (dir !== prev);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function block(reason) {
|
|
57
|
+
process.stderr.write(`GSD windsurf pre_write_code guard: ${reason}\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function allow() {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let input = '';
|
|
66
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
|
67
|
+
process.stdin.setEncoding('utf8');
|
|
68
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
69
|
+
process.stdin.on('end', () => {
|
|
70
|
+
clearTimeout(stdinTimeout);
|
|
71
|
+
try {
|
|
72
|
+
const data = JSON.parse(input || '{}');
|
|
73
|
+
const toolInfo = (data && typeof data.tool_info === 'object' && data.tool_info) || {};
|
|
74
|
+
const rawFilePath = typeof toolInfo.file_path === 'string' ? toolInfo.file_path : '';
|
|
75
|
+
if (!rawFilePath) { allow(); return; }
|
|
76
|
+
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
|
|
79
|
+
// Determine the active project's git root. No git root at all -> nothing
|
|
80
|
+
// to enforce a boundary against -> fail open.
|
|
81
|
+
const cwdTopResult = git(['rev-parse', '--show-toplevel'], cwd);
|
|
82
|
+
if (cwdTopResult.status !== 0 || !cwdTopResult.stdout) { allow(); return; }
|
|
83
|
+
const cwdTopRaw = cwdTopResult.stdout.trim();
|
|
84
|
+
|
|
85
|
+
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(cwd, rawFilePath);
|
|
86
|
+
|
|
87
|
+
// Find the nearest existing ancestor of filePath so we can ask git for its
|
|
88
|
+
// toplevel. The file itself may not exist yet (a write can create it).
|
|
89
|
+
const checkDir = nearestExistingDir(
|
|
90
|
+
(() => {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
|
|
93
|
+
} catch {
|
|
94
|
+
return path.dirname(filePath);
|
|
95
|
+
}
|
|
96
|
+
})(),
|
|
97
|
+
);
|
|
98
|
+
if (!checkDir) { allow(); return; } // synthetic path with no existing ancestor — fail open
|
|
99
|
+
|
|
100
|
+
const fileTopResult = git(['rev-parse', '--show-toplevel'], checkDir);
|
|
101
|
+
if (fileTopResult.status !== 0 || !fileTopResult.stdout) {
|
|
102
|
+
// Not inside any git worktree. Distinguish "inside a .git/ internals
|
|
103
|
+
// directory" (dangerous — BLOCK) from "outside all git repos entirely"
|
|
104
|
+
// (not the escape vector this guard targets — fail open).
|
|
105
|
+
const insideGitDir = git(['rev-parse', '--is-inside-git-dir'], checkDir);
|
|
106
|
+
if (insideGitDir.status === 0 && insideGitDir.stdout && insideGitDir.stdout.trim() === 'true') {
|
|
107
|
+
block(
|
|
108
|
+
`'${filePath}' is inside a git internal (.git) directory, not the active project at ` +
|
|
109
|
+
`'${cwdTopRaw}'. Writing to repository internals via an absolute path is not permitted. ` +
|
|
110
|
+
`Use a relative path. (cwd: '${cwd}')`,
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
allow();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fileTopRaw = fileTopResult.stdout.trim();
|
|
119
|
+
if (fileTopRaw === cwdTopRaw) { allow(); return; }
|
|
120
|
+
|
|
121
|
+
// BLOCK: file resolves to a different git root than the active project.
|
|
122
|
+
block(
|
|
123
|
+
`'${filePath}' resolves to git root '${fileTopRaw}' which differs from the active project root ` +
|
|
124
|
+
`'${cwdTopRaw}'. This likely means an absolute path was derived from a different repository. ` +
|
|
125
|
+
`Use a relative path within the active project, or re-derive the base directory with ` +
|
|
126
|
+
`\`git rev-parse --show-toplevel\` from the active project. (cwd: '${cwd}')`,
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
// Silent fail-open — never block a valid tool call due to a hook bug.
|
|
130
|
+
allow();
|
|
131
|
+
}
|
|
132
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.7.0
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
".opencode",
|
|
21
21
|
"GEMINI.md",
|
|
22
22
|
"hooks",
|
|
23
|
-
"scripts"
|
|
23
|
+
"scripts",
|
|
24
|
+
"pi",
|
|
25
|
+
"vscode"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"claude",
|
|
@@ -87,6 +89,9 @@
|
|
|
87
89
|
"gen:loop-host-contract": "node scripts/gen-loop-host-contract.cjs --write",
|
|
88
90
|
"gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
|
|
89
91
|
"gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
|
|
92
|
+
"gen:registry": "node scripts/gen-registry.cjs --write",
|
|
93
|
+
"gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs",
|
|
94
|
+
"validate:registry": "node scripts/validate-registry.cjs",
|
|
90
95
|
"prepack": "npm run build:lib",
|
|
91
96
|
"prepare": "npm run build:lib",
|
|
92
97
|
"version": "node scripts/sync-manifest-versions.cjs --stage && node scripts/gen-capability-registry.cjs --write && git add gsd-core/bin/lib/capability-registry.cjs",
|
|
@@ -95,7 +100,8 @@
|
|
|
95
100
|
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
96
101
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
97
102
|
"lint:fix": "eslint . --fix",
|
|
98
|
-
"lint:
|
|
103
|
+
"lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
|
|
104
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
|
|
99
105
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
100
106
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
101
107
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -103,7 +109,7 @@
|
|
|
103
109
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
|
104
110
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
|
105
111
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
|
106
|
-
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check",
|
|
112
|
+
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
|
|
107
113
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
108
114
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
109
115
|
"ci:test-scope": "node scripts/ci-test-scope.cjs",
|
package/pi/gsd.cjs
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD extension for pi (pi.dev) — ADR-1239 Phase D / #1944, upgraded #2102 Stage 2.
|
|
5
|
+
*
|
|
6
|
+
* pi is a Programmatic-CLI host whose TS extensions implement the ExtensionAPI
|
|
7
|
+
* (`@earendil-works/pi-coding-agent`): registerCommand({handler(args, ctx)}) /
|
|
8
|
+
* registerTool({execute(toolCallId, params, signal, onUpdate, ctx)}) / pi.on(event, handler).
|
|
9
|
+
* This extension binds GSD's command surface to pi via the imperative adapter
|
|
10
|
+
* path — the programmatic-CLI peer of the OpenCode worked binding.
|
|
11
|
+
*
|
|
12
|
+
* Installation: copy this file to ~/.pi/agent/extensions/gsd.cjs (pi loads
|
|
13
|
+
* extensions via jiti from that dir). The engine is resolved from the installed
|
|
14
|
+
* GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
|
|
15
|
+
* (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
|
|
16
|
+
* capabilities/pi/capability.json does NOT set
|
|
17
|
+
* `hostBehaviors.skipSharedHooksInstall` (#2102 Stage 2 fix; pi is
|
|
18
|
+
* architecturally identical to OpenCode here: `hooksSurface: 'none'` + a
|
|
19
|
+
* native extension that spawns the staged hooks — not Kilo/ZCode's
|
|
20
|
+
* no-plugin-surface case, where the same hooks would be genuine dead weight).
|
|
21
|
+
* This is what makes the event bridges below (and the tokenizer require)
|
|
22
|
+
* resolve for real in an installed tree, not just in this dev repo.
|
|
23
|
+
*
|
|
24
|
+
* Engine entry: dispatch is SUBPROCESS-REUSE to gsd-tools.cjs (bounded,
|
|
25
|
+
* no-throw — dispatchGsdCommand in shell-command-projection.cjs), NOT an
|
|
26
|
+
* in-process command-routing hub. No fully-populated hub factory exists
|
|
27
|
+
* anywhere in gsd-core — every createHub() caller in the tree builds a
|
|
28
|
+
* single-family hub for its own narrow purpose — so the "in-process createHub"
|
|
29
|
+
* framing of the original #1944 cut was aspirational and is not achievable
|
|
30
|
+
* without a hub factory that doesn't exist. This mirrors the precedent already
|
|
31
|
+
* established for the OpenCode/Kilo hook bridge (.opencode/plugins/gsd-core.js
|
|
32
|
+
* header: "Architecture: SUBPROCESS REUSE ... spawns existing hook scripts as
|
|
33
|
+
* child processes") — the same pattern, applied to command dispatch. The
|
|
34
|
+
* companion MCP server (gsd-mcp-server) dispatches through the SAME shared
|
|
35
|
+
* helper for out-of-process hosts.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const { spawnSync } = require('child_process');
|
|
43
|
+
|
|
44
|
+
// Resolve the GSD engine tree (the dir holding gsd-core/ + hooks/).
|
|
45
|
+
// Works across dev (<root>/pi/gsd.cjs → <root>) and installed layouts.
|
|
46
|
+
function resolveEngineRoot(startDir) {
|
|
47
|
+
let dir = startDir;
|
|
48
|
+
for (let i = 0; i < 6; i++) {
|
|
49
|
+
if (fs.existsSync(path.join(dir, 'gsd-core'))) return dir;
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir) break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return path.resolve(startDir, '..');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ENGINE_ROOT = resolveEngineRoot(__dirname);
|
|
58
|
+
const GSD_CORE = path.join(ENGINE_ROOT, 'gsd-core');
|
|
59
|
+
|
|
60
|
+
// ── curated top-level command families (gsd-tools.cjs TOP_LEVEL_USAGE) ──────
|
|
61
|
+
// readCmdNames() (scripts/fix-slash-commands.cjs) reads commands/, which pi
|
|
62
|
+
// does NOT install (it ships a single native-extension file, no shared
|
|
63
|
+
// commands/ dir) — it would always return []. This is a self-contained,
|
|
64
|
+
// hand-curated subset of the STABLE top-level families documented by
|
|
65
|
+
// `node gsd-core/bin/gsd-tools.cjs --help` (gsd-tools.cjs:689-705). Named +
|
|
66
|
+
// exported (via _internals) so a test can assert against it directly.
|
|
67
|
+
const PI_COMMAND_FAMILIES = Object.freeze([
|
|
68
|
+
'agent', 'capability', 'check', 'commit', 'config-get', 'config-path',
|
|
69
|
+
'config-set', 'effort', 'git', 'graphify', 'init', 'intel', 'learnings',
|
|
70
|
+
'list-todos', 'loop', 'milestone', 'phase', 'phases', 'progress',
|
|
71
|
+
'requirements', 'research-plan', 'research-store', 'resolve-granularity',
|
|
72
|
+
'resolve-model', 'roadmap', 'scaffold', 'smart-entry', 'state', 'task',
|
|
73
|
+
'template', 'user-story', 'validate', 'verify', 'workstream', 'worktree',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Filter PI_COMMAND_FAMILIES by prefix (startsWith). Returns null when there
|
|
78
|
+
* are no matches, per pi's `AutocompleteItem[]|null` contract.
|
|
79
|
+
* @param {string} prefix
|
|
80
|
+
* @returns {{value: string, label: string}[] | null}
|
|
81
|
+
*/
|
|
82
|
+
function getArgumentCompletions(prefix) {
|
|
83
|
+
const p = typeof prefix === 'string' ? prefix : '';
|
|
84
|
+
const matches = PI_COMMAND_FAMILIES.filter((name) => name.startsWith(p));
|
|
85
|
+
if (matches.length === 0) return null;
|
|
86
|
+
return matches.map((value) => ({ value, label: value }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tokenize the raw `/gsd <args>` string into { family, subcommand, args }.
|
|
91
|
+
* Reuses the quote-aware whitespace tokenizer already shipped for hooks
|
|
92
|
+
* (hooks/lib/git-cmd.js's `tokenize`) rather than re-implementing shell-word
|
|
93
|
+
* splitting a second time. #2102 Stage 2: pi's capability descriptor no
|
|
94
|
+
* longer sets `hostBehaviors.skipSharedHooksInstall` (adversarial-review
|
|
95
|
+
* finding #1/#2 — pi ships NO hooks/ with that flag set, so this require was
|
|
96
|
+
* dead in a real install), so the shared hooks/ bundle — including
|
|
97
|
+
* hooks/lib/git-cmd.js — is installed alongside the extension for real
|
|
98
|
+
* (mirrors OpenCode, whose native plugin also spawns the staged hooks/*.js
|
|
99
|
+
* bundle). The require below is therefore the PRIMARY, live path in an
|
|
100
|
+
* installed pi tree; the whitespace-split fallback stays as defense-in-depth
|
|
101
|
+
* for a corrupted/partial install (e.g. a user who deleted hooks/lib/ by
|
|
102
|
+
* hand) rather than the only-ever-taken path.
|
|
103
|
+
* @param {string} rawArgs
|
|
104
|
+
* @returns {{ family: string, subcommand?: string, args: string[] }}
|
|
105
|
+
*/
|
|
106
|
+
function parseGsdCommandArgs(rawArgs) {
|
|
107
|
+
let tokenize;
|
|
108
|
+
try {
|
|
109
|
+
({ tokenize } = require(path.join(ENGINE_ROOT, 'hooks', 'lib', 'git-cmd.js')));
|
|
110
|
+
} catch {
|
|
111
|
+
tokenize = (s) => String(s || '').split(/\s+/).filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
const tokens = tokenize(typeof rawArgs === 'string' ? rawArgs : '');
|
|
114
|
+
return {
|
|
115
|
+
// Empty args → dispatch gsd-tools.cjs's own --help surface (a real,
|
|
116
|
+
// working, ok:true default — NOT the 'query'/'help' pairing the original
|
|
117
|
+
// #1944 cut used, which is not a valid gsd-tools.cjs command).
|
|
118
|
+
family: tokens[0] || '--help',
|
|
119
|
+
subcommand: tokens[1],
|
|
120
|
+
args: tokens.slice(2),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Best-effort TypeBox schema for gsd_invoke's `parameters`, falling back to a
|
|
126
|
+
* plain JSON-Schema object when the `typebox` package is unavailable (it is
|
|
127
|
+
* NOT a gsd-core dependency — pi's own ExtensionAPI contract expects TypeBox,
|
|
128
|
+
* but nothing in this repo installs it). TypeBox schemas ARE JSON Schema, so
|
|
129
|
+
* the fallback object is structurally equivalent for hosts that accept plain
|
|
130
|
+
* JSON Schema; this is a best-effort shim for the flat-file extension case.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
function buildGsdInvokeParameters() {
|
|
134
|
+
try {
|
|
135
|
+
const typebox = require('typebox');
|
|
136
|
+
const Type = typebox && typebox.Type;
|
|
137
|
+
if (Type) {
|
|
138
|
+
return Type.Object({
|
|
139
|
+
family: Type.String(),
|
|
140
|
+
subcommand: Type.Optional(Type.String()),
|
|
141
|
+
args: Type.Optional(Type.Array(Type.String())),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// typebox is not installed in this environment — fall through.
|
|
146
|
+
}
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
'gsd: typebox unavailable — gsd_invoke "parameters" falling back to a plain JSON-schema object.\n',
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
family: { type: 'string' },
|
|
154
|
+
subcommand: { type: 'string' },
|
|
155
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
156
|
+
},
|
|
157
|
+
required: ['family'],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build the `before_provider_request` handler that steers pi's model
|
|
163
|
+
* selection to GSD's tier-resolved id (modelMode: 'active' per
|
|
164
|
+
* capabilities/pi/capability.json). GSD does NOT call `pi.registerProvider` —
|
|
165
|
+
* that registers a NEW model provider; GSD's job here is only to pick a
|
|
166
|
+
* tier-appropriate id AMONG pi's EXISTING built-in anthropic models, so
|
|
167
|
+
* registerProvider would be the wrong primitive (it would wrongly add a fake
|
|
168
|
+
* provider instead of steering the real one).
|
|
169
|
+
*
|
|
170
|
+
* v1 tier policy: GSD does not yet expose a per-turn/per-agent tier signal to
|
|
171
|
+
* this event, so a conservative fixed default tier is used (parameterized —
|
|
172
|
+
* default 'sonnet' — so a future richer signal, or a test, can override it).
|
|
173
|
+
*
|
|
174
|
+
* ASSUMPTION (flagged — verify against a live pi host): the event payload's
|
|
175
|
+
* model field is named `model`, matching the anthropic-messages payload shape
|
|
176
|
+
* (Context7-confirmed for the wire protocol; pi's own before_provider_request
|
|
177
|
+
* event schema was not independently verifiable in this environment). If pi's
|
|
178
|
+
* actual field name differs, this returns the WRONG key and pi's fail-open
|
|
179
|
+
* default takes over only because bare-model-id mismatches degrade to
|
|
180
|
+
* provider-level errors, not GSD-level ones — a discrepancy here needs a
|
|
181
|
+
* live-host smoke test before shipping past this stage.
|
|
182
|
+
*
|
|
183
|
+
* Fail-open: any resolution failure (or a null/falsy resolved model — e.g. an
|
|
184
|
+
* unrecognized tier) returns `undefined`, leaving pi's model choice untouched.
|
|
185
|
+
* NEVER returns a payload with a missing/empty model id.
|
|
186
|
+
*
|
|
187
|
+
* @param {{ tier?: string }} [opts]
|
|
188
|
+
* @returns {(event: object, ctx: object) => Promise<object|undefined>}
|
|
189
|
+
*/
|
|
190
|
+
function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
|
|
191
|
+
return async function onBeforeProviderRequest(event, ctx) {
|
|
192
|
+
try {
|
|
193
|
+
const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
|
|
194
|
+
const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
|
|
195
|
+
const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
|
|
196
|
+
const config = loadConfig(effectiveCwd);
|
|
197
|
+
const overrides = (config && config.model_profile_overrides) || undefined;
|
|
198
|
+
const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
|
|
199
|
+
const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
|
|
200
|
+
if (!modelId) return undefined; // fail-open — leave pi's model untouched
|
|
201
|
+
const basePayload = (event && typeof event === 'object' && event.payload && typeof event.payload === 'object')
|
|
202
|
+
? event.payload
|
|
203
|
+
: {};
|
|
204
|
+
return { ...basePayload, model: modelId };
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined; // fail-open on any resolution error
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Bounded subprocess bridge to GSD's Claude Code hook scripts. Mirrors
|
|
213
|
+
* .opencode/plugins/gsd-core.js's `runHook` (SUBPROCESS-REUSE): spawns
|
|
214
|
+
* `node <hooks/hookFile>` with the payload piped to stdin, on a bounded
|
|
215
|
+
* timeout. NEVER throws — a missing hook file, a spawn error, or a timeout
|
|
216
|
+
* all degrade to a silent-allow result so a hook problem can never block pi.
|
|
217
|
+
* @param {string} hookFile filename under hooks/, e.g. "gsd-context-monitor.js"
|
|
218
|
+
* @param {object} payload
|
|
219
|
+
* @param {{ timeout?: number, cwd?: string }} [opts]
|
|
220
|
+
* @returns {{ stdout: string, exitCode: number, timedOut: boolean }}
|
|
221
|
+
*/
|
|
222
|
+
function runHook(hookFile, payload, opts = {}) {
|
|
223
|
+
const hookPath = path.join(ENGINE_ROOT, 'hooks', hookFile);
|
|
224
|
+
if (!fs.existsSync(hookPath)) return { stdout: '', exitCode: 0, timedOut: false };
|
|
225
|
+
const timeout = opts.timeout || 8000;
|
|
226
|
+
let result;
|
|
227
|
+
try {
|
|
228
|
+
result = spawnSync(process.execPath, [hookPath], {
|
|
229
|
+
input: JSON.stringify(payload || {}),
|
|
230
|
+
encoding: 'utf8',
|
|
231
|
+
timeout,
|
|
232
|
+
cwd: opts.cwd || process.cwd(),
|
|
233
|
+
windowsHide: true,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
return { stdout: '', exitCode: 0, timedOut: false };
|
|
237
|
+
}
|
|
238
|
+
const stdout = (result && typeof result.stdout === 'string') ? result.stdout.trim() : '';
|
|
239
|
+
const exitCode = (result && result.status != null) ? result.status : 0;
|
|
240
|
+
return { stdout, exitCode, timedOut: !!(result && result.signal === 'SIGTERM') };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = function gsdPiExtension(pi) {
|
|
244
|
+
if (!pi || typeof pi !== 'object') {
|
|
245
|
+
throw new TypeError('gsdPiExtension: pi ExtensionAPI is required');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── /gsd command: dispatch through gsd-tools.cjs (subprocess-reuse) ──────
|
|
249
|
+
pi.registerCommand('gsd', {
|
|
250
|
+
description: 'Invoke a GSD command via the embedded engine (subprocess-reuse adapter).',
|
|
251
|
+
getArgumentCompletions,
|
|
252
|
+
handler: async (args, ctx) => {
|
|
253
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
254
|
+
const { family, subcommand, args: rest } = parseGsdCommandArgs(args);
|
|
255
|
+
let dispatchGsdCommand;
|
|
256
|
+
try {
|
|
257
|
+
({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return `GSD engine unavailable: ${e && e.message ? e.message : String(e)}`;
|
|
260
|
+
}
|
|
261
|
+
const result = dispatchGsdCommand({ family, subcommand, args: rest, cwd });
|
|
262
|
+
if (result.ok) return result.stdout;
|
|
263
|
+
return `GSD error: ${result.stderr || result.stdout || `dispatch failed (exit ${result.code})`}`;
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── gsd_invoke tool: programmatic command invocation ────────────────────
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: 'gsd_invoke',
|
|
270
|
+
label: 'GSD Invoke',
|
|
271
|
+
description: 'Invoke a GSD command family/subcommand through the engine.',
|
|
272
|
+
parameters: buildGsdInvokeParameters(),
|
|
273
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
274
|
+
const p = (params && typeof params === 'object') ? params : {};
|
|
275
|
+
const family = typeof p.family === 'string' ? p.family : '';
|
|
276
|
+
if (!family) {
|
|
277
|
+
return { content: [{ type: 'text', text: 'gsd_invoke requires a non-empty string "family".' }] };
|
|
278
|
+
}
|
|
279
|
+
const subcommand = typeof p.subcommand === 'string' ? p.subcommand : undefined;
|
|
280
|
+
const invokeArgs = Array.isArray(p.args) ? p.args : [];
|
|
281
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
282
|
+
let dispatchGsdCommand;
|
|
283
|
+
try {
|
|
284
|
+
({ dispatchGsdCommand } = require(path.join(GSD_CORE, 'bin', 'lib', 'shell-command-projection.cjs')));
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { content: [{ type: 'text', text: `GSD engine unavailable: ${e && e.message ? e.message : String(e)}` }] };
|
|
287
|
+
}
|
|
288
|
+
const result = dispatchGsdCommand({ family, subcommand, args: invokeArgs, cwd });
|
|
289
|
+
const text = result.ok ? result.stdout : (result.stderr || result.stdout || `dispatch failed (exit ${result.code})`);
|
|
290
|
+
return { content: [{ type: 'text', text }] };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── before_provider_request: active-model steering (modelMode: 'active') ──
|
|
295
|
+
// GSD steers pi's EXISTING built-in anthropic models; it does NOT call
|
|
296
|
+
// pi.registerProvider (that would wrongly register a NEW fake provider —
|
|
297
|
+
// see buildBeforeProviderRequestHandler's doc comment).
|
|
298
|
+
pi.on('before_provider_request', buildBeforeProviderRequestHandler());
|
|
299
|
+
|
|
300
|
+
// ── Event bindings: bounded subprocess bridge to GSD's hook scripts ──────
|
|
301
|
+
// Each binding fails open — a hook error/timeout/missing-file never blocks
|
|
302
|
+
// pi (mirrors .opencode/plugins/gsd-core.js's runHook SUBPROCESS-REUSE
|
|
303
|
+
// pattern, applied to pi's ExtensionAPI event names).
|
|
304
|
+
|
|
305
|
+
// session_start → SessionStart-equivalent bootstrap.
|
|
306
|
+
pi.on('session_start', async (event, ctx) => {
|
|
307
|
+
try {
|
|
308
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
309
|
+
runHook('gsd-ensure-canonical-path.js', { hook_event_name: 'SessionStart', cwd }, { cwd });
|
|
310
|
+
} catch { /* fail-open */ }
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// before_agent_start → workflow-guard bridge. Forward-compatible binding:
|
|
314
|
+
// gsd-workflow-guard.js's current triggers are tool-scoped (Write/Edit/
|
|
315
|
+
// Bash via tool_name/tool_input), so with no tool_name in the payload it
|
|
316
|
+
// fires as a safe no-op today — wired so a future agent-start-scoped check
|
|
317
|
+
// can attach without a plugin change (mirrors the OpenCode session.idle
|
|
318
|
+
// recognized-but-unused sentinel pattern).
|
|
319
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
320
|
+
try {
|
|
321
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
322
|
+
runHook('gsd-workflow-guard.js', { hook_event_name: 'before_agent_start', cwd }, { cwd });
|
|
323
|
+
} catch { /* fail-open */ }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// session_before_compact → PreCompact-equivalent (context-usage bridge).
|
|
327
|
+
pi.on('session_before_compact', async (event, ctx) => {
|
|
328
|
+
try {
|
|
329
|
+
const cwd = (ctx && ctx.cwd) || process.cwd();
|
|
330
|
+
runHook('gsd-context-monitor.js', { hook_event_name: 'PreCompact', cwd }, { cwd });
|
|
331
|
+
} catch { /* fail-open */ }
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// tool_call event: lifecycle hook bridge attachment point (kept from the
|
|
335
|
+
// original cut — the PreToolUse/PostToolUse tool_name/tool_input mapping
|
|
336
|
+
// is a follow-up once pi's tool_call payload shape is verified against a
|
|
337
|
+
// live host).
|
|
338
|
+
pi.on('tool_call', async function () {
|
|
339
|
+
/* GSD hook bridge attachment point (PreToolUse/PostToolUse mapping). */
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Test-only internals (mirrors the OpenCode plugin pattern) — wired to the
|
|
344
|
+
// real functions (not stubs) so tests can exercise parsing/completions/model
|
|
345
|
+
// resolution WITHOUT a live pi runtime.
|
|
346
|
+
module.exports._internals = {
|
|
347
|
+
resolveEngineRoot,
|
|
348
|
+
parseGsdCommandArgs,
|
|
349
|
+
getArgumentCompletions,
|
|
350
|
+
PI_COMMAND_FAMILIES,
|
|
351
|
+
buildBeforeProviderRequestHandler,
|
|
352
|
+
buildGsdInvokeParameters,
|
|
353
|
+
runHook,
|
|
354
|
+
};
|
package/scripts/build-hooks.js
CHANGED
|
@@ -44,6 +44,9 @@ const HOOKS_TO_COPY = [
|
|
|
44
44
|
'gsd-cursor-stop.js',
|
|
45
45
|
'gsd-cursor-subagent-start.js',
|
|
46
46
|
'gsd-cursor-subagent-stop.js',
|
|
47
|
+
// Windsurf/Cascade lifecycle hooks (ADR-1239/#2100 Stage 2): 2 blocking events
|
|
48
|
+
'gsd-windsurf-pre-write.js',
|
|
49
|
+
'gsd-windsurf-pre-command.js',
|
|
47
50
|
// Claude Code FileChanged hook (#770) — hot-reloads gsd config when
|
|
48
51
|
// .planning/config.json changes mid-session. Must ship to dist so the
|
|
49
52
|
// installer can copy it to the target hooks/ dir and register FileChanged.
|
|
@@ -145,6 +145,37 @@ const RULES = [
|
|
|
145
145
|
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
|
146
146
|
],
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: 'shipped install content (golden-parity drift guard, #2267)',
|
|
150
|
+
// Every source file the installer EMITS into a runtime layout is captured by
|
|
151
|
+
// golden-install-parity + the install-tree snapshot. A source edit here that
|
|
152
|
+
// changes emitted output MUST re-verify the fixtures — otherwise stale golden
|
|
153
|
+
// fixtures merge silently (#2266: a hooks/gsd-statusline.js edit changed
|
|
154
|
+
// installed output but no rule selected golden-parity, so stale fixtures
|
|
155
|
+
// shipped to next undetected). Union semantics: this ADDS the parity guard on
|
|
156
|
+
// top of each path's existing content-specific tests. Targeted lane only (the
|
|
157
|
+
// golden test skips win32 by design), no fullMatrix.
|
|
158
|
+
// NOTE: intentionally NOT a blanket 'gsd-core/' prefix, for two reasons:
|
|
159
|
+
// (1) gsd-core/bin/** is tsc-compiled runtime output — EXCLUDED_PREFIXES-
|
|
160
|
+
// excluded from both manifests, and already covered by the 'installer and
|
|
161
|
+
// package layout' rule (path.startsWith('gsd-core/bin/')) — so matching it
|
|
162
|
+
// here would be pure noise; and
|
|
163
|
+
// (2) enumerating only the installer-shipped content subtrees preserves the
|
|
164
|
+
// bug-408 unit-fallback contract: a gsd-core/ path that is NOT shipped
|
|
165
|
+
// verbatim (the bug-408 test uses gsd-core/src/some-util.js) must still
|
|
166
|
+
// fall back to ['unit'] when no rule matches.
|
|
167
|
+
// Listed: the four gsd-core content subtrees the installer ships verbatim
|
|
168
|
+
// (contexts, references, templates, workflows) + bin/shared/*.json data files.
|
|
169
|
+
// Verify against Object.keys(golden fixture) grouped by gsd-core/<subdir>.
|
|
170
|
+
match: path =>
|
|
171
|
+
['hooks/', 'commands/', 'agents/', 'skills/', 'gsd-core/workflows/', 'gsd-core/templates/', 'gsd-core/references/', 'gsd-core/contexts/', 'scripts/changeset/', 'scripts/lib/'].some(p => path.startsWith(p)) ||
|
|
172
|
+
(path.startsWith('gsd-core/bin/shared/') && path.endsWith('.json')) ||
|
|
173
|
+
['scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs'].includes(path),
|
|
174
|
+
tests: [
|
|
175
|
+
'tests/golden-install-parity.test.cjs',
|
|
176
|
+
'tests/golden-install-tree.test.cjs',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
148
179
|
{
|
|
149
180
|
name: 'hooks',
|
|
150
181
|
match: path => path.startsWith('hooks/'),
|
|
@@ -364,8 +395,15 @@ function classify(files) {
|
|
|
364
395
|
for (const file of files) {
|
|
365
396
|
// Determine if this file is product/pipeline code.
|
|
366
397
|
// docs/ and root-level .md files are intentionally excluded.
|
|
398
|
+
// 'skills/' is shipped agent-skill content installed into every runtime by
|
|
399
|
+
// the installer (see the 'shipped install content' RULES entry below) — it
|
|
400
|
+
// must be product code, or a skills/-only change silently gets
|
|
401
|
+
// code_changed=false and skips the ENTIRE CI matrix, not merely golden-parity
|
|
402
|
+
// (found while verifying the #2267 golden-parity rule against skills/**: the
|
|
403
|
+
// rule fired in `reasons` but classify()'s codeChanged gate zeroed out every
|
|
404
|
+
// targeted test because 'skills/' was absent from this list).
|
|
367
405
|
if (
|
|
368
|
-
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'tests/', 'scripts/', 'eslint-rules/'].some(p => file.startsWith(p)) ||
|
|
406
|
+
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'skills/', 'tests/', 'scripts/', 'eslint-rules/'].some(p => file.startsWith(p)) ||
|
|
369
407
|
file === 'package.json' || file === 'package-lock.json' ||
|
|
370
408
|
(file.startsWith('tsconfig') && file.endsWith('.json')) ||
|
|
371
409
|
file.startsWith('.github/rulesets/')
|