@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +199 -365
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +695 -5
- package/gsd-core/bin/lib/adr-parser.cjs +45 -23
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +98 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +7 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +33 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +40 -15
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +4 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +12 -12
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +6 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/run-tests.cjs +14 -0
- package/scripts/sync-manifest-versions.cjs +77 -5
|
@@ -45,6 +45,10 @@ const federatedConfigModule = require("./federated-config.cjs");
|
|
|
45
45
|
const { mergeFederatedConfig } = federatedConfigModule;
|
|
46
46
|
// The capability-registry.cjs is generated and lives in the same gsd-core/bin/lib/ output dir.
|
|
47
47
|
// Both config-loader.cjs and capability-registry.cjs land in gsd-core/bin/lib/ at build time.
|
|
48
|
+
// This is the FROZEN first-party registry — used as the test-seam default and the
|
|
49
|
+
// fallback. Overlay (installed third-party) config-key federation is cwd-dependent
|
|
50
|
+
// and composed PER loadConfig CALL by _federatedConfigSchema(cwd) below (ADR-1244 D2),
|
|
51
|
+
// never eagerly at module load.
|
|
48
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
49
53
|
const _capabilityRegistryReal = require('./capability-registry.cjs');
|
|
50
54
|
// Module-level registry reference. Defaults to the real generated registry.
|
|
@@ -131,6 +135,12 @@ function _deepMergeConfig(base, overlay) {
|
|
|
131
135
|
return overlay;
|
|
132
136
|
const result = { ...base };
|
|
133
137
|
for (const key of Object.keys(overlay)) {
|
|
138
|
+
// Prototype-pollution guard — mirrors the four sibling guards in this file
|
|
139
|
+
// (lines ~315/319/331/341/549). Without it a workstream/root config.json with
|
|
140
|
+
// {"__proto__": {...}} pollutes this merged object's prototype chain and can
|
|
141
|
+
// spoof unset config flags. (Per-object pollution, not global Object.prototype.)
|
|
142
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
143
|
+
continue;
|
|
134
144
|
if (overlay[key] !== null && typeof overlay[key] === 'object' && !Array.isArray(overlay[key])) {
|
|
135
145
|
result[key] = _deepMergeConfig((base[key] ?? {}), overlay[key]);
|
|
136
146
|
}
|
|
@@ -324,8 +334,32 @@ function _applyFederatedValues(obj, values, validKeys) {
|
|
|
324
334
|
* When validKeys is non-empty, applies values into a shallow clone to avoid
|
|
325
335
|
* mutating shared CONFIG_DEFAULTS/module constants.
|
|
326
336
|
*/
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
// Resolve the federated capability config-schema for a project (ADR-1244 D2).
|
|
338
|
+
// A test override (via _setFederatedRegistryForTests) wins; otherwise, when a
|
|
339
|
+
// project cwd is available, compose the installed overlay for THAT project —
|
|
340
|
+
// LAZILY (never at module load, so a bare require never scans the filesystem and
|
|
341
|
+
// the result is never cached for the wrong cwd) — falling back to the frozen
|
|
342
|
+
// first-party schema when there is no cwd or the loader is unavailable.
|
|
343
|
+
function _federatedConfigSchema(cwd) {
|
|
344
|
+
if (_capabilityRegistry !== _capabilityRegistryReal) {
|
|
345
|
+
return _capabilityRegistry.configSchema; // explicit test override
|
|
346
|
+
}
|
|
347
|
+
if (typeof cwd === 'string' && cwd) {
|
|
348
|
+
try {
|
|
349
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
350
|
+
const loaderMod = require('./capability-loader.cjs');
|
|
351
|
+
// #1459 IC-04: thread the consent home explicitly so a consented project cap's federated config
|
|
352
|
+
// key resolves at the SAME user-owned home that gated its activation (never the wrong home).
|
|
353
|
+
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] }).configSchema;
|
|
354
|
+
if (schema && typeof schema === 'object')
|
|
355
|
+
return schema;
|
|
356
|
+
}
|
|
357
|
+
catch { /* fall back to first-party */ }
|
|
358
|
+
}
|
|
359
|
+
return _capabilityRegistryReal.configSchema;
|
|
360
|
+
}
|
|
361
|
+
function _applyFederatedOverlay(baseConfig, userConfig, cwd) {
|
|
362
|
+
const _fedRegistrySchema = _federatedConfigSchema(cwd);
|
|
329
363
|
if (!_fedRegistrySchema || typeof _fedRegistrySchema !== 'object')
|
|
330
364
|
return baseConfig;
|
|
331
365
|
const _fedOverlay = mergeFederatedConfig({
|
|
@@ -341,26 +375,39 @@ function _applyFederatedOverlay(baseConfig, userConfig) {
|
|
|
341
375
|
_applyFederatedValues(cloned, _fedOverlay.values, _fedOverlay.validKeys);
|
|
342
376
|
return cloned;
|
|
343
377
|
}
|
|
344
|
-
|
|
378
|
+
/**
|
|
379
|
+
* loadConfigResolved — provenance-aware config loading (#1415, ADR-1411 P2).
|
|
380
|
+
*
|
|
381
|
+
* Identical to loadConfig in every observable way except it returns
|
|
382
|
+
* { config, source, degraded } instead of just the config object.
|
|
383
|
+
* loadConfig now delegates to this function (byte-identical back-compat).
|
|
384
|
+
*
|
|
385
|
+
* Branch → source/degraded mapping:
|
|
386
|
+
* A1: ws set + ws config.json found → source:'workstream', degraded:false
|
|
387
|
+
* A2: ws null + config.json found → source:'root', degraded:false
|
|
388
|
+
* B: catch + .planning/ + rootParsed set (ws fallback) → source:'root', degraded:true
|
|
389
|
+
* C: catch + .planning/ + rootParsed null (federated defaults) → source:'builtin-defaults', degraded:false
|
|
390
|
+
* D: catch + no .planning/ + ~/.gsd/defaults.json readable → source:'global-defaults', degraded:false
|
|
391
|
+
* E: catch + no .planning/ + no global → source:'builtin-defaults', degraded:false
|
|
392
|
+
*/
|
|
393
|
+
function loadConfigResolved(cwd, options = {}) {
|
|
394
|
+
// NOTE: loadConfigResolved resolves from cwd AS-IS (no walk-up).
|
|
395
|
+
// Callers that need ancestor-anchoring (e.g. cmdAgentSkills) must do so
|
|
396
|
+
// themselves via findProjectRoot() before calling this function.
|
|
397
|
+
// This preserves back-compat for the ~30 other loadConfig callers (#1415).
|
|
345
398
|
const activeWorkstream = Object.prototype.hasOwnProperty.call(options, 'workstream')
|
|
346
399
|
? options['workstream']
|
|
347
400
|
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
|
|
348
401
|
? options['workstreamContext']['ws']
|
|
349
402
|
: (process.env['GSD_WORKSTREAM'] || null);
|
|
350
|
-
// When GSD_WORKSTREAM is set, load root config first so workstream config
|
|
351
|
-
// can inherit from it. This prevents users from duplicating model_overrides,
|
|
352
|
-
// workflow.*, etc. across every workstream config (#2714).
|
|
353
403
|
const ws = typeof activeWorkstream === 'string' ? activeWorkstream : (activeWorkstream === null ? null : null);
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
// so separate loadConfig invocations each get a fresh scan.
|
|
404
|
+
// wsRequested: true when caller explicitly requested a non-empty workstream.
|
|
405
|
+
// Used for source labeling (Fix 4) and early absent-dir intercept (Fix 2).
|
|
406
|
+
const wsRequested = ws != null && ws !== '';
|
|
358
407
|
let cachedSubRepos;
|
|
359
408
|
const getDetectedSubRepos = () => {
|
|
360
409
|
if (cachedSubRepos === undefined)
|
|
361
410
|
cachedSubRepos = detectSubRepos(cwd);
|
|
362
|
-
// Return a copy: original detectSubRepos returned a fresh array per call,
|
|
363
|
-
// so each site must keep an independent array (avoid cross-site aliasing).
|
|
364
411
|
return cachedSubRepos.slice();
|
|
365
412
|
};
|
|
366
413
|
let rootParsed = null;
|
|
@@ -371,10 +418,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
371
418
|
if (raw === null)
|
|
372
419
|
throw new Error('missing');
|
|
373
420
|
rootParsed = JSON.parse(raw);
|
|
374
|
-
// Cycle 4: delegate all legacy-key normalization to the Configuration Module.
|
|
375
421
|
const { parsed: rootNormalized, normalizations: rootNorms } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
|
|
376
422
|
if (rootNorms.length > 0) {
|
|
377
|
-
// Resolve filesystem-dependent normalizations (multiRepo → planning.sub_repos)
|
|
378
423
|
for (const norm of rootNorms) {
|
|
379
424
|
if (norm.requiresFilesystem && !rootNormalized.planning?.['sub_repos']) {
|
|
380
425
|
const detected = getDetectedSubRepos();
|
|
@@ -406,22 +451,14 @@ function loadConfig(cwd, options = {}) {
|
|
|
406
451
|
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(configPath);
|
|
407
452
|
if (raw === null)
|
|
408
453
|
throw new Error('missing');
|
|
409
|
-
// `fileData` is the parsed content of the config.json file on disk — used
|
|
410
|
-
// for migrations and writes so we never persist merged values back to disk.
|
|
411
454
|
const fileData = JSON.parse(raw);
|
|
412
|
-
// Cycle 4: Single normalizeLegacyKeys call replaces all four inline migration
|
|
413
|
-
// blocks (depth→granularity, multiRepo→planning.sub_repos, sub_repos→planning.sub_repos,
|
|
414
|
-
// branching_strategy→git.branching_strategy). The Module is pure (no I/O); disk
|
|
415
|
-
// writeback is handled below with the existing platformWriteSync pattern.
|
|
416
455
|
let configDirty = false;
|
|
417
456
|
{
|
|
418
457
|
const { parsed: normalized, normalizations } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
|
|
419
458
|
if (normalizations.length > 0) {
|
|
420
|
-
// Merge normalized values back into fileData (mutation-in-place for legacy code below)
|
|
421
459
|
Object.keys(fileData).forEach(k => delete fileData[k]);
|
|
422
460
|
Object.assign(fileData, normalized);
|
|
423
461
|
configDirty = true;
|
|
424
|
-
// Resolve filesystem-dependent normalizations (multiRepo → planning.sub_repos).
|
|
425
462
|
for (const norm of normalizations) {
|
|
426
463
|
if (norm.requiresFilesystem && !fileData.planning?.['sub_repos']) {
|
|
427
464
|
const detected = getDetectedSubRepos();
|
|
@@ -435,7 +472,6 @@ function loadConfig(cwd, options = {}) {
|
|
|
435
472
|
}
|
|
436
473
|
}
|
|
437
474
|
}
|
|
438
|
-
// Keep planning.sub_repos in sync with actual filesystem
|
|
439
475
|
const currentSubRepos = fileData.planning?.['sub_repos'] || [];
|
|
440
476
|
if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) {
|
|
441
477
|
const detected = getDetectedSubRepos();
|
|
@@ -449,36 +485,24 @@ function loadConfig(cwd, options = {}) {
|
|
|
449
485
|
}
|
|
450
486
|
}
|
|
451
487
|
}
|
|
452
|
-
// Persist sub_repos changes (migration or sync) — write only the on-disk
|
|
453
|
-
// file contents, never the merged result, to avoid polluting workstream configs.
|
|
454
488
|
if (configDirty) {
|
|
455
489
|
try {
|
|
456
490
|
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(fileData, null, 2));
|
|
457
491
|
}
|
|
458
492
|
catch { /* ignore */ }
|
|
459
493
|
}
|
|
460
|
-
// Now apply root→workstream inheritance. `parsed` is the effective config
|
|
461
|
-
// used for value extraction below; fileData is kept for disk writes only.
|
|
462
494
|
const parsed = rootParsed
|
|
463
495
|
? (_deepMergeConfig(rootParsed, fileData) ?? fileData)
|
|
464
496
|
: fileData;
|
|
465
|
-
// Warn about unrecognized top-level keys so users don't silently lose config.
|
|
466
497
|
const KNOWN_TOP_LEVEL = new Set([
|
|
467
|
-
// Extract top-level key names from dot-notation paths (e.g., 'workflow.research' → 'workflow')
|
|
468
498
|
...[...VALID_CONFIG_KEYS].map((k) => k.split('.')[0]),
|
|
469
|
-
// Dynamic-pattern top-level containers (e.g. review, model_profile_overrides)
|
|
470
499
|
...DYNAMIC_KEY_PATTERNS.map(p => p.topLevel),
|
|
471
|
-
// Internal keys loadConfig reads but config-set doesn't expose
|
|
472
500
|
'model_overrides', 'context_window', 'resolve_model_ids', 'claude_md_path', 'effort', 'fast_mode',
|
|
473
|
-
// Deprecated keys (still accepted for migration, not in config-set)
|
|
474
501
|
'depth', 'multiRepo', 'branching_strategy', 'research',
|
|
475
502
|
]);
|
|
476
|
-
// FIX 3: Compute federated overlay BEFORE the unknown-key warning, so that
|
|
477
|
-
// federated top-level keys are added to KNOWN_TOP_LEVEL before the check runs.
|
|
478
|
-
// This is hoisted out of the try-catch below so validKeys are available here.
|
|
479
503
|
let _preWarningFedValidKeys = [];
|
|
480
504
|
try {
|
|
481
|
-
const _fedRegistrySchemaEarly =
|
|
505
|
+
const _fedRegistrySchemaEarly = _federatedConfigSchema(cwd);
|
|
482
506
|
if (_fedRegistrySchemaEarly && typeof _fedRegistrySchemaEarly === 'object') {
|
|
483
507
|
const _earlyOverlay = mergeFederatedConfig({
|
|
484
508
|
configSchema: _fedRegistrySchemaEarly,
|
|
@@ -495,7 +519,7 @@ function loadConfig(cwd, options = {}) {
|
|
|
495
519
|
}
|
|
496
520
|
}
|
|
497
521
|
catch {
|
|
498
|
-
// Defensive
|
|
522
|
+
// Defensive
|
|
499
523
|
}
|
|
500
524
|
const unknownKeys = Object.keys(parsed).filter(k => !KNOWN_TOP_LEVEL.has(k));
|
|
501
525
|
if (unknownKeys.length > 0) {
|
|
@@ -505,7 +529,6 @@ function loadConfig(cwd, options = {}) {
|
|
|
505
529
|
process.stderr.write(`gsd-tools: warning: unknown config key(s) in .planning/config.json: ${unknownKeys.join(', ')} — these will be ignored\n`);
|
|
506
530
|
}
|
|
507
531
|
}
|
|
508
|
-
// #2517 — Validate runtime/tier values
|
|
509
532
|
_warnUnknownProfileOverrides(parsed, '.planning/config.json');
|
|
510
533
|
const get = (key, nested) => {
|
|
511
534
|
if (parsed[key] !== undefined)
|
|
@@ -530,11 +553,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
530
553
|
model_profile: get('model_profile') ?? defaults.model_profile,
|
|
531
554
|
commit_docs: (() => {
|
|
532
555
|
const explicit = get('commit_docs', { section: 'planning', field: 'commit_docs' });
|
|
533
|
-
// If explicitly set in config, respect the user's choice
|
|
534
556
|
if (explicit !== undefined)
|
|
535
557
|
return explicit;
|
|
536
|
-
// Auto-detection: when no explicit value and .planning/ is gitignored,
|
|
537
|
-
// default to false instead of true
|
|
538
558
|
if (isGitIgnored(cwd, '.planning/'))
|
|
539
559
|
return false;
|
|
540
560
|
return defaults.commit_docs;
|
|
@@ -565,22 +585,14 @@ function loadConfig(cwd, options = {}) {
|
|
|
565
585
|
project_code: get('project_code') ?? defaults.project_code,
|
|
566
586
|
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
|
567
587
|
model_overrides: (parsed['model_overrides']) || null,
|
|
568
|
-
// #3023 — per-phase-type model map.
|
|
569
588
|
models: (parsed['models']) || null,
|
|
570
|
-
// #68 — top-level granularity
|
|
571
589
|
granularity: parsed['granularity'] !== undefined ? parsed['granularity'] : null,
|
|
572
|
-
// #68 — per-phase-type granularity map.
|
|
573
590
|
granularities: (parsed['granularities']) || null,
|
|
574
|
-
// #68 — planning sub-object
|
|
575
591
|
planning: (parsed['planning']) || null,
|
|
576
|
-
// #3024 — dynamic routing block.
|
|
577
592
|
dynamic_routing: (parsed['dynamic_routing']) || null,
|
|
578
|
-
// #2517 — runtime-aware profiles.
|
|
579
593
|
runtime: (parsed['runtime']) || null,
|
|
580
594
|
model_profile_overrides: (parsed['model_profile_overrides']) || null,
|
|
581
|
-
// #49 — provider-neutral model policy presets.
|
|
582
595
|
model_policy: (parsed['model_policy']) || null,
|
|
583
|
-
// #443 — effort/fast_mode
|
|
584
596
|
effort: (parsed['effort']) || null,
|
|
585
597
|
fast_mode: (parsed['fast_mode']) || null,
|
|
586
598
|
agent_skills: (parsed['agent_skills']) || {},
|
|
@@ -590,57 +602,53 @@ function loadConfig(cwd, options = {}) {
|
|
|
590
602
|
claude_md_path: get('claude_md_path') || null,
|
|
591
603
|
claude_md_assembly: (parsed['claude_md_assembly']) || null,
|
|
592
604
|
};
|
|
593
|
-
//
|
|
594
|
-
// FIX 2: Use the pre-computed _preWarningFedValidKeys (from the FIX 3 block above)
|
|
595
|
-
// plus a fresh overlay call to get values. The KNOWN_TOP_LEVEL was already updated.
|
|
596
|
-
// TODAY: every UI key is still in the central config-schema, so isCentralKey()
|
|
597
|
-
// returns true for all of them → validKeys is empty → _baseConfig is returned UNCHANGED
|
|
598
|
-
// (true no-op: no clone, no reorder, byte-identical output).
|
|
599
|
-
// This becomes a live channel once a key is atomically removed from the central schema.
|
|
605
|
+
// ADR-857 phase 3b: federated config overlay
|
|
600
606
|
try {
|
|
601
607
|
if (_preWarningFedValidKeys.length > 0) {
|
|
602
|
-
|
|
603
|
-
// (we run mergeFederatedConfig again here to get the values map; the validKeys
|
|
604
|
-
// are guaranteed identical since it's the same inputs).
|
|
605
|
-
const _fedRegistrySchema = _capabilityRegistry.configSchema;
|
|
608
|
+
const _fedRegistrySchema = _federatedConfigSchema(cwd);
|
|
606
609
|
if (_fedRegistrySchema && typeof _fedRegistrySchema === 'object') {
|
|
607
610
|
const _fedOverlay = mergeFederatedConfig({
|
|
608
611
|
configSchema: _fedRegistrySchema,
|
|
609
612
|
isCentralKey: (key) => _isCentralConfigKeyFn(key),
|
|
610
613
|
userConfig: parsed,
|
|
611
614
|
});
|
|
612
|
-
// Apply dotted-path values (e.g. "workflow.ui_phase" → _baseConfig.workflow.ui_phase)
|
|
613
|
-
// WITHOUT clobbering existing keys. N-level nesting supported.
|
|
614
615
|
_applyFederatedValues(_baseConfig, _fedOverlay.values, _fedOverlay.validKeys);
|
|
615
616
|
}
|
|
616
617
|
}
|
|
617
|
-
// Pending-migration warnings are suppressed at load time to avoid noisy output on
|
|
618
|
-
// every loadConfig call. They are surfaced at registry-generation time (--check/--write).
|
|
619
618
|
}
|
|
620
619
|
catch {
|
|
621
|
-
// Defensive:
|
|
622
|
-
// This keeps loadConfig's no-throw contract intact regardless of capability registry state.
|
|
620
|
+
// Defensive: keep no-throw contract
|
|
623
621
|
}
|
|
624
|
-
|
|
622
|
+
// A1 vs A2: disambiguate by whether a real workstream was requested.
|
|
623
|
+
// Fix 4: empty-string ws ('') resolves the root path → source:'root'.
|
|
624
|
+
const source = wsRequested ? 'workstream' : 'root';
|
|
625
|
+
return { config: _baseConfig, source, degraded: false };
|
|
625
626
|
}
|
|
626
627
|
catch {
|
|
627
|
-
//
|
|
628
|
+
// Fix 2: Early intercept — workstream requested but ws config.json absent (or dir absent)
|
|
629
|
+
// AND root config was loaded. Covers BOTH "dir exists, no config.json" AND "dir absent".
|
|
630
|
+
// This delivers the #1366 acceptance criterion: nonexistent GSD_WORKSTREAM yields root, degraded.
|
|
631
|
+
if (wsRequested && rootParsed) {
|
|
632
|
+
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
633
|
+
return { config: fb.config, source: 'root', degraded: true };
|
|
634
|
+
}
|
|
635
|
+
// Branch B, C, D, E
|
|
628
636
|
if (node_fs_1.default.existsSync(planningDir(cwd, ws))) {
|
|
629
637
|
if (rootParsed) {
|
|
630
|
-
//
|
|
631
|
-
// (
|
|
632
|
-
|
|
638
|
+
// Branch B: workstream requested but ws config.json absent; root config present.
|
|
639
|
+
// (Only reached when wsRequested is false — e.g. ws='' with .planning/workstreams//config.json)
|
|
640
|
+
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
641
|
+
return { config: fb.config, source: 'root', degraded: true };
|
|
633
642
|
}
|
|
634
|
-
//
|
|
635
|
-
// Migrated Capability keys are surfaced from the generated registry even
|
|
636
|
-
// when the project has no config.json, so schema defaults still apply.
|
|
643
|
+
// Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
|
|
637
644
|
try {
|
|
638
|
-
return _applyFederatedOverlay(defaults, {});
|
|
645
|
+
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
639
646
|
}
|
|
640
647
|
catch {
|
|
641
|
-
return defaults;
|
|
648
|
+
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
642
649
|
}
|
|
643
650
|
}
|
|
651
|
+
// Branch D or E: no .planning/
|
|
644
652
|
try {
|
|
645
653
|
const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
|
|
646
654
|
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
@@ -675,29 +683,35 @@ function loadConfig(cwd, options = {}) {
|
|
|
675
683
|
agent_skills: (globalDefaults['agent_skills']) || {},
|
|
676
684
|
response_language: (globalDefaults['response_language']) || null,
|
|
677
685
|
};
|
|
678
|
-
//
|
|
679
|
-
// With the current registry this is a true no-op (returns _globalBaseCfg unchanged).
|
|
686
|
+
// Branch D: global-defaults
|
|
680
687
|
try {
|
|
681
|
-
return _applyFederatedOverlay(_globalBaseCfg, globalDefaults);
|
|
688
|
+
return { config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false };
|
|
682
689
|
}
|
|
683
690
|
catch {
|
|
684
|
-
return _globalBaseCfg;
|
|
691
|
+
return { config: _globalBaseCfg, source: 'global-defaults', degraded: false };
|
|
685
692
|
}
|
|
686
693
|
}
|
|
687
694
|
catch {
|
|
688
|
-
//
|
|
689
|
-
// With the current registry this is a true no-op (returns `defaults` unchanged).
|
|
695
|
+
// Branch E: no global defaults
|
|
690
696
|
try {
|
|
691
|
-
return _applyFederatedOverlay(defaults, {});
|
|
697
|
+
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
692
698
|
}
|
|
693
699
|
catch {
|
|
694
|
-
return defaults;
|
|
700
|
+
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
695
701
|
}
|
|
696
702
|
}
|
|
697
703
|
}
|
|
698
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* loadConfig — backwards-compatible config loading, now a thin wrapper over loadConfigResolved.
|
|
707
|
+
* Returns the config object only; for provenance metadata use loadConfigResolved.
|
|
708
|
+
*/
|
|
709
|
+
function loadConfig(cwd, options = {}) {
|
|
710
|
+
return loadConfigResolved(cwd, options).config;
|
|
711
|
+
}
|
|
699
712
|
module.exports = {
|
|
700
713
|
loadConfig,
|
|
714
|
+
loadConfigResolved,
|
|
701
715
|
isGitIgnored,
|
|
702
716
|
CONFIG_DEFAULTS,
|
|
703
717
|
_getConfigDefault,
|
|
@@ -16,15 +16,34 @@
|
|
|
16
16
|
* the prior hand-written .cjs; only types are added.
|
|
17
17
|
*/
|
|
18
18
|
const configuration_cjs_1 = require("./configuration.cjs");
|
|
19
|
+
// Frozen first-party capability config-schema — the fallback when no project cwd
|
|
20
|
+
// is available (cwd-agnostic call sites).
|
|
19
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
22
|
const capabilityRegistry = require('./capability-registry.cjs');
|
|
21
|
-
|
|
23
|
+
// Resolve the capability config-schema for a project (ADR-1244 D2). When a cwd is
|
|
24
|
+
// supplied, compose installed overlay capabilities for THAT project — LAZILY (never
|
|
25
|
+
// at module load: a bare require of this module never scans the filesystem) —
|
|
26
|
+
// falling back to the frozen first-party schema. Without a cwd, first-party only.
|
|
27
|
+
function _capabilityConfigSchema(cwd) {
|
|
28
|
+
if (typeof cwd === 'string' && cwd) {
|
|
29
|
+
try {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
31
|
+
const loaderMod = require('./capability-loader.cjs');
|
|
32
|
+
// #1459 IC-04: thread the consent home explicitly so a consented project cap's config key
|
|
33
|
+
// federates at the SAME user-owned home that gated its activation.
|
|
34
|
+
const schema = loaderMod.loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] }).configSchema;
|
|
35
|
+
if (schema && typeof schema === 'object')
|
|
36
|
+
return schema;
|
|
37
|
+
}
|
|
38
|
+
catch { /* fall back to first-party */ }
|
|
39
|
+
}
|
|
40
|
+
const fp = capabilityRegistry.configSchema;
|
|
41
|
+
return fp && typeof fp === 'object' ? fp : {};
|
|
42
|
+
}
|
|
43
|
+
function isCapabilityConfigKey(keyPath, cwd) {
|
|
22
44
|
if (typeof keyPath !== 'string')
|
|
23
45
|
return false;
|
|
24
|
-
|
|
25
|
-
if (!schema || typeof schema !== 'object')
|
|
26
|
-
return false;
|
|
27
|
-
return Object.prototype.hasOwnProperty.call(schema, keyPath);
|
|
46
|
+
return Object.prototype.hasOwnProperty.call(_capabilityConfigSchema(cwd), keyPath);
|
|
28
47
|
}
|
|
29
48
|
/**
|
|
30
49
|
* Returns true for keys owned by the central schema adapter rather than a
|
|
@@ -43,10 +62,10 @@ function isCentralConfigKey(keyPath) {
|
|
|
43
62
|
* Returns true if keyPath is a valid central, runtime-state, dynamic, or
|
|
44
63
|
* federated Capability config key.
|
|
45
64
|
*/
|
|
46
|
-
function isValidConfigKey(keyPath) {
|
|
65
|
+
function isValidConfigKey(keyPath, cwd) {
|
|
47
66
|
if (isCentralConfigKey(keyPath))
|
|
48
67
|
return true;
|
|
49
|
-
return isCapabilityConfigKey(keyPath);
|
|
68
|
+
return isCapabilityConfigKey(keyPath, cwd);
|
|
50
69
|
}
|
|
51
70
|
module.exports = {
|
|
52
71
|
VALID_CONFIG_KEYS: configuration_cjs_1.VALID_CONFIG_KEYS,
|
|
@@ -210,6 +210,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
210
210
|
ui_safety_gate: true,
|
|
211
211
|
ai_integration_phase: true,
|
|
212
212
|
human_verify_mode: 'end-of-phase',
|
|
213
|
+
context_guard_mode: 'warn',
|
|
213
214
|
text_mode: false,
|
|
214
215
|
research_before_questions: false,
|
|
215
216
|
discuss_mode: 'discuss',
|
|
@@ -505,7 +506,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
505
506
|
const kp = keyPath;
|
|
506
507
|
const val = value;
|
|
507
508
|
validateKnownConfigKeyPath(kp);
|
|
508
|
-
if (!isValidConfigKey(kp)) {
|
|
509
|
+
if (!isValidConfigKey(kp, cwd)) {
|
|
509
510
|
error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>`, ERROR_REASON.CONFIG_INVALID_KEY);
|
|
510
511
|
}
|
|
511
512
|
// Parse value (handle booleans, numbers, and JSON arrays/objects)
|
|
@@ -556,6 +557,11 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
556
557
|
if (kp === 'workflow.human_verify_mode' && !VALID_HUMAN_VERIFY_MODES.includes(String(parsedValue))) {
|
|
557
558
|
error(`Invalid workflow.human_verify_mode '${val}'. Valid values: ${VALID_HUMAN_VERIFY_MODES.join(', ')}`);
|
|
558
559
|
}
|
|
560
|
+
// Context exhaustion guard mode (#1452)
|
|
561
|
+
const VALID_CONTEXT_GUARD_MODES = ['auto', 'warn', 'off'];
|
|
562
|
+
if (kp === 'workflow.context_guard_mode' && !VALID_CONTEXT_GUARD_MODES.includes(String(parsedValue))) {
|
|
563
|
+
error(`Invalid workflow.context_guard_mode '${val}'. Valid values: ${VALID_CONTEXT_GUARD_MODES.join(', ')}`);
|
|
564
|
+
}
|
|
559
565
|
// Context position enum validation (#2937)
|
|
560
566
|
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
|
|
561
567
|
if (kp === 'statusline.context_position' && !VALID_CONTEXT_POSITIONS.includes(String(parsedValue))) {
|