@opengsd/gsd-core 1.7.0-rc.6 → 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 +31 -3
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1157 -84
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +31 -1
- 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/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +1866 -2434
- 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 +341 -49
- package/gsd-core/bin/lib/audit.cjs +7 -6
- 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 +157 -88
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +129 -26
- 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/clock.cjs +19 -0
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +129 -13
- package/gsd-core/bin/lib/config-loader.cjs +20 -4
- package/gsd-core/bin/lib/config.cjs +81 -18
- package/gsd-core/bin/lib/core-utils.cjs +14 -3
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +138 -60
- package/gsd-core/bin/lib/install-engine.cjs +301 -25
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -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 +45 -6
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +463 -43
- 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-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +636 -72
- package/gsd-core/bin/lib/plan-scan.cjs +73 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
- package/gsd-core/bin/lib/roadmap.cjs +113 -52
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +73 -7
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +122 -46
- package/gsd-core/bin/lib/state.cjs +456 -137
- package/gsd-core/bin/lib/surface.cjs +53 -11
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +474 -13
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- 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 +224 -25
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +13 -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 +4 -2
- 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 +27 -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 +30 -37
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/fast.md +8 -22
- 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 +33 -5
- package/gsd-core/workflows/quick.md +21 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +123 -68
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +4 -1
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/transition.md +1 -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 +252 -17
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +252 -17
- package/package.json +8 -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/ci-test-scope.cjs +39 -1
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-table-schema-drift.cjs +157 -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 +31 -1
- 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/skills/gsd-surface/SKILL.md +6 -6
- 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");
|
|
@@ -45,7 +48,46 @@ const { extractFrontmatter } = frontmatter;
|
|
|
45
48
|
const modelProfiles = require("./model-profiles.cjs");
|
|
46
49
|
const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
|
|
47
50
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
51
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
48
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
|
+
}
|
|
49
91
|
/**
|
|
50
92
|
* Determine phase status by checking plan/summary counts AND verification state.
|
|
51
93
|
* Introduces "Executed" for phases with all summaries but no passing verification.
|
|
@@ -127,6 +169,9 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
127
169
|
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
128
170
|
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
129
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);
|
|
130
175
|
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
131
176
|
// Apply area filter if specified
|
|
132
177
|
if (area && todoArea !== area)
|
|
@@ -138,6 +183,7 @@ function cmdListTodos(cwd, area, raw) {
|
|
|
138
183
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
139
184
|
area: todoArea,
|
|
140
185
|
path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(pendingDir, file))),
|
|
186
|
+
...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
|
|
141
187
|
});
|
|
142
188
|
}
|
|
143
189
|
}
|
|
@@ -393,7 +439,8 @@ function cmdResolveGranularity(cwd, phaseType, raw, override) {
|
|
|
393
439
|
* { model, profile, effort, effort_rendered, effort_param, effort_propagation,
|
|
394
440
|
* fast_mode, fast_mode_supported, [unknown_agent] }
|
|
395
441
|
*
|
|
396
|
-
* Flags: --effort <level>, --fast-mode <true|false>, --attempt <n
|
|
442
|
+
* Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>,
|
|
443
|
+
* --failure-class <class> (#2296)
|
|
397
444
|
*/
|
|
398
445
|
function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
399
446
|
if (!agentType) {
|
|
@@ -402,7 +449,29 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
402
449
|
opts = opts || {};
|
|
403
450
|
const config = loadConfig(cwd);
|
|
404
451
|
const profile = config['model_profile'] || 'balanced';
|
|
405
|
-
|
|
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
|
+
}
|
|
406
475
|
const effortOpts = {};
|
|
407
476
|
if (typeof opts.effortOverride === 'string')
|
|
408
477
|
effortOpts['override'] = opts.effortOverride;
|
|
@@ -429,6 +498,8 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
429
498
|
};
|
|
430
499
|
if (!agentModels)
|
|
431
500
|
result['unknown_agent'] = true;
|
|
501
|
+
if (escalation)
|
|
502
|
+
result['escalation'] = escalation;
|
|
432
503
|
output(result, raw, effort);
|
|
433
504
|
}
|
|
434
505
|
/**
|
|
@@ -592,6 +663,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
592
663
|
// Stage files
|
|
593
664
|
const explicitFiles = files && files.length > 0;
|
|
594
665
|
const filesToStage = explicitFiles ? files : ['.planning/'];
|
|
666
|
+
const stagedPaths = [];
|
|
595
667
|
for (const file of filesToStage) {
|
|
596
668
|
const fullPath = node_path_1.default.join(cwd, file);
|
|
597
669
|
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
@@ -607,12 +679,32 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
607
679
|
}
|
|
608
680
|
else {
|
|
609
681
|
(0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
|
|
682
|
+
stagedPaths.push(file);
|
|
610
683
|
}
|
|
611
684
|
}
|
|
612
|
-
// Commit
|
|
613
|
-
|
|
685
|
+
// Commit — when the caller declared a scope (--files), append a pathspec so
|
|
686
|
+
// only the declared files land in the commit, not the entire index (#2112).
|
|
687
|
+
// The pathspec uses stagedPaths (not filesToStage) so skipped missing files
|
|
688
|
+
// are excluded — otherwise git would record them as deletions (#2014).
|
|
689
|
+
// During a merge, git refuses partial commits — fall back to a bare commit.
|
|
690
|
+
// --amend is left without a pathspec: amending with -- <paths> is a different
|
|
691
|
+
// operation that rewrites the tip with only those paths.
|
|
692
|
+
if (explicitFiles && stagedPaths.length === 0 && !amend) {
|
|
693
|
+
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
694
|
+
output(result, raw, 'nothing');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
|
|
698
|
+
const canScope = explicitFiles && stagedPaths.length > 0 && !amend
|
|
699
|
+
&& !isMergeInProgress;
|
|
700
|
+
const commitArgs = amend
|
|
701
|
+
? ['commit', '--amend', '--no-edit']
|
|
702
|
+
: ['commit', '-m', sanitizedMessage];
|
|
614
703
|
if (noVerify)
|
|
615
704
|
commitArgs.push('--no-verify');
|
|
705
|
+
if (canScope) {
|
|
706
|
+
commitArgs.push('--', ...stagedPaths);
|
|
707
|
+
}
|
|
616
708
|
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
|
|
617
709
|
if (commitResult.exitCode !== 0) {
|
|
618
710
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
@@ -710,12 +802,21 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
710
802
|
for (const [repo, repoFiles] of Object.entries(grouped)) {
|
|
711
803
|
const repoCwd = node_path_1.default.join(cwd, repo);
|
|
712
804
|
// Stage files (strip sub-repo prefix for paths relative to that repo)
|
|
805
|
+
const stagedRelPaths = [];
|
|
713
806
|
for (const file of repoFiles) {
|
|
714
807
|
const relativePath = file.slice(repo.length + 1);
|
|
715
|
-
(0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
808
|
+
const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
809
|
+
if (addResult.exitCode === 0) {
|
|
810
|
+
stagedRelPaths.push(relativePath);
|
|
811
|
+
}
|
|
716
812
|
}
|
|
717
|
-
// Commit
|
|
718
|
-
const
|
|
813
|
+
// Commit — pathspec limits the commit to the staged files only (#2112)
|
|
814
|
+
const isMergeInProgressSub = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
|
|
815
|
+
const canScopeSub = stagedRelPaths.length > 0 && !isMergeInProgressSub;
|
|
816
|
+
const commitArgs = canScopeSub
|
|
817
|
+
? ['commit', '-m', message, '--', ...stagedRelPaths]
|
|
818
|
+
: ['commit', '-m', message];
|
|
819
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
719
820
|
if (commitResult.exitCode !== 0) {
|
|
720
821
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
721
822
|
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
@@ -837,8 +938,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
837
938
|
error(`Failed to stage ${file} in ${repo}: ${addResult.stderr}`);
|
|
838
939
|
}
|
|
839
940
|
}
|
|
840
|
-
// 5. Commit
|
|
841
|
-
|
|
941
|
+
// 5. Commit — pathspec limits the commit to the staged files only (#2112).
|
|
942
|
+
// changedFiles includes both old and new paths for renames so the full
|
|
943
|
+
// rename is captured atomically (pathspec on newPath alone would leave the
|
|
944
|
+
// deletion of oldPath stranded in the index).
|
|
945
|
+
const isMergeInProgressPr = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
|
|
946
|
+
const canScopePr = changedFiles.length > 0 && !isMergeInProgressPr;
|
|
947
|
+
const commitArgs = canScopePr
|
|
948
|
+
? ['commit', '-m', commitMessage, '--', ...changedFiles]
|
|
949
|
+
: ['commit', '-m', commitMessage];
|
|
950
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
842
951
|
if (commitResult.exitCode !== 0) {
|
|
843
952
|
rollback();
|
|
844
953
|
error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
|
|
@@ -1223,7 +1332,7 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
1223
1332
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
|
|
1224
1333
|
// Read, add completion timestamp, move
|
|
1225
1334
|
let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
1226
|
-
const today =
|
|
1335
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
1227
1336
|
content = `completed: ${today}\n` + content;
|
|
1228
1337
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), content);
|
|
1229
1338
|
node_fs_1.default.unlinkSync(sourcePath);
|
|
@@ -1232,7 +1341,7 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
1232
1341
|
function cmdScaffold(cwd, type, options, raw) {
|
|
1233
1342
|
const { phase, name } = options;
|
|
1234
1343
|
const padded = phase ? normalizePhaseName(phase) : '00';
|
|
1235
|
-
const today =
|
|
1344
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
1236
1345
|
// Find phase directory
|
|
1237
1346
|
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
1238
1347
|
const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
|
|
@@ -1346,7 +1455,12 @@ function cmdStats(cwd, format, raw) {
|
|
|
1346
1455
|
name: existing?.name || phaseName,
|
|
1347
1456
|
plans: (existing?.plans || 0) + plans,
|
|
1348
1457
|
summaries: (existing?.summaries || 0) + summaries,
|
|
1349
|
-
|
|
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,
|
|
1350
1464
|
});
|
|
1351
1465
|
}
|
|
1352
1466
|
}
|
|
@@ -1468,6 +1582,8 @@ function cmdCheckCommit(cwd, raw) {
|
|
|
1468
1582
|
module.exports = {
|
|
1469
1583
|
groupFilesBySubrepo,
|
|
1470
1584
|
determinePhaseStatus,
|
|
1585
|
+
foldPhaseStatus,
|
|
1586
|
+
PHASE_STATUS_PRECEDENCE,
|
|
1471
1587
|
cmdGenerateSlug,
|
|
1472
1588
|
cmdCurrentTimestamp,
|
|
1473
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 };
|
|
@@ -158,17 +158,25 @@ const _warnedUnknownConfigKeys = new Set();
|
|
|
158
158
|
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
159
159
|
const _gitIgnoredCache = new Map();
|
|
160
160
|
function isGitIgnored(cwd, targetPath) {
|
|
161
|
-
|
|
161
|
+
// #2206: strip trailing slashes — `git check-ignore` has a quirk where a
|
|
162
|
+
// CRLF .gitignore with blank lines falsely reports a trailing-slash path
|
|
163
|
+
// (e.g. `.planning/`) as ignored. Normalizing here protects every call site.
|
|
164
|
+
const normalized = targetPath.replace(/\/+$/, '');
|
|
165
|
+
const key = cwd + '::' + normalized;
|
|
162
166
|
if (_gitIgnoredCache.has(key))
|
|
163
167
|
return _gitIgnoredCache.get(key);
|
|
164
168
|
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
165
|
-
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--',
|
|
169
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd });
|
|
166
170
|
const ignored = result.exitCode === 0;
|
|
167
171
|
_gitIgnoredCache.set(key, ignored);
|
|
168
172
|
return ignored;
|
|
169
173
|
}
|
|
170
174
|
// ─── Model alias resolution ───────────────────────────────────────────────────
|
|
171
|
-
|
|
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;
|
|
172
180
|
const _warnedConfigKeys = new Set();
|
|
173
181
|
function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
174
182
|
if (!parsed || typeof parsed !== 'object')
|
|
@@ -683,6 +691,14 @@ function loadConfigResolved(cwd, options = {}) {
|
|
|
683
691
|
fast_mode: (globalDefaults['fast_mode']) || null,
|
|
684
692
|
agent_skills: (globalDefaults['agent_skills']) || {},
|
|
685
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,
|
|
686
702
|
};
|
|
687
703
|
// Branch D: global-defaults
|
|
688
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];
|
|
@@ -692,6 +731,22 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
692
731
|
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
|
|
693
732
|
if (kp === 'statusline.context_position')
|
|
694
733
|
assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
|
|
734
|
+
// statusline.show_context_tokens — boolean only
|
|
735
|
+
if (kp === 'statusline.show_context_tokens') {
|
|
736
|
+
if (typeof parsedValue !== 'boolean') {
|
|
737
|
+
error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
|
|
738
|
+
}
|
|
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');
|
|
744
|
+
// statusline.show_git — boolean only
|
|
745
|
+
if (kp === 'statusline.show_git') {
|
|
746
|
+
if (typeof parsedValue !== 'boolean') {
|
|
747
|
+
error(`Invalid statusline.show_git '${val}'. Must be a boolean (true or false).`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
695
750
|
// Fallow scope + profile enum validation (#3424)
|
|
696
751
|
const VALID_FALLOW_SCOPES = ['phase', 'repo'];
|
|
697
752
|
if (kp === 'code_quality.fallow.scope')
|
|
@@ -813,16 +868,15 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
813
868
|
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
814
869
|
}
|
|
815
870
|
else if (hasDefault) {
|
|
816
|
-
|
|
817
|
-
output(defaultValue, raw, String(defaultValue));
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
|
|
821
|
-
const def = SCHEMA_DEFAULTS[kp];
|
|
822
|
-
output(def, raw, String(def));
|
|
871
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
823
872
|
return;
|
|
824
873
|
}
|
|
825
874
|
else {
|
|
875
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
876
|
+
if (sd.found) {
|
|
877
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
826
880
|
error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
|
|
827
881
|
}
|
|
828
882
|
}
|
|
@@ -836,29 +890,38 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
|
|
836
890
|
let current = config;
|
|
837
891
|
for (const key of keys) {
|
|
838
892
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
839
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
840
893
|
if (hasDefault) {
|
|
841
|
-
|
|
894
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
842
895
|
return;
|
|
843
896
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
897
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
898
|
+
if (sd.found) {
|
|
899
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
847
900
|
return;
|
|
848
901
|
}
|
|
849
902
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
850
903
|
}
|
|
851
|
-
|
|
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;
|
|
852
916
|
}
|
|
853
917
|
if (current === undefined) {
|
|
854
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
855
918
|
if (hasDefault) {
|
|
856
|
-
|
|
919
|
+
emitResolvedDefault(kp, defaultValue, raw);
|
|
857
920
|
return;
|
|
858
921
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
922
|
+
const sd = resolveSchemaDefault(cwd, kp);
|
|
923
|
+
if (sd.found) {
|
|
924
|
+
emitResolvedDefault(kp, sd.value, raw);
|
|
862
925
|
return;
|
|
863
926
|
}
|
|
864
927
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
|
@@ -27,10 +27,16 @@ const { comparePhaseNum } = phaseIdModule;
|
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
28
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
29
29
|
const { findContextMdIn } = planningWorkspace;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const shellCommandProjection = require("./shell-command-projection.cjs");
|
|
30
32
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
31
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a relative path to always use forward slashes (cross-platform).
|
|
35
|
+
* Delegates to the single separator seam in shell-command-projection so there is
|
|
36
|
+
* exactly one implementation of native→POSIX conversion across the codebase.
|
|
37
|
+
*/
|
|
32
38
|
function toPosixPath(p) {
|
|
33
|
-
return
|
|
39
|
+
return shellCommandProjection.toPosixPath(p);
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Scan immediate child directories for separate git repos.
|
|
@@ -180,8 +186,13 @@ function extractCanonicalPlanId(filename) {
|
|
|
180
186
|
// or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
|
|
181
187
|
// so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
|
|
182
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');
|
|
183
194
|
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
|
184
|
-
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length &&
|
|
195
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && planTokenRe.test(parts[phaseIdx + 1])) {
|
|
185
196
|
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
186
197
|
}
|
|
187
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 };
|
|
@@ -69,7 +69,7 @@ const SAFE_PATH_RE = /^(?!.*\.\.)(?:[A-Za-z0-9_.][A-Za-z0-9_.\-]*)(?:\/[A-Za-z0-
|
|
|
69
69
|
function classifyFile(file) {
|
|
70
70
|
if (typeof file !== 'string' || !file)
|
|
71
71
|
return null;
|
|
72
|
-
const norm =
|
|
72
|
+
const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
|
|
73
73
|
if (MIGRATION_RES.some((r) => r.test(norm)))
|
|
74
74
|
return 'migration';
|
|
75
75
|
if (ROUTE_RES.some((r) => r.test(norm)))
|
|
@@ -87,7 +87,7 @@ function classifyFile(file) {
|
|
|
87
87
|
* check `structureMd.includes('src/lib')` holds.
|
|
88
88
|
*/
|
|
89
89
|
function isPathMapped(file, structureMd) {
|
|
90
|
-
const norm =
|
|
90
|
+
const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
|
|
91
91
|
const parts = norm.split('/');
|
|
92
92
|
// Check prefixes from longest to shortest; any hit means "mapped".
|
|
93
93
|
for (let i = parts.length - 1; i >= 1; i--) {
|
|
@@ -130,7 +130,7 @@ function detectDrift(input) {
|
|
|
130
130
|
const elements = [];
|
|
131
131
|
const seen = new Map();
|
|
132
132
|
for (const rawFile of added) {
|
|
133
|
-
const file =
|
|
133
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
|
|
134
134
|
const specific = classifyFile(file);
|
|
135
135
|
let category = specific;
|
|
136
136
|
if (!category) {
|
|
@@ -251,7 +251,7 @@ function chooseAffectedPaths(paths) {
|
|
|
251
251
|
for (const raw of paths || []) {
|
|
252
252
|
if (typeof raw !== 'string' || !raw)
|
|
253
253
|
continue;
|
|
254
|
-
const file =
|
|
254
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(raw);
|
|
255
255
|
const parts = file.split('/');
|
|
256
256
|
if (parts.length === 0)
|
|
257
257
|
continue;
|
|
@@ -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) {
|