@opengsd/gsd-core 1.14.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -35,7 +35,7 @@ const frontmatterModule = require("./frontmatter.cjs");
|
|
|
35
35
|
const { extractFrontmatter } = frontmatterModule;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
37
|
const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
|
|
38
|
-
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphModule;
|
|
38
|
+
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphModule;
|
|
39
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
40
|
const roadmapParserModule = require("./roadmap-parser.cjs");
|
|
41
41
|
const { getMilestonePhaseFilter } = roadmapParserModule;
|
|
@@ -146,13 +146,15 @@ function listArchiveVersionDirs(cwd, wsOverride) {
|
|
|
146
146
|
}
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
149
|
-
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
149
|
+
function searchPhaseInDir(baseDir, relBase, normalized, convention) {
|
|
150
150
|
try {
|
|
151
151
|
const dirs = readSubdirectories(baseDir, true);
|
|
152
152
|
// #2528: canonical two-pass selection (exact token match, then the
|
|
153
153
|
// bare-integer leading-digit-run fallback) shared with the find-phase and
|
|
154
154
|
// phase-plan-index scans — see phase-id.cts::matchPhaseDirs.
|
|
155
|
-
|
|
155
|
+
// #4801: the convention is threaded (optional) so bracket-convention
|
|
156
|
+
// consumers get exact token matching here too.
|
|
157
|
+
const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized, convention);
|
|
156
158
|
if (matches.length === 0)
|
|
157
159
|
return null;
|
|
158
160
|
// #2237: fail loud when multiple directories match the same bare phase
|
|
@@ -176,6 +178,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
176
178
|
halted_plans: [],
|
|
177
179
|
blocked_by: {},
|
|
178
180
|
runnable_plans: [],
|
|
181
|
+
ready_plans: [],
|
|
179
182
|
};
|
|
180
183
|
}
|
|
181
184
|
const match = matches[0];
|
|
@@ -208,12 +211,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
208
211
|
const planIds = plans.map(p => p.replace('-PLAN.md', '').replace('PLAN.md', ''));
|
|
209
212
|
const planIdByLower = new Map(planIds.map(id => [id.toLowerCase(), id]));
|
|
210
213
|
const canonicalToPlanId = new Map(plans.map((p, i) => [extractCanonicalPlanId(p).toLowerCase(), planIds[i]]));
|
|
214
|
+
// #4628: raw depends_on per plan, so readiness can fail closed on a
|
|
215
|
+
// DROPPED edge (a dep token that resolves to nothing carries no evidence)
|
|
216
|
+
// instead of the resolution silently shrinking the dependency list.
|
|
217
|
+
const rawDeps = plans.map((p) => parsePlanDependsOn(phaseDir, p));
|
|
218
|
+
// #4628: completion evidence excludes status:blocked summaries (#3345) —
|
|
219
|
+
// a failure record is not completion, matching cmdPhasePlanIndex's count.
|
|
220
|
+
const completionEvidence = buildSummaryFileIndex(summaries.filter((f) => !isSummaryFileBlocked(node_path_1.default.join(phaseDir, f))), extractCanonicalPlanId);
|
|
211
221
|
const haltNodes = plans.map((p, i) => {
|
|
212
222
|
const planId = planIds[i];
|
|
213
223
|
const canonical = extractCanonicalPlanId(p);
|
|
214
224
|
const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(canonical);
|
|
215
225
|
const halted = summaryFile !== undefined && isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile));
|
|
216
|
-
const resolvedDependsOn =
|
|
226
|
+
const resolvedDependsOn = rawDeps[i]
|
|
217
227
|
.map((dep) => {
|
|
218
228
|
const lower = dep.toLowerCase();
|
|
219
229
|
return planIdByLower.get(lower) ?? canonicalToPlanId.get(lower) ?? null;
|
|
@@ -226,6 +236,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
226
236
|
const incompletePlanSet = new Set(incompletePlans);
|
|
227
237
|
const blockedByFiles = {};
|
|
228
238
|
const runnablePlans = [];
|
|
239
|
+
const readyPlans = [];
|
|
229
240
|
for (let i = 0; i < plans.length; i++) {
|
|
230
241
|
const p = plans[i];
|
|
231
242
|
if (!incompletePlanSet.has(p))
|
|
@@ -233,10 +244,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
233
244
|
const causes = blockedBy.get(planIds[i]) ?? [];
|
|
234
245
|
if (causes.length > 0) {
|
|
235
246
|
blockedByFiles[p] = causes;
|
|
247
|
+
continue;
|
|
236
248
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
249
|
+
runnablePlans.push(p);
|
|
250
|
+
// #4628: DAG-ready on top of runnable — every dependency must have
|
|
251
|
+
// completion evidence (a matching, non-blocked SUMMARY) and no dropped
|
|
252
|
+
// edge: both readers of this contract fail closed identically.
|
|
253
|
+
const depsComplete = rawDeps[i].length === haltNodes[i].resolvedDependsOn.length &&
|
|
254
|
+
haltNodes[i].resolvedDependsOn.every((dep) => completionEvidence.has(dep));
|
|
255
|
+
if (depsComplete)
|
|
256
|
+
readyPlans.push(p);
|
|
240
257
|
}
|
|
241
258
|
return {
|
|
242
259
|
found: true,
|
|
@@ -259,19 +276,21 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
259
276
|
halted_plans: haltedPlans,
|
|
260
277
|
blocked_by: blockedByFiles,
|
|
261
278
|
runnable_plans: runnablePlans,
|
|
279
|
+
ready_plans: readyPlans,
|
|
262
280
|
};
|
|
263
281
|
}
|
|
264
282
|
catch {
|
|
265
283
|
return null;
|
|
266
284
|
}
|
|
267
285
|
}
|
|
268
|
-
function findPhaseInternal(cwd, phase) {
|
|
286
|
+
function findPhaseInternal(cwd, phase, convention) {
|
|
269
287
|
if (!phase)
|
|
270
288
|
return null;
|
|
271
289
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
272
290
|
const normalized = normalizePhaseName(phase);
|
|
273
291
|
const relPhasesDir = toPosixPath(node_path_1.default.relative(cwd, phasesDir));
|
|
274
|
-
|
|
292
|
+
// #4801: convention threaded through to the matcher (see searchPhaseInDir).
|
|
293
|
+
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized, convention);
|
|
275
294
|
if (current)
|
|
276
295
|
return current;
|
|
277
296
|
// #2855: scope the archived-milestone fallback to the SAME workstream as the
|
|
@@ -285,7 +304,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
285
304
|
// getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
|
|
286
305
|
for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
|
|
287
306
|
const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
|
|
288
|
-
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
307
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized, convention);
|
|
289
308
|
if (result) {
|
|
290
309
|
result.archived = version;
|
|
291
310
|
return result;
|
|
@@ -65,6 +65,7 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
65
65
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
|
|
66
66
|
const stateMod = require("./state.cjs");
|
|
67
67
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
68
|
+
const planning_document_cjs_1 = require("./planning-document.cjs");
|
|
68
69
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
69
70
|
const clock_cjs_1 = require("./clock.cjs");
|
|
70
71
|
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
@@ -86,7 +87,7 @@ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
|
86
87
|
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
|
|
87
88
|
// #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
|
|
88
89
|
// guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
|
|
89
|
-
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
|
|
90
|
+
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolveEnvWorkstream, resolvePhaseIdConvention, } = planningWorkspace;
|
|
90
91
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
|
|
91
92
|
const milestoneLockMod = require("./milestone-lock.cjs");
|
|
92
93
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -348,6 +349,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
|
|
|
348
349
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
349
350
|
const fullContent = stripShippedMilestones(rawContent);
|
|
350
351
|
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
352
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
351
353
|
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
|
|
352
354
|
for (const content of [milestoneContent, fullContent]) {
|
|
353
355
|
const headerMatch = content.match(phaseHeader);
|
|
@@ -592,8 +594,16 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
592
594
|
* none left in this file) degrades to the pre-#3897 two-tier behavior rather
|
|
593
595
|
* than throwing on a missing argument.
|
|
594
596
|
*/
|
|
597
|
+
/**
|
|
598
|
+
* The dependency resolver's one comparison normalization. Callers that must
|
|
599
|
+
* predict whether a token names a plan reuse this seam instead of copying its
|
|
600
|
+
* case-folding rule.
|
|
601
|
+
*/
|
|
602
|
+
function normalizeDependencyToken(token) {
|
|
603
|
+
return String(token).toLowerCase();
|
|
604
|
+
}
|
|
595
605
|
function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
|
|
596
|
-
const lower = dep
|
|
606
|
+
const lower = normalizeDependencyToken(dep);
|
|
597
607
|
if (planMap.has(lower))
|
|
598
608
|
return planMap.get(lower).id;
|
|
599
609
|
if (canonicalToId.has(lower))
|
|
@@ -634,7 +644,7 @@ function buildShortFormToId(rawPlans) {
|
|
|
634
644
|
const canonical = extractCanonicalPlanId(p.id);
|
|
635
645
|
const lastDash = canonical.lastIndexOf('-');
|
|
636
646
|
if (lastDash > 0 && lastDash < canonical.length - 1) {
|
|
637
|
-
const shortForm = canonical.slice(lastDash + 1)
|
|
647
|
+
const shortForm = normalizeDependencyToken(canonical.slice(lastDash + 1));
|
|
638
648
|
if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
|
|
639
649
|
shortFormToId.set(shortForm, p.id);
|
|
640
650
|
}
|
|
@@ -744,12 +754,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
744
754
|
phase: normalized,
|
|
745
755
|
error: `Phase ${normalized} is ambiguous: ${ambiguousMatches.length} directories match (${ambiguousMatches.map((m) => `"${m}"`).join(', ')}).`,
|
|
746
756
|
ambiguous_matches: ambiguousMatches,
|
|
747
|
-
plans: [], waves: {}, incomplete: [], has_checkpoints: false,
|
|
757
|
+
plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false,
|
|
748
758
|
}, raw);
|
|
749
759
|
return;
|
|
750
760
|
}
|
|
751
761
|
if (!phaseDir) {
|
|
752
|
-
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
|
|
762
|
+
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false }, raw);
|
|
753
763
|
return;
|
|
754
764
|
}
|
|
755
765
|
void phaseDirName; // used only to set phaseDir above
|
|
@@ -844,7 +854,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
844
854
|
// ── Pass 2: topological level assignment via depends_on DAG ──────────────
|
|
845
855
|
const seenLower = new Map();
|
|
846
856
|
for (const p of rawPlans) {
|
|
847
|
-
const lower = p.id
|
|
857
|
+
const lower = normalizeDependencyToken(p.id);
|
|
848
858
|
const existing = seenLower.get(lower);
|
|
849
859
|
if (existing !== undefined) {
|
|
850
860
|
error(`depends_on index collision in phase ${normalized}: plan IDs '${existing}' and '${p.id}' are identical when case-folded. Rename one file to avoid ambiguous dependency resolution.`);
|
|
@@ -852,8 +862,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
852
862
|
}
|
|
853
863
|
seenLower.set(lower, p.id);
|
|
854
864
|
}
|
|
855
|
-
const planMap = new Map(rawPlans.map((p) => [p.id
|
|
856
|
-
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id)
|
|
865
|
+
const planMap = new Map(rawPlans.map((p) => [normalizeDependencyToken(p.id), p]));
|
|
866
|
+
const canonicalToId = new Map(rawPlans.map((p) => [normalizeDependencyToken(extractCanonicalPlanId(p.id)), p.id]));
|
|
857
867
|
// #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
|
|
858
868
|
// Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
|
|
859
869
|
// plan id. In-phase only by construction (T49): the map is built from THIS
|
|
@@ -888,6 +898,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
888
898
|
const waves = {};
|
|
889
899
|
const incomplete = [];
|
|
890
900
|
const runnable = [];
|
|
901
|
+
// #4628: DAG-ready view — see the per-plan emission below. Keyed by id so
|
|
902
|
+
// the per-plan pass can resolve each plan's dependency edges.
|
|
903
|
+
const resolvedDepsByPlan = new Map(haltNodes.map((n) => [n.id, n.resolvedDependsOn]));
|
|
904
|
+
const readyPlans = [];
|
|
891
905
|
let hasCheckpoints = false;
|
|
892
906
|
const warnings = [];
|
|
893
907
|
// #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
|
|
@@ -897,8 +911,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
897
911
|
// plan with no dropped edges and a genuinely wrong `wave:` still warns
|
|
898
912
|
// (N3, D6, T25).
|
|
899
913
|
const plansWithUnresolvedTokens = new Set();
|
|
914
|
+
const unresolvedTokensByPlan = new Map();
|
|
900
915
|
for (const { plan, token } of unresolved) {
|
|
901
916
|
plansWithUnresolvedTokens.add(plan);
|
|
917
|
+
const tokens = unresolvedTokensByPlan.get(plan) ?? [];
|
|
918
|
+
tokens.push(formatDiagnosticToken(token));
|
|
919
|
+
unresolvedTokensByPlan.set(plan, tokens);
|
|
902
920
|
warnings.push(`Plan ${plan}: depends_on token ${formatDiagnosticToken(token)} does not resolve to any plan in this phase — edge dropped, wave placement for this plan may be unreliable`);
|
|
903
921
|
}
|
|
904
922
|
for (const rawPlan of rawPlans) {
|
|
@@ -906,14 +924,34 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
906
924
|
hasCheckpoints = true;
|
|
907
925
|
}
|
|
908
926
|
const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
|
|
927
|
+
// #4628: readiness fails closed — every resolved dependency must have
|
|
928
|
+
// completion evidence (has_summary), and a depends_on edge that never
|
|
929
|
+
// resolved (#3427 — a dropped edge) carries no evidence to check. A
|
|
930
|
+
// single evaluation feeds both the ready_plans list and the per-plan
|
|
931
|
+
// `ready` / `unresolved_dependencies` fields below.
|
|
932
|
+
// Case-fold the dep before the planMap lookup: planMap is lowercase-keyed
|
|
933
|
+
// (#2237) while resolveDependencyId returns the raw id — a mixed-case id
|
|
934
|
+
// must not read as 'no evidence' (over-blocking a ready plan).
|
|
935
|
+
const missingEvidence = (resolvedDepsByPlan.get(rawPlan.id) ?? [])
|
|
936
|
+
.filter((dep) => planMap.get(dep.toLowerCase())?.hasSummary !== true);
|
|
937
|
+
if (plansWithUnresolvedTokens.has(rawPlan.id)) {
|
|
938
|
+
missingEvidence.push(...(unresolvedTokensByPlan.get(rawPlan.id) ?? []));
|
|
939
|
+
}
|
|
940
|
+
const isReady = blockedByIds.length === 0 && missingEvidence.length === 0;
|
|
909
941
|
if (!rawPlan.hasSummary) {
|
|
910
942
|
incomplete.push(rawPlan.id);
|
|
911
943
|
// #2830: the runnable-only view — incomplete AND not transitively
|
|
912
944
|
// blocked by a halted upstream plan. Additive alongside `incomplete`,
|
|
913
945
|
// which keeps its existing "no SUMMARY yet" meaning unchanged.
|
|
946
|
+
// #4628: runnable says NOTHING about completion evidence — a runnable
|
|
947
|
+
// plan whose dependencies lack a SUMMARY is not DAG-ready. Consumers
|
|
948
|
+
// dispatch from `ready_plans`.
|
|
914
949
|
if (blockedByIds.length === 0) {
|
|
915
950
|
runnable.push(rawPlan.id);
|
|
916
951
|
}
|
|
952
|
+
if (isReady) {
|
|
953
|
+
readyPlans.push(rawPlan.id);
|
|
954
|
+
}
|
|
917
955
|
}
|
|
918
956
|
const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
|
|
919
957
|
const effectiveWave = computedWave;
|
|
@@ -955,6 +993,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
955
993
|
halted: rawPlan.halted,
|
|
956
994
|
blocked_by: blockedByIds,
|
|
957
995
|
};
|
|
996
|
+
if (!rawPlan.hasSummary) {
|
|
997
|
+
// #4628: readiness is a property of INCOMPLETE plans (a summarized plan
|
|
998
|
+
// is filtered by has_summary before readiness is ever consulted). The
|
|
999
|
+
// unresolved_dependencies list names exactly which predecessors lack
|
|
1000
|
+
// completion evidence, for the named-skip report.
|
|
1001
|
+
plan['ready'] = isReady;
|
|
1002
|
+
if (missingEvidence.length > 0)
|
|
1003
|
+
plan['unresolved_dependencies'] = missingEvidence;
|
|
1004
|
+
}
|
|
958
1005
|
plans.push(plan);
|
|
959
1006
|
const waveKey = String(effectiveWave);
|
|
960
1007
|
if (!waves[waveKey]) {
|
|
@@ -968,6 +1015,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
968
1015
|
waves,
|
|
969
1016
|
incomplete,
|
|
970
1017
|
runnable,
|
|
1018
|
+
ready_plans: readyPlans,
|
|
971
1019
|
has_checkpoints: hasCheckpoints,
|
|
972
1020
|
};
|
|
973
1021
|
if (planNamingWarning)
|
|
@@ -1115,6 +1163,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1115
1163
|
const siblingPlanningDir = (wt) => planningDir(wt, ws);
|
|
1116
1164
|
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
1117
1165
|
// Same header shape the allocators scan locally (#1729 tag tolerance).
|
|
1166
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1118
1167
|
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1119
1168
|
for (const line of porcelain.split('\n')) {
|
|
1120
1169
|
if (!line.startsWith('worktree '))
|
|
@@ -1180,6 +1229,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1180
1229
|
// (section header, roadmap bullet, or on-disk directory) is counted:
|
|
1181
1230
|
// 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
|
|
1182
1231
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1232
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1183
1233
|
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1184
1234
|
// 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
|
|
1185
1235
|
// The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
|
|
@@ -1289,6 +1339,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1289
1339
|
// bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
|
|
1290
1340
|
// `Phase N` row was invisible to batch allocation (#3849 secondary).
|
|
1291
1341
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1342
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1292
1343
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1293
1344
|
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
1294
1345
|
let m;
|
|
@@ -1416,7 +1467,9 @@ function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
|
|
|
1416
1467
|
decimalSet.add(parseInt(dm[1], 10));
|
|
1417
1468
|
}
|
|
1418
1469
|
}
|
|
1419
|
-
const rmPhasePattern = new RegExp(
|
|
1470
|
+
const rmPhasePattern = new RegExp(
|
|
1471
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1472
|
+
`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
1420
1473
|
let rmMatch;
|
|
1421
1474
|
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
1422
1475
|
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
@@ -1443,9 +1496,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
1443
1496
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1444
1497
|
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
1445
1498
|
const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
|
|
1499
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1446
1500
|
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
1447
1501
|
const headingMatch = targetPattern.test(content);
|
|
1448
1502
|
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
1503
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1449
1504
|
const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
|
|
1450
1505
|
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
1451
1506
|
const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
|
|
@@ -1522,7 +1577,9 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested'
|
|
|
1522
1577
|
}
|
|
1523
1578
|
else {
|
|
1524
1579
|
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
|
|
1525
|
-
const headerPattern = new RegExp(
|
|
1580
|
+
const headerPattern = new RegExp(
|
|
1581
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1582
|
+
`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
|
|
1526
1583
|
const headerMatch = rawContent.match(headerPattern);
|
|
1527
1584
|
if (!headerMatch) {
|
|
1528
1585
|
error(`Could not find Phase ${afterPhase} header`);
|
|
@@ -1844,7 +1901,9 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
1844
1901
|
if (!isDecimal) {
|
|
1845
1902
|
// #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
|
|
1846
1903
|
// is re-emitted verbatim — a tagged later phase still gets renumbered.
|
|
1847
|
-
content = content.replace(
|
|
1904
|
+
content = content.replace(
|
|
1905
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1906
|
+
/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
|
|
1848
1907
|
content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
1849
1908
|
// ORDINAL-RENUMBER — CELL EDIT (not row-deletion) — migrated onto
|
|
1850
1909
|
// updateTableCell (ADR-2143 §7, sibling of the deleteTableRow scoping
|
|
@@ -2177,6 +2236,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
2177
2236
|
if (!roadmapContent || !phaseNum)
|
|
2178
2237
|
return null;
|
|
2179
2238
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
2239
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
2180
2240
|
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
2181
2241
|
if (!heading)
|
|
2182
2242
|
return null;
|
|
@@ -2838,7 +2898,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2838
2898
|
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
|
2839
2899
|
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
|
2840
2900
|
// which would misreport a present-but-bad marker as no marker at all.
|
|
2841
|
-
const resolvedWorkstream =
|
|
2901
|
+
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
|
2842
2902
|
if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
|
|
2843
2903
|
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
|
2844
2904
|
// from the shared .planning/active-workstream marker, so reaching this
|
|
@@ -3130,7 +3190,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3130
3190
|
// whole-slice `.replace()` onto the seam. Applied per single physical
|
|
3131
3191
|
// line by updateBullet, so the pattern no longer needs the `m` flag
|
|
3132
3192
|
// (it never sees more than one line at a time); see
|
|
3133
|
-
//
|
|
3193
|
+
// writePlansField below for the sites that were migrated onto
|
|
3134
3194
|
// withPhaseSection instead.
|
|
3135
3195
|
//
|
|
3136
3196
|
// #2245 review Fix 6: this is behaviour-preserving for GSD-GENERATED
|
|
@@ -3183,12 +3243,105 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3183
3243
|
};
|
|
3184
3244
|
// ADR-2143 §4: the plan-count write is now routed through
|
|
3185
3245
|
// withPhaseSection (see mutateMilestonePhase below), which hands this
|
|
3186
|
-
//
|
|
3187
|
-
//
|
|
3188
|
-
//
|
|
3189
|
-
//
|
|
3190
|
-
//
|
|
3191
|
-
|
|
3246
|
+
// seam call ONLY phase N's own detail-section body — the section
|
|
3247
|
+
// boundary itself confines the write (the #2067/#2200 boundary-
|
|
3248
|
+
// crossing class is structurally impossible for this site).
|
|
3249
|
+
//
|
|
3250
|
+
// #4906 Phase 2 (#4917/ADR-4910): migrated off the one-capture-group
|
|
3251
|
+
// regex that replaced to end of line, dropping any hand-written
|
|
3252
|
+
// trailing prose after the count (#4852) — onto the PlanningDoc
|
|
3253
|
+
// `boldField` write seam, whose `valueSpan`/`trailingSpan` split
|
|
3254
|
+
// never touches the trailing annotation.
|
|
3255
|
+
const writePlansField = (body) => {
|
|
3256
|
+
const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(body, 'ROADMAP.md');
|
|
3257
|
+
if (!parsed.ok) {
|
|
3258
|
+
preservationWarnings.push({ field: 'Plans', reason: parsed.reason });
|
|
3259
|
+
return body;
|
|
3260
|
+
}
|
|
3261
|
+
const fieldId = (0, planning_document_cjs_1.findField)(parsed.value, 'Plans');
|
|
3262
|
+
if (!fieldId) {
|
|
3263
|
+
// #4906 regression (#1163 parity, caught by gsd-test against
|
|
3264
|
+
// roadmap.cts's sibling site): a hand-edited or pre-template
|
|
3265
|
+
// ROADMAP.md may carry a PLAIN (non-bold) `Plans:` line rather
|
|
3266
|
+
// than the canonical `**Plans**:`/`**Plans:**` bold field.
|
|
3267
|
+
// BOLD_FIELD_RE stays bold-only (widening it would register
|
|
3268
|
+
// ordinary prose as a spurious field seam-wide) — this fallback
|
|
3269
|
+
// mirrors roadmap.cts's identical one, kept in parity per
|
|
3270
|
+
// Decision 2 rather than letting the two sites diverge on which
|
|
3271
|
+
// legacy shapes they tolerate.
|
|
3272
|
+
const plainMatch = body.match(/^([ \t]*)Plans:([ \t]*)([^\r\n]*)$/m);
|
|
3273
|
+
if (!plainMatch) {
|
|
3274
|
+
// No `**Plans:**`/`**Plans**:`/plain `Plans:` line in this
|
|
3275
|
+
// phase's section — nothing to write; not a failure (mirrors
|
|
3276
|
+
// the old regex's silent no-match no-op).
|
|
3277
|
+
return body;
|
|
3278
|
+
}
|
|
3279
|
+
const [whole, indent, spacing, plainValue] = plainMatch;
|
|
3280
|
+
const plainCountPrefixMatch = plainValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
|
|
3281
|
+
const plainIsTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(plainValue.trim());
|
|
3282
|
+
if (!plainCountPrefixMatch && !plainIsTemplatePlaceholder) {
|
|
3283
|
+
// Arm 3: freeform prose, TBD, a bracketed human annotation, or
|
|
3284
|
+
// an empty value — leave the field exactly as it was.
|
|
3285
|
+
return body;
|
|
3286
|
+
}
|
|
3287
|
+
const plainNewCountText = `${summaryCount}/${planCount} plans complete`;
|
|
3288
|
+
const plainSuffix = plainCountPrefixMatch ? plainValue.slice(plainCountPrefixMatch[0].length) : '';
|
|
3289
|
+
const newPlainLine = `${indent}Plans:${spacing}${plainNewCountText}${plainSuffix}`;
|
|
3290
|
+
const start = plainMatch.index ?? body.indexOf(whole);
|
|
3291
|
+
return body.slice(0, start) + newPlainLine + body.slice(start + whole.length);
|
|
3292
|
+
}
|
|
3293
|
+
// #4906 regression fix: PREFIX-match the existing value's count
|
|
3294
|
+
// token and re-glue whatever follows it VERBATIM — a glued-on
|
|
3295
|
+
// annotation with no ` — ` separator (e.g. a parenthetical like
|
|
3296
|
+
// `0/1 plans executed (11-16 are gap closure from VERIFICATION)`)
|
|
3297
|
+
// lives entirely inside `value` (`TRAILING_SEPARATOR_RE` in
|
|
3298
|
+
// planning-document.cts only splits on ` — `, unchanged/correct),
|
|
3299
|
+
// so overwriting `value` outright previously destroyed it.
|
|
3300
|
+
//
|
|
3301
|
+
// #4906 review finding (isolated adversarial pass): the prior
|
|
3302
|
+
// version of this migration preserved this site's OLD
|
|
3303
|
+
// unconditional-overwrite behavior for the no-count-prefix case,
|
|
3304
|
+
// which clobbers arm 3 (freeform prose / TBD / a bracketed human
|
|
3305
|
+
// annotation like `[Deferred pending re-scope]`) — a real
|
|
3306
|
+
// regression against the design doc's own Behavior table row 4,
|
|
3307
|
+
// not an accepted trade-off. Fixed here by adopting the SAME
|
|
3308
|
+
// template-placeholder / arm-3-untouched classification
|
|
3309
|
+
// roadmap.cts's sibling site already uses (isTemplatePlaceholder +
|
|
3310
|
+
// "no count prefix and not a placeholder => leave untouched"),
|
|
3311
|
+
// rather than letting the two migrated sites diverge on this.
|
|
3312
|
+
const newCountText = `${summaryCount}/${planCount} plans complete`;
|
|
3313
|
+
const current = (0, planning_document_cjs_1.readNode)(parsed.value, fieldId);
|
|
3314
|
+
if (!current.ok) {
|
|
3315
|
+
return body;
|
|
3316
|
+
}
|
|
3317
|
+
const currentValue = current.value;
|
|
3318
|
+
const countPrefixMatch = currentValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
|
|
3319
|
+
const isTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(currentValue.trim());
|
|
3320
|
+
if (!countPrefixMatch && !isTemplatePlaceholder) {
|
|
3321
|
+
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
3322
|
+
// empty value — leave the field exactly as it was.
|
|
3323
|
+
return body;
|
|
3324
|
+
}
|
|
3325
|
+
const newValueToWrite = countPrefixMatch
|
|
3326
|
+
? newCountText + currentValue.slice(countPrefixMatch[0].length)
|
|
3327
|
+
: newCountText;
|
|
3328
|
+
const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
|
|
3329
|
+
if (!staged.ok) {
|
|
3330
|
+
preservationWarnings.push({ field: 'Plans', reason: staged.reason });
|
|
3331
|
+
return body;
|
|
3332
|
+
}
|
|
3333
|
+
const out = (0, planning_document_cjs_1.serialize)(staged.value);
|
|
3334
|
+
if (!out.ok) {
|
|
3335
|
+
// `hasUnreadableNodes` refusal (ADR-4910 amendment) — a ragged
|
|
3336
|
+
// SIBLING node elsewhere in this same section refuses the whole
|
|
3337
|
+
// splice. Never throw / crash the phase-complete transaction over
|
|
3338
|
+
// a node unrelated to this write; surface it and leave `body`
|
|
3339
|
+
// unchanged, same as any other preservation warning.
|
|
3340
|
+
preservationWarnings.push({ field: 'Plans', reason: out.reason });
|
|
3341
|
+
return body;
|
|
3342
|
+
}
|
|
3343
|
+
return out.value;
|
|
3344
|
+
};
|
|
3192
3345
|
const phaseInfoSummaries = phaseInfo['summaries'];
|
|
3193
3346
|
// #2200: apply the phase-checkbox flip, the plan-count write, and the
|
|
3194
3347
|
// per-plan checkbox flips ONLY within the current milestone's region(s)
|
|
@@ -3225,7 +3378,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3225
3378
|
// section's body, so neither regex can escape into a sibling
|
|
3226
3379
|
// phase's section, a shipped milestone, or a Backlog entry.
|
|
3227
3380
|
s = withPhaseSection(s, phaseNum, (body) => {
|
|
3228
|
-
let b = body
|
|
3381
|
+
let b = writePlansField(body);
|
|
3229
3382
|
for (const summaryFile of phaseInfoSummaries) {
|
|
3230
3383
|
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
3231
3384
|
if (!planId)
|
|
@@ -3270,14 +3423,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3270
3423
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
3271
3424
|
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
3272
3425
|
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
3273
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
|
|
3426
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
|
|
3427
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
3428
|
+
`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
3274
3429
|
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
3275
|
-
|
|
3430
|
+
// #4731: multiline-aware — hard-wrapped Requirements read past the
|
|
3431
|
+
// line break before the ID scan. The shared extractor also stops at
|
|
3432
|
+
// headings and table rows, so a Requirements field followed by the
|
|
3433
|
+
// Traceability table cannot bleed other phases' REQ-IDs into the
|
|
3434
|
+
// citation scan (isolated-review MEDIUM on the inline lookahead,
|
|
3435
|
+
// whose lazy capture swallowed everything to section end).
|
|
3436
|
+
const reqLine = sectionText
|
|
3437
|
+
? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
|
|
3438
|
+
: null;
|
|
3276
3439
|
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
3277
3440
|
let reqContent = originalReqContent;
|
|
3278
3441
|
// #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
|
|
3279
3442
|
// line for this phase actually cites — is hoisted out of the
|
|
3280
|
-
// `if (
|
|
3443
|
+
// `if (reqLine)` block (previously scoped only inside it) so the
|
|
3281
3444
|
// ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
|
|
3282
3445
|
// literal placeholder `phase.add`/`-batch`/`-insert` seed
|
|
3283
3446
|
// (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
|
|
@@ -3289,16 +3452,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3289
3452
|
// otherwise) — the `if (reqUpdate.ok)` below previously had no
|
|
3290
3453
|
// `else`, discarding this fact silently instead of surfacing it.
|
|
3291
3454
|
const traceabilityWriteMisses = [];
|
|
3292
|
-
if (
|
|
3455
|
+
if (reqLine) {
|
|
3293
3456
|
// #2334 HIGH 3 + #3697: selection and under-selection detection both
|
|
3294
3457
|
// live in `analyzeRequirementsLine` (module scope, above), extracted in
|
|
3295
3458
|
// round 3 so the parser is directly testable — a closure in here is
|
|
3296
3459
|
// reachable only by spawning the CLI, which no fast-check property test
|
|
3297
3460
|
// can do. `citedReqIds` is byte-identical to the expression that stood
|
|
3298
3461
|
// here; nothing about what phase-complete MARKS has changed.
|
|
3299
|
-
const reqLineAnalysis = analyzeRequirementsLine(
|
|
3462
|
+
const reqLineAnalysis = analyzeRequirementsLine(reqLine);
|
|
3300
3463
|
citedReqIds = reqLineAnalysis.citedReqIds;
|
|
3301
|
-
const reqLineWarning = formatRequirementsLineWarning(phaseNum,
|
|
3464
|
+
const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqLine, reqLineAnalysis);
|
|
3302
3465
|
if (reqLineWarning) {
|
|
3303
3466
|
warnings.push(reqLineWarning.message);
|
|
3304
3467
|
// Carried out to the JSON result as its own field — see
|
|
@@ -3584,6 +3747,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3584
3747
|
let diskNextName = null;
|
|
3585
3748
|
let roadmapNextNum = null;
|
|
3586
3749
|
let roadmapNextName = null;
|
|
3750
|
+
// #4699: a phase whose roadmap checkbox is `[x]` is already complete and
|
|
3751
|
+
// must never be selected as next_phase — out-of-order completion (a
|
|
3752
|
+
// reopened phase finished after later phases shipped) otherwise persists
|
|
3753
|
+
// the already-done phase as STATE.md current_phase. Collected from the
|
|
3754
|
+
// same milestone-scoped text the roadmap scan walks; membership is
|
|
3755
|
+
// comparePhaseNum-based so `02` and `2` dedupe. With no ROADMAP.md (or no
|
|
3756
|
+
// parseable rows) the set is empty and the scans behave exactly as
|
|
3757
|
+
// before.
|
|
3758
|
+
const roadmapCompleteNums = [];
|
|
3759
|
+
if (roadmapContent !== null) {
|
|
3760
|
+
try {
|
|
3761
|
+
const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
|
|
3762
|
+
const completePattern = new RegExp(`-\\s*\\[[xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi');
|
|
3763
|
+
let cm;
|
|
3764
|
+
while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
|
|
3765
|
+
if (isSentinelPhaseId(cm[1]))
|
|
3766
|
+
continue;
|
|
3767
|
+
if (!roadmapCompleteNums.some((n) => comparePhaseNum(cm[1], n) === 0)) {
|
|
3768
|
+
roadmapCompleteNums.push(cm[1]);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
catch {
|
|
3773
|
+
/* best-effort: an unreadable milestone section leaves the complete
|
|
3774
|
+
* set empty — the scans then behave exactly as they did pre-#4699. */
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
const isCompletePhaseNum = (num) => roadmapCompleteNums.some((n) => comparePhaseNum(num, n) === 0);
|
|
3587
3778
|
try {
|
|
3588
3779
|
// #3185 (ADR-3180 Decision 1): "which phase directories belong to
|
|
3589
3780
|
// the CURRENT milestone" — routed through the canonical owner
|
|
@@ -3599,6 +3790,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3599
3790
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
3600
3791
|
if (isSentinelPhaseId(dm[1]))
|
|
3601
3792
|
continue;
|
|
3793
|
+
// #4699: an already-complete phase (roadmap checkbox [x]) is never
|
|
3794
|
+
// a next_phase candidate — out-of-order completion must skip it.
|
|
3795
|
+
if (roadmapContent !== null && isCompletePhaseNum(dm[1]))
|
|
3796
|
+
continue;
|
|
3602
3797
|
// Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
|
|
3603
3798
|
// does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
|
|
3604
3799
|
// correct today — but that makes this scan's correctness depend on an
|
|
@@ -3666,6 +3861,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3666
3861
|
// stage 2's heading scan must not advance into backlog headings either.
|
|
3667
3862
|
if (isSentinelPhaseId(pmNum))
|
|
3668
3863
|
continue;
|
|
3864
|
+
// #4699: skip complete phases — a `[x]` checkbox row and the
|
|
3865
|
+
// `## Phase Details` heading of an already-done phase both name a
|
|
3866
|
+
// phase that must never be next_phase.
|
|
3867
|
+
if (roadmapContent !== null && isCompletePhaseNum(pmNum))
|
|
3868
|
+
continue;
|
|
3669
3869
|
// #3701 review: the numeric MINIMUM above N, not the first row above N in
|
|
3670
3870
|
// DOCUMENT order. This scan walks raw roadmap text, and one global regex
|
|
3671
3871
|
// sweeps both the `## Phases` checklist and the `## Phase Details`
|
|
@@ -4057,4 +4257,5 @@ module.exports = {
|
|
|
4057
4257
|
cmdPhaseListPlans,
|
|
4058
4258
|
computeDependencyLevels,
|
|
4059
4259
|
buildShortFormToId,
|
|
4260
|
+
normalizeDependencyToken,
|
|
4060
4261
|
};
|