@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.3
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-mempalace-curator.md +47 -0
- package/bin/install.js +25 -7
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +110 -92
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +31 -18
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +1 -2
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/init.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -1
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +71 -11
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +221 -35
- package/gsd-core/bin/lib/verify.cjs +19 -4
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +82 -143
- package/gsd-core/workflows/help/modes/full.md +3 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/plan-phase.md +89 -221
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +9 -6
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/ship.md +57 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-work.md +27 -11
- package/package.json +3 -2
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/gen-capability-registry.cjs +359 -48
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +8 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/run-tests.cjs +74 -14
|
@@ -16,14 +16,43 @@
|
|
|
16
16
|
* the prior hand-written .cjs; only types are added.
|
|
17
17
|
*/
|
|
18
18
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
+
const capabilityRegistry = require('./capability-registry.cjs');
|
|
21
|
+
function isCapabilityConfigKey(keyPath) {
|
|
22
|
+
if (typeof keyPath !== 'string')
|
|
23
|
+
return false;
|
|
24
|
+
const schema = capabilityRegistry.configSchema;
|
|
25
|
+
if (!schema || typeof schema !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
return Object.prototype.hasOwnProperty.call(schema, keyPath);
|
|
28
|
+
}
|
|
19
29
|
/**
|
|
20
|
-
* Returns true
|
|
30
|
+
* Returns true for keys owned by the central schema adapter rather than a
|
|
31
|
+
* federated Capability config slice.
|
|
21
32
|
*/
|
|
22
|
-
function
|
|
33
|
+
function isCentralConfigKey(keyPath) {
|
|
34
|
+
if (typeof keyPath !== 'string')
|
|
35
|
+
return false;
|
|
23
36
|
if (configuration_cjs_1.VALID_CONFIG_KEYS.has(keyPath))
|
|
24
37
|
return true;
|
|
25
38
|
if (configuration_cjs_1.RUNTIME_STATE_KEYS.has(keyPath))
|
|
26
39
|
return true;
|
|
27
40
|
return configuration_cjs_1.DYNAMIC_KEY_PATTERNS.some((p) => p.test(keyPath));
|
|
28
41
|
}
|
|
29
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if keyPath is a valid central, runtime-state, dynamic, or
|
|
44
|
+
* federated Capability config key.
|
|
45
|
+
*/
|
|
46
|
+
function isValidConfigKey(keyPath) {
|
|
47
|
+
if (isCentralConfigKey(keyPath))
|
|
48
|
+
return true;
|
|
49
|
+
return isCapabilityConfigKey(keyPath);
|
|
50
|
+
}
|
|
51
|
+
module.exports = {
|
|
52
|
+
VALID_CONFIG_KEYS: configuration_cjs_1.VALID_CONFIG_KEYS,
|
|
53
|
+
RUNTIME_STATE_KEYS: configuration_cjs_1.RUNTIME_STATE_KEYS,
|
|
54
|
+
DYNAMIC_KEY_PATTERNS: configuration_cjs_1.DYNAMIC_KEY_PATTERNS,
|
|
55
|
+
isCapabilityConfigKey,
|
|
56
|
+
isCentralConfigKey,
|
|
57
|
+
isValidConfigKey,
|
|
58
|
+
};
|
|
@@ -206,7 +206,6 @@ function buildNewProjectConfig(userChoices) {
|
|
|
206
206
|
ui_phase: true,
|
|
207
207
|
ui_safety_gate: true,
|
|
208
208
|
ai_integration_phase: true,
|
|
209
|
-
tdd_mode: false,
|
|
210
209
|
human_verify_mode: 'end-of-phase',
|
|
211
210
|
text_mode: false,
|
|
212
211
|
research_before_questions: false,
|
|
@@ -234,7 +233,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
234
233
|
project_code: null,
|
|
235
234
|
phase_naming: 'sequential',
|
|
236
235
|
agent_skills: {},
|
|
237
|
-
claude_md_path: '
|
|
236
|
+
claude_md_path: './.claude/CLAUDE.md',
|
|
238
237
|
plan_review: {
|
|
239
238
|
source_grounding: true,
|
|
240
239
|
source_grounding_authority: 'grep',
|
|
@@ -47,7 +47,7 @@ const { loadConfig, isGitIgnored, CONFIG_DEFAULTS, _warnUnknownProfileOverrides,
|
|
|
47
47
|
// ─── Model Resolver Module (extracted from core, ADR-857 phase 2f / #888) ────
|
|
48
48
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
49
|
const modelResolverModule = require("./model-resolver.cjs");
|
|
50
|
-
const { resolveTierEntry, resolveModelPolicy, resolveModelInternal, VALID_GRANULARITIES, resolveGranularityInternal, assertValidGranularityOverride, resolveModelForTier, VALID_EFFORTS, EFFORT_SET, nextEffort, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, } = modelResolverModule;
|
|
50
|
+
const { resolveTierEntry, resolveModelPolicy, resolveModelInternal, VALID_GRANULARITIES, resolveGranularityInternal, assertValidGranularityOverride, resolveModelForTier, VALID_EFFORTS, EFFORT_SET, nextEffort, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, _resetModelPolicyWarningCacheForTests, } = modelResolverModule;
|
|
51
51
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
52
52
|
// toPosixPath and detectSubRepos moved to core-utils.cjs (ADR-857 phase 2c / #877).
|
|
53
53
|
// The destructured bindings above (from coreUtilsModule) make them available to
|
|
@@ -305,7 +305,10 @@ module.exports = {
|
|
|
305
305
|
resolveTierEntry,
|
|
306
306
|
resolveModelPolicy,
|
|
307
307
|
KNOWN_PROVIDERS: model_catalog_cjs_1.KNOWN_PROVIDERS,
|
|
308
|
-
_resetRuntimeWarningCacheForTests
|
|
308
|
+
_resetRuntimeWarningCacheForTests: () => {
|
|
309
|
+
_resetRuntimeWarningCacheForTests();
|
|
310
|
+
_resetModelPolicyWarningCacheForTests();
|
|
311
|
+
},
|
|
309
312
|
pathExistsInternal,
|
|
310
313
|
gitWorktreeInfoInternal,
|
|
311
314
|
generateSlugInternal,
|
|
@@ -182,10 +182,11 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
182
182
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
183
183
|
: null;
|
|
184
184
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
185
|
+
const wf = (config.workflow ?? {});
|
|
185
186
|
const result = {
|
|
186
187
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
187
188
|
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
|
|
188
|
-
tdd_mode: options['tdd'] ||
|
|
189
|
+
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
189
190
|
commit_docs: config.commit_docs,
|
|
190
191
|
sub_repos: config.sub_repos,
|
|
191
192
|
parallelization: config.parallelization,
|
|
@@ -300,15 +301,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
300
301
|
const granularityOverride = options['granularity'];
|
|
301
302
|
assertValidGranularityOverride(granularityOverride, error);
|
|
302
303
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
304
|
+
const wf = (config.workflow ?? {});
|
|
303
305
|
const result = {
|
|
304
306
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
305
307
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
306
308
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
307
|
-
tdd_mode: options['tdd'] ||
|
|
309
|
+
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
308
310
|
granularity,
|
|
309
|
-
research_enabled:
|
|
311
|
+
research_enabled: wf['research'],
|
|
310
312
|
plan_checker_enabled: config.plan_checker,
|
|
311
|
-
nyquist_validation_enabled:
|
|
313
|
+
nyquist_validation_enabled: wf['nyquist_validation'],
|
|
312
314
|
commit_docs: config.commit_docs,
|
|
313
315
|
text_mode: config.text_mode,
|
|
314
316
|
auto_advance: !!(config.auto_advance),
|
|
@@ -507,12 +509,13 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
507
509
|
catch {
|
|
508
510
|
/* intentionally empty */
|
|
509
511
|
}
|
|
512
|
+
const wf = (config.workflow ?? {});
|
|
510
513
|
const result = {
|
|
511
514
|
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
|
|
512
515
|
synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
|
|
513
516
|
roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
|
|
514
517
|
commit_docs: config.commit_docs,
|
|
515
|
-
research_enabled:
|
|
518
|
+
research_enabled: wf['research'],
|
|
516
519
|
current_milestone: milestone['version'],
|
|
517
520
|
current_milestone_name: milestone['name'],
|
|
518
521
|
latest_completed_milestone: latestCompleted?.version || null,
|
|
@@ -111,7 +111,8 @@ function parseCallsAgents(content) {
|
|
|
111
111
|
* `gsd-*` agent name references. Agent stems are stored under the special
|
|
112
112
|
* key `_calls_agents_<stem>` so they don't conflict with skill stems.
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
const DEFAULT_COMMANDS_DIR = node_path_1.default.resolve(__dirname, '..', '..', '..', 'commands', 'gsd');
|
|
115
|
+
function loadSkillsManifest(commandsDir = DEFAULT_COMMANDS_DIR) {
|
|
115
116
|
const manifest = new Map();
|
|
116
117
|
if (!node_fs_1.default.existsSync(commandsDir))
|
|
117
118
|
return manifest;
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* filters the materialized Capability Registry by config activation and returns
|
|
7
7
|
* the active hooks as a JSON envelope with a rendered-markdown field.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Consumed live by the landed phase-6 loop-hook cutovers: plan-phase.md / autonomous.md
|
|
10
|
+
* at plan:pre (ui-phase) and autonomous.md at verify:post (ui-review). Further per-feature
|
|
11
|
+
* cutovers are ongoing.
|
|
10
12
|
*
|
|
11
13
|
* Command surface: gsd-tools loop render-hooks <point>
|
|
12
14
|
*
|
|
@@ -38,6 +40,9 @@ const { output: coreOutput, error: coreError } = core;
|
|
|
38
40
|
const configLoaderModule = require("./config-loader.cjs");
|
|
39
41
|
const { loadConfig } = configLoaderModule;
|
|
40
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const capabilityStateModule = require("./capability-state.cjs");
|
|
44
|
+
const { resolveCapabilityRuntimeState } = capabilityStateModule;
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
46
|
const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
42
47
|
const { planningDir, planningRoot } = planningWorkspaceMod;
|
|
43
48
|
// ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
|
|
@@ -213,7 +218,7 @@ function _resolveActivationValue(dotKey, config, cwd, registry) {
|
|
|
213
218
|
* resolved against `config`; active iff truthy. Inactive hooks are filtered out.
|
|
214
219
|
*/
|
|
215
220
|
function resolveLoopHooks(input) {
|
|
216
|
-
const { point, registry, config, cwd } = input;
|
|
221
|
+
const { point, registry, config, cwd, capabilityStatesById } = input;
|
|
217
222
|
// Validate point
|
|
218
223
|
const canonicalPoints = _getCanonicalPoints(registry);
|
|
219
224
|
if (!canonicalPoints.includes(point)) {
|
|
@@ -243,31 +248,112 @@ function resolveLoopHooks(input) {
|
|
|
243
248
|
return false;
|
|
244
249
|
return _resolveActivationValue(when, config, cwd, registry);
|
|
245
250
|
}
|
|
251
|
+
function isCapabilityEnabled(capId) {
|
|
252
|
+
if (!capabilityStatesById)
|
|
253
|
+
return true;
|
|
254
|
+
const state = capabilityStatesById instanceof Map
|
|
255
|
+
? capabilityStatesById.get(capId)
|
|
256
|
+
: capabilityStatesById[capId];
|
|
257
|
+
if (!state)
|
|
258
|
+
return false;
|
|
259
|
+
return state.enabled !== false;
|
|
260
|
+
}
|
|
246
261
|
// Helper: safe string array
|
|
247
262
|
function toStringArray(v) {
|
|
248
263
|
if (!Array.isArray(v))
|
|
249
264
|
return [];
|
|
250
265
|
return v.filter((x) => typeof x === 'string');
|
|
251
266
|
}
|
|
267
|
+
function toFragment(v) {
|
|
268
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
269
|
+
return undefined;
|
|
270
|
+
const raw = v;
|
|
271
|
+
const fragment = {};
|
|
272
|
+
if (typeof raw.inline === 'string')
|
|
273
|
+
fragment.inline = raw.inline;
|
|
274
|
+
if (typeof raw.path === 'string')
|
|
275
|
+
fragment.path = raw.path;
|
|
276
|
+
return Object.keys(fragment).length > 0 ? fragment : undefined;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve declared configValues for a contribution hook.
|
|
280
|
+
* The hook may carry `configValues: { alias: "dotted.key", ... }`.
|
|
281
|
+
* Each key is resolved using the same four-level precedence as activation resolution,
|
|
282
|
+
* but returning the raw value (not coerced to boolean) so numeric/string config values
|
|
283
|
+
* are preserved (e.g. security_asvs_level: 2, security_block_on: "medium").
|
|
284
|
+
*/
|
|
285
|
+
function resolveConfigValues(hook) {
|
|
286
|
+
const raw = hook['configValues'];
|
|
287
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
288
|
+
return undefined;
|
|
289
|
+
const rawMap = raw;
|
|
290
|
+
const resolved = {};
|
|
291
|
+
for (const [alias, dotKey] of Object.entries(rawMap)) {
|
|
292
|
+
// Prototype-pollution guard (inline literal, CodeQL barrier)
|
|
293
|
+
if (alias === '__proto__' || alias === 'constructor' || alias === 'prototype')
|
|
294
|
+
continue;
|
|
295
|
+
if (typeof dotKey !== 'string')
|
|
296
|
+
continue;
|
|
297
|
+
// Level 1: loadConfig result
|
|
298
|
+
const fromConfig = _getNestedConfigValue(config, dotKey);
|
|
299
|
+
if (fromConfig.found) {
|
|
300
|
+
resolved[alias] = fromConfig.value;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Level 2 + 3: raw config.json files
|
|
304
|
+
if (cwd) {
|
|
305
|
+
const wsConfigPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
306
|
+
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
307
|
+
const fromWs = _readRawConfigKey(wsConfigPath, dotKey);
|
|
308
|
+
if (fromWs.found) {
|
|
309
|
+
resolved[alias] = fromWs.value;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (wsConfigPath !== rootConfigPath) {
|
|
313
|
+
const fromRoot = _readRawConfigKey(rootConfigPath, dotKey);
|
|
314
|
+
if (fromRoot.found) {
|
|
315
|
+
resolved[alias] = fromRoot.value;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Level 4: registry configSchema default
|
|
321
|
+
const schemaEntry = registry['configSchema']?.[dotKey];
|
|
322
|
+
if (schemaEntry && typeof schemaEntry === 'object' && schemaEntry !== null) {
|
|
323
|
+
const def = schemaEntry['default'];
|
|
324
|
+
if (def !== undefined) {
|
|
325
|
+
resolved[alias] = def;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Level 5: absent → undefined (omit from resolved map)
|
|
330
|
+
}
|
|
331
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
332
|
+
}
|
|
252
333
|
// Process steps
|
|
253
334
|
const stepsRaw = entryMap['steps'];
|
|
254
335
|
const steps = Array.isArray(stepsRaw) ? stepsRaw : [];
|
|
255
336
|
for (const hook of steps) {
|
|
256
337
|
if (!hook || typeof hook !== 'object')
|
|
257
338
|
continue;
|
|
339
|
+
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
340
|
+
if (!isCapabilityEnabled(capId))
|
|
341
|
+
continue;
|
|
258
342
|
if (!isActive(hook))
|
|
259
343
|
continue;
|
|
260
|
-
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
261
344
|
const ref = (typeof hook['ref'] === 'object' && hook['ref'] !== null)
|
|
262
345
|
? hook['ref']
|
|
263
346
|
: undefined;
|
|
264
347
|
const when = typeof hook['when'] === 'string' ? hook['when'] : undefined;
|
|
348
|
+
const fragment = toFragment(hook['fragment']);
|
|
265
349
|
const produces = toStringArray(hook['produces']);
|
|
266
350
|
const consumes = toStringArray(hook['consumes']);
|
|
267
351
|
const onError = typeof hook['onError'] === 'string' ? hook['onError'] : undefined;
|
|
268
352
|
const active = { capId, kind: 'step' };
|
|
269
353
|
if (ref !== undefined)
|
|
270
354
|
active.ref = ref;
|
|
355
|
+
if (fragment !== undefined)
|
|
356
|
+
active.fragment = fragment;
|
|
271
357
|
if (when !== undefined)
|
|
272
358
|
active.when = when;
|
|
273
359
|
if (produces.length > 0)
|
|
@@ -284,17 +370,23 @@ function resolveLoopHooks(input) {
|
|
|
284
370
|
for (const hook of contributions) {
|
|
285
371
|
if (!hook || typeof hook !== 'object')
|
|
286
372
|
continue;
|
|
373
|
+
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
374
|
+
if (!isCapabilityEnabled(capId))
|
|
375
|
+
continue;
|
|
287
376
|
if (!isActive(hook))
|
|
288
377
|
continue;
|
|
289
|
-
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
290
378
|
const into = typeof hook['into'] === 'string' ? hook['into'] : undefined;
|
|
379
|
+
const fragment = toFragment(hook['fragment']);
|
|
291
380
|
const when = typeof hook['when'] === 'string' ? hook['when'] : undefined;
|
|
292
381
|
const produces = toStringArray(hook['produces']);
|
|
293
382
|
const consumes = toStringArray(hook['consumes']);
|
|
294
383
|
const onError = typeof hook['onError'] === 'string' ? hook['onError'] : undefined;
|
|
384
|
+
const configValuesResolved = resolveConfigValues(hook);
|
|
295
385
|
const active = { capId, kind: 'contribution' };
|
|
296
386
|
if (into !== undefined)
|
|
297
387
|
active.into = into;
|
|
388
|
+
if (fragment !== undefined)
|
|
389
|
+
active.fragment = fragment;
|
|
298
390
|
if (when !== undefined)
|
|
299
391
|
active.when = when;
|
|
300
392
|
if (produces.length > 0)
|
|
@@ -303,6 +395,8 @@ function resolveLoopHooks(input) {
|
|
|
303
395
|
active.consumes = consumes;
|
|
304
396
|
if (onError !== undefined)
|
|
305
397
|
active.onError = onError;
|
|
398
|
+
if (configValuesResolved !== undefined)
|
|
399
|
+
active.configValues = configValuesResolved;
|
|
306
400
|
activeHooks.push(active);
|
|
307
401
|
}
|
|
308
402
|
// Process gates
|
|
@@ -311,9 +405,11 @@ function resolveLoopHooks(input) {
|
|
|
311
405
|
for (const hook of gates) {
|
|
312
406
|
if (!hook || typeof hook !== 'object')
|
|
313
407
|
continue;
|
|
408
|
+
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
409
|
+
if (!isCapabilityEnabled(capId))
|
|
410
|
+
continue;
|
|
314
411
|
if (!isActive(hook))
|
|
315
412
|
continue;
|
|
316
|
-
const capId = typeof hook['capId'] === 'string' ? hook['capId'] : '';
|
|
317
413
|
const when = typeof hook['when'] === 'string' ? hook['when'] : undefined;
|
|
318
414
|
const check = hook['check'] !== undefined ? hook['check'] : undefined;
|
|
319
415
|
const blocking = typeof hook['blocking'] === 'boolean' ? hook['blocking'] : undefined;
|
|
@@ -352,7 +448,9 @@ function renderLoopHooks(resolved) {
|
|
|
352
448
|
stepOrdinal += 1;
|
|
353
449
|
const refStr = hook.ref?.skill
|
|
354
450
|
? `skill:${hook.ref.skill}`
|
|
355
|
-
:
|
|
451
|
+
: hook.ref?.agent
|
|
452
|
+
? `agent:${hook.ref.agent}`
|
|
453
|
+
: JSON.stringify(hook.ref ?? {});
|
|
356
454
|
lines.push(`### Step ${stepOrdinal}: ${refStr} (${hook.capId})`);
|
|
357
455
|
if (hook.produces && hook.produces.length > 0) {
|
|
358
456
|
lines.push(`- produces: ${hook.produces.join(', ')}`);
|
|
@@ -366,10 +464,24 @@ function renderLoopHooks(resolved) {
|
|
|
366
464
|
if (hook.onError) {
|
|
367
465
|
lines.push(`- onError: ${hook.onError}`);
|
|
368
466
|
}
|
|
467
|
+
if (hook.fragment?.inline) {
|
|
468
|
+
lines.push('');
|
|
469
|
+
lines.push(hook.fragment.inline);
|
|
470
|
+
}
|
|
471
|
+
else if (hook.fragment?.path) {
|
|
472
|
+
lines.push('');
|
|
473
|
+
lines.push(`_Step fragment path is declared but not rendered by loop-resolver: ${hook.fragment.path}_`);
|
|
474
|
+
}
|
|
369
475
|
lines.push('');
|
|
370
476
|
}
|
|
371
477
|
else if (hook.kind === 'contribution') {
|
|
372
|
-
lines.push(`<contribution from="${hook.capId}" into="${hook.into ?? '(unset)'}"
|
|
478
|
+
lines.push(`<contribution from="${hook.capId}" into="${hook.into ?? '(unset)'}">`);
|
|
479
|
+
if (hook.fragment?.inline) {
|
|
480
|
+
lines.push(hook.fragment.inline);
|
|
481
|
+
}
|
|
482
|
+
else if (hook.fragment?.path) {
|
|
483
|
+
lines.push(`_Contribution fragment path is declared but not rendered by loop-resolver: ${hook.fragment.path}_`);
|
|
484
|
+
}
|
|
373
485
|
if (hook.produces && hook.produces.length > 0) {
|
|
374
486
|
lines.push(`- produces: ${hook.produces.join(', ')}`);
|
|
375
487
|
}
|
|
@@ -379,6 +491,10 @@ function renderLoopHooks(resolved) {
|
|
|
379
491
|
if (hook.when) {
|
|
380
492
|
lines.push(`- when: \`${hook.when}\``);
|
|
381
493
|
}
|
|
494
|
+
if (hook.onError) {
|
|
495
|
+
lines.push(`- onError: ${hook.onError}`);
|
|
496
|
+
}
|
|
497
|
+
lines.push('</contribution>');
|
|
382
498
|
lines.push('');
|
|
383
499
|
}
|
|
384
500
|
else if (hook.kind === 'gate') {
|
|
@@ -417,34 +533,59 @@ function renderLoopHooks(resolved) {
|
|
|
417
533
|
* merged-object-from-untrusted-keys security concern and correctly handles
|
|
418
534
|
* pre-cutover keys like `workflow.ui_phase` that live in config.json but are not
|
|
419
535
|
* yet exposed through loadConfig's whitelist.
|
|
536
|
+
*
|
|
537
|
+
* --active-cap <capId>: when present, resolves hooks for <point> exactly as the
|
|
538
|
+
* normal path does, then prints exactly `true` (if any resolved activeHook has
|
|
539
|
+
* capId === <capId>) or `false` followed by a single newline, and exits 0.
|
|
540
|
+
* No JSON envelope is emitted — output is clean for shell $(…) capture.
|
|
541
|
+
* Missing <capId> value → coreError + non-zero exit.
|
|
542
|
+
* Unknown/inactive capId → `false` (not an error).
|
|
420
543
|
*/
|
|
421
|
-
function cmdLoopRenderHooks(cwd, point, raw,
|
|
544
|
+
function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
422
545
|
if (!point) {
|
|
423
546
|
coreError('loop render-hooks requires a <point> argument. Valid points: ' + CANONICAL_POINTS.join(', '));
|
|
424
547
|
return;
|
|
425
548
|
}
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
549
|
+
// --active-cap <capId> mode: emit 'true' or 'false' only (scanner-safe, no JSON envelope)
|
|
550
|
+
const activeCapId = typeof options['activeCap'] === 'string' ? options['activeCap'] : undefined;
|
|
551
|
+
if (activeCapId !== undefined && activeCapId === '') {
|
|
552
|
+
coreError('--active-cap requires a <capId> value (e.g. --active-cap tdd)');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const runtimeConfigDir = typeof options['configDir'] === 'string'
|
|
556
|
+
? options['configDir']
|
|
557
|
+
: undefined;
|
|
558
|
+
const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir);
|
|
559
|
+
const registry = state.registry;
|
|
560
|
+
const config = state.config || loadConfig(cwd);
|
|
561
|
+
const capabilityStatesById = new Map();
|
|
562
|
+
for (const cap of state.capabilities || []) {
|
|
563
|
+
capabilityStatesById.set(cap.id, cap);
|
|
564
|
+
}
|
|
433
565
|
let resolved;
|
|
434
566
|
try {
|
|
435
|
-
resolved = resolveLoopHooks({ point, registry, config, cwd });
|
|
567
|
+
resolved = resolveLoopHooks({ point, registry, config, cwd, capabilityStatesById });
|
|
436
568
|
}
|
|
437
569
|
catch (err) {
|
|
438
570
|
const msg = (err instanceof Error) ? err.message : String(err);
|
|
439
571
|
coreError(msg);
|
|
440
572
|
return;
|
|
441
573
|
}
|
|
574
|
+
// --active-cap mode: print exactly 'true' or 'false' with no envelope
|
|
575
|
+
if (activeCapId !== undefined) {
|
|
576
|
+
const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
|
|
577
|
+
process.stdout.write(isActive ? 'true\n' : 'false\n');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
442
580
|
const rendered = renderLoopHooks(resolved);
|
|
443
581
|
const envelope = {
|
|
444
582
|
point: resolved.point,
|
|
445
583
|
activeHooks: resolved.activeHooks,
|
|
446
584
|
rendered,
|
|
447
585
|
};
|
|
586
|
+
if (state.warnings && state.warnings.length > 0) {
|
|
587
|
+
envelope.warnings = state.warnings;
|
|
588
|
+
}
|
|
448
589
|
coreOutput(envelope, raw);
|
|
449
590
|
}
|
|
450
591
|
module.exports = {
|
|
@@ -55,6 +55,30 @@ function _resolveRuntimeTier(config, tier) {
|
|
|
55
55
|
overrides: config['model_profile_overrides'],
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
// Reverse of the Claude tier-default IDs, plus the Fable alias which Claude
|
|
59
|
+
// Code's Agent tool accepts but which is not a GSD model-profile tier (#1133).
|
|
60
|
+
const CLAUDE_POLICY_ID_TO_ALIAS = {
|
|
61
|
+
...Object.fromEntries(Object.entries(model_catalog_cjs_1.MODEL_ALIAS_MAP)
|
|
62
|
+
.filter((e) => typeof e[1] === 'string')
|
|
63
|
+
.map(([aliasName, id]) => [id, aliasName])),
|
|
64
|
+
'claude-fable-5': 'fable',
|
|
65
|
+
};
|
|
66
|
+
const CLAUDE_AGENT_ALIASES = new Set(['opus', 'sonnet', 'haiku', 'fable']);
|
|
67
|
+
// Dedupe stderr warnings so repeated agent resolutions don't spam (#1133).
|
|
68
|
+
const _modelPolicyUnmappableWarned = new Set();
|
|
69
|
+
function warnModelPolicyUnmappable(agentType, policyModel, tier) {
|
|
70
|
+
const key = `${agentType}::${policyModel}::${tier}`;
|
|
71
|
+
if (_modelPolicyUnmappableWarned.has(key))
|
|
72
|
+
return;
|
|
73
|
+
_modelPolicyUnmappableWarned.add(key);
|
|
74
|
+
// MUST go to stderr — resolve-model's JSON result is parsed from stdout.
|
|
75
|
+
process.stderr.write(`gsd: warning — model_policy resolved "${policyModel}" for ${agentType}, ` +
|
|
76
|
+
`but it has no Claude agent alias; using "${tier}" instead.\n`);
|
|
77
|
+
}
|
|
78
|
+
// Test-only: reset the model_policy warn-dedupe cache between cases (#1133).
|
|
79
|
+
function _resetModelPolicyWarningCacheForTests() {
|
|
80
|
+
_modelPolicyUnmappableWarned.clear();
|
|
81
|
+
}
|
|
58
82
|
/**
|
|
59
83
|
* #49 — Provider-neutral model policy preset resolution.
|
|
60
84
|
*/
|
|
@@ -132,15 +156,32 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
132
156
|
: (profile === 'inherit'
|
|
133
157
|
? 'inherit'
|
|
134
158
|
: (agentModels ? (agentModels[profile] || agentModels['balanced']) : null));
|
|
135
|
-
// 2.5. model_policy preset (#49)
|
|
159
|
+
// 2.5. model_policy preset (#49, #1133)
|
|
136
160
|
const configRuntime = config['runtime'];
|
|
137
|
-
if (
|
|
161
|
+
if (tier && tier !== 'inherit') {
|
|
162
|
+
const onClaude = !configRuntime || configRuntime === 'claude';
|
|
163
|
+
const effectiveRuntime = configRuntime || 'claude';
|
|
138
164
|
const mergedPolicy = config['model_policy']
|
|
139
|
-
? { ...config['model_policy'], runtime:
|
|
165
|
+
? { ...config['model_policy'], runtime: effectiveRuntime }
|
|
140
166
|
: null;
|
|
141
167
|
const policyModel = resolveModelPolicy(mergedPolicy, tier);
|
|
142
|
-
if (policyModel)
|
|
143
|
-
|
|
168
|
+
if (policyModel) {
|
|
169
|
+
// Non-Claude runtimes take full model IDs verbatim (unchanged behavior).
|
|
170
|
+
if (!onClaude)
|
|
171
|
+
return policyModel;
|
|
172
|
+
// Claude Code's Agent tool takes tier aliases (opus/sonnet/haiku/fable),
|
|
173
|
+
// not full model IDs — map the policy-resolved ID back to an alias (#1133).
|
|
174
|
+
const aliasForId = CLAUDE_POLICY_ID_TO_ALIAS[policyModel];
|
|
175
|
+
if (aliasForId)
|
|
176
|
+
return aliasForId;
|
|
177
|
+
// The policy value may already be a bare Claude agent alias (e.g. "fable").
|
|
178
|
+
if (CLAUDE_AGENT_ALIASES.has(policyModel))
|
|
179
|
+
return policyModel;
|
|
180
|
+
// No Claude alias for this ID (e.g. a pinned minor version like
|
|
181
|
+
// claude-opus-4-5). Warn once and fall through to the tier alias rather
|
|
182
|
+
// than returning an ID Claude Code cannot spawn.
|
|
183
|
+
warnModelPolicyUnmappable(agentType, policyModel, tier);
|
|
184
|
+
}
|
|
144
185
|
}
|
|
145
186
|
// 3. Runtime-aware resolution (#2517)
|
|
146
187
|
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
|
@@ -413,6 +454,7 @@ module.exports = {
|
|
|
413
454
|
resolveTierEntry,
|
|
414
455
|
resolveModelPolicy,
|
|
415
456
|
resolveModelInternal,
|
|
457
|
+
_resetModelPolicyWarningCacheForTests,
|
|
416
458
|
VALID_GRANULARITIES,
|
|
417
459
|
resolveGranularityInternal,
|
|
418
460
|
assertValidGranularityOverride,
|
|
@@ -32,6 +32,7 @@ const stateMod = require("./state.cjs");
|
|
|
32
32
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
33
33
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
34
34
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
35
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
35
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
|
36
37
|
const uatPredicate = require("./uat-predicate.cjs");
|
|
37
38
|
const { evaluateUatPassed } = uatPredicate;
|
|
@@ -1019,7 +1020,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1019
1020
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1020
1021
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1021
1022
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1022
|
-
const today =
|
|
1023
|
+
const today = clock_cjs_1.realClock.today();
|
|
1023
1024
|
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum);
|
|
1024
1025
|
if (!phaseInfoRaw) {
|
|
1025
1026
|
error(`Phase ${phaseNum} not found`);
|
|
@@ -1049,9 +1050,17 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1049
1050
|
}
|
|
1050
1051
|
for (const file of phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
1051
1052
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
1052
|
-
|
|
1053
|
+
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
|
|
1054
|
+
// from historical metadata in the file body (e.g. `previous_status: gaps_found`).
|
|
1055
|
+
// A full-text regex like /status: gaps_found/ matches the substring inside
|
|
1056
|
+
// `previous_status: gaps_found`, producing spurious warnings even when the
|
|
1057
|
+
// current frontmatter status is `passed`.
|
|
1058
|
+
const verFm = extractFrontmatter(content);
|
|
1059
|
+
// Normalise to lower-case so `status: Passed` (title-case) is not missed.
|
|
1060
|
+
const verStatus = typeof verFm['status'] === 'string' ? verFm['status'].trim().toLowerCase() : '';
|
|
1061
|
+
if (verStatus === 'human_needed')
|
|
1053
1062
|
warnings.push(`${file}: needs human verification`);
|
|
1054
|
-
if (
|
|
1063
|
+
if (verStatus === 'gaps_found')
|
|
1055
1064
|
warnings.push(`${file}: has unresolved gaps`);
|
|
1056
1065
|
}
|
|
1057
1066
|
}
|
|
@@ -1074,15 +1083,20 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1074
1083
|
const tableRowPattern = new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
1075
1084
|
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
1076
1085
|
const cells = fullRow.split('|').slice(1, -1);
|
|
1086
|
+
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
|
1077
1087
|
if (cells.length === 5) {
|
|
1078
1088
|
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
1079
1089
|
cells[3] = ' Complete ';
|
|
1080
|
-
|
|
1090
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
1091
|
+
const existingDate5 = cells[4].trim();
|
|
1092
|
+
cells[4] = dateShape.test(existingDate5) ? cells[4] : ` ${today} `;
|
|
1081
1093
|
}
|
|
1082
1094
|
else if (cells.length === 4) {
|
|
1083
1095
|
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
1084
1096
|
cells[2] = ' Complete ';
|
|
1085
|
-
|
|
1097
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
1098
|
+
const existingDate4 = cells[3].trim();
|
|
1099
|
+
cells[3] = dateShape.test(existingDate4) ? cells[3] : ` ${today} `;
|
|
1086
1100
|
}
|
|
1087
1101
|
return '|' + cells.join('|') + '|';
|
|
1088
1102
|
});
|
|
@@ -1123,13 +1137,59 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1123
1137
|
reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'), '$1 Complete $2');
|
|
1124
1138
|
}
|
|
1125
1139
|
}
|
|
1140
|
+
// #1159 (Defect B): collect requirement IDs only from ACTIVE sections.
|
|
1141
|
+
// Requirements under headings whose text contains "deferred", "backlog",
|
|
1142
|
+
// "future", or "v2" (case-insensitive) are explicitly out of current scope
|
|
1143
|
+
// and must not be flagged as missing from the Traceability table.
|
|
1144
|
+
//
|
|
1145
|
+
// Strategy: walk lines, track heading depth, and toggle a "deferred" flag
|
|
1146
|
+
// when a heading matching the pattern is encountered. A sub-heading (higher
|
|
1147
|
+
// depth) that is ITSELF in a deferred parent remains deferred unless it
|
|
1148
|
+
// opens a same-or-shallower heading that does NOT match the pattern.
|
|
1149
|
+
// Lines inside fenced code blocks (``` or ~~~) are treated as content, not
|
|
1150
|
+
// headings, to avoid false deferred-section detection from code examples.
|
|
1151
|
+
const DEFERRED_HEADING_RE = /\b(?:deferred|backlog|future|v\d+)\b/i;
|
|
1126
1152
|
const bodyReqIds = [];
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1153
|
+
// deferredDepth: the heading level that opened the current deferred block,
|
|
1154
|
+
// or 0 when we are in an active section.
|
|
1155
|
+
let deferredDepth = 0;
|
|
1156
|
+
let inFence = false;
|
|
1157
|
+
for (const line of reqContent.split(/\r?\n/)) {
|
|
1158
|
+
// Track fenced code blocks (``` or ~~~).
|
|
1159
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
1160
|
+
inFence = !inFence;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (inFence)
|
|
1164
|
+
continue; // ignore content inside a code fence
|
|
1165
|
+
const headingM = line.match(/^(#{1,6})\s+(.*)/);
|
|
1166
|
+
if (headingM) {
|
|
1167
|
+
const depth = headingM[1].length;
|
|
1168
|
+
const text = headingM[2];
|
|
1169
|
+
if (deferredDepth > 0 && depth > deferredDepth) {
|
|
1170
|
+
// Sub-heading inside a deferred block: stays deferred regardless of name.
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
// Heading at same level or shallower than current deferred opener,
|
|
1174
|
+
// or no active deferred block yet.
|
|
1175
|
+
if (DEFERRED_HEADING_RE.test(text)) {
|
|
1176
|
+
deferredDepth = depth; // enter a deferred block
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
deferredDepth = 0; // back in an active section
|
|
1180
|
+
}
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (deferredDepth > 0)
|
|
1184
|
+
continue; // skip content in deferred sections
|
|
1185
|
+
// Collect bold REQ-ID patterns from active-section lines.
|
|
1186
|
+
const reqPat = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
|
1187
|
+
let bodyMatch;
|
|
1188
|
+
while ((bodyMatch = reqPat.exec(line)) !== null) {
|
|
1189
|
+
const id = bodyMatch[1];
|
|
1190
|
+
if (!bodyReqIds.includes(id))
|
|
1191
|
+
bodyReqIds.push(id);
|
|
1192
|
+
}
|
|
1133
1193
|
}
|
|
1134
1194
|
const traceabilityHeadingMatch = reqContent.match(/^#{1,6}\s+Traceability\b/im);
|
|
1135
1195
|
const traceabilitySection = traceabilityHeadingMatch
|