@opengsd/gsd-core 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -120,6 +120,7 @@ const CONFIG_DEFAULTS = {
|
|
|
120
120
|
security_asvs_level: _getNestedConfigDefault('workflow', 'security_asvs_level'),
|
|
121
121
|
security_block_on: _getNestedConfigDefault('workflow', 'security_block_on'),
|
|
122
122
|
post_planning_gaps: _getNestedConfigDefault('workflow', 'post_planning_gaps'),
|
|
123
|
+
smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
|
|
123
124
|
};
|
|
124
125
|
/**
|
|
125
126
|
* Deep-merge two plain config objects. `overlay` wins on key conflict.
|
|
@@ -280,8 +281,15 @@ function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
|
280
281
|
}
|
|
281
282
|
// Internal helper exposed for tests so per-process warning state can be reset
|
|
282
283
|
// between cases that intentionally exercise the warning path repeatedly.
|
|
284
|
+
// Clears BOTH dedup sets: _warnedConfigKeys (runtime/model-policy/tier warnings)
|
|
285
|
+
// and _warnedUnknownConfigKeys (unknown top-level keys). Omitting the latter made
|
|
286
|
+
// this a silent no-op for the suite that exists to test it — the leaked state
|
|
287
|
+
// suppressed any later case reusing a key, and the existing cases only passed
|
|
288
|
+
// because each picked a key name no other case reused (#2674).
|
|
283
289
|
function _resetRuntimeWarningCacheForTests() {
|
|
284
290
|
_warnedConfigKeys.clear();
|
|
291
|
+
_warnedUnknownConfigKeys.clear();
|
|
292
|
+
_warnedUnusableConfig.clear();
|
|
285
293
|
}
|
|
286
294
|
// ─── FIX 2: Federated overlay helpers ────────────────────────────────────────
|
|
287
295
|
/**
|
|
@@ -384,6 +392,102 @@ function _applyFederatedOverlay(baseConfig, userConfig, cwd) {
|
|
|
384
392
|
_applyFederatedValues(cloned, _fedOverlay.values, _fedOverlay.validKeys);
|
|
385
393
|
return cloned;
|
|
386
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Result of loadConfigResolved — wraps the config object with provenance metadata.
|
|
397
|
+
* - source: which layer supplied the config
|
|
398
|
+
* - degraded: true when the resolution did not deliver the configuration it
|
|
399
|
+
* should have — either a workstream was requested but its
|
|
400
|
+
* config.json was absent (fell back to root), or a file on the
|
|
401
|
+
* resolution path exists but is unusable (#1880). `reason` says which.
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Machine-readable outcome of a config resolution (#1880, ADR-1411 amendment
|
|
405
|
+
* "corrupt is not absent"). `Resolution<T>`'s four documented values all
|
|
406
|
+
* describe a resolution *miss*; the two `config_un*` values below are the
|
|
407
|
+
* unusable-input class that amendment introduced, and they are what makes a
|
|
408
|
+
* corrupt file distinguishable from an absent one.
|
|
409
|
+
*
|
|
410
|
+
* Frozen enum rather than bare strings so tests assert on the typed surface
|
|
411
|
+
* instead of diagnostic prose (CONTRIBUTING.md — Prohibited: Raw Text Matching
|
|
412
|
+
* on Test Outputs).
|
|
413
|
+
*/
|
|
414
|
+
const CONFIG_REASON = Object.freeze({
|
|
415
|
+
/** A config file was found, parsed, and supplied at least one setting. */
|
|
416
|
+
RESOLVED: 'resolved',
|
|
417
|
+
/** No config file exists at the resolved path. Genuine absence — NOT degraded. */
|
|
418
|
+
NOT_CONFIGURED: 'not_configured',
|
|
419
|
+
/** A config file exists and parsed, but carried no settings (`{}`). */
|
|
420
|
+
CONFIGURED_EMPTY: 'configured_empty',
|
|
421
|
+
/** A workstream was requested but had no config; fell back to root. */
|
|
422
|
+
WORKSTREAM_FALLBACK: 'workstream_fallback',
|
|
423
|
+
/** The file exists but is not valid JSON — settings were NOT applied. */
|
|
424
|
+
CONFIG_UNPARSEABLE: 'config_unparseable',
|
|
425
|
+
/** The file exists but could not be read (EACCES/EIO/…) — NOT applied. */
|
|
426
|
+
CONFIG_UNREADABLE: 'config_unreadable',
|
|
427
|
+
});
|
|
428
|
+
/**
|
|
429
|
+
* Read + JSON-parse a config file, keeping *absent* distinguishable from
|
|
430
|
+
* *unusable*. `platformReadSync` returns null on ENOENT and re-throws every
|
|
431
|
+
* other errno, which is the seam that makes this separable at all.
|
|
432
|
+
*/
|
|
433
|
+
function _readConfigFile(filePath) {
|
|
434
|
+
let raw;
|
|
435
|
+
try {
|
|
436
|
+
raw = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
const code = err.code ?? 'EUNKNOWN';
|
|
440
|
+
return { kind: 'fault', fault: { reason: CONFIG_REASON.CONFIG_UNREADABLE, path: filePath, code } };
|
|
441
|
+
}
|
|
442
|
+
if (raw === null)
|
|
443
|
+
return { kind: 'absent' };
|
|
444
|
+
let parsed;
|
|
445
|
+
try {
|
|
446
|
+
parsed = JSON.parse(raw);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return { kind: 'fault', fault: { reason: CONFIG_REASON.CONFIG_UNPARSEABLE, path: filePath, code: '' } };
|
|
450
|
+
}
|
|
451
|
+
// Shape, not just parseability (ADR-227). `0`, `"x"`, `[]` and `null` are all
|
|
452
|
+
// valid JSON but are not a config object. Accepting them let a PRESENT file
|
|
453
|
+
// parse "ok", then throw downstream, and be reported not_configured by the
|
|
454
|
+
// outer catch — a corrupt file indistinguishable from an absent one, which is
|
|
455
|
+
// the exact defect this change closes. Caught by the fast-check property.
|
|
456
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
457
|
+
return { kind: 'fault', fault: { reason: CONFIG_REASON.CONFIG_UNPARSEABLE, path: filePath, code: '' } };
|
|
458
|
+
}
|
|
459
|
+
return { kind: 'ok', data: parsed };
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Dedup set for the unusable-config diagnostic. Keyed on resolved path + errno
|
|
463
|
+
* per the ADR-1411 amendment — never on message text, which would couple the
|
|
464
|
+
* guard to wording, and never on the errno alone, which would suppress a
|
|
465
|
+
* genuine second failure in a different file.
|
|
466
|
+
*/
|
|
467
|
+
const _warnedUnusableConfig = new Set();
|
|
468
|
+
/**
|
|
469
|
+
* The wiring clause (ADR-1411 amendment). `reason` lives on `ConfigResolution`,
|
|
470
|
+
* but `loadConfig` — the wrapper roughly fifty call sites use — returns
|
|
471
|
+
* `.config` alone and would never surface it. Without this diagnostic the field
|
|
472
|
+
* is unreachable to almost every consumer, and the user whose config was
|
|
473
|
+
* silently discarded still gets no signal. That was the whole defect in #1880.
|
|
474
|
+
*/
|
|
475
|
+
function _warnUnusableConfig(fault) {
|
|
476
|
+
// The NUL separators are load-bearing: without them `path`+`reason`+`code` is bare
|
|
477
|
+
// concatenation and two distinct faults can key alike. They are written as escapes rather
|
|
478
|
+
// than literal 0x00 bytes because a literal NUL makes the whole file binary to file(1) and
|
|
479
|
+
// grep(1), which silently skipped it — RULESET.AUDIT.search-source-not-generated tells
|
|
480
|
+
// agents to search this exact source to confirm an invariant exists, and it was returning
|
|
481
|
+
// nothing. Same runtime string, still greppable.
|
|
482
|
+
const key = `${fault.path}\u0000${fault.reason}\u0000${fault.code}`;
|
|
483
|
+
if (_warnedUnusableConfig.has(key))
|
|
484
|
+
return;
|
|
485
|
+
_warnedUnusableConfig.add(key);
|
|
486
|
+
const what = fault.reason === CONFIG_REASON.CONFIG_UNPARSEABLE
|
|
487
|
+
? 'is not valid JSON'
|
|
488
|
+
: `could not be read (${fault.code})`;
|
|
489
|
+
process.stderr.write(`gsd-tools: warning: ${fault.path} ${what} — its settings were NOT applied; using defaults instead\n`);
|
|
490
|
+
}
|
|
387
491
|
/**
|
|
388
492
|
* loadConfigResolved — provenance-aware config loading (#1415, ADR-1411 P2).
|
|
389
493
|
*
|
|
@@ -391,13 +495,21 @@ function _applyFederatedOverlay(baseConfig, userConfig, cwd) {
|
|
|
391
495
|
* { config, source, degraded } instead of just the config object.
|
|
392
496
|
* loadConfig now delegates to this function (byte-identical back-compat).
|
|
393
497
|
*
|
|
394
|
-
* Branch → source/degraded mapping:
|
|
395
|
-
* A1: ws set + ws config.json found → source:'workstream', degraded:false
|
|
396
|
-
* A2: ws null + config.json found → source:'root', degraded:false
|
|
397
|
-
* B: catch + .planning/ + rootParsed set (ws fallback) → source:'root', degraded:true
|
|
398
|
-
* C: catch + .planning/ + rootParsed null (federated defaults) → source:'builtin-defaults', degraded:false
|
|
399
|
-
* D: catch + no .planning/ + ~/.gsd/defaults.json readable → source:'global-defaults', degraded:false
|
|
400
|
-
* E: catch + no .planning/ + no global → source:'builtin-defaults', degraded:false
|
|
498
|
+
* Branch → source/degraded/reason mapping:
|
|
499
|
+
* A1: ws set + ws config.json found → source:'workstream', degraded:false, reason:'resolved'|'configured_empty'
|
|
500
|
+
* A2: ws null + config.json found → source:'root', degraded:false, reason:'resolved'|'configured_empty'
|
|
501
|
+
* B: catch + .planning/ + rootParsed set (ws fallback) → source:'root', degraded:true, reason:'workstream_fallback'
|
|
502
|
+
* C: catch + .planning/ + rootParsed null (federated defaults) → source:'builtin-defaults', degraded:false, reason:'not_configured'
|
|
503
|
+
* D: catch + no .planning/ + ~/.gsd/defaults.json readable → source:'global-defaults', degraded:false, reason:'not_configured'
|
|
504
|
+
* E: catch + no .planning/ + no global → source:'builtin-defaults', degraded:false, reason:'not_configured'
|
|
505
|
+
*
|
|
506
|
+
* ORTHOGONAL to all of the above (#1880, ADR-1411 "corrupt is not absent"): if
|
|
507
|
+
* any config file on the resolution path exists but is UNUSABLE — invalid JSON,
|
|
508
|
+
* or an errno such as EACCES — every branch instead returns degraded:true with
|
|
509
|
+
* reason:'config_unparseable'|'config_unreadable', and a deduplicated stderr
|
|
510
|
+
* diagnostic names the file. Before this, a trailing comma in config.json was
|
|
511
|
+
* byte-identical to the file not existing: builtin defaults, degraded:false,
|
|
512
|
+
* and the user's entire configuration silently discarded.
|
|
401
513
|
*/
|
|
402
514
|
function loadConfigResolved(cwd, options = {}) {
|
|
403
515
|
// NOTE: loadConfigResolved resolves from cwd AS-IS (no walk-up).
|
|
@@ -419,14 +531,39 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
419
531
|
cachedSubRepos = detectSubRepos(cwd);
|
|
420
532
|
return cachedSubRepos.slice();
|
|
421
533
|
};
|
|
534
|
+
// Faults are captured, not thrown: the existing control flow (one broad catch
|
|
535
|
+
// that falls back to defaults) is preserved exactly — see #1880. All that is
|
|
536
|
+
// added is knowing WHY the fallback fired, which is the whole defect.
|
|
537
|
+
let configFault = null;
|
|
538
|
+
/**
|
|
539
|
+
* Stamp a fallback return with its reason. Every branch below reaches defaults
|
|
540
|
+
* (or the root config) — what differs is WHY, and before #1880 that was
|
|
541
|
+
* unrecoverable: a corrupt file and an absent one produced identical objects.
|
|
542
|
+
*
|
|
543
|
+
* An unusable file always wins and always sets `degraded:true`; genuine
|
|
544
|
+
* absence keeps whatever `degraded` the branch already decided, so the
|
|
545
|
+
* existing #1366 workstream-fallback semantics are untouched.
|
|
546
|
+
*/
|
|
547
|
+
const fallback = (r) => {
|
|
548
|
+
if (configFault)
|
|
549
|
+
return { ...r, degraded: true, reason: configFault.reason };
|
|
550
|
+
return {
|
|
551
|
+
...r,
|
|
552
|
+
reason: r.degraded ? CONFIG_REASON.WORKSTREAM_FALLBACK : CONFIG_REASON.NOT_CONFIGURED,
|
|
553
|
+
};
|
|
554
|
+
};
|
|
422
555
|
let rootParsed = null;
|
|
423
556
|
if (ws) {
|
|
424
557
|
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
425
558
|
try {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
559
|
+
const rootRead = _readConfigFile(rootConfigPath);
|
|
560
|
+
if (rootRead.kind === 'fault') {
|
|
561
|
+
configFault = rootRead.fault;
|
|
562
|
+
_warnUnusableConfig(rootRead.fault);
|
|
563
|
+
}
|
|
564
|
+
if (rootRead.kind !== 'ok')
|
|
565
|
+
throw new Error('root config absent or unusable');
|
|
566
|
+
rootParsed = rootRead.data;
|
|
430
567
|
const { parsed: rootNormalized, normalizations: rootNorms } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
|
|
431
568
|
if (rootNorms.length > 0) {
|
|
432
569
|
for (const norm of rootNorms) {
|
|
@@ -457,10 +594,18 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
457
594
|
const configPath = node_path_1.default.join(planningDir(cwd, ws), 'config.json');
|
|
458
595
|
const defaults = CONFIG_DEFAULTS;
|
|
459
596
|
try {
|
|
460
|
-
const
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
597
|
+
const read = _readConfigFile(configPath);
|
|
598
|
+
if (read.kind === 'fault') {
|
|
599
|
+
// The workstream/root config that ACTUALLY governs this resolution is
|
|
600
|
+
// unusable. This outranks any earlier root-config fault for reporting.
|
|
601
|
+
configFault = read.fault;
|
|
602
|
+
_warnUnusableConfig(read.fault);
|
|
603
|
+
}
|
|
604
|
+
if (read.kind !== 'ok')
|
|
605
|
+
throw new Error('config absent or unusable');
|
|
606
|
+
const fileData = read.data;
|
|
607
|
+
// Snapshot BEFORE normalizeLegacyKeys mutates fileData in place.
|
|
608
|
+
const fileHadKeys = Object.keys(read.data).length > 0;
|
|
464
609
|
let configDirty = false;
|
|
465
610
|
{
|
|
466
611
|
const { parsed: normalized, normalizations } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
|
|
@@ -631,7 +776,24 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
631
776
|
// A1 vs A2: disambiguate by whether a real workstream was requested.
|
|
632
777
|
// Fix 4: empty-string ws ('') resolves the root path → source:'root'.
|
|
633
778
|
const source = wsRequested ? 'workstream' : 'root';
|
|
634
|
-
|
|
779
|
+
// This config parsed — but a DIFFERENT file on the resolution path may not
|
|
780
|
+
// have. A workstream config that loads cleanly while the root config it
|
|
781
|
+
// inherits from is corrupt is still a degraded resolution: the root's
|
|
782
|
+
// settings were silently dropped. Reporting `resolved` here would reopen
|
|
783
|
+
// the exact hole this change closes, for the common case of a project that
|
|
784
|
+
// uses workstreams at all.
|
|
785
|
+
if (configFault) {
|
|
786
|
+
return { config: _baseConfig, source, degraded: true, reason: configFault.reason };
|
|
787
|
+
}
|
|
788
|
+
// Emptiness is judged on the FILE THAT WAS READ, not on `parsed` (the
|
|
789
|
+
// root+workstream merge). An empty workstream file inheriting a non-empty
|
|
790
|
+
// root would otherwise report `resolved` while carrying no settings of its
|
|
791
|
+
// own — the opposite of the not-configured/configured-empty distinction
|
|
792
|
+
// ADR-1411 rule 3 requires.
|
|
793
|
+
const reason = fileHadKeys
|
|
794
|
+
? CONFIG_REASON.RESOLVED
|
|
795
|
+
: CONFIG_REASON.CONFIGURED_EMPTY;
|
|
796
|
+
return { config: _baseConfig, source, degraded: false, reason };
|
|
635
797
|
}
|
|
636
798
|
catch {
|
|
637
799
|
// Fix 2: Early intercept — workstream requested but ws config.json absent (or dir absent)
|
|
@@ -639,7 +801,7 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
639
801
|
// This delivers the #1366 acceptance criterion: nonexistent GSD_WORKSTREAM yields root, degraded.
|
|
640
802
|
if (wsRequested && rootParsed) {
|
|
641
803
|
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
642
|
-
return { config: fb.config, source: 'root', degraded: true };
|
|
804
|
+
return fallback({ config: fb.config, source: 'root', degraded: true });
|
|
643
805
|
}
|
|
644
806
|
// Branch B, C, D, E
|
|
645
807
|
if (node_fs_1.default.existsSync(planningDir(cwd, ws))) {
|
|
@@ -647,24 +809,32 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
647
809
|
// Branch B: workstream requested but ws config.json absent; root config present.
|
|
648
810
|
// (Only reached when wsRequested is false — e.g. ws='' with .planning/workstreams//config.json)
|
|
649
811
|
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
650
|
-
return { config: fb.config, source: 'root', degraded: true };
|
|
812
|
+
return fallback({ config: fb.config, source: 'root', degraded: true });
|
|
651
813
|
}
|
|
652
814
|
// Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
|
|
653
815
|
try {
|
|
654
|
-
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
816
|
+
return fallback({ config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false });
|
|
655
817
|
}
|
|
656
818
|
catch {
|
|
657
|
-
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
819
|
+
return fallback({ config: defaults, source: 'builtin-defaults', degraded: false });
|
|
658
820
|
}
|
|
659
821
|
}
|
|
660
822
|
// Branch D or E: no .planning/
|
|
661
823
|
try {
|
|
662
824
|
const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
|
|
663
825
|
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
826
|
+
const globalRead = _readConfigFile(globalDefaultsPath);
|
|
827
|
+
if (globalRead.kind === 'fault') {
|
|
828
|
+
// ~/.gsd/defaults.json is present but unusable. Only report it when the
|
|
829
|
+
// project config did not already fail — the nearer file is the one the
|
|
830
|
+
// user is most likely to be able to act on.
|
|
831
|
+
if (!configFault)
|
|
832
|
+
configFault = globalRead.fault;
|
|
833
|
+
_warnUnusableConfig(globalRead.fault);
|
|
834
|
+
}
|
|
835
|
+
if (globalRead.kind !== 'ok')
|
|
836
|
+
throw new Error('global defaults absent or unusable');
|
|
837
|
+
const globalDefaults = globalRead.data;
|
|
668
838
|
const _globalBaseCfg = {
|
|
669
839
|
...defaults,
|
|
670
840
|
model_profile: (globalDefaults['model_profile']) ?? defaults.model_profile,
|
|
@@ -702,19 +872,19 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
702
872
|
};
|
|
703
873
|
// Branch D: global-defaults
|
|
704
874
|
try {
|
|
705
|
-
return { config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false };
|
|
875
|
+
return fallback({ config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false });
|
|
706
876
|
}
|
|
707
877
|
catch {
|
|
708
|
-
return { config: _globalBaseCfg, source: 'global-defaults', degraded: false };
|
|
878
|
+
return fallback({ config: _globalBaseCfg, source: 'global-defaults', degraded: false });
|
|
709
879
|
}
|
|
710
880
|
}
|
|
711
881
|
catch {
|
|
712
882
|
// Branch E: no global defaults
|
|
713
883
|
try {
|
|
714
|
-
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
884
|
+
return fallback({ config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false });
|
|
715
885
|
}
|
|
716
886
|
catch {
|
|
717
|
-
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
887
|
+
return fallback({ config: defaults, source: 'builtin-defaults', degraded: false });
|
|
718
888
|
}
|
|
719
889
|
}
|
|
720
890
|
}
|
|
@@ -729,6 +899,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
729
899
|
module.exports = {
|
|
730
900
|
loadConfig,
|
|
731
901
|
loadConfigResolved,
|
|
902
|
+
CONFIG_REASON,
|
|
903
|
+
_warnedUnusableConfig,
|
|
732
904
|
isGitIgnored,
|
|
733
905
|
CONFIG_DEFAULTS,
|
|
734
906
|
_getConfigDefault,
|
|
@@ -21,7 +21,7 @@ const { CONFIG_DEFAULTS } = configLoader;
|
|
|
21
21
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
23
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
24
|
-
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
24
|
+
const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
26
|
const modelProfiles = require("./model-profiles.cjs");
|
|
27
27
|
const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
|
|
@@ -66,6 +66,9 @@ const SCHEMA_DEFAULTS = {
|
|
|
66
66
|
'executor.stall_detect_interval_minutes': 5,
|
|
67
67
|
'executor.stall_threshold_minutes': 10,
|
|
68
68
|
'git.create_tag': true,
|
|
69
|
+
// Derived from the defaults manifest rather than restated, so the manifest
|
|
70
|
+
// stays the single source of truth for the smart-zone budget (#2630).
|
|
71
|
+
'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
|
|
69
72
|
};
|
|
70
73
|
/**
|
|
71
74
|
* Resolve a schema-level default for an absent key (#2256). Checks the legacy
|
|
@@ -704,6 +707,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
704
707
|
error(`Invalid context_window '${val}'. Must be a positive integer (token count).`, ERROR_REASON.USAGE);
|
|
705
708
|
}
|
|
706
709
|
}
|
|
710
|
+
// Smart-zone token budget (#2630, ADR-2629). Same shape as context_window:
|
|
711
|
+
// a positive integer token count. A POLICY default, not a benchmark constant.
|
|
712
|
+
// Number.isSafeInteger, NOT Number.isInteger: the read side
|
|
713
|
+
// (estimate-cli readSmartZoneBudget) accepts only safe integers, so an
|
|
714
|
+
// isInteger-only gate would let config-set 'succeed' on a value past 2^53
|
|
715
|
+
// that estimate-check then silently ignores in favour of the default.
|
|
716
|
+
// Accept and honour must agree.
|
|
717
|
+
if (kp === 'workflow.smart_zone_tokens') {
|
|
718
|
+
if (typeof parsedValue !== 'number' || !Number.isSafeInteger(parsedValue) || parsedValue < 1) {
|
|
719
|
+
error(`Invalid workflow.smart_zone_tokens '${val}'. Must be a positive integer (token count).`, ERROR_REASON.USAGE);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
707
722
|
// Post-planning gap checker (#2493)
|
|
708
723
|
if (kp === 'workflow.post_planning_gaps') {
|
|
709
724
|
if (typeof parsedValue !== 'boolean') {
|
|
@@ -867,11 +882,21 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
867
882
|
if (node_fs_1.default.existsSync(configPath)) {
|
|
868
883
|
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
869
884
|
}
|
|
870
|
-
else if (hasDefault) {
|
|
871
|
-
emitResolvedDefault(kp, defaultValue, raw);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
885
|
else {
|
|
886
|
+
// #2702: when a workstream is active and has no config.json of its own, fall
|
|
887
|
+
// back to the project ROOT config first — a key the user configured at root is
|
|
888
|
+
// a real, present value and must inherit (per #1893: a present key wins over
|
|
889
|
+
// --default). Only when root also misses do --default / schema default apply.
|
|
890
|
+
// (When no workstream is active, resolveFromRootConfig is a no-op: same file.)
|
|
891
|
+
const rootVal = resolveFromRootConfig(cwd, kp);
|
|
892
|
+
if (rootVal.found) {
|
|
893
|
+
emitResolvedDefault(kp, rootVal.value, raw);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (hasDefault) {
|
|
897
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
875
900
|
const sd = resolveSchemaDefault(cwd, kp);
|
|
876
901
|
if (sd.found) {
|
|
877
902
|
emitResolvedDefault(kp, sd.value, raw);
|
|
@@ -890,6 +915,12 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
890
915
|
let current = config;
|
|
891
916
|
for (const key of keys) {
|
|
892
917
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
918
|
+
// #2702: root-config inheritance before --default / schema default (see above).
|
|
919
|
+
const rootVal = resolveFromRootConfig(cwd, kp);
|
|
920
|
+
if (rootVal.found) {
|
|
921
|
+
emitResolvedDefault(kp, rootVal.value, raw);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
893
924
|
if (hasDefault) {
|
|
894
925
|
emitResolvedDefault(kp, defaultValue, raw);
|
|
895
926
|
return;
|
|
@@ -915,6 +946,12 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
915
946
|
: undefined;
|
|
916
947
|
}
|
|
917
948
|
if (current === undefined) {
|
|
949
|
+
// #2702: root-config inheritance before --default / schema default (see above).
|
|
950
|
+
const rootVal = resolveFromRootConfig(cwd, kp);
|
|
951
|
+
if (rootVal.found) {
|
|
952
|
+
emitResolvedDefault(kp, rootVal.value, raw);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
918
955
|
if (hasDefault) {
|
|
919
956
|
emitResolvedDefault(kp, defaultValue, raw);
|
|
920
957
|
return;
|
|
@@ -935,6 +972,54 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
935
972
|
}
|
|
936
973
|
output(current, raw, String(current));
|
|
937
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* #2702: resolve a dot-notation key against the project ROOT config
|
|
977
|
+
* (`.planning/config.json`), ignoring any active workstream scope. Returns
|
|
978
|
+
* `{found:false}` when the root config is absent, unparseable, or does not
|
|
979
|
+
* contain the key. This is the inheritance rung `cmdConfigGet` was missing —
|
|
980
|
+
* when a workstream's own config doesn't set a key, the project root value
|
|
981
|
+
* must show through (workstream overrides root; it never fully replaces it),
|
|
982
|
+
* exactly as `loadConfigResolved`'s root+workstream merge already does for
|
|
983
|
+
* every other config consumer. No-op (found:false) when no workstream is
|
|
984
|
+
* active, because `planningDir === planningRoot` and the caller already read
|
|
985
|
+
* that file directly.
|
|
986
|
+
*/
|
|
987
|
+
function resolveFromRootConfig(cwd, kp) {
|
|
988
|
+
// Only meaningful when a workstream is active (GSD_WORKSTREAM set) — that is what
|
|
989
|
+
// redirects planningDir away from root AND what loadConfigResolved gates root-reading
|
|
990
|
+
// on. Gating on `process.env.GSD_WORKSTREAM` (not on a planningDir !== planningRoot
|
|
991
|
+
// path inequality) avoids a false trigger under GSD_PROJECT alone, where planningDir
|
|
992
|
+
// diverges from planningRoot without a workstream and loadConfigResolved does NOT
|
|
993
|
+
// inherit root — matching the runtime's own `if (ws)` gate keeps the two surfaces
|
|
994
|
+
// from diverging on the project-scoped (non-workstream) case.
|
|
995
|
+
if (!process.env['GSD_WORKSTREAM'])
|
|
996
|
+
return { found: false, value: undefined };
|
|
997
|
+
const root = planningRoot(cwd);
|
|
998
|
+
const rootConfigPath = node_path_1.default.join(root, 'config.json');
|
|
999
|
+
let rootConfig;
|
|
1000
|
+
try {
|
|
1001
|
+
if (!node_fs_1.default.existsSync(rootConfigPath))
|
|
1002
|
+
return { found: false, value: undefined };
|
|
1003
|
+
rootConfig = JSON.parse(node_fs_1.default.readFileSync(rootConfigPath, 'utf-8'));
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
// Unparseable root config → don't inherit (do not let a corrupt root file
|
|
1007
|
+
// change config-get's verdict). Fall through to schema default / error.
|
|
1008
|
+
return { found: false, value: undefined };
|
|
1009
|
+
}
|
|
1010
|
+
let current = rootConfig;
|
|
1011
|
+
for (const key of kp.split('.')) {
|
|
1012
|
+
if (current === undefined || current === null || typeof current !== 'object') {
|
|
1013
|
+
return { found: false, value: undefined };
|
|
1014
|
+
}
|
|
1015
|
+
current = Object.prototype.hasOwnProperty.call(current, key)
|
|
1016
|
+
? current[key]
|
|
1017
|
+
: undefined;
|
|
1018
|
+
}
|
|
1019
|
+
if (current === undefined)
|
|
1020
|
+
return { found: false, value: undefined };
|
|
1021
|
+
return { found: true, value: current };
|
|
1022
|
+
}
|
|
938
1023
|
/**
|
|
939
1024
|
* Command to set the model profile in the config file.
|
|
940
1025
|
*
|