@opengsd/gsd-core 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- 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 +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- 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 +1450 -160
- 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/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- 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 +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- 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/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 +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- 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 +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- 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 +4 -15
- 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/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -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/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- 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/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -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/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -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/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- 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 +97 -9
- 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-context-monitor.js +27 -9
- 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 +97 -9
- 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 +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- 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-test-timings.cjs +201 -0
- 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-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - ./planning-workspace.cjs (planningDir, planningRoot)
|
|
19
19
|
* - ./shell-command-projection.cjs (execGit, platformWriteSync, platformReadSync)
|
|
20
20
|
* - ./core-utils.cjs (detectSubRepos)
|
|
21
|
-
* - ./model-catalog.cjs (KNOWN_RUNTIMES, KNOWN_PROVIDERS)
|
|
21
|
+
* - ./model-catalog.cjs (KNOWN_RUNTIMES, KNOWN_PROVIDERS, ADAPTIVE_TIER_VALUES)
|
|
22
22
|
*/
|
|
23
23
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
24
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -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.
|
|
@@ -172,7 +173,11 @@ function isGitIgnored(cwd, targetPath) {
|
|
|
172
173
|
return ignored;
|
|
173
174
|
}
|
|
174
175
|
// ─── Model alias resolution ───────────────────────────────────────────────────
|
|
175
|
-
|
|
176
|
+
// Catalog-derived (model-catalog.cts) so this vocabulary can never drift from
|
|
177
|
+
// VALID_TIERS in verify.cts — see #2070 "Generative Fix Divergence". Excludes
|
|
178
|
+
// 'inherit' (unlike VALID_TIERS): runtime overrides always resolve to a
|
|
179
|
+
// concrete tier, never the adaptive sentinel.
|
|
180
|
+
const RUNTIME_OVERRIDE_TIERS = model_catalog_cjs_1.ADAPTIVE_TIER_VALUES;
|
|
176
181
|
const _warnedConfigKeys = new Set();
|
|
177
182
|
function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
178
183
|
if (!parsed || typeof parsed !== 'object')
|
|
@@ -276,8 +281,15 @@ function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
|
276
281
|
}
|
|
277
282
|
// Internal helper exposed for tests so per-process warning state can be reset
|
|
278
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).
|
|
279
289
|
function _resetRuntimeWarningCacheForTests() {
|
|
280
290
|
_warnedConfigKeys.clear();
|
|
291
|
+
_warnedUnknownConfigKeys.clear();
|
|
292
|
+
_warnedUnusableConfig.clear();
|
|
281
293
|
}
|
|
282
294
|
// ─── FIX 2: Federated overlay helpers ────────────────────────────────────────
|
|
283
295
|
/**
|
|
@@ -380,6 +392,102 @@ function _applyFederatedOverlay(baseConfig, userConfig, cwd) {
|
|
|
380
392
|
_applyFederatedValues(cloned, _fedOverlay.values, _fedOverlay.validKeys);
|
|
381
393
|
return cloned;
|
|
382
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
|
+
}
|
|
383
491
|
/**
|
|
384
492
|
* loadConfigResolved — provenance-aware config loading (#1415, ADR-1411 P2).
|
|
385
493
|
*
|
|
@@ -387,13 +495,21 @@ function _applyFederatedOverlay(baseConfig, userConfig, cwd) {
|
|
|
387
495
|
* { config, source, degraded } instead of just the config object.
|
|
388
496
|
* loadConfig now delegates to this function (byte-identical back-compat).
|
|
389
497
|
*
|
|
390
|
-
* Branch → source/degraded mapping:
|
|
391
|
-
* A1: ws set + ws config.json found → source:'workstream', degraded:false
|
|
392
|
-
* A2: ws null + config.json found → source:'root', degraded:false
|
|
393
|
-
* B: catch + .planning/ + rootParsed set (ws fallback) → source:'root', degraded:true
|
|
394
|
-
* C: catch + .planning/ + rootParsed null (federated defaults) → source:'builtin-defaults', degraded:false
|
|
395
|
-
* D: catch + no .planning/ + ~/.gsd/defaults.json readable → source:'global-defaults', degraded:false
|
|
396
|
-
* 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.
|
|
397
513
|
*/
|
|
398
514
|
function loadConfigResolved(cwd, options = {}) {
|
|
399
515
|
// NOTE: loadConfigResolved resolves from cwd AS-IS (no walk-up).
|
|
@@ -415,14 +531,39 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
415
531
|
cachedSubRepos = detectSubRepos(cwd);
|
|
416
532
|
return cachedSubRepos.slice();
|
|
417
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
|
+
};
|
|
418
555
|
let rootParsed = null;
|
|
419
556
|
if (ws) {
|
|
420
557
|
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
421
558
|
try {
|
|
422
|
-
const
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
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;
|
|
426
567
|
const { parsed: rootNormalized, normalizations: rootNorms } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
|
|
427
568
|
if (rootNorms.length > 0) {
|
|
428
569
|
for (const norm of rootNorms) {
|
|
@@ -453,10 +594,18 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
453
594
|
const configPath = node_path_1.default.join(planningDir(cwd, ws), 'config.json');
|
|
454
595
|
const defaults = CONFIG_DEFAULTS;
|
|
455
596
|
try {
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
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;
|
|
460
609
|
let configDirty = false;
|
|
461
610
|
{
|
|
462
611
|
const { parsed: normalized, normalizations } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
|
|
@@ -627,7 +776,24 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
627
776
|
// A1 vs A2: disambiguate by whether a real workstream was requested.
|
|
628
777
|
// Fix 4: empty-string ws ('') resolves the root path → source:'root'.
|
|
629
778
|
const source = wsRequested ? 'workstream' : 'root';
|
|
630
|
-
|
|
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 };
|
|
631
797
|
}
|
|
632
798
|
catch {
|
|
633
799
|
// Fix 2: Early intercept — workstream requested but ws config.json absent (or dir absent)
|
|
@@ -635,7 +801,7 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
635
801
|
// This delivers the #1366 acceptance criterion: nonexistent GSD_WORKSTREAM yields root, degraded.
|
|
636
802
|
if (wsRequested && rootParsed) {
|
|
637
803
|
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
638
|
-
return { config: fb.config, source: 'root', degraded: true };
|
|
804
|
+
return fallback({ config: fb.config, source: 'root', degraded: true });
|
|
639
805
|
}
|
|
640
806
|
// Branch B, C, D, E
|
|
641
807
|
if (node_fs_1.default.existsSync(planningDir(cwd, ws))) {
|
|
@@ -643,24 +809,32 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
643
809
|
// Branch B: workstream requested but ws config.json absent; root config present.
|
|
644
810
|
// (Only reached when wsRequested is false — e.g. ws='' with .planning/workstreams//config.json)
|
|
645
811
|
const fb = loadConfigResolved(cwd, { workstream: null });
|
|
646
|
-
return { config: fb.config, source: 'root', degraded: true };
|
|
812
|
+
return fallback({ config: fb.config, source: 'root', degraded: true });
|
|
647
813
|
}
|
|
648
814
|
// Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
|
|
649
815
|
try {
|
|
650
|
-
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
816
|
+
return fallback({ config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false });
|
|
651
817
|
}
|
|
652
818
|
catch {
|
|
653
|
-
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
819
|
+
return fallback({ config: defaults, source: 'builtin-defaults', degraded: false });
|
|
654
820
|
}
|
|
655
821
|
}
|
|
656
822
|
// Branch D or E: no .planning/
|
|
657
823
|
try {
|
|
658
824
|
const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
|
|
659
825
|
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
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;
|
|
664
838
|
const _globalBaseCfg = {
|
|
665
839
|
...defaults,
|
|
666
840
|
model_profile: (globalDefaults['model_profile']) ?? defaults.model_profile,
|
|
@@ -687,22 +861,30 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
687
861
|
fast_mode: (globalDefaults['fast_mode']) || null,
|
|
688
862
|
agent_skills: (globalDefaults['agent_skills']) || {},
|
|
689
863
|
response_language: (globalDefaults['response_language']) || null,
|
|
864
|
+
// #2069: forward model_policy / model_profile_overrides / runtime so the global-defaults
|
|
865
|
+
// path is at parity with the project-config path (which forwards these three from
|
|
866
|
+
// parsed['…'] at the top of this function). Without these entries, ~/.gsd/defaults.json
|
|
867
|
+
// silently drops them — model_policy/provider/budget etc. are honored when set in a
|
|
868
|
+
// project but ignored when set globally.
|
|
869
|
+
runtime: (globalDefaults['runtime']) || null,
|
|
870
|
+
model_profile_overrides: (globalDefaults['model_profile_overrides']) || null,
|
|
871
|
+
model_policy: (globalDefaults['model_policy']) || null,
|
|
690
872
|
};
|
|
691
873
|
// Branch D: global-defaults
|
|
692
874
|
try {
|
|
693
|
-
return { config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false };
|
|
875
|
+
return fallback({ config: _applyFederatedOverlay(_globalBaseCfg, globalDefaults, cwd), source: 'global-defaults', degraded: false });
|
|
694
876
|
}
|
|
695
877
|
catch {
|
|
696
|
-
return { config: _globalBaseCfg, source: 'global-defaults', degraded: false };
|
|
878
|
+
return fallback({ config: _globalBaseCfg, source: 'global-defaults', degraded: false });
|
|
697
879
|
}
|
|
698
880
|
}
|
|
699
881
|
catch {
|
|
700
882
|
// Branch E: no global defaults
|
|
701
883
|
try {
|
|
702
|
-
return { config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false };
|
|
884
|
+
return fallback({ config: _applyFederatedOverlay(defaults, {}, cwd), source: 'builtin-defaults', degraded: false });
|
|
703
885
|
}
|
|
704
886
|
catch {
|
|
705
|
-
return { config: defaults, source: 'builtin-defaults', degraded: false };
|
|
887
|
+
return fallback({ config: defaults, source: 'builtin-defaults', degraded: false });
|
|
706
888
|
}
|
|
707
889
|
}
|
|
708
890
|
}
|
|
@@ -717,6 +899,8 @@ function loadConfig(cwd, options = {}) {
|
|
|
717
899
|
module.exports = {
|
|
718
900
|
loadConfig,
|
|
719
901
|
loadConfigResolved,
|
|
902
|
+
CONFIG_REASON,
|
|
903
|
+
_warnedUnusableConfig,
|
|
720
904
|
isGitIgnored,
|
|
721
905
|
CONFIG_DEFAULTS,
|
|
722
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,7 +66,49 @@ 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
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a schema-level default for an absent key (#2256). Checks the legacy
|
|
75
|
+
* hardcoded SCHEMA_DEFAULTS first, then the capability-registry configSchema
|
|
76
|
+
* default — the same registry default the runtime's capability-activation
|
|
77
|
+
* resolver (resolveConfigKey Level 4, capability-activation.cts) already honors,
|
|
78
|
+
* so `query config-get` can no longer disagree with the runtime about an absent
|
|
79
|
+
* key's effective value.
|
|
80
|
+
*/
|
|
81
|
+
function resolveSchemaDefault(cwd, kp) {
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
|
|
83
|
+
return { found: true, value: SCHEMA_DEFAULTS[kp] };
|
|
84
|
+
}
|
|
85
|
+
const capSchema = getCapabilityConfigSchema(cwd);
|
|
86
|
+
if (capSchema && typeof capSchema === 'object'
|
|
87
|
+
&& Object.prototype.hasOwnProperty.call(capSchema, kp)) {
|
|
88
|
+
const entry = capSchema[kp];
|
|
89
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
90
|
+
const def = entry['default'];
|
|
91
|
+
if (def !== undefined)
|
|
92
|
+
return { found: true, value: def };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { found: false, value: undefined };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Emit a schema-resolved default (#2256), applying the same secret-masking
|
|
99
|
+
* invariant the found-key path applies. getCapabilityConfigSchema is a
|
|
100
|
+
* federated, third-party-extensible surface (ADR-1244) — a future key-name
|
|
101
|
+
* collision with a secret key must not leak a declared default in plaintext.
|
|
102
|
+
* Centralizing emission here means masking can't be missed at a call site.
|
|
103
|
+
*/
|
|
104
|
+
function emitResolvedDefault(kp, value, raw) {
|
|
105
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
106
|
+
const masked = (0, secrets_cjs_1.maskSecret)(value);
|
|
107
|
+
output(masked, raw, masked);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
output(value, raw, String(value));
|
|
111
|
+
}
|
|
70
112
|
// ─── Validation helpers ───────────────────────────────────────────────────────
|
|
71
113
|
function validateKnownConfigKeyPath(keyPath) {
|
|
72
114
|
const suggested = CONFIG_KEY_SUGGESTIONS[keyPath];
|
|
@@ -665,6 +707,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
665
707
|
error(`Invalid context_window '${val}'. Must be a positive integer (token count).`, ERROR_REASON.USAGE);
|
|
666
708
|
}
|
|
667
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
|
+
}
|
|
668
722
|
// Post-planning gap checker (#2493)
|
|
669
723
|
if (kp === 'workflow.post_planning_gaps') {
|
|
670
724
|
if (typeof parsedValue !== 'boolean') {
|
|
@@ -698,6 +752,10 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
698
752
|
error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
|
|
699
753
|
}
|
|
700
754
|
}
|
|
755
|
+
// Statusline GSD-state format enum validation
|
|
756
|
+
const VALID_STATE_FORMATS = ['full', 'compact'];
|
|
757
|
+
if (kp === 'statusline.state_format')
|
|
758
|
+
assertEnumValue(parsedValue, val, VALID_STATE_FORMATS, 'statusline.state_format');
|
|
701
759
|
// statusline.show_git — boolean only
|
|
702
760
|
if (kp === 'statusline.show_git') {
|
|
703
761
|
if (typeof parsedValue !== 'boolean') {
|
|
@@ -824,17 +882,26 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
824
882
|
if (node_fs_1.default.existsSync(configPath)) {
|
|
825
883
|
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
826
884
|
}
|
|
827
|
-
else if (hasDefault) {
|
|
828
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
829
|
-
output(defaultValue, raw, String(defaultValue));
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
|
|
833
|
-
const def = SCHEMA_DEFAULTS[kp];
|
|
834
|
-
output(def, raw, String(def));
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
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
|
+
}
|
|
900
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
901
|
+
if (sd.found) {
|
|
902
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
838
905
|
error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
|
|
839
906
|
}
|
|
840
907
|
}
|
|
@@ -848,29 +915,50 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
848
915
|
let current = config;
|
|
849
916
|
for (const key of keys) {
|
|
850
917
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
851
|
-
//
|
|
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
|
+
}
|
|
852
924
|
if (hasDefault) {
|
|
853
|
-
|
|
925
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
854
926
|
return;
|
|
855
927
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
928
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
929
|
+
if (sd.found) {
|
|
930
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
859
931
|
return;
|
|
860
932
|
}
|
|
861
933
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
862
934
|
}
|
|
863
|
-
|
|
935
|
+
// Own-property gate: bracket access on a plain object walks the
|
|
936
|
+
// prototype chain, so an unqualified `current[key]` would resolve
|
|
937
|
+
// '__proto__' / 'constructor' / 'hasOwnProperty' (and other
|
|
938
|
+
// Object.prototype members) to their inherited values instead of
|
|
939
|
+
// correctly reporting them as absent. hasOwnProperty.call only
|
|
940
|
+
// returns true for a key JSON.parse actually assigned as data on
|
|
941
|
+
// this object (including a literal "__proto__" JSON key, which
|
|
942
|
+
// JSON.parse defines as an own data property, not the accessor) —
|
|
943
|
+
// never for something inherited from the prototype chain.
|
|
944
|
+
current = Object.prototype.hasOwnProperty.call(current, key)
|
|
945
|
+
? current[key]
|
|
946
|
+
: undefined;
|
|
864
947
|
}
|
|
865
948
|
if (current === undefined) {
|
|
866
|
-
//
|
|
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
|
+
}
|
|
867
955
|
if (hasDefault) {
|
|
868
|
-
|
|
956
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
869
957
|
return;
|
|
870
958
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
959
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
960
|
+
if (sd.found) {
|
|
961
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
874
962
|
return;
|
|
875
963
|
}
|
|
876
964
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
@@ -884,6 +972,54 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
884
972
|
}
|
|
885
973
|
output(current, raw, String(current));
|
|
886
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
|
+
}
|
|
887
1023
|
/**
|
|
888
1024
|
* Command to set the model profile in the config file.
|
|
889
1025
|
*
|
|
@@ -186,8 +186,13 @@ function extractCanonicalPlanId(filename) {
|
|
|
186
186
|
// or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
|
|
187
187
|
// so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
|
|
188
188
|
const tokenRe = /^(?:\d{2,}[A-Z]?|\d[A-Z])(?:\.\d+)*$/i;
|
|
189
|
+
// #2232: the PAIRED plan component is a zero-padded continuation segment
|
|
190
|
+
// (exactly 2 digits), so a ≥3-digit slug word (a year) is not paired into a
|
|
191
|
+
// bogus "14-2026" id. The leading phase component keeps tokenRe's unbounded
|
|
192
|
+
// \d{2,} — phase numbers ≥100 are legitimate; only continuations are capped.
|
|
193
|
+
const planTokenRe = new RegExp(`^(?:${phaseIdModule.PHASE_CONTINUATION_SEGMENT_SOURCE}[A-Z]?|\\d[A-Z])(?:\\.\\d+)*$`, 'i');
|
|
189
194
|
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
|
190
|
-
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length &&
|
|
195
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && planTokenRe.test(parts[phaseIdx + 1])) {
|
|
191
196
|
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
192
197
|
}
|
|
193
198
|
return base;
|