@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
|
@@ -56,6 +56,11 @@ const uatPredicate = require("./uat-predicate.cjs");
|
|
|
56
56
|
const { evaluateUatPassed } = uatPredicate;
|
|
57
57
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
58
58
|
const verificationMod = require("./verification.cjs");
|
|
59
|
+
// #2572: the artifact↔disk core behind the `verify-summary` verb. `verify.cts`
|
|
60
|
+
// has no transitive import path back to `phase.cts`, so this edge introduces no
|
|
61
|
+
// cycle (the reverse edge, `state.cts → verify.cjs`, would).
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verify.cjs is an export= CommonJS module
|
|
63
|
+
const verifyMod = require("./verify.cjs");
|
|
59
64
|
const { readVerificationStatus } = verificationMod;
|
|
60
65
|
const { planningDir, withPlanningLock, listAvailableWorkstreams, getActiveWorkstream } = planningWorkspace;
|
|
61
66
|
const { extractFrontmatter } = frontmatterMod;
|
|
@@ -106,6 +111,19 @@ function updateTraceabilityCell(text, match, column, newValue) {
|
|
|
106
111
|
return result;
|
|
107
112
|
return { ok: true, value: before + result.value + after };
|
|
108
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Extract the MAJOR version segment from a version-ish string: "v1", "v1.3",
|
|
116
|
+
* "V1.0", and "1.0" all yield "1"; "v2" yields "2". Used (#2334 BLOCKER fix)
|
|
117
|
+
* to compare a `## v<N> ...` REQUIREMENTS.md heading against the current
|
|
118
|
+
* milestone's version at MAJOR-version granularity only — "v1" heading vs
|
|
119
|
+
* milestone "v1.3" is the SAME major version and must not be treated as a
|
|
120
|
+
* version mismatch. Returns null when `raw` has no leading digit run (not a
|
|
121
|
+
* version-shaped string), which the caller treats as "cannot resolve".
|
|
122
|
+
*/
|
|
123
|
+
function extractMajorVersion(raw) {
|
|
124
|
+
const m = raw.trim().match(/^v?(\d+)/i);
|
|
125
|
+
return m ? m[1] : null;
|
|
126
|
+
}
|
|
109
127
|
function describeNonCanonicalPlans(dirFiles, matchedFiles) {
|
|
110
128
|
const matched = new Set(matchedFiles);
|
|
111
129
|
const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f));
|
|
@@ -127,8 +145,13 @@ function extractCanonicalPlanId(filename) {
|
|
|
127
145
|
// or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
|
|
128
146
|
// so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
|
|
129
147
|
const tokenRe = /^(?:\d{2,}[A-Z]?|\d[A-Z])(?:\.\d+)*$/i;
|
|
148
|
+
// #2232: the PAIRED plan component is a zero-padded continuation segment
|
|
149
|
+
// (exactly 2 digits), so a ≥3-digit slug word (a year) is not paired into a
|
|
150
|
+
// bogus "14-2026" id. The leading phase component keeps tokenRe's unbounded
|
|
151
|
+
// \d{2,} — phase numbers ≥100 are legitimate; only continuations are capped.
|
|
152
|
+
const planTokenRe = new RegExp(`^(?:${phaseIdMod.PHASE_CONTINUATION_SEGMENT_SOURCE}[A-Z]?|\\d[A-Z])(?:\\.\\d+)*$`, 'i');
|
|
130
153
|
const phaseIdx = parts.findIndex((p) => tokenRe.test(p));
|
|
131
|
-
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length &&
|
|
154
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && planTokenRe.test(parts[phaseIdx + 1])) {
|
|
132
155
|
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
133
156
|
}
|
|
134
157
|
return base;
|
|
@@ -494,7 +517,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
494
517
|
const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
495
518
|
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
496
519
|
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
497
|
-
|
|
520
|
+
// Pass planPath so a truncated PLAN.md names the file in the #1882 diagnostic.
|
|
521
|
+
const fm = extractFrontmatter(content, planPath);
|
|
498
522
|
const xmlTasks = content.match(/<task[\s>]/gi) || [];
|
|
499
523
|
const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
500
524
|
const taskCount = xmlTasks.length || mdTasks.length;
|
|
@@ -603,6 +627,29 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
603
627
|
result['warnings'] = warnings;
|
|
604
628
|
output(result, raw);
|
|
605
629
|
}
|
|
630
|
+
// #2390 — phase.add title-shape heuristic. A description at or under this many
|
|
631
|
+
// characters, and with no sentence-ending punctuation followed by more text,
|
|
632
|
+
// reads as a short Title. Anything longer or multi-sentence reads as a Goal,
|
|
633
|
+
// not a Title. phase.add still writes the phase verbatim (it never mangles
|
|
634
|
+
// ROADMAP.md), but when the description looks goal-shaped the JSON result
|
|
635
|
+
// gains a `warning` key naming the gap, so the caller — or the orchestrating
|
|
636
|
+
// add-phase workflow — can split title vs. goal instead of the whole paragraph
|
|
637
|
+
// landing silently in the `### Phase N:` header.
|
|
638
|
+
const PHASE_ADD_TITLE_MAX_LEN = 80;
|
|
639
|
+
const PHASE_ADD_MULTI_SENTENCE_RE = /[.!?]['")\]]?\s+\S/;
|
|
640
|
+
function describeGoalShapedTitle(description) {
|
|
641
|
+
const trimmed = description.trim();
|
|
642
|
+
const tooLong = trimmed.length > PHASE_ADD_TITLE_MAX_LEN;
|
|
643
|
+
const multiSentence = PHASE_ADD_MULTI_SENTENCE_RE.test(trimmed);
|
|
644
|
+
if (!tooLong && !multiSentence)
|
|
645
|
+
return null;
|
|
646
|
+
const reasons = [
|
|
647
|
+
tooLong ? `${trimmed.length} chars (over the ${PHASE_ADD_TITLE_MAX_LEN}-char title threshold)` : null,
|
|
648
|
+
multiSentence ? 'multiple sentences' : null,
|
|
649
|
+
].filter(Boolean).join(', ');
|
|
650
|
+
return (`description looks goal-shaped, not title-shaped (${reasons}). It was written verbatim ` +
|
|
651
|
+
`as the phase title; consider a short title with the detail moved to **Goal:**.`);
|
|
652
|
+
}
|
|
606
653
|
function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
607
654
|
if (!description) {
|
|
608
655
|
error('description required for phase add');
|
|
@@ -690,6 +737,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
690
737
|
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
|
|
691
738
|
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
|
692
739
|
});
|
|
740
|
+
const titleWarning = describeGoalShapedTitle(description);
|
|
693
741
|
const result = {
|
|
694
742
|
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
695
743
|
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
@@ -698,7 +746,9 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
698
746
|
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
699
747
|
naming_mode: config.phase_naming,
|
|
700
748
|
};
|
|
701
|
-
|
|
749
|
+
if (titleWarning)
|
|
750
|
+
result['warning'] = titleWarning;
|
|
751
|
+
output(result, raw, result['padded']);
|
|
702
752
|
}
|
|
703
753
|
function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
704
754
|
if (!Array.isArray(descriptions) || descriptions.length === 0) {
|
|
@@ -1358,13 +1408,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1358
1408
|
warnings.push(`${file}: has diagnosed gaps`);
|
|
1359
1409
|
}
|
|
1360
1410
|
for (const file of phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
1361
|
-
const
|
|
1411
|
+
const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
|
|
1412
|
+
const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
|
|
1362
1413
|
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
|
|
1363
1414
|
// from historical metadata in the file body (e.g. `previous_status: gaps_found`).
|
|
1364
1415
|
// A full-text regex like /status: gaps_found/ matches the substring inside
|
|
1365
1416
|
// `previous_status: gaps_found`, producing spurious warnings even when the
|
|
1366
1417
|
// current frontmatter status is `passed`.
|
|
1367
|
-
const verFm = extractFrontmatter(content);
|
|
1418
|
+
const verFm = extractFrontmatter(content, verificationFilePath);
|
|
1368
1419
|
// Normalise to lower-case so `status: Passed` (title-case) is not missed.
|
|
1369
1420
|
const verStatus = typeof verFm['status'] === 'string' ? verFm['status'].trim().toLowerCase() : '';
|
|
1370
1421
|
if (verStatus === 'human_needed')
|
|
@@ -1380,11 +1431,51 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1380
1431
|
* mechanism). A readdirSync/readFileSync failure here just means fewer
|
|
1381
1432
|
* warnings are surfaced this run, not a blocked or corrupted completion. */
|
|
1382
1433
|
}
|
|
1434
|
+
// #2572: artifact↔disk advisory for the SUMMARYs of the phase being completed.
|
|
1435
|
+
//
|
|
1436
|
+
// A SUMMARY asserts "I created these files". Nothing checked that claim for
|
|
1437
|
+
// phase summaries — the `verify-summary` verb has existed since the beginning
|
|
1438
|
+
// but was only ever pointed at `.planning/research/SUMMARY.md`. An interrupted
|
|
1439
|
+
// or over-reported phase therefore counted toward 100% silently.
|
|
1440
|
+
//
|
|
1441
|
+
// Joins the same ADVISORY channel as the pre-scan above: findings land in
|
|
1442
|
+
// `warnings[]` (rendered by execute-phase.md's "If has_warnings is true"
|
|
1443
|
+
// step), never in the completion GATE (readVerificationStatus below).
|
|
1444
|
+
// Completion is never blocked.
|
|
1445
|
+
//
|
|
1446
|
+
// `checkCommits: false` — only the file-existence half is surfaced here, so
|
|
1447
|
+
// the `git cat-file` probes would be spawned and their result discarded. The
|
|
1448
|
+
// hash pattern is a loose `\b[0-9a-f]{7,40}\b` that matches any hex-shaped
|
|
1449
|
+
// token in prose, too noisy to put in front of a user even as a warning.
|
|
1450
|
+
//
|
|
1451
|
+
// `Infinity` — report every referenced file, not the CLI verb's default first
|
|
1452
|
+
// two, so a phase that lists twelve files and landed three says so. The verb
|
|
1453
|
+
// keeps its 2-file default; only this caller opts out of the cap.
|
|
1454
|
+
try {
|
|
1455
|
+
const phaseDirRel = phaseInfo['directory'];
|
|
1456
|
+
// `summaries` arrives pre-sorted from the phase locator, so warning order is
|
|
1457
|
+
// deterministic across platforms rather than readdir-dependent.
|
|
1458
|
+
const summaryNames = phaseInfo['summaries'] || [];
|
|
1459
|
+
for (const summaryName of summaryNames) {
|
|
1460
|
+
const v = verifyMod.verifySummaryCore(cwd, `${phaseDirRel}/${summaryName}`, Infinity, { checkCommits: false });
|
|
1461
|
+
const missing = v.checks.files_created.missing;
|
|
1462
|
+
if (missing.length > 0) {
|
|
1463
|
+
warnings.push(`${summaryName}: references ${missing.length} file(s) not on disk: ${missing.join(', ')}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
/* best-effort, same posture as the #2245 pre-scan above: an unreadable
|
|
1469
|
+
* SUMMARY means one fewer advisory this run, never a blocked completion. */
|
|
1470
|
+
}
|
|
1383
1471
|
let nextPhaseNum = null;
|
|
1384
1472
|
let nextPhaseName = null;
|
|
1385
1473
|
let isLastPhase = true;
|
|
1386
1474
|
const verificationBlocked = withPlanningLock(cwd, () => {
|
|
1387
|
-
|
|
1475
|
+
// #2617: pass the project's runtime so the blocked-completion error below
|
|
1476
|
+
// suggests the command surface this runtime actually installs
|
|
1477
|
+
// ($gsd-… on Codex) rather than a hard-coded Claude-style string.
|
|
1478
|
+
const verificationStatus = readVerificationStatus(phaseFullDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
|
|
1388
1479
|
if (verificationStatus.status !== 'passed') {
|
|
1389
1480
|
return verificationStatus;
|
|
1390
1481
|
}
|
|
@@ -1553,13 +1644,43 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1553
1644
|
const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
|
|
1554
1645
|
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
1555
1646
|
let reqContent = originalReqContent;
|
|
1647
|
+
// #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
|
|
1648
|
+
// line for this phase actually cites — is hoisted out of the
|
|
1649
|
+
// `if (reqMatch)` block (previously scoped only inside it) so the
|
|
1650
|
+
// ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
|
|
1651
|
+
// literal placeholder `phase.add`/`-batch`/`-insert` seed
|
|
1652
|
+
// (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
|
|
1653
|
+
// real REQ-ID, so it is filtered out wherever a cited-ID list feeds
|
|
1654
|
+
// a warning (#2316-7 boundary).
|
|
1655
|
+
const isPlaceholderReqId = (id) => id.toUpperCase() === 'TBD';
|
|
1656
|
+
let citedReqIds = [];
|
|
1657
|
+
// #2316-1: Traceability-row writes that matched NO row (ghost or
|
|
1658
|
+
// otherwise) — the `if (reqUpdate.ok)` below previously had no
|
|
1659
|
+
// `else`, discarding this fact silently instead of surfacing it.
|
|
1660
|
+
const traceabilityWriteMisses = [];
|
|
1556
1661
|
if (reqMatch) {
|
|
1557
|
-
|
|
1662
|
+
// #2334 HIGH 3: filter the tokenized capture to the REQ-ID SHAPE —
|
|
1663
|
+
// the SAME shape bodyReqIds (`\*\*([A-Z][A-Z0-9]*-\d+)\*\*`, below)
|
|
1664
|
+
// and tableReqIds (`([A-Z][A-Z0-9]*-\d+)`, below) already require —
|
|
1665
|
+
// so the ghost-ID / unregistered comparisons stay shape-symmetric.
|
|
1666
|
+
// Without this, `[^\n]+` split on `[,\s]+` turned EVERY word after
|
|
1667
|
+
// the ID list into a "cited REQ-ID": the shipped
|
|
1668
|
+
// `templates/roadmap.md:32` line
|
|
1669
|
+
// `**Requirements**: [REQ-01, REQ-02] <!-- brackets optional, ... -->`
|
|
1670
|
+
// warned to register `<!--`, `brackets`, `optional`, `-->`, etc., and
|
|
1671
|
+
// `**Requirements:** None` warned to register the literal word
|
|
1672
|
+
// `None`. This subsumes the `TBD` placeholder special-case (`TBD`
|
|
1673
|
+
// does not match the REQ-ID shape either); `isPlaceholderReqId` is
|
|
1674
|
+
// kept below as a defensive no-op for any caller that still hands
|
|
1675
|
+
// it a raw token.
|
|
1676
|
+
const REQ_ID_SHAPE_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
|
|
1677
|
+
citedReqIds = reqMatch[1]
|
|
1558
1678
|
.replace(/[\[\]]/g, '')
|
|
1559
1679
|
.split(/[,\s]+/)
|
|
1560
1680
|
.map((r) => r.trim())
|
|
1561
|
-
.filter(Boolean)
|
|
1562
|
-
|
|
1681
|
+
.filter(Boolean)
|
|
1682
|
+
.filter((r) => REQ_ID_SHAPE_RE.test(r));
|
|
1683
|
+
for (const reqId of citedReqIds) {
|
|
1563
1684
|
const reqEscaped = escapeRegex(reqId);
|
|
1564
1685
|
reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'), '$1x$2');
|
|
1565
1686
|
// Traceability row: | <REQ-ID> | Phase N | Pending|In Progress | ->
|
|
@@ -1578,14 +1699,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1578
1699
|
// Complete" gate is folded into the newValue callback so one
|
|
1579
1700
|
// updateTableCell call both probes and writes.
|
|
1580
1701
|
const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (current) => /^(?:pending|in progress)$/i.test(current.trim()) ? ' Complete ' : current);
|
|
1581
|
-
if (reqUpdate.ok)
|
|
1702
|
+
if (reqUpdate.ok) {
|
|
1582
1703
|
reqContent = reqUpdate.value;
|
|
1704
|
+
}
|
|
1705
|
+
else if (!isPlaceholderReqId(reqId)) {
|
|
1706
|
+
traceabilityWriteMisses.push(reqId);
|
|
1707
|
+
}
|
|
1583
1708
|
}
|
|
1584
1709
|
}
|
|
1585
1710
|
// #1159 (Defect B): collect requirement IDs only from ACTIVE sections.
|
|
1586
1711
|
// Requirements under headings whose text contains "deferred", "backlog",
|
|
1587
|
-
// "future", or
|
|
1588
|
-
// and must not be flagged as missing from the
|
|
1712
|
+
// "future", or an OFF-milestone `v<N>` (case-insensitive) are explicitly
|
|
1713
|
+
// out of current scope and must not be flagged as missing from the
|
|
1714
|
+
// Traceability table.
|
|
1589
1715
|
//
|
|
1590
1716
|
// Strategy: walk lines, track heading depth, and toggle a "deferred" flag
|
|
1591
1717
|
// when a heading matching the pattern is encountered. A sub-heading (higher
|
|
@@ -1593,7 +1719,47 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1593
1719
|
// opens a same-or-shallower heading that does NOT match the pattern.
|
|
1594
1720
|
// Lines inside fenced code blocks (``` or ~~~) are treated as content, not
|
|
1595
1721
|
// headings, to avoid false deferred-section detection from code examples.
|
|
1596
|
-
|
|
1722
|
+
//
|
|
1723
|
+
// #2334 BLOCKER fix (regresses closed bug #1159 against GSD's OWN
|
|
1724
|
+
// shipped template): #2316-4a dropped the bare `v\d+` alternative
|
|
1725
|
+
// entirely to stop it over-matching an ACTIVE heading like "## v1
|
|
1726
|
+
// Requirements" — but the shipped `templates/requirements.md:35`
|
|
1727
|
+
// scaffold ships `## v2 Requirements` / "Deferred to future release"
|
|
1728
|
+
// as its ONLY deferred marker, and `v\d+` was the ONLY alternative
|
|
1729
|
+
// that ever matched a bare version heading (the deferred-ness lives
|
|
1730
|
+
// in body prose, not the heading text). Dropping it regressed #1159
|
|
1731
|
+
// for every project scaffolded from the shipped template.
|
|
1732
|
+
//
|
|
1733
|
+
// Fix: make the `v<N>` alternative MILESTONE-AWARE instead of
|
|
1734
|
+
// deleting it. A `## v<N> ...` heading is deferred ONLY when `<N>`
|
|
1735
|
+
// (MAJOR version only — "v1" vs milestone "v1.3" is the SAME major
|
|
1736
|
+
// version) does not match the CURRENT milestone's major version,
|
|
1737
|
+
// resolved via `stateExtractField` against STATE.md's `milestone:`
|
|
1738
|
+
// frontmatter field (the same seam `getMilestoneInfo`/state.cts's
|
|
1739
|
+
// frontmatter builder already use — no bespoke frontmatter parsing).
|
|
1740
|
+
// "## v1 Requirements" while the milestone is v1.x is the ACTIVE
|
|
1741
|
+
// milestone's own section (#2316's original ask) and must NOT be
|
|
1742
|
+
// swallowed; "## v2 Requirements" while the milestone is v1.x is a
|
|
1743
|
+
// genuinely future milestone (#1159's ask, and the literal shipped-
|
|
1744
|
+
// template shape) and MUST stay suppressed. `deferred`/`backlog`/
|
|
1745
|
+
// `future` are unaffected by milestone resolution — a genuinely
|
|
1746
|
+
// deferred heading always spells one of those words too (see
|
|
1747
|
+
// #2316-5 regression guard: "## Deferred v2 Requirements", "##
|
|
1748
|
+
// Future Backlog", "## Deferred", "## Backlog", "## Future").
|
|
1749
|
+
//
|
|
1750
|
+
// Fail-safe: when the milestone version cannot be resolved at all
|
|
1751
|
+
// (no STATE.md, or no `milestone:` field), fall back to the OLD
|
|
1752
|
+
// pre-#2316-4a behavior and treat every `v\d+` heading as deferred.
|
|
1753
|
+
// A false "deferred" here only ever SUPPRESSES a warning — strictly
|
|
1754
|
+
// safer than spamming a warning on every v\d+-headed scaffold when
|
|
1755
|
+
// we cannot tell whether it names the active milestone.
|
|
1756
|
+
const DEFERRED_KEYWORD_RE = /\b(?:deferred|backlog|future)\b/i;
|
|
1757
|
+
const HEADING_VERSION_RE = /\bv(\d+)(?:\.\d+)*\b/i;
|
|
1758
|
+
const stateRawForMilestone = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
|
|
1759
|
+
const currentMilestoneRaw = stateRawForMilestone
|
|
1760
|
+
? stateExtractField(stateRawForMilestone, 'milestone')
|
|
1761
|
+
: null;
|
|
1762
|
+
const currentMilestoneMajor = currentMilestoneRaw ? extractMajorVersion(currentMilestoneRaw) : null;
|
|
1597
1763
|
const bodyReqIds = [];
|
|
1598
1764
|
// deferredDepth: the heading level that opened the current deferred block,
|
|
1599
1765
|
// or 0 when we are in an active section.
|
|
@@ -1617,11 +1783,21 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1617
1783
|
}
|
|
1618
1784
|
// Heading at same level or shallower than current deferred opener,
|
|
1619
1785
|
// or no active deferred block yet.
|
|
1620
|
-
if (
|
|
1786
|
+
if (DEFERRED_KEYWORD_RE.test(text)) {
|
|
1621
1787
|
deferredDepth = depth; // enter a deferred block
|
|
1622
1788
|
}
|
|
1623
1789
|
else {
|
|
1624
|
-
|
|
1790
|
+
const versionMatch = text.match(HEADING_VERSION_RE);
|
|
1791
|
+
if (versionMatch) {
|
|
1792
|
+
const headingMajor = versionMatch[1];
|
|
1793
|
+
deferredDepth =
|
|
1794
|
+
currentMilestoneMajor === null || headingMajor !== currentMilestoneMajor
|
|
1795
|
+
? depth // unresolved milestone (fail-safe) or off-milestone version -> deferred
|
|
1796
|
+
: 0; // same major version as the current milestone -> active
|
|
1797
|
+
}
|
|
1798
|
+
else {
|
|
1799
|
+
deferredDepth = 0; // back in an active section
|
|
1800
|
+
}
|
|
1625
1801
|
}
|
|
1626
1802
|
continue;
|
|
1627
1803
|
}
|
|
@@ -1654,8 +1830,68 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1654
1830
|
if (unregistered.length > 0) {
|
|
1655
1831
|
warnings.push(`REQUIREMENTS.md: ${unregistered.length} REQ-ID(s) found in body but missing from Traceability table: ${unregistered.join(', ')} — add them manually to keep traceability in sync`);
|
|
1656
1832
|
}
|
|
1833
|
+
// #2316-1: ghost REQ-IDs — cited by ROADMAP's own **Requirements:**
|
|
1834
|
+
// line for this phase, but registered NOWHERE in REQUIREMENTS.md
|
|
1835
|
+
// (neither its body nor its Traceability table). The `unregistered`
|
|
1836
|
+
// check above only ever compares REQUIREMENTS.md's own body against
|
|
1837
|
+
// its own Traceability table; it never consults `citedReqIds`, so an
|
|
1838
|
+
// ID that ROADMAP cites but REQUIREMENTS.md never defines at all was
|
|
1839
|
+
// previously invisible to every guard. `TBD` (the phase.add/-batch/
|
|
1840
|
+
// -insert placeholder) is excluded — see #2316-7 boundary.
|
|
1841
|
+
//
|
|
1842
|
+
// #2334 HIGH 2: classify "ghost" by PROBING THE ACTUAL WRITE
|
|
1843
|
+
// SURFACES this same function just wrote to (:1947 checkbox,
|
|
1844
|
+
// :1967 Traceability row) — case-insensitively — mirroring
|
|
1845
|
+
// milestone.cts's `notFound`/`hasRow`/`doneCheckbox` classification
|
|
1846
|
+
// (src/milestone.cts:117-141,209-215), instead of set-differencing
|
|
1847
|
+
// `bodyReqIds` (deferred-filtered, case-sensitive, bold-only) and
|
|
1848
|
+
// `tableReqIds` (case-sensitive) against `citedReqIds`. Those two
|
|
1849
|
+
// indexes can disagree with the writes: an ID under a `##
|
|
1850
|
+
// Deferred` heading gets its checkbox ticked by the write loop
|
|
1851
|
+
// above but is deliberately EXCLUDED from `bodyReqIds` by the
|
|
1852
|
+
// deferred-heading filter (#1159), so the old set-diff reported it
|
|
1853
|
+
// as an unregistered ghost in the SAME response that just ticked
|
|
1854
|
+
// its checkbox; a case-mismatched citation (`known-01` vs
|
|
1855
|
+
// `**KNOWN-01**`) lands its write via the writes' case-insensitive
|
|
1856
|
+
// regexes but failed the old set-diff's case-SENSITIVE
|
|
1857
|
+
// `Array.includes`/`Set.has`. An ID whose checkbox OR Traceability
|
|
1858
|
+
// row actually matched is registered — not a ghost — regardless of
|
|
1859
|
+
// which section (deferred or not) it lives under.
|
|
1860
|
+
const reqIsRegisteredAnywhere = (id) => {
|
|
1861
|
+
const reqEscaped = escapeRegex(id);
|
|
1862
|
+
// Surface 1 — checkbox, EITHER state (`[ ]` or `[x]`), case-
|
|
1863
|
+
// insensitive: existence check, not the write's space-only match.
|
|
1864
|
+
if (new RegExp(`-\\s*\\[[ xX]\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i').test(reqContent)) {
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
// Surface 2 — Traceability row exists at all (any Status value),
|
|
1868
|
+
// via the SAME no-op-probe-through-updateTraceabilityCell
|
|
1869
|
+
// technique milestone.cts's `hasRow` uses (:210-214): a case-
|
|
1870
|
+
// insensitive first-cell match, regardless of current Status.
|
|
1871
|
+
const rowProbeMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === id.toLowerCase();
|
|
1872
|
+
return updateTraceabilityCell(reqContent, rowProbeMatch, 'Status', (current) => current).ok;
|
|
1873
|
+
};
|
|
1874
|
+
const ghostReqIds = citedReqIds.filter((id) => !isPlaceholderReqId(id) && !reqIsRegisteredAnywhere(id));
|
|
1875
|
+
if (ghostReqIds.length > 0) {
|
|
1876
|
+
warnings.push(`ROADMAP Phase ${phaseNum} cites REQ-ID(s) not registered anywhere in REQUIREMENTS.md (neither body nor Traceability table): ${ghostReqIds.join(', ')} — add them to REQUIREMENTS.md or correct the ROADMAP citation`);
|
|
1877
|
+
}
|
|
1878
|
+
// #2316-1 cont.: a cited ID whose Traceability-row write matched no
|
|
1879
|
+
// row for a reason OTHER than being a ghost (e.g. a malformed table)
|
|
1880
|
+
// still deserves a warning instead of a silent discard — but skip
|
|
1881
|
+
// IDs already reported above as ghosts to avoid a duplicate message
|
|
1882
|
+
// for the same root cause.
|
|
1883
|
+
const traceabilityWriteFailures = traceabilityWriteMisses.filter((id) => !ghostReqIds.includes(id));
|
|
1884
|
+
if (traceabilityWriteFailures.length > 0) {
|
|
1885
|
+
warnings.push(`REQUIREMENTS.md: Traceability row write skipped for REQ-ID(s) cited by ROADMAP (no matching row found): ${traceabilityWriteFailures.join(', ')}`);
|
|
1886
|
+
}
|
|
1657
1887
|
writes.push({ filePath: reqPath, before: originalReqContent, after: reqContent });
|
|
1658
|
-
|
|
1888
|
+
// #2316-3: `requirements_updated` must reflect whether REQUIREMENTS.md
|
|
1889
|
+
// content actually CHANGED, not merely that the file existed in the
|
|
1890
|
+
// transaction — mirrors the `writes.push({filePath,before,after})`
|
|
1891
|
+
// diff-tracking pattern used for the ROADMAP write above. A phase
|
|
1892
|
+
// whose citations match nothing (ghost REQ-IDs only) must report
|
|
1893
|
+
// `false`, not a bare "the file was present" `true`.
|
|
1894
|
+
requirementsUpdated = reqContent !== originalReqContent;
|
|
1659
1895
|
}
|
|
1660
1896
|
}
|
|
1661
1897
|
try {
|
|
@@ -1802,10 +2038,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1802
2038
|
clock: clock_cjs_1.realClock,
|
|
1803
2039
|
progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
|
|
1804
2040
|
roadmapProvider: () => roadmapContent,
|
|
2041
|
+
sourcePath: statePath,
|
|
1805
2042
|
});
|
|
1806
2043
|
stateContent = completeResult.content;
|
|
1807
2044
|
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
1808
|
-
|
|
2045
|
+
// #2736: the transition holds the next phase's exact display name in
|
|
2046
|
+
// the intent; pass it as authoritative so the sync's prose
|
|
2047
|
+
// re-derivation cannot rewrite current_phase_name to the name's own
|
|
2048
|
+
// parenthetical (`Closer-ruling measurement (D1a)` → `D1a`).
|
|
2049
|
+
stateContent = syncStateFrontmatter(stateContent, cwd, nextPhaseDisplayName ? { current_phase_name: nextPhaseDisplayName } : undefined);
|
|
1809
2050
|
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
1810
2051
|
}
|
|
1811
2052
|
writePlanningFileSet(writes);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ADR-22 Drift-Guard Decision Module
|
|
4
4
|
*
|
|
5
5
|
* Implements the authority ladder and severity classification table from
|
|
6
|
-
* ADR-22 (docs/adr/
|
|
6
|
+
* ADR-22 (docs/adr/22-plan-drift-guard.md).
|
|
7
7
|
*
|
|
8
8
|
* Design constraints:
|
|
9
9
|
* - Pure module: no I/O, no require() calls, no side effects.
|
|
@@ -12,10 +12,62 @@ const node_path_1 = require("node:path");
|
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
13
|
const coreUtils = require("./core-utils.cjs");
|
|
14
14
|
const { countMatchedSummaries } = coreUtils;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
17
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
15
18
|
// Excluded derivative files
|
|
16
19
|
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
|
17
20
|
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
|
18
21
|
const PLAN_REVIEW_RE = /-PLAN-REVIEW\.md$/i;
|
|
22
|
+
// #2349: a plan's frontmatter always sits at byte 0 and closes well before the
|
|
23
|
+
// body, so only a bounded prefix is ever needed to read the `status` marker.
|
|
24
|
+
// Capping the read keeps scanPhasePlans — which loops over every phase directory
|
|
25
|
+
// on hot paths (state sync/validate, roadmap progress) — from slurping a
|
|
26
|
+
// pathologically large committed plan file into memory just to inspect one key.
|
|
27
|
+
const PLAN_FRONTMATTER_READ_CAP = 64 * 1024;
|
|
28
|
+
/**
|
|
29
|
+
* #2349: a plan whose frontmatter declares `status: superseded` was deliberately
|
|
30
|
+
* reassigned or never executed — its work moved to a later plan, so it can never
|
|
31
|
+
* gain a matching `*-SUMMARY.md`. Like a retired phase (#1514, one level up), such
|
|
32
|
+
* a plan must be excluded from BOTH the plan and summary counts; otherwise a phase
|
|
33
|
+
* with a deliberately-unexecuted plan reads `completed: false` forever, pinning the
|
|
34
|
+
* milestone below 100%. Reading only the frontmatter `status` key is the same seam
|
|
35
|
+
* verify.cts / phase.cts already use for plan metadata; a plan without the marker is
|
|
36
|
+
* counted exactly as before.
|
|
37
|
+
*
|
|
38
|
+
* This is the only path in scanPhasePlans that opens file *contents* (the rest is
|
|
39
|
+
* filename matching), so it is hardened accordingly: `statSync().isFile()` rejects
|
|
40
|
+
* anything that is not a regular file — a directory, socket, or a symlink resolving
|
|
41
|
+
* to a device such as `/dev/zero` (a git-committable DoS vector; cf. #2378/#2383) —
|
|
42
|
+
* BEFORE any open, and the read is bounded to a fixed prefix. Fail-safe throughout:
|
|
43
|
+
* a non-regular or unreadable plan is treated as a normal (counted) plan, never
|
|
44
|
+
* silently dropped.
|
|
45
|
+
*/
|
|
46
|
+
function isPlanSuperseded(planFullPath) {
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
const st = (0, node_fs_1.statSync)(planFullPath); // follows symlinks → resolves to the target's real type
|
|
50
|
+
if (!st.isFile())
|
|
51
|
+
return false;
|
|
52
|
+
const length = Math.min(st.size, PLAN_FRONTMATTER_READ_CAP);
|
|
53
|
+
if (length === 0)
|
|
54
|
+
return false;
|
|
55
|
+
const fd = (0, node_fs_1.openSync)(planFullPath, 'r');
|
|
56
|
+
try {
|
|
57
|
+
const buf = Buffer.allocUnsafe(length);
|
|
58
|
+
const bytesRead = (0, node_fs_1.readSync)(fd, buf, 0, length, 0);
|
|
59
|
+
content = buf.toString('utf8', 0, bytesRead);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
(0, node_fs_1.closeSync)(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const status = extractFrontmatter(content, planFullPath)['status'];
|
|
69
|
+
return typeof status === 'string' && status.trim().toLowerCase() === 'superseded';
|
|
70
|
+
}
|
|
19
71
|
function isRootPlanFile(fileName) {
|
|
20
72
|
if (PLAN_OUTLINE_RE.test(fileName))
|
|
21
73
|
return false;
|
|
@@ -75,7 +127,16 @@ function scanPhasePlans(phaseDir) {
|
|
|
75
127
|
}
|
|
76
128
|
catch { /* ignore unreadable nested layout */ }
|
|
77
129
|
}
|
|
78
|
-
const
|
|
130
|
+
const allPlanFiles = rootPlanFiles.concat(nestedPlanFiles);
|
|
131
|
+
// #2349: drop plans explicitly marked `status: superseded` from the plan set
|
|
132
|
+
// BEFORE counting, so they inflate neither the denominator (planCount) nor,
|
|
133
|
+
// via countMatchedSummaries below, the numerator (summaryCount). Plans without
|
|
134
|
+
// the marker are untouched, so behaviour is byte-for-behaviour identical for
|
|
135
|
+
// every existing phase — only a phase carrying the new marker changes.
|
|
136
|
+
const supersededPlanFiles = allPlanFiles.filter((f) => isPlanSuperseded((0, node_path_1.join)(phaseDir, f)));
|
|
137
|
+
const planFiles = supersededPlanFiles.length === 0
|
|
138
|
+
? allPlanFiles
|
|
139
|
+
: allPlanFiles.filter((f) => !supersededPlanFiles.includes(f));
|
|
79
140
|
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
|
80
141
|
const planCount = planFiles.length;
|
|
81
142
|
// Count only summaries that are the PLAN→SUMMARY partner of an existing plan
|
|
@@ -87,7 +148,14 @@ function scanPhasePlans(phaseDir) {
|
|
|
87
148
|
return {
|
|
88
149
|
planCount,
|
|
89
150
|
summaryCount,
|
|
90
|
-
|
|
151
|
+
// #2349: gate completion on whether the phase had ANY plans on disk
|
|
152
|
+
// (allPlanFiles), NOT on the post-exclusion planCount. A phase whose plans
|
|
153
|
+
// were ALL marked superseded has planCount 0, but it is NOT an unplanned
|
|
154
|
+
// empty phase — there is simply no remaining work, so it must read complete
|
|
155
|
+
// (0 >= 0) rather than being pinned below 100% forever, which is the very
|
|
156
|
+
// failure this fix removes. A genuinely empty phase (no plans authored)
|
|
157
|
+
// still has allPlanFiles.length 0 and stays not-completed, exactly as before.
|
|
158
|
+
completed: allPlanFiles.length > 0 && summaryCount >= planCount,
|
|
91
159
|
hasNestedPlans,
|
|
92
160
|
planFiles,
|
|
93
161
|
summaryFiles,
|
|
@@ -369,8 +369,15 @@ function findContextMdIn(absDirOrFiles) {
|
|
|
369
369
|
return 'CONTEXT.md';
|
|
370
370
|
return files.find((f) => f.endsWith('-CONTEXT.md')) ?? null;
|
|
371
371
|
}
|
|
372
|
-
catch {
|
|
373
|
-
|
|
372
|
+
catch (err) {
|
|
373
|
+
// #1883: distinguish genuine absence from a permission/I-O failure. ENOENT
|
|
374
|
+
// ("nothing there") keeps the long-standing null contract the callers rely
|
|
375
|
+
// on; every other error (EACCES, EIO, …) is a real read failure that must
|
|
376
|
+
// propagate — otherwise an unreadable phase dir is silently reported as
|
|
377
|
+
// "no CONTEXT.md" and the discuss/plan gates wrongly skip context.
|
|
378
|
+
if (err.code === 'ENOENT')
|
|
379
|
+
return null;
|
|
380
|
+
throw err;
|
|
374
381
|
}
|
|
375
382
|
}
|
|
376
383
|
module.exports = {
|
|
@@ -920,24 +920,50 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
|
|
920
920
|
'<!-- GSD:profile-end -->',
|
|
921
921
|
];
|
|
922
922
|
const sectionContent = sectionLines.join('\n');
|
|
923
|
+
// #2565: resolve target through effective runtime policy instead of
|
|
924
|
+
// hardcoded .claude/CLAUDE.md. Mirrors the #3163 fix applied to
|
|
925
|
+
// cmdGenerateClaudeMd above; that fix diverged when it didn't propagate
|
|
926
|
+
// here, leaving /gsd-profile-user writing Claude files on Codex installs.
|
|
927
|
+
// - Project scope: getProjectInstructionFile(runtime) is the single source
|
|
928
|
+
// of truth (AGENTS.md for codex/opencode/kilo/kimi/unknown; GEMINI.md for
|
|
929
|
+
// antigravity; .github/copilot-instructions.md for copilot).
|
|
930
|
+
// - Global scope: ~/.<config-home>/<instruction-basename>, derived from
|
|
931
|
+
// getGlobalConfigDir + basename(getProjectInstructionFile), so codex lands
|
|
932
|
+
// at ~/.codex/AGENTS.md. Claude global is preserved byte-for-byte (no
|
|
933
|
+
// env-var drift beyond the prior hardcoded path).
|
|
934
|
+
// - GSD_RUNTIME env var takes precedence over config.runtime (mirrors the
|
|
935
|
+
// #3163 env-precedence contract). Non-claude always wins over a stale
|
|
936
|
+
// claude_md_path (#3163 rationale: an AGENTS-native project must never
|
|
937
|
+
// write to CLAUDE.md even if a prior Claude setup left claude_md_path).
|
|
938
|
+
let config = {};
|
|
939
|
+
try {
|
|
940
|
+
config = loadConfig(cwd);
|
|
941
|
+
}
|
|
942
|
+
catch { /* use defaults */ }
|
|
943
|
+
const effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime']);
|
|
944
|
+
const isClaudeRuntime = !effectiveRuntime || effectiveRuntime === 'claude';
|
|
923
945
|
let targetPath;
|
|
924
946
|
if (options.global) {
|
|
925
|
-
|
|
947
|
+
if (isClaudeRuntime) {
|
|
948
|
+
targetPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'CLAUDE.md');
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
targetPath = node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(effectiveRuntime), node_path_1.default.basename((0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime)));
|
|
952
|
+
}
|
|
926
953
|
}
|
|
927
954
|
else if (options.output) {
|
|
928
955
|
targetPath = node_path_1.default.isAbsolute(options.output) ? options.output : node_path_1.default.join(cwd, options.output);
|
|
929
956
|
}
|
|
930
957
|
else {
|
|
931
|
-
// Read claude_md_path from config; #1098 default is
|
|
958
|
+
// Read claude_md_path from config; #1098 default is .claude/CLAUDE.md
|
|
932
959
|
// (kept consistent with cmdGenerateClaudeMd so the profile section and the
|
|
933
960
|
// managed sections land in the same file on a config-less project).
|
|
934
|
-
let configClaudeMdPath = '
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
961
|
+
let configClaudeMdPath = '.claude/CLAUDE.md';
|
|
962
|
+
if (config['claude_md_path'])
|
|
963
|
+
configClaudeMdPath = config['claude_md_path'];
|
|
964
|
+
if (!isClaudeRuntime) {
|
|
965
|
+
configClaudeMdPath = (0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime);
|
|
939
966
|
}
|
|
940
|
-
catch { /* use default */ }
|
|
941
967
|
targetPath = node_path_1.default.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : node_path_1.default.join(cwd, configClaudeMdPath);
|
|
942
968
|
}
|
|
943
969
|
let action;
|