@opengsd/gsd-core 1.9.0 → 1.10.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -29,10 +29,10 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
29
29
|
const { loadConfig } = configLoaderMod;
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
31
31
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
32
|
-
const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
|
|
32
|
+
const { toPosixPath, generateSlugInternal, readSubdirectories, findUnsummarizedPlans } = coreUtilsMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
34
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdMod;
|
|
35
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, isSentinelPhaseId, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdMod;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
37
37
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
38
38
|
const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
|
|
@@ -62,6 +62,9 @@ const verificationMod = require("./verification.cjs");
|
|
|
62
62
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verify.cjs is an export= CommonJS module
|
|
63
63
|
const verifyMod = require("./verify.cjs");
|
|
64
64
|
const { readVerificationStatus } = verificationMod;
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-dependency-graph.cjs is an export= CommonJS module
|
|
66
|
+
const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
67
|
+
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphMod;
|
|
65
68
|
const { planningDir, withPlanningLock, listAvailableWorkstreams, getActiveWorkstream } = planningWorkspace;
|
|
66
69
|
const { extractFrontmatter } = frontmatterMod;
|
|
67
70
|
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
@@ -421,9 +424,26 @@ function extractObjective(content) {
|
|
|
421
424
|
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
422
425
|
return m ? m[1].trim() : null;
|
|
423
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolve a raw `depends_on` token to the `RawPlan.id` it refers to
|
|
429
|
+
* (case-folded exact match, falling back to canonical-id matching). Returns
|
|
430
|
+
* `null` when the token does not resolve to any plan in this phase (a typo
|
|
431
|
+
* or a cross-phase reference) — every call site treats that as "ignore this
|
|
432
|
+
* edge", never a throw. Shared by `computeDependencyLevels`'s DAG-edge
|
|
433
|
+
* resolution, the `depends_on` display mapping, and (#2830) the
|
|
434
|
+
* halt-propagation node resolution, so the three can never disagree about
|
|
435
|
+
* which token resolves to which plan.
|
|
436
|
+
*/
|
|
437
|
+
function resolveDependencyId(dep, planMap, canonicalToId) {
|
|
438
|
+
const lower = dep.toLowerCase();
|
|
439
|
+
return planMap.has(lower) ? planMap.get(lower).id : (canonicalToId.get(lower) ?? null);
|
|
440
|
+
}
|
|
424
441
|
// O(V + E). Assigns each in-phase plan its longest-path topological level over the
|
|
425
|
-
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number
|
|
426
|
-
// visited < rawPlans.length signals a dependency cycle.
|
|
442
|
+
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number,
|
|
443
|
+
// order: string[] }. visited < rawPlans.length signals a dependency cycle. `order` (#2830) is
|
|
444
|
+
// the exact dequeue order this pass already produces — a valid topological order — passed to
|
|
445
|
+
// computeHaltPropagation as `precomputedOrder` so halt propagation does not re-run Kahn's
|
|
446
|
+
// algorithm a second time over the same graph.
|
|
427
447
|
function computeDependencyLevels(rawPlans, planMap, canonicalToId) {
|
|
428
448
|
const level = new Map();
|
|
429
449
|
const inDeg = new Map();
|
|
@@ -434,10 +454,7 @@ function computeDependencyLevels(rawPlans, planMap, canonicalToId) {
|
|
|
434
454
|
if (!adj.has(p.id))
|
|
435
455
|
adj.set(p.id, []);
|
|
436
456
|
for (const dep of p.dependsOn) {
|
|
437
|
-
const
|
|
438
|
-
const resolvedDep = planMap.has(depLower)
|
|
439
|
-
? planMap.get(depLower).id
|
|
440
|
-
: canonicalToId.get(depLower);
|
|
457
|
+
const resolvedDep = resolveDependencyId(dep, planMap, canonicalToId);
|
|
441
458
|
if (!resolvedDep)
|
|
442
459
|
continue;
|
|
443
460
|
if (!adj.has(resolvedDep))
|
|
@@ -472,7 +489,7 @@ function computeDependencyLevels(rawPlans, planMap, canonicalToId) {
|
|
|
472
489
|
}
|
|
473
490
|
}
|
|
474
491
|
}
|
|
475
|
-
return { level, visited };
|
|
492
|
+
return { level, visited, order: queue };
|
|
476
493
|
}
|
|
477
494
|
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
478
495
|
if (!phase) {
|
|
@@ -498,7 +515,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
498
515
|
// phases dir doesn't exist
|
|
499
516
|
}
|
|
500
517
|
if (!phaseDir) {
|
|
501
|
-
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
|
|
518
|
+
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
|
|
502
519
|
return;
|
|
503
520
|
}
|
|
504
521
|
void phaseDirName; // used only to set phaseDir above
|
|
@@ -511,6 +528,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
511
528
|
const canonical = extractCanonicalPlanId(s);
|
|
512
529
|
return canonical === exact ? [exact] : [exact, canonical];
|
|
513
530
|
}));
|
|
531
|
+
// #2830: reverse lookup from a completed plan's id (exact or canonical) to
|
|
532
|
+
// the actual summary filename, so a plan's own SUMMARY frontmatter can be
|
|
533
|
+
// read for its `status`. Shared builder (also used by phase-locator.cts's
|
|
534
|
+
// searchPhaseInDir) so the two can never disagree about which summary
|
|
535
|
+
// belongs to which plan.
|
|
536
|
+
const summaryFileByPlanId = buildSummaryFileIndex(summaryFiles, extractCanonicalPlanId);
|
|
514
537
|
// ── Pass 1: parse each plan file ─────────────────────────────────────────
|
|
515
538
|
const rawPlans = [];
|
|
516
539
|
for (const planFile of planFiles) {
|
|
@@ -544,6 +567,14 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
544
567
|
filesModified = Array.isArray(fmFiles) ? fmFiles.map(String) : [String(fmFiles)];
|
|
545
568
|
}
|
|
546
569
|
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
|
570
|
+
// #2830: a plan can have a SUMMARY (hasSummary=true) and still be halted —
|
|
571
|
+
// a designed stop still writes a completion record, just one whose status
|
|
572
|
+
// says "halted" rather than "complete". Only look up the summary file
|
|
573
|
+
// when one exists; there is nothing to read otherwise.
|
|
574
|
+
const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(extractCanonicalPlanId(planFile));
|
|
575
|
+
const halted = hasSummary && summaryFile !== undefined
|
|
576
|
+
? isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile))
|
|
577
|
+
: false;
|
|
547
578
|
rawPlans.push({
|
|
548
579
|
id: planId,
|
|
549
580
|
declaredWave,
|
|
@@ -553,6 +584,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
553
584
|
filesModified,
|
|
554
585
|
taskCount,
|
|
555
586
|
hasSummary,
|
|
587
|
+
halted,
|
|
556
588
|
});
|
|
557
589
|
}
|
|
558
590
|
// ── Pass 2: topological level assignment via depends_on DAG ──────────────
|
|
@@ -568,26 +600,47 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
568
600
|
}
|
|
569
601
|
const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
|
|
570
602
|
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
|
|
571
|
-
const { level, visited } = computeDependencyLevels(rawPlans, planMap, canonicalToId);
|
|
603
|
+
const { level, visited, order } = computeDependencyLevels(rawPlans, planMap, canonicalToId);
|
|
572
604
|
if (visited < rawPlans.length) {
|
|
573
605
|
const cycleNodes = rawPlans.filter((p) => !level.has(p.id)).map((p) => p.id);
|
|
574
606
|
error(`depends_on cycle detected in phase ${normalized} — cycle involves: ${cycleNodes.join(', ')}`);
|
|
575
607
|
return;
|
|
576
608
|
}
|
|
609
|
+
// #2830: single shared halt-propagation pass, reusing the SAME id
|
|
610
|
+
// resolution (planMap/canonicalToId) AND the SAME topological order
|
|
611
|
+
// (`order`, computeDependencyLevels's own Kahn's-algorithm dequeue
|
|
612
|
+
// sequence) — passed as `precomputedOrder` so computeHaltPropagation does
|
|
613
|
+
// NOT run Kahn's algorithm a second time over this graph.
|
|
614
|
+
const haltNodes = rawPlans.map((p) => ({
|
|
615
|
+
id: p.id,
|
|
616
|
+
resolvedDependsOn: p.dependsOn
|
|
617
|
+
.map((dep) => resolveDependencyId(String(dep), planMap, canonicalToId))
|
|
618
|
+
.filter((id) => id !== null),
|
|
619
|
+
halted: p.halted,
|
|
620
|
+
}));
|
|
621
|
+
const { blockedBy } = computeHaltPropagation(haltNodes, order);
|
|
577
622
|
// ── Pass 3: determine lowest bucket key and build output ─────────────────
|
|
578
623
|
const anyWaveZero = rawPlans.some((p) => p.declaredWave === 0);
|
|
579
624
|
const levelOffset = anyWaveZero ? 0 : 1;
|
|
580
625
|
const plans = [];
|
|
581
626
|
const waves = {};
|
|
582
627
|
const incomplete = [];
|
|
628
|
+
const runnable = [];
|
|
583
629
|
let hasCheckpoints = false;
|
|
584
630
|
const warnings = [];
|
|
585
631
|
for (const rawPlan of rawPlans) {
|
|
586
632
|
if (!rawPlan.autonomous) {
|
|
587
633
|
hasCheckpoints = true;
|
|
588
634
|
}
|
|
635
|
+
const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
|
|
589
636
|
if (!rawPlan.hasSummary) {
|
|
590
637
|
incomplete.push(rawPlan.id);
|
|
638
|
+
// #2830: the runnable-only view — incomplete AND not transitively
|
|
639
|
+
// blocked by a halted upstream plan. Additive alongside `incomplete`,
|
|
640
|
+
// which keeps its existing "no SUMMARY yet" meaning unchanged.
|
|
641
|
+
if (blockedByIds.length === 0) {
|
|
642
|
+
runnable.push(rawPlan.id);
|
|
643
|
+
}
|
|
591
644
|
}
|
|
592
645
|
const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
|
|
593
646
|
const effectiveWave = computedWave;
|
|
@@ -597,6 +650,13 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
597
650
|
const plan = {
|
|
598
651
|
id: rawPlan.id,
|
|
599
652
|
wave: effectiveWave,
|
|
653
|
+
// DELIBERATELY not `resolveDependencyId`: the emitted field is a DISPLAY
|
|
654
|
+
// mapping, not the DAG resolution. It rewrites a dep only when it names a
|
|
655
|
+
// plan directly (planMap) and otherwise passes it through verbatim — a
|
|
656
|
+
// short canonical prefix like `24-01` stays `24-01` rather than becoming
|
|
657
|
+
// `24-01-auth-hardening`. #3785 pins that contract. Full resolution via
|
|
658
|
+
// canonicalToId is used for the wave DAG and #2830 halt propagation only;
|
|
659
|
+
// routing this line through it too silently changed the output shape.
|
|
600
660
|
depends_on: rawPlan.dependsOn.map((dep) => {
|
|
601
661
|
const lower = String(dep).toLowerCase();
|
|
602
662
|
return planMap.has(lower) ? planMap.get(lower).id : dep;
|
|
@@ -606,6 +666,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
606
666
|
files_modified: rawPlan.filesModified,
|
|
607
667
|
task_count: rawPlan.taskCount,
|
|
608
668
|
has_summary: rawPlan.hasSummary,
|
|
669
|
+
// #2830: additive fields — halted is this plan's OWN status; blocked_by
|
|
670
|
+
// names the halted plan(s) transitively upstream of it (empty when not
|
|
671
|
+
// blocked). Neither mutates has_summary/incomplete's existing meaning.
|
|
672
|
+
halted: rawPlan.halted,
|
|
673
|
+
blocked_by: blockedByIds,
|
|
609
674
|
};
|
|
610
675
|
plans.push(plan);
|
|
611
676
|
const waveKey = String(effectiveWave);
|
|
@@ -619,6 +684,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
619
684
|
plans,
|
|
620
685
|
waves,
|
|
621
686
|
incomplete,
|
|
687
|
+
runnable,
|
|
622
688
|
has_checkpoints: hasCheckpoints,
|
|
623
689
|
};
|
|
624
690
|
if (planNamingWarning)
|
|
@@ -1289,19 +1355,50 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
1289
1355
|
}
|
|
1290
1356
|
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
1291
1357
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1358
|
+
let stateUpdated = false;
|
|
1292
1359
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
1293
|
-
|
|
1294
|
-
|
|
1360
|
+
// #2640: report whether STATE.md content actually changed, not just file
|
|
1361
|
+
// existence (fs.existsSync was trivially true). Also ensure the body
|
|
1362
|
+
// transform produces a diff so readModifyWriteStateMd's no-op guard
|
|
1363
|
+
// (#948) doesn't skip the frontmatter resync — without that, the
|
|
1364
|
+
// progress.* frontmatter block stays stale when the body has no
|
|
1365
|
+
// 'Total Phases:' or 'of N' phrase.
|
|
1366
|
+
stateUpdated = readModifyWriteStateMd(statePath, (stateContent) => {
|
|
1367
|
+
let modified = stateContent;
|
|
1368
|
+
const totalRaw = stateExtractField(modified, 'Total Phases');
|
|
1295
1369
|
if (totalRaw) {
|
|
1296
|
-
|
|
1297
|
-
stateReplaceField(
|
|
1298
|
-
|
|
1370
|
+
modified =
|
|
1371
|
+
stateReplaceField(modified, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) ||
|
|
1372
|
+
modified;
|
|
1299
1373
|
}
|
|
1300
|
-
const ofMatch =
|
|
1374
|
+
const ofMatch = modified.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
1301
1375
|
if (ofMatch) {
|
|
1302
|
-
|
|
1376
|
+
modified = modified.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
1303
1377
|
}
|
|
1304
|
-
|
|
1378
|
+
// #2640: if neither body field was found, the transform is a no-op.
|
|
1379
|
+
// readModifyWriteStateMd's no-op guard (#948) would then skip the
|
|
1380
|
+
// frontmatter resync, leaving progress.* stale. Force a body diff
|
|
1381
|
+
// ONLY when a phase directory was actually removed (targetDir !== null)
|
|
1382
|
+
// so the guard passes and syncStateFrontmatter rebuilds the frontmatter
|
|
1383
|
+
// from the post-deletion disk/ROADMAP state. Without the targetDir gate,
|
|
1384
|
+
// a no-op removal (ROADMAP-only phase, no directory) would inject a
|
|
1385
|
+
// spurious 'Total Phases:' line into a body that intentionally lacked one.
|
|
1386
|
+
if (targetDir && modified === stateContent) {
|
|
1387
|
+
// subdirs was read before the deletion; excluding the removed target
|
|
1388
|
+
// gives the remaining count. Renumbering changes names but not count.
|
|
1389
|
+
const remainingPhases = subdirs.filter((d) => phaseTokenMatches(d, normalized) === false).length;
|
|
1390
|
+
if (totalRaw) {
|
|
1391
|
+
modified =
|
|
1392
|
+
stateReplaceField(modified, 'Total Phases', String(remainingPhases)) || modified;
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
// No 'Total Phases:' field in the body — append one so the no-op
|
|
1396
|
+
// guard sees a diff. syncStateFrontmatter will then rebuild the
|
|
1397
|
+
// frontmatter progress.* block from the real disk/ROADMAP count.
|
|
1398
|
+
modified = `Total Phases: ${remainingPhases}\n` + modified;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return modified;
|
|
1305
1402
|
}, cwd);
|
|
1306
1403
|
}
|
|
1307
1404
|
output({
|
|
@@ -1310,7 +1407,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
1310
1407
|
renamed_directories: renamedDirs,
|
|
1311
1408
|
renamed_files: renamedFiles,
|
|
1312
1409
|
roadmap_updated: true,
|
|
1313
|
-
state_updated:
|
|
1410
|
+
state_updated: stateUpdated,
|
|
1314
1411
|
}, raw);
|
|
1315
1412
|
}
|
|
1316
1413
|
function writePlanningFileSet(writes) {
|
|
@@ -1393,7 +1490,78 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1393
1490
|
: 0;
|
|
1394
1491
|
let requirementsUpdated = false;
|
|
1395
1492
|
const warnings = [];
|
|
1493
|
+
// #3057 B3: mirrors `verification_stale_check_indeterminate` on init.cts /
|
|
1494
|
+
// roadmap.cts / uat-predicate.cts's outputs — set on the non-blocking path
|
|
1495
|
+
// below (inside withPlanningLock) alongside the warnings[] entry, so a
|
|
1496
|
+
// caller can assert on the typed field instead of the warning's prose.
|
|
1497
|
+
let staleCheckIndeterminate = false;
|
|
1396
1498
|
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1499
|
+
// #2648: fail-closed plan-coverage gate. phase.complete used to gate ONLY on a
|
|
1500
|
+
// single *-VERIFICATION.md status, so a phase could close "complete" while an
|
|
1501
|
+
// arbitrary number of its plans — including plans a lock/recovery decision
|
|
1502
|
+
// silently dropped — had no completion record (a confirmed production incident
|
|
1503
|
+
// closed a phase with 6/30 plans unexecuted, including its entire final UI
|
|
1504
|
+
// scope, with every tool-reported signal green). Now refuse completion when any
|
|
1505
|
+
// plan lacks a matching *-SUMMARY.md, UNLESS that plan is explicitly retired
|
|
1506
|
+
// via machine-readable `status: superseded` frontmatter (the #2349 marker).
|
|
1507
|
+
//
|
|
1508
|
+
// scanPhasePlans is the superseded-AWARE counter (it drops status: superseded
|
|
1509
|
+
// plans from planFiles before returning), so a deliberately-retired plan never
|
|
1510
|
+
// appears in the unsummarized set and never blocks completion — closing the
|
|
1511
|
+
// Goodhart hole (delete a SUMMARY to raise the %) without regressing the
|
|
1512
|
+
// legitimate lock/recovery pattern (retire a plan instead of executing it).
|
|
1513
|
+
// This is evaluated BEFORE the verification-gate transaction below so a
|
|
1514
|
+
// plan-coverage refusal fails fast without mutating ROADMAP/STATE. The count
|
|
1515
|
+
// path (cmdPhaseComplete's own planCount/summaryCount above) is NOT superseded-
|
|
1516
|
+
// aware (it comes from findPhaseInternal/phase-locator.cts); that is fine for
|
|
1517
|
+
// DISPLAY (the X/Y cell) but must not be the gate — the gate needs the
|
|
1518
|
+
// superseded-adjusted set so retired plans don't re-block the very phases the
|
|
1519
|
+
// marker exists to unblock. Matches roadmap.cts's already-correct-but-unenforced
|
|
1520
|
+
// `summaryCount >= planCount` predicate, now enforced at the completion seam.
|
|
1521
|
+
const coverageScan = scanPhasePlans(phaseFullDir);
|
|
1522
|
+
// #2648 security: fail CLOSED when the phase directory cannot be read.
|
|
1523
|
+
// scanPhasePlans deliberately swallows readdirSync errors and returns an empty
|
|
1524
|
+
// plan set ({planFiles: []}), which is indistinguishable from a readable empty
|
|
1525
|
+
// phase. For a COVERAGE gate that is the wrong posture: "I could not read the
|
|
1526
|
+
// plans" must mean "I cannot prove coverage," not "all plans are summarized" —
|
|
1527
|
+
// otherwise any I/O failure (permissions, ENOTDIR, EBUSY on Windows, a dir
|
|
1528
|
+
// present in ROADMAP.md but missing/unreadable on disk) silently re-opens the
|
|
1529
|
+
// exact hole this gate exists to close. Distinguish the two: a readable
|
|
1530
|
+
// directory with zero plans is a legitimately complete empty phase; an
|
|
1531
|
+
// UNREADABLE directory is a fail-closed refusal. Mirrors cmdPhaseInsert's own
|
|
1532
|
+
// readdirSync-fail-closed posture (a swallow there used to risk writing a
|
|
1533
|
+
// colliding phase number).
|
|
1534
|
+
try {
|
|
1535
|
+
node_fs_1.default.readdirSync(phaseFullDir);
|
|
1536
|
+
}
|
|
1537
|
+
catch (readErr) {
|
|
1538
|
+
error(`Phase ${phaseNum} cannot be completed: its plan directory is unreadable (${phaseInfo['directory']}: ${readErr.code || readErr.message}), so plan coverage cannot be verified. Restore read access and retry — a coverage gate that passes when it cannot read the plans is no gate at all (#2648).`, ERROR_REASON.PHASE_PLAN_COVERAGE_INCOMPLETE);
|
|
1539
|
+
}
|
|
1540
|
+
const unsummarizedPlans = findUnsummarizedPlans(coverageScan.planFiles, coverageScan.summaryFiles);
|
|
1541
|
+
if (unsummarizedPlans.length > 0) {
|
|
1542
|
+
// Sanitize plan filenames before interpolation: they come raw from
|
|
1543
|
+
// readdirSync and could carry C0 control chars / DEL (a committable filename
|
|
1544
|
+
// could spoof the terminal in plain-error mode). Strip them so the message is
|
|
1545
|
+
// safe to print regardless of --json-errors. Path traversal sequences are not
|
|
1546
|
+
// a code-execution vector here (printed only, never reopened from the message).
|
|
1547
|
+
const sanitize = (name) => name.replace(/[\u0000-\u001f\u007f]/g, '?');
|
|
1548
|
+
const listed = unsummarizedPlans.slice(0, 20).map(sanitize).join(', ');
|
|
1549
|
+
const more = unsummarizedPlans.length > 20 ? ` (and ${unsummarizedPlans.length - 20} more)` : '';
|
|
1550
|
+
// Audit surface (#2648 review M1): name how many plans were excluded as
|
|
1551
|
+
// superseded so a reviewer can see WHICH work was declared retired, not just
|
|
1552
|
+
// that some plans are missing summaries. The status: superseded marker is a
|
|
1553
|
+
// committable, review-time-trusted bypass; surfacing its count keeps that
|
|
1554
|
+
// bypass visible rather than silent.
|
|
1555
|
+
const phaseInfoPlanCount = Array.isArray(phaseInfo['plans']) ? phaseInfo['plans'].length : 0;
|
|
1556
|
+
const supersededCount = coverageScan.planFiles.length === 0 ? 0 : Math.max(0, phaseInfoPlanCount - coverageScan.planFiles.length);
|
|
1557
|
+
const supersededNote = supersededCount > 0
|
|
1558
|
+
? ` ${supersededCount} plan(s) excluded as status: superseded (retired).`
|
|
1559
|
+
: '';
|
|
1560
|
+
error(`Phase ${phaseNum} cannot be completed: ${unsummarizedPlans.length} plan(s) have no completion record (*-SUMMARY.md): ${listed}${more}.` +
|
|
1561
|
+
supersededNote +
|
|
1562
|
+
` Execute the plans and write their summaries, or retire a plan with machine-readable \`status: superseded\` frontmatter (#2349) if it was deliberately dropped — a retired plan is excluded from this gate. ` +
|
|
1563
|
+
`Completing a phase with unexecuted plans is what lost an entire promised deliverable silently (#2648).`, ERROR_REASON.PHASE_PLAN_COVERAGE_INCOMPLETE);
|
|
1564
|
+
}
|
|
1397
1565
|
try {
|
|
1398
1566
|
const phaseFiles = node_fs_1.default.readdirSync(phaseFullDir);
|
|
1399
1567
|
for (const file of phaseFiles.filter((f) => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
@@ -1476,6 +1644,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1476
1644
|
// suggests the command surface this runtime actually installs
|
|
1477
1645
|
// ($gsd-… on Codex) rather than a hard-coded Claude-style string.
|
|
1478
1646
|
const verificationStatus = readVerificationStatus(phaseFullDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
|
|
1647
|
+
// #3057 B3: the staleness check inside readVerificationStatus can itself
|
|
1648
|
+
// fail (fs / scanPhasePlans / clock error), in which case `status` above
|
|
1649
|
+
// was routed as if nothing were stale (unchanged fail-open routing) — but
|
|
1650
|
+
// that must not be silently identical to a check that actually ran and
|
|
1651
|
+
// found nothing stale. Join the SAME advisory channel the UAT/VERIFICATION
|
|
1652
|
+
// pre-scan above already uses (`warnings[]`, rendered by execute-phase.md's
|
|
1653
|
+
// "If has_warnings is true" step) rather than inventing a new one. This
|
|
1654
|
+
// only fires on the non-blocking path (status resolves to 'passed' despite
|
|
1655
|
+
// the indeterminate check) — the blocked path below carries its own note.
|
|
1656
|
+
if (verificationStatus.staleCheckIndeterminate) {
|
|
1657
|
+
staleCheckIndeterminate = true;
|
|
1658
|
+
warnings.push(`verification staleness check could not complete for phase ${phaseNum} — routed as not-stale, but this was not actually verified (#3057)`);
|
|
1659
|
+
}
|
|
1479
1660
|
if (verificationStatus.status !== 'passed') {
|
|
1480
1661
|
return verificationStatus;
|
|
1481
1662
|
}
|
|
@@ -1682,7 +1863,17 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1682
1863
|
.filter((r) => REQ_ID_SHAPE_RE.test(r));
|
|
1683
1864
|
for (const reqId of citedReqIds) {
|
|
1684
1865
|
const reqEscaped = escapeRegex(reqId);
|
|
1685
|
-
|
|
1866
|
+
// Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**.
|
|
1867
|
+
// #2945: the flip is CONDITIONAL (porting #2788 defect-2's rollback from
|
|
1868
|
+
// cmdRequirementsMarkComplete). Capture the pre-flip content; if a
|
|
1869
|
+
// traceability row EXISTS for this ID below but its Status write is rejected
|
|
1870
|
+
// (Out/Deferred/Blocked), the checkbox is rolled back so the two surfaces
|
|
1871
|
+
// cannot silently diverge. A requirement recorded as deferred must not read
|
|
1872
|
+
// as shipped.
|
|
1873
|
+
const checkboxRe = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
|
1874
|
+
const beforeCheckbox = reqContent;
|
|
1875
|
+
reqContent = reqContent.replace(checkboxRe, '$1x$2');
|
|
1876
|
+
const checkboxFlipped = reqContent !== beforeCheckbox;
|
|
1686
1877
|
// Traceability row: | <REQ-ID> | Phase N | Pending|In Progress | ->
|
|
1687
1878
|
// ... Complete | via the markdown-table seam (ADR-2143 §7). Match the
|
|
1688
1879
|
// row by its FIRST cell's value (the requirement-ID column) regardless
|
|
@@ -1698,13 +1889,33 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1698
1889
|
// requirement's write. The "only flip Pending/In Progress ->
|
|
1699
1890
|
// Complete" gate is folded into the newValue callback so one
|
|
1700
1891
|
// updateTableCell call both probes and writes.
|
|
1701
|
-
|
|
1892
|
+
// #2945: track tableHit (did the callback actually CHANGE the value?) so the
|
|
1893
|
+
// checkbox rollback below can distinguish "row existed and accepted" from
|
|
1894
|
+
// "row existed and rejected".
|
|
1895
|
+
let tableHit = false;
|
|
1896
|
+
const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (current) => {
|
|
1897
|
+
// #2788: accept `Gaps Found` too so a phase stranded by revert-phase (the
|
|
1898
|
+
// gaps_found response) can complete without hand-editing the table.
|
|
1899
|
+
if (/^(?:pending|in progress|gaps found)$/i.test(current.trim())) {
|
|
1900
|
+
tableHit = true;
|
|
1901
|
+
return ' Complete ';
|
|
1902
|
+
}
|
|
1903
|
+
return current;
|
|
1904
|
+
});
|
|
1702
1905
|
if (reqUpdate.ok) {
|
|
1703
1906
|
reqContent = reqUpdate.value;
|
|
1704
1907
|
}
|
|
1705
1908
|
else if (!isPlaceholderReqId(reqId)) {
|
|
1706
1909
|
traceabilityWriteMisses.push(reqId);
|
|
1707
1910
|
}
|
|
1911
|
+
// #2945 defect-2 (port of milestone.cts:200-210): if a row EXISTS for this
|
|
1912
|
+
// ID but its Status write was rejected (row reads Out/Deferred/Blocked,
|
|
1913
|
+
// which the callback returned unchanged), roll the checkbox back so the
|
|
1914
|
+
// checkbox and the row cannot silently diverge. reqUpdate.ok === a row
|
|
1915
|
+
// matched (existence probe); !tableHit === the callback did not advance it.
|
|
1916
|
+
if (checkboxFlipped && reqUpdate.ok && !tableHit) {
|
|
1917
|
+
reqContent = beforeCheckbox;
|
|
1918
|
+
}
|
|
1708
1919
|
}
|
|
1709
1920
|
}
|
|
1710
1921
|
// #1159 (Defect B): collect requirement IDs only from ACTIVE sections.
|
|
@@ -1946,6 +2157,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1946
2157
|
const phasePattern = new RegExp(`(?:#{2,4}|-\\s*\\[[ xX]\\])\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n*]+)`, 'gi');
|
|
1947
2158
|
let pm;
|
|
1948
2159
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
2160
|
+
// #2786: skip sentinel phase ids (999.x backlog, 0.x drafts) — stage 1
|
|
2161
|
+
// already skips 999 dirs on disk; stage 2's heading scan must not
|
|
2162
|
+
// advance into backlog headings. Mirrors the /^999(?:\.|$)/ guard
|
|
2163
|
+
// stage 1 uses at line 2536, but via isSentinelPhaseId for both ranges.
|
|
2164
|
+
if (isSentinelPhaseId(pm[1]))
|
|
2165
|
+
continue;
|
|
1949
2166
|
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
1950
2167
|
nextPhaseNum = pm[1];
|
|
1951
2168
|
nextPhaseName = pm[2]
|
|
@@ -1987,7 +2204,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1987
2204
|
let lowestOutstanding = null;
|
|
1988
2205
|
while ((cbm = cbPattern.exec(milestoneScope)) !== null) {
|
|
1989
2206
|
const isChecked = cbm[1].toLowerCase() === 'x';
|
|
1990
|
-
|
|
2207
|
+
// #2949: exclude sentinel-range phase ids (0.x backlog, 999.x) from candidacy.
|
|
2208
|
+
// comparePhaseNum("0.1","12") === -12, so without this guard an unchecked 0.x
|
|
2209
|
+
// backlog row sorts below every real phase and is wrongly selected as next_phase,
|
|
2210
|
+
// corrupting STATE.md and desyncing current_phase from current_phase_name.
|
|
2211
|
+
// isSentinelPhaseId covers both sentinel ranges (SENTINEL_RANGES = [0, 999]); a
|
|
2212
|
+
// real lower-numbered outstanding phase (e.g. Phase 9) is NOT a sentinel and is
|
|
2213
|
+
// still selected, preserving #2028's out-of-order-completion behavior.
|
|
2214
|
+
if (!isChecked && !isSentinelPhaseId(cbm[2]) && comparePhaseNum(cbm[2], phaseNum) < 0) {
|
|
1991
2215
|
if (lowestOutstanding === null || comparePhaseNum(cbm[2], lowestOutstanding.num) < 0) {
|
|
1992
2216
|
lowestOutstanding = {
|
|
1993
2217
|
num: cbm[2],
|
|
@@ -2036,7 +2260,6 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2036
2260
|
summaryCount,
|
|
2037
2261
|
}, {
|
|
2038
2262
|
clock: clock_cjs_1.realClock,
|
|
2039
|
-
progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
|
|
2040
2263
|
roadmapProvider: () => roadmapContent,
|
|
2041
2264
|
sourcePath: statePath,
|
|
2042
2265
|
});
|
|
@@ -2063,7 +2286,18 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2063
2286
|
const nextStep = verificationBlocked.next_command
|
|
2064
2287
|
? ` Next: ${verificationBlocked.next_command}`
|
|
2065
2288
|
: '';
|
|
2066
|
-
|
|
2289
|
+
// #3057 B3: purely additive to the message text — does not change WHETHER
|
|
2290
|
+
// this blocks (verificationBlocked was already truthy) or the
|
|
2291
|
+
// ERROR_REASON, only whether the operator can see the staleness check
|
|
2292
|
+
// itself did not complete. The same fact is also attached as a typed
|
|
2293
|
+
// field (`verification_stale_check_indeterminate`) on the JSON-error-mode
|
|
2294
|
+
// payload so a test can assert on it by value instead of regexing this
|
|
2295
|
+
// human-readable note.
|
|
2296
|
+
const staleCheckIndeterminate = verificationBlocked.staleCheckIndeterminate === true;
|
|
2297
|
+
const indeterminateNote = staleCheckIndeterminate
|
|
2298
|
+
? ' (staleness check could not complete — see #3057)'
|
|
2299
|
+
: '';
|
|
2300
|
+
error(`Phase ${phaseNum} verification is incomplete: ${verificationBlocked.next_action}${nextStep}${indeterminateNote}`, ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE, { verification_stale_check_indeterminate: staleCheckIndeterminate });
|
|
2067
2301
|
}
|
|
2068
2302
|
let autoPruned = false;
|
|
2069
2303
|
try {
|
|
@@ -2097,6 +2331,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2097
2331
|
auto_pruned: autoPruned,
|
|
2098
2332
|
warnings,
|
|
2099
2333
|
has_warnings: warnings.length > 0,
|
|
2334
|
+
verification_stale_check_indeterminate: staleCheckIndeterminate,
|
|
2100
2335
|
};
|
|
2101
2336
|
output(result, raw);
|
|
2102
2337
|
}
|