@opengsd/gsd-core 1.7.0 → 1.8.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 +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +29 -2
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1152 -80
- 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 +3 -2
- package/gsd-core/bin/gsd-tools.cjs +1878 -2507
- 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 +338 -45
- 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-registry.cjs +155 -86
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +128 -25
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +81 -4
- package/gsd-core/bin/lib/config-loader.cjs +14 -2
- package/gsd-core/bin/lib/config.cjs +69 -18
- 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/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/init.cjs +111 -47
- package/gsd-core/bin/lib/install-engine.cjs +298 -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 +246 -12
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase.cjs +201 -12
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
- package/gsd-core/bin/lib/roadmap.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
- package/gsd-core/bin/lib/smart-entry.cjs +69 -4
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +22 -1
- package/gsd-core/bin/lib/state.cjs +65 -11
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat.cjs +420 -5
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +220 -22
- package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
- 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/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/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/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- 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 +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +25 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -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 +25 -34
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- 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 +19 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +89 -73
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -0
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +50 -3
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +88 -3
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +88 -3
- package/package.json +6 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- 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 +3 -2
- package/vscode/package.json +1 -1
|
@@ -33,7 +33,10 @@ const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
|
33
33
|
const { extractCurrentMilestone, stripShippedMilestones: _stripShippedMilestones, getMilestoneInfo, getMilestonePhaseFilter, getRoadmapPhaseInternal } = roadmapParserMod;
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
35
|
const modelResolverMod = require("./model-resolver.cjs");
|
|
36
|
-
const { resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
|
|
36
|
+
const { resolveModelInternal, resolveModelForTier, resolveProviderEscalation, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const agentCommandRouterMod = require("./agent-command-router.cjs");
|
|
39
|
+
const { AGENT_FAILURE_CLASSES } = agentCommandRouterMod;
|
|
37
40
|
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
38
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
42
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -47,6 +50,44 @@ const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
|
|
|
47
50
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
48
51
|
const clock_cjs_1 = require("./clock.cjs");
|
|
49
52
|
// ─── Phase Status ─────────────────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Phase-status precedence ladder — furthest-along wins (#2408).
|
|
55
|
+
*
|
|
56
|
+
* `cmdStats` builds `phasesByNumber` by scanning on-disk phase directories.
|
|
57
|
+
* When two directories normalize to the same phase key (e.g. `05-real/` and
|
|
58
|
+
* `05-real-stray/`), the status field must be folded by precedence rather
|
|
59
|
+
* than overwritten last-write-wins — otherwise `/gsd-stats` reports whatever
|
|
60
|
+
* directory `fs.readdirSync` happened to yield last, which is non-deterministic
|
|
61
|
+
* across platforms and can silently call a `Complete` phase `Not Started`.
|
|
62
|
+
*/
|
|
63
|
+
const PHASE_STATUS_PRECEDENCE = [
|
|
64
|
+
'Complete',
|
|
65
|
+
'Needs Review',
|
|
66
|
+
'Executed',
|
|
67
|
+
'In Progress',
|
|
68
|
+
'Planned',
|
|
69
|
+
'Not Started',
|
|
70
|
+
'Pending',
|
|
71
|
+
];
|
|
72
|
+
const PHASE_STATUS_RANK = new Map(PHASE_STATUS_PRECEDENCE.map((s, i) => [s, i]));
|
|
73
|
+
/**
|
|
74
|
+
* Fold two phase statuses by precedence — returns whichever is further along
|
|
75
|
+
* the {@link PHASE_STATUS_PRECEDENCE} ladder. Unrecognized statuses fall behind
|
|
76
|
+
* every recognized one (so a recognized status always wins over an unknown one;
|
|
77
|
+
* two unrecognized statuses favor `a` for determinism).
|
|
78
|
+
*/
|
|
79
|
+
function foldPhaseStatus(a, b) {
|
|
80
|
+
const ra = PHASE_STATUS_RANK.get(a);
|
|
81
|
+
const rb = PHASE_STATUS_RANK.get(b);
|
|
82
|
+
if (ra === undefined && rb === undefined)
|
|
83
|
+
return a;
|
|
84
|
+
if (ra === undefined)
|
|
85
|
+
return b;
|
|
86
|
+
if (rb === undefined)
|
|
87
|
+
return a;
|
|
88
|
+
// Lower rank = higher precedence (Complete=0 wins over Not Started=5).
|
|
89
|
+
return ra <= rb ? a : b;
|
|
90
|
+
}
|
|
50
91
|
/**
|
|
51
92
|
* Determine phase status by checking plan/summary counts AND verification state.
|
|
52
93
|
* Introduces "Executed" for phases with all summaries but no passing verification.
|
|
@@ -128,6 +169,9 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
128
169
|
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
129
170
|
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
130
171
|
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
|
172
|
+
// #2337: surface severity when present. Omit the key entirely for todos
|
|
173
|
+
// with no severity line so existing consumers of this JSON are unaffected.
|
|
174
|
+
const severityMatch = content.match(/^severity:\s*(.+)$/m);
|
|
131
175
|
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
132
176
|
// Apply area filter if specified
|
|
133
177
|
if (area && todoArea !== area)
|
|
@@ -139,6 +183,7 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
139
183
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
140
184
|
area: todoArea,
|
|
141
185
|
path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(pendingDir, file))),
|
|
186
|
+
...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
|
|
142
187
|
});
|
|
143
188
|
}
|
|
144
189
|
}
|
|
@@ -394,7 +439,8 @@ function cmdResolveGranularity(cwd, phaseType, raw, override) {
|
|
|
394
439
|
* { model, profile, effort, effort_rendered, effort_param, effort_propagation,
|
|
395
440
|
* fast_mode, fast_mode_supported, [unknown_agent] }
|
|
396
441
|
*
|
|
397
|
-
* Flags: --effort <level>, --fast-mode <true|false>, --attempt <n
|
|
442
|
+
* Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>,
|
|
443
|
+
* --failure-class <class> (#2296)
|
|
398
444
|
*/
|
|
399
445
|
function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
400
446
|
if (!agentType) {
|
|
@@ -403,7 +449,29 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
403
449
|
opts = opts || {};
|
|
404
450
|
const config = loadConfig(cwd);
|
|
405
451
|
const profile = config['model_profile'] || 'balanced';
|
|
406
|
-
|
|
452
|
+
// #2068: resolve the model per-attempt so dynamic_routing escalates the MODEL
|
|
453
|
+
// (heavy tier) alongside effort. Gated on an explicit --attempt exactly like the
|
|
454
|
+
// effort resolution below, so the two fields stay symmetric: with no --attempt
|
|
455
|
+
// the model comes from the classic profile path (unchanged for everyone,
|
|
456
|
+
// including dynamic_routing-enabled users who don't pass --attempt), and only an
|
|
457
|
+
// explicit attempt routes through the tier ladder. resolveModelForTier itself
|
|
458
|
+
// still falls back to resolveModelInternal when dynamic_routing is off.
|
|
459
|
+
let model = (opts.attempt !== undefined && opts.attempt !== null)
|
|
460
|
+
? resolveModelForTier(cwd, agentType, opts.attempt)
|
|
461
|
+
: resolveModelInternal(cwd, agentType);
|
|
462
|
+
// #2296: when the caller reports WHY the previous attempt failed, consult the
|
|
463
|
+
// provider-escalation ladder. Only a quota/rate-limit class warrants it — a
|
|
464
|
+
// heavier tier on the same throttled provider is still throttled, so this
|
|
465
|
+
// ladder swaps providers instead. Gated on an explicit --failure-class so the
|
|
466
|
+
// JSON contract is byte-identical for every existing caller.
|
|
467
|
+
let escalation;
|
|
468
|
+
if (opts.failureClass !== undefined) {
|
|
469
|
+
const applicable = opts.failureClass === AGENT_FAILURE_CLASSES.QUOTA_EXCEEDED;
|
|
470
|
+
const resolved = resolveProviderEscalation(cwd, agentType, opts.attempt, applicable);
|
|
471
|
+
if (resolved.escalated)
|
|
472
|
+
model = resolved.to;
|
|
473
|
+
escalation = { class: opts.failureClass, ...resolved };
|
|
474
|
+
}
|
|
407
475
|
const effortOpts = {};
|
|
408
476
|
if (typeof opts.effortOverride === 'string')
|
|
409
477
|
effortOpts['override'] = opts.effortOverride;
|
|
@@ -430,6 +498,8 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
430
498
|
};
|
|
431
499
|
if (!agentModels)
|
|
432
500
|
result['unknown_agent'] = true;
|
|
501
|
+
if (escalation)
|
|
502
|
+
result['escalation'] = escalation;
|
|
433
503
|
output(result, raw, effort);
|
|
434
504
|
}
|
|
435
505
|
/**
|
|
@@ -1385,7 +1455,12 @@ function cmdStats(cwd, format, raw) {
|
|
|
1385
1455
|
name: existing?.name || phaseName,
|
|
1386
1456
|
plans: (existing?.plans || 0) + plans,
|
|
1387
1457
|
summaries: (existing?.summaries || 0) + summaries,
|
|
1388
|
-
|
|
1458
|
+
// #2408: fold colliding statuses by precedence rather than overwriting
|
|
1459
|
+
// last-write-wins. fs.readdirSync order is non-deterministic across
|
|
1460
|
+
// platforms, so a naive overwrite can report a Complete phase as Not
|
|
1461
|
+
// Started (or vice versa) depending on read order. The fold picks the
|
|
1462
|
+
// furthest-along status, matching what an operator expects.
|
|
1463
|
+
status: existing ? foldPhaseStatus(existing.status, status) : status,
|
|
1389
1464
|
});
|
|
1390
1465
|
}
|
|
1391
1466
|
}
|
|
@@ -1507,6 +1582,8 @@ function cmdCheckCommit(cwd, raw) {
|
|
|
1507
1582
|
module.exports = {
|
|
1508
1583
|
groupFilesBySubrepo,
|
|
1509
1584
|
determinePhaseStatus,
|
|
1585
|
+
foldPhaseStatus,
|
|
1586
|
+
PHASE_STATUS_PRECEDENCE,
|
|
1510
1587
|
cmdGenerateSlug,
|
|
1511
1588
|
cmdCurrentTimestamp,
|
|
1512
1589
|
cmdListTodos,
|
|
@@ -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 };
|
|
@@ -172,7 +172,11 @@ function isGitIgnored(cwd, targetPath) {
|
|
|
172
172
|
return ignored;
|
|
173
173
|
}
|
|
174
174
|
// ─── Model alias resolution ───────────────────────────────────────────────────
|
|
175
|
-
|
|
175
|
+
// Catalog-derived (model-catalog.cts) so this vocabulary can never drift from
|
|
176
|
+
// VALID_TIERS in verify.cts — see #2070 "Generative Fix Divergence". Excludes
|
|
177
|
+
// 'inherit' (unlike VALID_TIERS): runtime overrides always resolve to a
|
|
178
|
+
// concrete tier, never the adaptive sentinel.
|
|
179
|
+
const RUNTIME_OVERRIDE_TIERS = model_catalog_cjs_1.ADAPTIVE_TIER_VALUES;
|
|
176
180
|
const _warnedConfigKeys = new Set();
|
|
177
181
|
function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
178
182
|
if (!parsed || typeof parsed !== 'object')
|
|
@@ -687,6 +691,14 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
687
691
|
fast_mode: (globalDefaults['fast_mode']) || null,
|
|
688
692
|
agent_skills: (globalDefaults['agent_skills']) || {},
|
|
689
693
|
response_language: (globalDefaults['response_language']) || null,
|
|
694
|
+
// #2069: forward model_policy / model_profile_overrides / runtime so the global-defaults
|
|
695
|
+
// path is at parity with the project-config path (which forwards these three from
|
|
696
|
+
// parsed['…'] at the top of this function). Without these entries, ~/.gsd/defaults.json
|
|
697
|
+
// silently drops them — model_policy/provider/budget etc. are honored when set in a
|
|
698
|
+
// project but ignored when set globally.
|
|
699
|
+
runtime: (globalDefaults['runtime']) || null,
|
|
700
|
+
model_profile_overrides: (globalDefaults['model_profile_overrides']) || null,
|
|
701
|
+
model_policy: (globalDefaults['model_policy']) || null,
|
|
690
702
|
};
|
|
691
703
|
// Branch D: global-defaults
|
|
692
704
|
try {
|
|
@@ -67,6 +67,45 @@ const SCHEMA_DEFAULTS = {
|
|
|
67
67
|
'executor.stall_threshold_minutes': 10,
|
|
68
68
|
'git.create_tag': true,
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a schema-level default for an absent key (#2256). Checks the legacy
|
|
72
|
+
* hardcoded SCHEMA_DEFAULTS first, then the capability-registry configSchema
|
|
73
|
+
* default — the same registry default the runtime's capability-activation
|
|
74
|
+
* resolver (resolveConfigKey Level 4, capability-activation.cts) already honors,
|
|
75
|
+
* so `query config-get` can no longer disagree with the runtime about an absent
|
|
76
|
+
* key's effective value.
|
|
77
|
+
*/
|
|
78
|
+
function resolveSchemaDefault(cwd, kp) {
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
|
|
80
|
+
return { found: true, value: SCHEMA_DEFAULTS[kp] };
|
|
81
|
+
}
|
|
82
|
+
const capSchema = getCapabilityConfigSchema(cwd);
|
|
83
|
+
if (capSchema && typeof capSchema === 'object'
|
|
84
|
+
&& Object.prototype.hasOwnProperty.call(capSchema, kp)) {
|
|
85
|
+
const entry = capSchema[kp];
|
|
86
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
87
|
+
const def = entry['default'];
|
|
88
|
+
if (def !== undefined)
|
|
89
|
+
return { found: true, value: def };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { found: false, value: undefined };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit a schema-resolved default (#2256), applying the same secret-masking
|
|
96
|
+
* invariant the found-key path applies. getCapabilityConfigSchema is a
|
|
97
|
+
* federated, third-party-extensible surface (ADR-1244) — a future key-name
|
|
98
|
+
* collision with a secret key must not leak a declared default in plaintext.
|
|
99
|
+
* Centralizing emission here means masking can't be missed at a call site.
|
|
100
|
+
*/
|
|
101
|
+
function emitResolvedDefault(kp, value, raw) {
|
|
102
|
+
if ((0, secrets_cjs_1.isSecretKey)(kp)) {
|
|
103
|
+
const masked = (0, secrets_cjs_1.maskSecret)(value);
|
|
104
|
+
output(masked, raw, masked);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
output(value, raw, String(value));
|
|
108
|
+
}
|
|
70
109
|
// ─── Validation helpers ───────────────────────────────────────────────────────
|
|
71
110
|
function validateKnownConfigKeyPath(keyPath) {
|
|
72
111
|
const suggested = CONFIG_KEY_SUGGESTIONS[keyPath];
|
|
@@ -698,6 +737,10 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
698
737
|
error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
|
|
699
738
|
}
|
|
700
739
|
}
|
|
740
|
+
// Statusline GSD-state format enum validation
|
|
741
|
+
const VALID_STATE_FORMATS = ['full', 'compact'];
|
|
742
|
+
if (kp === 'statusline.state_format')
|
|
743
|
+
assertEnumValue(parsedValue, val, VALID_STATE_FORMATS, 'statusline.state_format');
|
|
701
744
|
// statusline.show_git — boolean only
|
|
702
745
|
if (kp === 'statusline.show_git') {
|
|
703
746
|
if (typeof parsedValue !== 'boolean') {
|
|
@@ -825,16 +868,15 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
825
868
|
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
826
869
|
}
|
|
827
870
|
else if (hasDefault) {
|
|
828
|
-
|
|
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));
|
|
871
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
835
872
|
return;
|
|
836
873
|
}
|
|
837
874
|
else {
|
|
875
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
876
|
+
if (sd.found) {
|
|
877
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
838
880
|
error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
|
|
839
881
|
}
|
|
840
882
|
}
|
|
@@ -848,29 +890,38 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
848
890
|
let current = config;
|
|
849
891
|
for (const key of keys) {
|
|
850
892
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
851
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
852
893
|
if (hasDefault) {
|
|
853
|
-
|
|
894
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
854
895
|
return;
|
|
855
896
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
897
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
898
|
+
if (sd.found) {
|
|
899
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
859
900
|
return;
|
|
860
901
|
}
|
|
861
902
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
862
903
|
}
|
|
863
|
-
|
|
904
|
+
// Own-property gate: bracket access on a plain object walks the
|
|
905
|
+
// prototype chain, so an unqualified `current[key]` would resolve
|
|
906
|
+
// '__proto__' / 'constructor' / 'hasOwnProperty' (and other
|
|
907
|
+
// Object.prototype members) to their inherited values instead of
|
|
908
|
+
// correctly reporting them as absent. hasOwnProperty.call only
|
|
909
|
+
// returns true for a key JSON.parse actually assigned as data on
|
|
910
|
+
// this object (including a literal "__proto__" JSON key, which
|
|
911
|
+
// JSON.parse defines as an own data property, not the accessor) —
|
|
912
|
+
// never for something inherited from the prototype chain.
|
|
913
|
+
current = Object.prototype.hasOwnProperty.call(current, key)
|
|
914
|
+
? current[key]
|
|
915
|
+
: undefined;
|
|
864
916
|
}
|
|
865
917
|
if (current === undefined) {
|
|
866
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
867
918
|
if (hasDefault) {
|
|
868
|
-
|
|
919
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
869
920
|
return;
|
|
870
921
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
922
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
923
|
+
if (sd.found) {
|
|
924
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
874
925
|
return;
|
|
875
926
|
}
|
|
876
927
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
@@ -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;
|
|
@@ -54,6 +54,26 @@ const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+
|
|
|
54
54
|
* only colon permitted before `**`. (#1639)
|
|
55
55
|
*/
|
|
56
56
|
const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:[^:*]*\*\*\s*(.*)$/;
|
|
57
|
+
/**
|
|
58
|
+
* #2347: format-agnostic evidence that a block/section holds real decision
|
|
59
|
+
* ENTRIES the parser could not read — a bullet whose bold lead-in is an
|
|
60
|
+
* ID-SHAPED token (uppercase prefix, optional digits, hyphen, alnum), whatever
|
|
61
|
+
* the exact ID grammar. The three parser grammars above all require a `D-`
|
|
62
|
+
* prefix; #1365's fail-loud guard reused that same `\bD-` test as its "is this
|
|
63
|
+
* decision-shaped?" evidence, so any other prefix (e.g. `D5-01`) was invisible
|
|
64
|
+
* to BOTH parser and guard, collapsing `could-not-parse` into a clean
|
|
65
|
+
* `none-present` pass.
|
|
66
|
+
*
|
|
67
|
+
* The ID-shape requirement (not "any bold bullet") is deliberate: a decisions
|
|
68
|
+
* block or `### Claude's Discretion` sub-section legitimately contains prose
|
|
69
|
+
* bullets with bold labels (`- **Scope:** …`, `- **Why:** …`, `- **Note:** …`).
|
|
70
|
+
* Those are NOT decision entries and must stay `none-present` — a false
|
|
71
|
+
* `could-not-parse` hard-blocks the plan gate. `[A-Z]+[0-9]*-[A-Za-z0-9]` matches
|
|
72
|
+
* `D-01` / `D5-01` / `DEC-01` but not `Scope:` / `Why:` / `Follow-up:` (mixed
|
|
73
|
+
* case) / `TODO:` (no `-<alnum>` id) — mirroring the parser's own `D-<alnum>`
|
|
74
|
+
* shape without hardcoding the `D`.
|
|
75
|
+
*/
|
|
76
|
+
const boldLeadInBulletRe = /^\s*-\s+\*\*[A-Z]+[0-9]*-[A-Za-z0-9]/m;
|
|
57
77
|
/**
|
|
58
78
|
* Parse decision lines from a block of text (the inner text of a <decisions>
|
|
59
79
|
* or markdown-header section body). Returns the extracted decisions and a count
|
|
@@ -193,11 +213,13 @@ function extractDecisions(content) {
|
|
|
193
213
|
}
|
|
194
214
|
// FIX A: Block present but 0 extracted and no parse-misses.
|
|
195
215
|
// Only report could-not-parse when there is genuine evidence of real decisions
|
|
196
|
-
// that failed to parse: a
|
|
197
|
-
//
|
|
198
|
-
//
|
|
216
|
+
// that failed to parse: a bold-lead-in bullet (`- **…**`, any ID grammar — #2347),
|
|
217
|
+
// a \bD- token in the block text, or an unterminated fence. An empty scaffold
|
|
218
|
+
// (<decisions></decisions>) or an all-prose block has no such evidence — treat
|
|
219
|
+
// as none-present so the gate passes cleanly.
|
|
199
220
|
const hasDecisionTokenInBlock = /\bD-[A-Za-z0-9]/m.test(combined);
|
|
200
|
-
|
|
221
|
+
const hasBoldLeadInBullet = boldLeadInBulletRe.test(combined);
|
|
222
|
+
if (hasDecisionTokenInBlock || hasBoldLeadInBullet || unterminatedFence) {
|
|
201
223
|
return { decisions: [], outcome: 'could-not-parse' };
|
|
202
224
|
}
|
|
203
225
|
return { decisions: [], outcome: 'none-present' };
|
|
@@ -217,11 +239,13 @@ function extractDecisions(content) {
|
|
|
217
239
|
return { decisions, outcome: 'could-not-parse' };
|
|
218
240
|
}
|
|
219
241
|
// FIX A: Heading found but 0 extracted and no parse-misses.
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
242
|
+
// Report could-not-parse when the section body holds a decision-entry-shaped
|
|
243
|
+
// bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a D- token. A
|
|
244
|
+
// heading with only prose, sub-headings, or all-discretion content (no such
|
|
245
|
+
// evidence) is a legitimate empty/discretion section → none-present.
|
|
223
246
|
const hasDecisionTokenInSection = /\bD-[A-Za-z0-9]/m.test(section.body);
|
|
224
|
-
|
|
247
|
+
const hasBoldLeadInBulletInSection = boldLeadInBulletRe.test(section.body);
|
|
248
|
+
if (hasDecisionTokenInSection || hasBoldLeadInBulletInSection) {
|
|
225
249
|
return { decisions: [], outcome: 'could-not-parse' };
|
|
226
250
|
}
|
|
227
251
|
return { decisions: [], outcome: 'none-present' };
|
|
@@ -259,6 +259,12 @@ function cmdDocsInit(cwd, raw) {
|
|
|
259
259
|
const agentStatus = checkAgentsInstalled();
|
|
260
260
|
result['agents_installed'] = agentStatus.agents_installed;
|
|
261
261
|
result['missing_agents'] = agentStatus.missing_agents;
|
|
262
|
+
// #2402: withProjectRoot injects response_language when set; cmdDocsInit predates
|
|
263
|
+
// that helper and never picked it up, so docs-update's orchestrator-owned prompts
|
|
264
|
+
// silently stayed English even with response_language configured.
|
|
265
|
+
if (config.response_language) {
|
|
266
|
+
result['response_language'] = config.response_language;
|
|
267
|
+
}
|
|
262
268
|
output(result, raw, undefined);
|
|
263
269
|
}
|
|
264
270
|
module.exports = { cmdDocsInit };
|
|
@@ -26,9 +26,21 @@ exports.isPathConfined = isPathConfined;
|
|
|
26
26
|
exports.assertDescriptorConfined = assertDescriptorConfined;
|
|
27
27
|
const node_path_1 = __importDefault(require("node:path"));
|
|
28
28
|
/**
|
|
29
|
-
* Pure path-containment check (cross-platform). `target` is confined
|
|
30
|
-
* iff resolving it relative to `root`
|
|
29
|
+
* Pure LEXICAL path-containment check (cross-platform). `target` is confined
|
|
30
|
+
* to `root` iff resolving it relative to `root` (via `path.resolve` — string
|
|
31
|
+
* manipulation, no filesystem access) yields a path equal to or under `root`.
|
|
31
32
|
* Absolute paths outside `root` and `..`-escapes return false.
|
|
33
|
+
*
|
|
34
|
+
* NOT a realpath check: this function never calls `fs.realpathSync` and does
|
|
35
|
+
* not detect a symlink along `target` (or an existing path component of
|
|
36
|
+
* `root`) that would redirect the LEXICALLY-confined path to a physically
|
|
37
|
+
* different, unconfined location on disk. A caller relying on this for a
|
|
38
|
+
* write-confinement guarantee against a symlink-planting attacker must pair
|
|
39
|
+
* it with a symlink check (or refuse to follow symlinks at write time) — see
|
|
40
|
+
* capability-source.cts's install adapters (:491,577,675), which is what
|
|
41
|
+
* currently keeps every caller of this function's callers symlink-safe: they
|
|
42
|
+
* reject symlinks upstream, before a target ever reaches a lexical-only check
|
|
43
|
+
* like this one.
|
|
32
44
|
*/
|
|
33
45
|
function isPathConfined(target, root) {
|
|
34
46
|
if (typeof target !== 'string' || typeof root !== 'string' || target.length === 0 || root.length === 0) {
|
|
@@ -298,7 +298,16 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
298
298
|
const mismatchMsg = '## Post-Planning Gap Analysis\n\nextracted 0 of N — possible format mismatch in CONTEXT.md decisions block.\n';
|
|
299
299
|
// If there are also requirement items, include them in the return with the
|
|
300
300
|
// mismatch summary appended, so the caller still sees requirement coverage.
|
|
301
|
-
|
|
301
|
+
// #2334 HIGH 1: gate on `ghostReqIds.length > 0` too — identical defect to
|
|
302
|
+
// the one fixed at #2316-6b (~34 lines below, at the `items.length === 0`
|
|
303
|
+
// early return): a phase whose EVERY cited REQ-ID is unregistered has
|
|
304
|
+
// `items.length === 0` (all its requirement items were filtered out at
|
|
305
|
+
// ~line 297) but still has real ghost rows to report. Without this guard,
|
|
306
|
+
// a single malformed `<decisions>` line in CONTEXT.md made an all-ghost
|
|
307
|
+
// phase's ghost rows silently vanish (this could-not-parse branch fell
|
|
308
|
+
// through to the bare `mismatchMsg`-only return below, dropping ghost
|
|
309
|
+
// rows that the general path further down correctly surfaces).
|
|
310
|
+
if (items.length > 0 || ghostReqIds.length > 0) {
|
|
302
311
|
const rows = sortRows([
|
|
303
312
|
...detectCoverage(items, planText),
|
|
304
313
|
...ghostReqIds.map(id => ({ source: 'REQUIREMENTS.md', item: id, status: 'Missing from REQUIREMENTS.md' })),
|
|
@@ -325,7 +334,13 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
325
334
|
};
|
|
326
335
|
}
|
|
327
336
|
// #1365: if no items at all, surface a clean no-check message.
|
|
328
|
-
|
|
337
|
+
// #2316-6b: this must NOT fire when `ghostReqIds` is non-empty — a phase
|
|
338
|
+
// whose EVERY cited REQ-ID is unregistered has `items.length === 0` (all
|
|
339
|
+
// its requirement items were filtered out at ~line 297) but still has real
|
|
340
|
+
// ghost rows to report below. Without this guard, an all-orphan phase
|
|
341
|
+
// reported LESS than a partially-orphan one (which falls through to the
|
|
342
|
+
// general path further down and correctly surfaces its ghost rows).
|
|
343
|
+
if (items.length === 0 && ghostReqIds.length === 0) {
|
|
329
344
|
return {
|
|
330
345
|
enabled: true,
|
|
331
346
|
rows: [],
|