@opengsd/gsd-core 1.5.0-rc.4 → 1.5.0-rc.5
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-phase-researcher.md +1 -1
- package/agents/gsd-planner.md +4 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-verifier.md +45 -13
- package/bin/install.js +3 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +49 -48
- package/gsd-core/bin/lib/agent-command-router.cjs +2 -2
- package/gsd-core/bin/lib/agent-install-check.cjs +143 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +4 -4
- package/gsd-core/bin/lib/capability-activation.cjs +37 -10
- package/gsd-core/bin/lib/capability-registry.cjs +2 -0
- package/gsd-core/bin/lib/capability-state.cjs +80 -30
- package/gsd-core/bin/lib/capability-writer.cjs +5 -4
- package/gsd-core/bin/lib/check-command-router.cjs +50 -51
- package/gsd-core/bin/lib/commands.cjs +20 -2
- package/gsd-core/bin/lib/config-loader.cjs +3 -4
- package/gsd-core/bin/lib/config-schema.cjs +1 -1
- package/gsd-core/bin/lib/config-types.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +5 -2
- package/gsd-core/bin/lib/docs.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +2 -2
- package/gsd-core/bin/lib/gap-checker.cjs +5 -2
- package/gsd-core/bin/lib/git-base-branch.cjs +27 -1
- package/gsd-core/bin/lib/graphify-command-router.cjs +6 -8
- package/gsd-core/bin/lib/graphify.cjs +7 -31
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -2
- package/gsd-core/bin/lib/init.cjs +30 -4
- package/gsd-core/bin/lib/intel-command-router.cjs +6 -3
- package/gsd-core/bin/lib/intel.cjs +28 -34
- package/gsd-core/bin/lib/io.cjs +2 -4
- package/gsd-core/bin/lib/learnings.cjs +2 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +45 -167
- package/gsd-core/bin/lib/milestone.cjs +13 -5
- package/gsd-core/bin/lib/model-resolver.cjs +3 -4
- package/gsd-core/bin/lib/phase-id.cjs +2 -4
- package/gsd-core/bin/lib/phase-locator.cjs +3 -6
- package/gsd-core/bin/lib/phase.cjs +18 -7
- package/gsd-core/bin/lib/probe-core.cjs +33 -11
- package/gsd-core/bin/lib/profile-output.cjs +5 -2
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +485 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +3 -5
- package/gsd-core/bin/lib/roadmap.cjs +9 -4
- package/gsd-core/bin/lib/state.cjs +11 -2
- package/gsd-core/bin/lib/task-command-router.cjs +2 -2
- package/gsd-core/bin/lib/template.cjs +11 -2
- package/gsd-core/bin/lib/uat.cjs +8 -2
- package/gsd-core/bin/lib/verification.cjs +8 -5
- package/gsd-core/bin/lib/verify.cjs +311 -4
- package/gsd-core/bin/lib/workstream-inventory.cjs +2 -2
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +37 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/planner-antipatterns.md +46 -0
- package/gsd-core/references/planning-config.md +5 -1
- package/gsd-core/references/prohibition-probe.md +51 -2
- package/gsd-core/templates/verification-report.md +16 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
- package/gsd-core/workflows/execute-phase.md +5 -1
- package/gsd-core/workflows/quick.md +3 -0
- package/gsd-core/workflows/settings-advanced.md +5 -5
- package/gsd-core/workflows/settings-integrations.md +5 -5
- package/gsd-core/workflows/spec-phase.md +25 -2
- package/gsd-core/workflows/verify-phase.md +22 -7
- package/package.json +2 -2
- package/scripts/gen-capability-registry.cjs +27 -1
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/research-profiles.cjs +2 -2
- package/gsd-core/bin/lib/core.cjs +0 -345
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent Install Check — moved from core.cts (ADR-857 T0 #1268 phase rehome-core-squatters).
|
|
4
|
+
*
|
|
5
|
+
* Owns:
|
|
6
|
+
* - getAgentsDir(runtime?): string
|
|
7
|
+
* - checkAgentsInstalled(runtime?): AgentsInstalledResult
|
|
8
|
+
*
|
|
9
|
+
* The core.cjs re-export spine was retired in epic #1267; callers import
|
|
10
|
+
* these symbols from agent-install-check.cjs directly.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const modelProfiles = require("./model-profiles.cjs");
|
|
19
|
+
const { MODEL_PROFILES } = modelProfiles;
|
|
20
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the agents directory for the given runtime.
|
|
23
|
+
*
|
|
24
|
+
* Priority:
|
|
25
|
+
* 1. GSD_AGENTS_DIR env var (explicit override, any runtime)
|
|
26
|
+
* 2. For claude runtime: __dirname-relative path (agents/ sibling of gsd-core/)
|
|
27
|
+
* This is correct for both repo runs and real installs (the runtime config dir's
|
|
28
|
+
* agents/ folder) because gsd-tools.cjs lives inside gsd-core/bin/ in both cases.
|
|
29
|
+
* 3. For non-claude runtimes: getGlobalConfigDir(runtime)/agents
|
|
30
|
+
*
|
|
31
|
+
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
|
|
32
|
+
*/
|
|
33
|
+
function getAgentsDir(runtime) {
|
|
34
|
+
if (process.env['GSD_AGENTS_DIR']) {
|
|
35
|
+
return process.env['GSD_AGENTS_DIR'];
|
|
36
|
+
}
|
|
37
|
+
const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
|
|
38
|
+
if (resolved === 'claude') {
|
|
39
|
+
return node_path_1.default.join(__dirname, '..', '..', '..', 'agents');
|
|
40
|
+
}
|
|
41
|
+
return node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(resolved), 'agents');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check which GSD agents are installed on disk.
|
|
45
|
+
*
|
|
46
|
+
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
|
|
47
|
+
*/
|
|
48
|
+
function checkAgentsInstalled(runtime) {
|
|
49
|
+
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
|
|
50
|
+
const agentsDir = getAgentsDir(resolvedRuntime);
|
|
51
|
+
const expectedAgents = Object.keys(MODEL_PROFILES);
|
|
52
|
+
const installed = [];
|
|
53
|
+
const missing = [];
|
|
54
|
+
if (!node_fs_1.default.existsSync(agentsDir)) {
|
|
55
|
+
return {
|
|
56
|
+
agents_installed: false,
|
|
57
|
+
missing_agents: expectedAgents,
|
|
58
|
+
installed_agents: [],
|
|
59
|
+
incomplete_agents: [],
|
|
60
|
+
agents_dir: agentsDir,
|
|
61
|
+
agent_runtime: resolvedRuntime,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
for (const agent of expectedAgents) {
|
|
65
|
+
const agentFile = node_path_1.default.join(agentsDir, `${agent}.md`);
|
|
66
|
+
const agentFileCopilot = node_path_1.default.join(agentsDir, `${agent}.agent.md`);
|
|
67
|
+
const agentFileCodex = node_path_1.default.join(agentsDir, `${agent}.toml`);
|
|
68
|
+
const agentFileKimiYaml = node_path_1.default.join(agentsDir, 'subagents', `${agent}.yaml`);
|
|
69
|
+
const agentFileKimiPrompt = node_path_1.default.join(agentsDir, 'subagents', `${agent}.md`);
|
|
70
|
+
const kimiAgentInstalled = resolvedRuntime === 'kimi' &&
|
|
71
|
+
node_fs_1.default.existsSync(agentFileKimiYaml) &&
|
|
72
|
+
node_fs_1.default.existsSync(agentFileKimiPrompt);
|
|
73
|
+
if (node_fs_1.default.existsSync(agentFile) ||
|
|
74
|
+
node_fs_1.default.existsSync(agentFileCopilot) ||
|
|
75
|
+
node_fs_1.default.existsSync(agentFileCodex) ||
|
|
76
|
+
kimiAgentInstalled) {
|
|
77
|
+
installed.push(agent);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
missing.push(agent);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Manifest-backed completeness check ──────────────────────────────────────
|
|
84
|
+
// If a gsd-file-manifest.json exists alongside the agents dir (parent dir),
|
|
85
|
+
// verify that every manifest-tracked file for each expected agent is present
|
|
86
|
+
// on disk. Missing manifest-tracked files indicate an incomplete install even
|
|
87
|
+
// when the plain presence check above passed (e.g. .md present, .toml absent).
|
|
88
|
+
// If no manifest is found the check is a no-op (graceful for claude/bundled).
|
|
89
|
+
const incomplete = [];
|
|
90
|
+
const manifestPath = node_path_1.default.join(node_path_1.default.dirname(agentsDir), 'gsd-file-manifest.json');
|
|
91
|
+
let manifestFiles = {};
|
|
92
|
+
try {
|
|
93
|
+
const raw = node_fs_1.default.readFileSync(manifestPath, 'utf8');
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (parsed !== null &&
|
|
96
|
+
typeof parsed === 'object' &&
|
|
97
|
+
'files' in parsed &&
|
|
98
|
+
typeof parsed['files'] === 'object' &&
|
|
99
|
+
parsed['files'] !== null) {
|
|
100
|
+
manifestFiles = parsed['files'];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// No manifest or unreadable — completeness check is skipped
|
|
105
|
+
}
|
|
106
|
+
if (Object.keys(manifestFiles).length > 0) {
|
|
107
|
+
for (const agent of expectedAgents) {
|
|
108
|
+
// Find all manifest keys that belong to this agent:
|
|
109
|
+
// key must be "agents/<agentName>.<ext>" with no further path segments.
|
|
110
|
+
const agentPrefix = `agents/${agent}.`;
|
|
111
|
+
const agentManifestKeys = Object.keys(manifestFiles).filter(key => {
|
|
112
|
+
if (!key.startsWith(agentPrefix))
|
|
113
|
+
return false;
|
|
114
|
+
const rest = key.slice(agentPrefix.length);
|
|
115
|
+
// rest must be a bare extension (no slashes, non-empty)
|
|
116
|
+
return rest.length > 0 && !rest.includes('/');
|
|
117
|
+
});
|
|
118
|
+
if (agentManifestKeys.length === 0) {
|
|
119
|
+
// Agent not tracked in manifest — skip completeness check for this agent
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const allPresent = agentManifestKeys.every(key => {
|
|
123
|
+
const basename = key.slice('agents/'.length);
|
|
124
|
+
return node_fs_1.default.existsSync(node_path_1.default.join(agentsDir, basename));
|
|
125
|
+
});
|
|
126
|
+
if (!allPresent) {
|
|
127
|
+
incomplete.push(agent);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
agents_installed: installed.length > 0 && missing.length === 0 && incomplete.length === 0,
|
|
133
|
+
missing_agents: missing,
|
|
134
|
+
installed_agents: installed,
|
|
135
|
+
incomplete_agents: incomplete,
|
|
136
|
+
agents_dir: agentsDir,
|
|
137
|
+
agent_runtime: resolvedRuntime,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
module.exports = {
|
|
141
|
+
getAgentsDir,
|
|
142
|
+
checkAgentsInstalled,
|
|
143
|
+
};
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* inline case arms which each required only their own module).
|
|
25
25
|
*/
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
-
const
|
|
27
|
+
const io = require("./io.cjs");
|
|
28
28
|
// ─── routeAuditUat ────────────────────────────────────────────────────────────
|
|
29
29
|
function routeAuditUat({ args, cwd, raw, error, _uat }) {
|
|
30
30
|
// Suppress unused-variable warnings for args/error — this command has no
|
|
@@ -42,16 +42,16 @@ function routeAuditOpen({ args, cwd, raw, error, _audit, _core }) {
|
|
|
42
42
|
void error;
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
44
44
|
const a = _audit ?? require('./audit.cjs');
|
|
45
|
-
const c = _core ??
|
|
45
|
+
const c = _core ?? io;
|
|
46
46
|
const wantJson = args.includes('--json');
|
|
47
47
|
const result = a.auditOpenArtifacts(cwd);
|
|
48
48
|
if (wantJson) {
|
|
49
|
-
//
|
|
49
|
+
// io.output JSON-stringifies its first arg; pass the object directly.
|
|
50
50
|
c.output(result, raw);
|
|
51
51
|
}
|
|
52
52
|
else {
|
|
53
53
|
// Human-readable report must bypass JSON encoding — use the rawValue
|
|
54
|
-
// form (third arg) which
|
|
54
|
+
// form (third arg) which io.output emits verbatim.
|
|
55
55
|
c.output(null, true, a.formatAuditReport(result));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -55,32 +55,59 @@ function _readRawConfigKey(filePath, dotKey) {
|
|
|
55
55
|
return { found: false, value: undefined };
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the raw value for a dotted config key using the four-level precedence
|
|
60
|
+
* walk. Returns { found, value } with the RAW value (not coerced to boolean),
|
|
61
|
+
* so callers can decide how to interpret the value (boolean gate vs. raw config
|
|
62
|
+
* value for numeric/string settings like security_asvs_level).
|
|
63
|
+
*
|
|
64
|
+
* Precedence (mirrors _resolveActivationValue):
|
|
65
|
+
* 1. loadConfig result (config arg) — guarded nested-lookup.
|
|
66
|
+
* 2. Workstream config.json at planningDir(cwd)/config.json.
|
|
67
|
+
* 3. Root config.json at planningRoot(cwd)/config.json (only if path differs).
|
|
68
|
+
* 4. registry.configSchema[dotKey].default — schema default.
|
|
69
|
+
* 5. Absent → { found: false, value: undefined }.
|
|
70
|
+
*/
|
|
71
|
+
function resolveConfigKey(dotKey, opts) {
|
|
72
|
+
const { config, cwd, registry } = opts;
|
|
73
|
+
// Level 1: loadConfig result
|
|
59
74
|
const fromConfig = _getNestedConfigValue(config, dotKey);
|
|
60
75
|
if (fromConfig.found)
|
|
61
|
-
return
|
|
76
|
+
return { found: true, value: fromConfig.value };
|
|
77
|
+
// Level 2 + 3: raw config.json files (only when cwd is available)
|
|
62
78
|
if (cwd) {
|
|
63
79
|
const wsConfigPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
64
80
|
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
65
81
|
const fromWs = _readRawConfigKey(wsConfigPath, dotKey);
|
|
66
82
|
if (fromWs.found)
|
|
67
|
-
return
|
|
83
|
+
return { found: true, value: fromWs.value };
|
|
68
84
|
if (wsConfigPath !== rootConfigPath) {
|
|
69
85
|
const fromRoot = _readRawConfigKey(rootConfigPath, dotKey);
|
|
70
86
|
if (fromRoot.found)
|
|
71
|
-
return
|
|
87
|
+
return { found: true, value: fromRoot.value };
|
|
72
88
|
}
|
|
73
89
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
// Level 4: registry configSchema default
|
|
91
|
+
const schemaMap = registry['configSchema'];
|
|
92
|
+
if (schemaMap && typeof schemaMap === 'object' && !Array.isArray(schemaMap)
|
|
93
|
+
&& Object.prototype.hasOwnProperty.call(schemaMap, dotKey)) {
|
|
94
|
+
const schemaEntry = schemaMap[dotKey];
|
|
95
|
+
if (schemaEntry && typeof schemaEntry === 'object' && schemaEntry !== null) {
|
|
96
|
+
const def = schemaEntry['default'];
|
|
97
|
+
if (def !== undefined)
|
|
98
|
+
return { found: true, value: def };
|
|
99
|
+
}
|
|
79
100
|
}
|
|
80
|
-
|
|
101
|
+
// Level 5: absent
|
|
102
|
+
return { found: false, value: undefined };
|
|
103
|
+
}
|
|
104
|
+
function _resolveActivationValue(dotKey, config, cwd, registry) {
|
|
105
|
+
const r = resolveConfigKey(dotKey, { config, cwd, registry });
|
|
106
|
+
return r.found ? Boolean(r.value) : false;
|
|
81
107
|
}
|
|
82
108
|
module.exports = {
|
|
83
109
|
_getNestedConfigValue,
|
|
84
110
|
_readRawConfigKey,
|
|
85
111
|
_resolveActivationValue,
|
|
112
|
+
resolveConfigKey,
|
|
86
113
|
};
|
|
@@ -762,6 +762,7 @@ const capabilities = {
|
|
|
762
762
|
"graphify"
|
|
763
763
|
],
|
|
764
764
|
"agents": [],
|
|
765
|
+
"activationKey": "graphify.enabled",
|
|
765
766
|
"config": {
|
|
766
767
|
"graphify.enabled": {
|
|
767
768
|
"type": "boolean",
|
|
@@ -845,6 +846,7 @@ const capabilities = {
|
|
|
845
846
|
},
|
|
846
847
|
"skills": [],
|
|
847
848
|
"agents": [],
|
|
849
|
+
"activationKey": "intel.enabled",
|
|
848
850
|
"config": {
|
|
849
851
|
"intel.enabled": {
|
|
850
852
|
"type": "boolean",
|
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
* pure, config-only resolution with no filesystem I/O.
|
|
19
19
|
* cmdCapabilityState is the I/O handler.
|
|
20
20
|
*
|
|
21
|
-
* Dependencies (leaf modules only — no
|
|
21
|
+
* Dependencies (leaf modules only — no circular risk):
|
|
22
22
|
* - node:path
|
|
23
|
-
* - ./
|
|
23
|
+
* - ./io.cjs (output, error)
|
|
24
24
|
* - ./capability-activation.cjs (_resolveActivationValue)
|
|
25
25
|
* - ./install-profiles.cjs (readActiveProfile, loadSkillsManifest, resolveProfile)
|
|
26
26
|
* - ./surface.cjs (resolveSurface)
|
|
27
27
|
* - ./config-loader.cjs (loadConfig)
|
|
28
28
|
* - ./runtime-homes.cjs (getGlobalConfigDir — for runtimeConfigDir auto-detection)
|
|
29
|
+
* - ./runtime-slash.cjs (resolveRuntime — GSD_RUNTIME > config.runtime > 'claude' precedence)
|
|
29
30
|
* - capability-registry.cjs (loaded at call time)
|
|
30
31
|
*/
|
|
31
32
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
@@ -34,8 +35,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
34
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
35
36
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
36
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
-
const
|
|
38
|
-
const { output: coreOutput, error: coreError } =
|
|
38
|
+
const ioMod = require("./io.cjs");
|
|
39
|
+
const { output: coreOutput, error: coreError } = ioMod;
|
|
39
40
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
41
|
const activationMod = require("./capability-activation.cjs");
|
|
41
42
|
const { _resolveActivationValue } = activationMod;
|
|
@@ -135,6 +136,19 @@ function resolveCapabilityState(input) {
|
|
|
135
136
|
surfaced = skills.every((s) => surfacedSkills.has(s));
|
|
136
137
|
}
|
|
137
138
|
const enabled = installed && surfaced;
|
|
139
|
+
// ── per-capability config activation ──────────────────────────────────────
|
|
140
|
+
// Resolve the capability's own activationKey (if present). This is the
|
|
141
|
+
// config-level toggle that gates the whole capability — separate from the
|
|
142
|
+
// per-hook `when` keys that gate individual hooks. When activationKey is
|
|
143
|
+
// absent, configActivation defaults to true (no config gate on the cap).
|
|
144
|
+
// active = enabled && configActivation (enabled unchanged: installed && surfaced)
|
|
145
|
+
const activationKey = typeof capObj['activationKey'] === 'string' && capObj['activationKey'].length > 0
|
|
146
|
+
? capObj['activationKey']
|
|
147
|
+
: undefined;
|
|
148
|
+
const configActivation = activationKey !== undefined
|
|
149
|
+
? _resolveActivationValue(activationKey, config, cwd, registry)
|
|
150
|
+
: true;
|
|
151
|
+
const active = enabled && configActivation;
|
|
138
152
|
// ── hooks ──────────────────────────────────────────────────────────────────
|
|
139
153
|
// Collect from steps, gates, contributions. Each may have a `when` key.
|
|
140
154
|
// Activation semantics (mirrors loop-resolver.isActive exactly):
|
|
@@ -165,7 +179,12 @@ function resolveCapabilityState(input) {
|
|
|
165
179
|
// (mirrors loop-resolver.isActive: `typeof when !== 'string' || when.length === 0` → false)
|
|
166
180
|
configured = false;
|
|
167
181
|
}
|
|
168
|
-
|
|
182
|
+
// Hook active = capability-level active AND hook's own config gate.
|
|
183
|
+
// The capability's `active` constant (= enabled && configActivation) is
|
|
184
|
+
// used here so that a config-disabled capability (active=false) cannot
|
|
185
|
+
// produce active hooks even when the hook's own `when` is unconditional
|
|
186
|
+
// (configured=true). The capability gate cascades to all its hooks.
|
|
187
|
+
hooks.push({ point, kind, when: whenRaw, configured, active: active && configured });
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
const stepsRaw = capObj['steps'];
|
|
@@ -174,7 +193,7 @@ function resolveCapabilityState(input) {
|
|
|
174
193
|
processHooks(Array.isArray(stepsRaw) ? stepsRaw : [], 'step');
|
|
175
194
|
processHooks(Array.isArray(gatesRaw) ? gatesRaw : [], 'gate');
|
|
176
195
|
processHooks(Array.isArray(contributionsRaw) ? contributionsRaw : [], 'contribution');
|
|
177
|
-
results.push({ id: capId, tier, skills, installed, surfaced, enabled, hooks });
|
|
196
|
+
results.push({ id: capId, tier, skills, installed, surfaced, enabled, active, hooks });
|
|
178
197
|
}
|
|
179
198
|
// Deterministic sort by id for stable output across calls
|
|
180
199
|
results.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
@@ -283,16 +302,20 @@ function _resolveManifest(commandsGsdDir, configDir) {
|
|
|
283
302
|
}
|
|
284
303
|
/**
|
|
285
304
|
* Command entry point: resolve install profile, surface, and config; compute
|
|
286
|
-
* capability state; emit the envelope via
|
|
305
|
+
* capability state; emit the envelope via io.output.
|
|
287
306
|
*
|
|
288
307
|
* Envelope: { runtimeConfigDir, warnings?: string[], capabilities: CapabilityStateEntry[] }
|
|
289
308
|
*
|
|
290
309
|
* runtimeConfigDir resolution (when not provided or empty):
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
310
|
+
* Detects the active runtime via the canonical precedence:
|
|
311
|
+
* process.env.GSD_RUNTIME → config.runtime → 'claude'
|
|
312
|
+
* (using resolveRuntime() from runtime-slash.cjs, the same precedence used
|
|
313
|
+
* by profile-output.cjs and the rest of the runtime resolution chain).
|
|
314
|
+
* Then calls getGlobalConfigDir(detectedRuntime) from runtime-homes.cjs —
|
|
315
|
+
* the same resolver used by install.js. This correctly handles all supported
|
|
316
|
+
* runtimes (claude, codex, cursor, gemini, opencode, grok, etc.) and their
|
|
317
|
+
* env-var overrides (CLAUDE_CONFIG_DIR, CODEX_HOME, CURSOR_CONFIG_DIR, …).
|
|
318
|
+
* Defaults to ~/.claude if either resolver throws.
|
|
296
319
|
*
|
|
297
320
|
* Failure surfacing: genuine resolution failures (manifest/profile/surface
|
|
298
321
|
* errors) are reported in the `warnings` array in the envelope. The output
|
|
@@ -308,15 +331,18 @@ function _resolveManifest(commandsGsdDir, configDir) {
|
|
|
308
331
|
* Providing a value without a next token (e.g. the flag
|
|
309
332
|
* is last in argv with no following value) should be
|
|
310
333
|
* caught by the caller before invoking this function.
|
|
311
|
-
* @param raw Whether to emit raw JSON (
|
|
334
|
+
* @param raw Whether to emit raw JSON (io.output raw mode)
|
|
312
335
|
* @param _options Reserved for future use
|
|
313
336
|
*/
|
|
314
|
-
function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
|
|
337
|
+
function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) {
|
|
315
338
|
const warnings = [];
|
|
316
339
|
// Resolve runtimeConfigDir using the canonical runtime-homes resolver.
|
|
317
|
-
// When not provided,
|
|
318
|
-
//
|
|
319
|
-
//
|
|
340
|
+
// When not provided, the active runtime is detected via the canonical
|
|
341
|
+
// precedence: process.env.GSD_RUNTIME → config.runtime → 'claude'
|
|
342
|
+
// (mirrors resolveRuntime() from runtime-slash.cjs and the precedence used
|
|
343
|
+
// by profile-output.cjs and the rest of the runtime resolution chain).
|
|
344
|
+
// getGlobalConfigDir(detectedRuntime) is then called, which honours the
|
|
345
|
+
// runtime-specific env-var override (CLAUDE_CONFIG_DIR, CODEX_HOME,
|
|
320
346
|
// CURSOR_CONFIG_DIR, GROK_AGENTS_HOME, etc.) correctly and without
|
|
321
347
|
// fabricating env vars that don't exist upstream.
|
|
322
348
|
let resolvedConfigDir = runtimeConfigDir || '';
|
|
@@ -324,13 +350,13 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
|
|
|
324
350
|
try {
|
|
325
351
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
326
352
|
const runtimeHomes = require('./runtime-homes.cjs');
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(
|
|
353
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
354
|
+
const runtimeSlash = require('./runtime-slash.cjs');
|
|
355
|
+
// Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
|
|
356
|
+
// resolveRuntime reads config.json directly (no side effects) and returns
|
|
357
|
+
// a lowercased canonical runtime name.
|
|
358
|
+
const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
|
|
359
|
+
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime);
|
|
334
360
|
}
|
|
335
361
|
catch {
|
|
336
362
|
// Defensive fallback: use ~/.claude if the canonical resolver throws.
|
|
@@ -392,12 +418,20 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
|
|
|
392
418
|
surfacedSkills = new Set();
|
|
393
419
|
}
|
|
394
420
|
// ── Load config ───────────────────────────────────────────────────────────────
|
|
421
|
+
// When the caller already holds a loadConfig snapshot (e.g. cmdLoopRenderHooks),
|
|
422
|
+
// accept it via configOverride so capability `active` and hook resolution
|
|
423
|
+
// share the SAME config object — single snapshot, no TOCTOU window.
|
|
395
424
|
let config;
|
|
396
|
-
|
|
397
|
-
config =
|
|
425
|
+
if (configOverride !== undefined) {
|
|
426
|
+
config = configOverride;
|
|
398
427
|
}
|
|
399
|
-
|
|
400
|
-
|
|
428
|
+
else {
|
|
429
|
+
try {
|
|
430
|
+
config = loadConfig(cwd);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
config = {};
|
|
434
|
+
}
|
|
401
435
|
}
|
|
402
436
|
// ── Resolve state ────────────────────────────────────────────────────────────
|
|
403
437
|
const result = resolveCapabilityState({
|
|
@@ -410,8 +444,6 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
|
|
|
410
444
|
return {
|
|
411
445
|
runtimeConfigDir: resolvedConfigDir,
|
|
412
446
|
warnings,
|
|
413
|
-
registry,
|
|
414
|
-
config,
|
|
415
447
|
capabilities: result.capabilities,
|
|
416
448
|
};
|
|
417
449
|
}
|
|
@@ -431,9 +463,27 @@ function cmdCapabilityState(cwd, runtimeConfigDir, raw, _options = {}) {
|
|
|
431
463
|
}
|
|
432
464
|
coreOutput(envelope, raw);
|
|
433
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Convenience predicate: returns true if the capability identified by `capId`
|
|
468
|
+
* is active (installed && surfaced && config-enabled) in the current runtime
|
|
469
|
+
* environment at `cwd`.
|
|
470
|
+
*
|
|
471
|
+
* Internally calls `resolveCapabilityRuntimeState(cwd, undefined)` and returns
|
|
472
|
+
* the `active` field of the matching CapabilityStateEntry.
|
|
473
|
+
* Returns `false` when the capability is not found in the registry.
|
|
474
|
+
*
|
|
475
|
+
* @param capId Capability identifier (e.g. 'graphify', 'intel')
|
|
476
|
+
* @param cwd Project root directory for config resolution
|
|
477
|
+
*/
|
|
478
|
+
function isCapabilityActive(capId, cwd) {
|
|
479
|
+
const result = resolveCapabilityRuntimeState(cwd, undefined);
|
|
480
|
+
const entry = result.capabilities.find((c) => c.id === capId);
|
|
481
|
+
return entry !== undefined ? entry.active : false;
|
|
482
|
+
}
|
|
434
483
|
module.exports = {
|
|
435
484
|
resolveCapabilityState,
|
|
436
485
|
resolveCapabilityRuntimeState,
|
|
486
|
+
isCapabilityActive,
|
|
437
487
|
cmdCapabilityState,
|
|
438
488
|
// Exported for tests
|
|
439
489
|
_resolveCommandsGsdDir,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - re-resolve: always calls resolveCapabilityRuntimeState for the return value
|
|
15
15
|
*
|
|
16
16
|
* Dependencies (leaf modules only — no circular risk):
|
|
17
|
-
* - ./
|
|
17
|
+
* - ./io.cjs (output, error)
|
|
18
18
|
* - ./capability-state.cjs (resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir)
|
|
19
19
|
* - ./surface.cjs (readSurface, writeSurface, applySurface)
|
|
20
20
|
* - ./install-profiles.cjs (readActiveProfile)
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* - capability-registry.cjs (loaded at call time)
|
|
24
24
|
*/
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
-
const
|
|
27
|
-
const { output: coreOutput, error: coreError } =
|
|
26
|
+
const ioMod = require("./io.cjs");
|
|
27
|
+
const { output: coreOutput, error: coreError } = ioMod;
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
29
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
30
30
|
const { resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir } = capabilityStateMod;
|
|
@@ -71,7 +71,8 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
71
71
|
const before = resolveCapabilityRuntimeState(cwd, runtimeConfigDir);
|
|
72
72
|
const resolvedConfigDir = before.runtimeConfigDir;
|
|
73
73
|
// ── Load registry ─────────────────────────────────────────────────────────
|
|
74
|
-
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const registry = require('./capability-registry.cjs');
|
|
75
76
|
const capabilitiesMap = (registry['capabilities'] && typeof registry['capabilities'] === 'object' && !Array.isArray(registry['capabilities'])
|
|
76
77
|
? registry['capabilities']
|
|
77
78
|
: {});
|