@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -884,19 +884,42 @@ function extractPhaseToken(dirName, convention) {
|
|
|
884
884
|
* cannot resolve a genuinely different phase's artifact (`02.1-...` still
|
|
885
885
|
* fails every `01`-rooted candidate).
|
|
886
886
|
*
|
|
887
|
-
* BRACKET CONVENTION (review item 7): a
|
|
888
|
-
* (`P0.3-2-slug`) is string-INDISTINGUISHABLE
|
|
889
|
-
* (`extractPhaseToken` above, gated on
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* (`
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
887
|
+
* BRACKET CONVENTION (review item 7; reworked by #612/#2761): a
|
|
888
|
+
* letter-prefixed-decimal dir (`P0.3-2-slug`) is string-INDISTINGUISHABLE
|
|
889
|
+
* from a bracket-dir token (`extractPhaseToken` above, gated on
|
|
890
|
+
* `convention === 'bracket'`) without an explicit convention signal. This
|
|
891
|
+
* predicate now takes the same optional trailing `convention` its sibling
|
|
892
|
+
* helpers do (ADR-2121 additive shape): when a call site threads
|
|
893
|
+
* `'bracket'` and the dir is a WELL-FORMED bracket dir
|
|
894
|
+
* (`BRACKET_DIR_TOKEN_RE` — the one grammar owner, shared with
|
|
895
|
+
* `extractPhaseToken`'s bracket branch), the dir's real phase token is
|
|
896
|
+
* readable and membership is scoped through the identical candidate
|
|
897
|
+
* comparison its legacy twin gets — so `GSD.02-02-two` reads exactly what
|
|
898
|
+
* `02-two` reads, which is PR-2's core invariant. Convention-less calls,
|
|
899
|
+
* bracket-MALFORMED names (1-digit milestone, letter-suffixed token — the
|
|
900
|
+
* shapes W005 reports), and legacy-shaped dirs inside a bracket repo all
|
|
901
|
+
* resolve to the unchanged body below.
|
|
902
|
+
*
|
|
903
|
+
* A filename carrying its own bracket-qualified stem is compared on the
|
|
904
|
+
* qualified key — see the branch body. An M-NN-stem artifact name
|
|
905
|
+
* (`02-01-VERIFICATION.md` in `GSD.02-01-one`) is excluded under the bracket
|
|
906
|
+
* convention, deliberately: that is exactly what the legacy twin does with
|
|
907
|
+
* the same file, twin-parity is this PR's contract, and migration-window
|
|
908
|
+
* artifact renaming belongs to the migrator slice per ADR-612 — the trade is
|
|
909
|
+
* pinned in tests/adr-612-bracket-phase-counting.test.cjs.
|
|
910
|
+
*
|
|
911
|
+
* For those unthreaded/unparseable cases the include-everything trade
|
|
912
|
+
* stands: rather than guess a reading it cannot know is active and risk
|
|
913
|
+
* excluding the phase's OWN artifact (the exact defect class this rework
|
|
914
|
+
* exists to fix), the ambiguous families widen to include-everything,
|
|
915
|
+
* accepted in trade for never dropping the phase's own report. Note the
|
|
916
|
+
* bracket family splits across BOTH fail-safes, not just one: a short
|
|
917
|
+
* digit-bearing project code (`A1.02-…`) lands in `firstLetterPrefixed`,
|
|
918
|
+
* but a 3+-letter code (`GSD.02-…`) derives ZERO token segments —
|
|
919
|
+
* `PROJECT_CODE_PREFIX_CAPTURE_RE_I` strips `{CODE}-`, not `{CODE}.` — so
|
|
920
|
+
* the zero-token fail-safe fires first. A fixer patching only the
|
|
921
|
+
* `firstLetterPrefixed` branch covers neither; the convention gate above
|
|
922
|
+
* covers both.
|
|
900
923
|
*
|
|
901
924
|
* FAIL-SAFE (#3511, unchanged): when dirName's own leading segment carries no
|
|
902
925
|
* phase-number token at all (`derivePhaseTokenSegments` finds zero segments —
|
|
@@ -933,7 +956,44 @@ function extractPhaseToken(dirName, convention) {
|
|
|
933
956
|
* the original pre-#3357 "alphabetically first of ALL dashed candidates"
|
|
934
957
|
* behavior, exactly as it always did for that directory shape.
|
|
935
958
|
*/
|
|
936
|
-
function isPhaseArtifact(fileName, phaseDirName) {
|
|
959
|
+
function isPhaseArtifact(fileName, phaseDirName, convention) {
|
|
960
|
+
// #612/#2761: the bracket branch — see "BRACKET CONVENTION" in the docblock.
|
|
961
|
+
// Gated on the same explicit signal AND the same grammar
|
|
962
|
+
// (`BRACKET_DIR_TOKEN_RE`) as `extractPhaseToken`'s bracket branch, so the
|
|
963
|
+
// two surfaces can never disagree about which names are bracket dirs. A
|
|
964
|
+
// name that fails the grammar falls through to the unchanged body below.
|
|
965
|
+
if (convention === 'bracket') {
|
|
966
|
+
const bracketDir = phaseDirName.match(BRACKET_DIR_TOKEN_RE);
|
|
967
|
+
if (bracketDir) {
|
|
968
|
+
// The convention must gate the FILENAME reading, not just the directory
|
|
969
|
+
// reading — a milestone-qualified artifact name (the layout the
|
|
970
|
+
// read-tolerance suite models) derives zero legacy token segments, so
|
|
971
|
+
// without this it enters FIX 2's token-less containment and any bracket
|
|
972
|
+
// dir admits it. Compared on the qualified key: the phase's own
|
|
973
|
+
// qualified artifact stays included — the exact key, and the dotted
|
|
974
|
+
// sub-phase continuation (`GSD.02-05.1-…` is `GSD.02-05`'s own file,
|
|
975
|
+
// the same dash-OR-dot rule matchesPhaseTokenCandidates applies; a
|
|
976
|
+
// dash-continuation never reaches this comparison, the phase group's
|
|
977
|
+
// `(?=-|$)` boundary already stopped the key before it) — while a
|
|
978
|
+
// WELL-FORMED qualified stem naming a different phase or milestone is
|
|
979
|
+
// excluded. A qualified-SHAPED but grammar-malformed stem (letter
|
|
980
|
+
// suffix, 1-digit milestone, 2-level dot — the W005 shapes) yields a
|
|
981
|
+
// null fileKey and keeps the module's documented include-everything
|
|
982
|
+
// fail-safe via FIX 2 below, same as every other undecidable name.
|
|
983
|
+
const fileKey = bracketQualifiedKey(fileName, convention);
|
|
984
|
+
if (fileKey !== null) {
|
|
985
|
+
const dirKey = bracketQualifiedKey(phaseDirName, convention);
|
|
986
|
+
if (dirKey !== null)
|
|
987
|
+
return fileKey === dirKey || fileKey.startsWith(`${dirKey}.`);
|
|
988
|
+
}
|
|
989
|
+
// Reached when the filename carries no well-formed qualified stem (the
|
|
990
|
+
// common case — every unqualified name), or when the DIR's own key is
|
|
991
|
+
// null despite matching the dir grammar (an unsafe-integer milestone or
|
|
992
|
+
// phase — bracketQualifiedKey refuses both rather than collide).
|
|
993
|
+
// Either way the candidate path decides, deliberately.
|
|
994
|
+
return matchesPhaseTokenCandidates(fileName, [bracketDir[1]]);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
937
997
|
const { tokenSegments, firstLetterPrefixed } = derivePhaseTokenSegments(phaseDirName);
|
|
938
998
|
if (tokenSegments.length === 0)
|
|
939
999
|
return true;
|
|
@@ -942,6 +1002,22 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
942
1002
|
const strippedToken = strippedDir !== phaseDirName ? extractPhaseToken(strippedDir) : literalToken;
|
|
943
1003
|
const leadingRunMatch = strippedDir.match(LEADING_DIGIT_RUN_RE);
|
|
944
1004
|
const rawCandidates = [literalToken, strippedToken, leadingRunMatch?.[1]].filter((t) => Boolean(t));
|
|
1005
|
+
if (matchesPhaseTokenCandidates(fileName, rawCandidates))
|
|
1006
|
+
return true;
|
|
1007
|
+
// Bracket-convention ambiguity fail-safe — see docblock above.
|
|
1008
|
+
if (firstLetterPrefixed)
|
|
1009
|
+
return true;
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Candidate-comparison core shared by `isPhaseArtifact`'s two entry paths —
|
|
1014
|
+
* the legacy segment-derived readings and the #612 bracket-dir token. One
|
|
1015
|
+
* comparison rule, not two that agree today and drift tomorrow: the padded/
|
|
1016
|
+
* de-padded expansion, the separator class, and FIX 2 apply identically
|
|
1017
|
+
* whichever path produced the candidates, which is what makes a bracket dir
|
|
1018
|
+
* read exactly what its legacy twin reads.
|
|
1019
|
+
*/
|
|
1020
|
+
function matchesPhaseTokenCandidates(fileName, rawCandidates) {
|
|
945
1021
|
// Each reading is compared in BOTH its padded and de-padded form: files are
|
|
946
1022
|
// written padded by `normalizePhaseName` (`cmdScaffold`) while directories
|
|
947
1023
|
// are often not (`1-unpadded`), and legacy trees carry the reverse pairing.
|
|
@@ -981,12 +1057,7 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
981
1057
|
}
|
|
982
1058
|
// FIX 2: token-less filename (bare "VERIFICATION.md"/"UAT.md") — containment
|
|
983
1059
|
// in this phase's own directory listing is sufficient.
|
|
984
|
-
|
|
985
|
-
return true;
|
|
986
|
-
// Bracket-convention ambiguity fail-safe — see docblock above.
|
|
987
|
-
if (firstLetterPrefixed)
|
|
988
|
-
return true;
|
|
989
|
-
return false;
|
|
1060
|
+
return derivePhaseTokenSegments(fileName).tokenSegments.length === 0;
|
|
990
1061
|
}
|
|
991
1062
|
/**
|
|
992
1063
|
* #3511: scope `fileNames` to the subset that passes
|
|
@@ -1035,9 +1106,19 @@ function isPhaseArtifact(fileName, phaseDirName) {
|
|
|
1035
1106
|
* re-derived per site (CLAUDE.md's Generative Fix Divergence class).
|
|
1036
1107
|
* `isPhaseArtifact` stays exported for single-item membership questions and
|
|
1037
1108
|
* its own unit tests.
|
|
1109
|
+
*
|
|
1110
|
+
* #612/#2761: both helpers take the same optional trailing `convention` their
|
|
1111
|
+
* sibling primitives (`extractPhaseToken`, `phaseTokenMatches`) do — the
|
|
1112
|
+
* ADR-2121 additive shape, so every existing two-argument call resolves to
|
|
1113
|
+
* the unchanged legacy body. A call site that already holds the resolved
|
|
1114
|
+
* convention threads it and bracket dirs scope exactly like their legacy
|
|
1115
|
+
* twins; convention-less call sites keep the documented include-everything
|
|
1116
|
+
* fail-safes and are the follow-up-slice work (they cannot scope a bracket
|
|
1117
|
+
* dir until they can resolve the convention, and guessing is the one thing
|
|
1118
|
+
* this predicate must never do).
|
|
1038
1119
|
*/
|
|
1039
|
-
function scopeToPhase(fileNames, phaseDirName) {
|
|
1040
|
-
return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName));
|
|
1120
|
+
function scopeToPhase(fileNames, phaseDirName, convention) {
|
|
1121
|
+
return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName, convention));
|
|
1041
1122
|
}
|
|
1042
1123
|
/**
|
|
1043
1124
|
* Canonical comparable key for a milestone-qualified bracket id or dir name.
|
|
@@ -1386,6 +1467,63 @@ function isForeignPrefixedPhaseQuery(phase, projectCode) {
|
|
|
1386
1467
|
* form so a canonical heading (`### Phase 117:`) is preferred over a drifted
|
|
1387
1468
|
* prefixed one (`### Phase MANIFOLD-117:`) when both exist in one ROADMAP.
|
|
1388
1469
|
*/
|
|
1470
|
+
// Separator characters a branch-name template may plausibly use to join
|
|
1471
|
+
// `{slug}` to its neighbours — the four this repo's shipped templates and
|
|
1472
|
+
// docs/CONFIGURATION.md's custom-template examples use (`/`, `-`, `_`, `.`).
|
|
1473
|
+
const BRANCH_TEMPLATE_SEP_RE = /[-_./]/;
|
|
1474
|
+
const BRANCH_TEMPLATE_SEP_RUN_RE = /([-_./])\1+/g;
|
|
1475
|
+
const BRANCH_TEMPLATE_EDGE_SEP_RE = /^[-_./]+|[-_./]+$/g;
|
|
1476
|
+
/**
|
|
1477
|
+
* #4126: the ONE branch-name-template renderer for the `phase` branching
|
|
1478
|
+
* strategy, shared by `cmdCommit` (src/commands.cts) and
|
|
1479
|
+
* `cmdInitExecutePhase` (src/init.cts) so the two call sites cannot diverge
|
|
1480
|
+
* on how an undeliverable `phaseSlug` degrades (they previously each
|
|
1481
|
+
* independently substituted the literal word `phase`, producing a
|
|
1482
|
+
* non-identifying branch name like `gsd/phase-08-phase` that contradicts an
|
|
1483
|
+
* honestly-reported `phase_slug: null`).
|
|
1484
|
+
*
|
|
1485
|
+
* `{project}` is deliberately NOT handled here — init.cts substitutes it
|
|
1486
|
+
* separately, before calling this, because it is a config-level field with
|
|
1487
|
+
* its own fallback (`''`), not a phase-derived one.
|
|
1488
|
+
*
|
|
1489
|
+
* When `phaseSlug` is a non-empty string, `{slug}` substitutes normally
|
|
1490
|
+
* (unchanged from prior behavior). When it is empty or not a string (e.g.
|
|
1491
|
+
* `null`, `undefined`, a number), the `{slug}` token is DROPPED — along with
|
|
1492
|
+
* one adjacent separator character — rather than replaced with a placeholder
|
|
1493
|
+
* word: a dropped token keeps the branch name honest about what it does not
|
|
1494
|
+
* know, where a placeholder reads as a real (but wrong) name. The result is
|
|
1495
|
+
* always a syntactically plausible git-ref fragment: no leading, trailing, or
|
|
1496
|
+
* doubled separator, for both the shipped default template
|
|
1497
|
+
* (`gsd/phase-{phase}-{slug}`) and a differently-shaped custom one (this
|
|
1498
|
+
* field is user-configurable per docs/CONFIGURATION.md).
|
|
1499
|
+
*/
|
|
1500
|
+
function renderPhaseBranchName(template, phaseNumber, phaseSlug) {
|
|
1501
|
+
const withPhase = template.replace('{phase}', normalizePhaseName(phaseNumber));
|
|
1502
|
+
const slug = typeof phaseSlug === 'string' ? phaseSlug : '';
|
|
1503
|
+
if (slug)
|
|
1504
|
+
return withPhase.replace('{slug}', slug);
|
|
1505
|
+
const at = withPhase.indexOf('{slug}');
|
|
1506
|
+
if (at === -1)
|
|
1507
|
+
return withPhase;
|
|
1508
|
+
let before = withPhase.slice(0, at);
|
|
1509
|
+
let after = withPhase.slice(at + '{slug}'.length);
|
|
1510
|
+
// Drop exactly ONE adjacent separator — preferring the one immediately
|
|
1511
|
+
// before the token — so `a-{slug}` and `{slug}-a` both degrade to `a`
|
|
1512
|
+
// rather than leaving a dangling `a-` / `-a`.
|
|
1513
|
+
if (before && BRANCH_TEMPLATE_SEP_RE.test(before[before.length - 1])) {
|
|
1514
|
+
before = before.slice(0, -1);
|
|
1515
|
+
}
|
|
1516
|
+
else if (after && BRANCH_TEMPLATE_SEP_RE.test(after[0])) {
|
|
1517
|
+
after = after.slice(1);
|
|
1518
|
+
}
|
|
1519
|
+
// Collapse any doubled separator the drop can leave behind (e.g. a
|
|
1520
|
+
// `feature//{slug}` template dropping to `feature/`→`feature`), then trim
|
|
1521
|
+
// a resulting leading/trailing separator — valid for the shipped default
|
|
1522
|
+
// template AND any user-configured shape, not just the one this repo ships.
|
|
1523
|
+
const joined = before + after;
|
|
1524
|
+
const collapsed = joined.replace(BRANCH_TEMPLATE_SEP_RUN_RE, '$1').replace(BRANCH_TEMPLATE_EDGE_SEP_RE, '');
|
|
1525
|
+
return collapsed || null;
|
|
1526
|
+
}
|
|
1389
1527
|
function roadmapPhaseLookupSources(phaseNum) {
|
|
1390
1528
|
const sources = [];
|
|
1391
1529
|
const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
|
|
@@ -1445,4 +1583,5 @@ module.exports = {
|
|
|
1445
1583
|
stripConfiguredProjectCodePrefix,
|
|
1446
1584
|
isForeignPrefixedPhaseQuery,
|
|
1447
1585
|
roadmapPhaseLookupSources,
|
|
1586
|
+
renderPhaseBranchName,
|
|
1448
1587
|
};
|
|
@@ -25,6 +25,8 @@ exports.locateProgressTable = locateProgressTable;
|
|
|
25
25
|
exports.deriveProgressFromRoadmap = deriveProgressFromRoadmap;
|
|
26
26
|
exports.clampPercentFromFraction = clampPercentFromFraction;
|
|
27
27
|
exports.clampPercent = clampPercent;
|
|
28
|
+
exports.progressBarFilledCells = progressBarFilledCells;
|
|
29
|
+
exports.renderProgressBar = renderProgressBar;
|
|
28
30
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
29
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
30
32
|
const phaseIdMod = require("./phase-id.cjs");
|
|
@@ -140,3 +142,62 @@ function clampPercent(completed, total) {
|
|
|
140
142
|
return 0;
|
|
141
143
|
return clampPercentFromFraction(completed / total);
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* How many cells of a `width`-cell progress bar are filled for `percent`.
|
|
147
|
+
*
|
|
148
|
+
* #4294: the RENDER half's kernel — the counterpart of `clampPercentFromFraction`
|
|
149
|
+
* one layer down. That function owns `fraction -> integer percent`; this one
|
|
150
|
+
* owns `integer percent -> filled cells`, and is the single place that rounding
|
|
151
|
+
* is expressed. Five inline copies of the rounding carried it before this
|
|
152
|
+
* change — three in `commands.cts`, and one each in `gsd2-import.cts` and
|
|
153
|
+
* `formatProgressMachineSegment` here; the latter two used `/ 10` with the
|
|
154
|
+
* width already substituted (`pct` in `gsd2-import.cts`, `clamped` in the
|
|
155
|
+
* formatter). (#4294 counts SIX call sites because it counts
|
|
156
|
+
* `cmdStateUpdateProgress` and `syncCore` separately; #4231 had already routed
|
|
157
|
+
* both through `formatProgressMachineSegment` — as it does
|
|
158
|
+
* `applyPostSyncPreservation` — so by this branch's base they share one copy.)
|
|
159
|
+
* Every copy saturated: at width 10 that rounds to a full bar from 95 up, at
|
|
160
|
+
* width 20 from 98 up, so a project at 19/20 plans drew the same bar as a
|
|
161
|
+
* shipped one beside a number that said otherwise.
|
|
162
|
+
*
|
|
163
|
+
* Contract:
|
|
164
|
+
* - A FULL bar is reserved for an actual 100. Below 100 the fill is held one
|
|
165
|
+
* cell short of the width. This is the only departure from the old formula:
|
|
166
|
+
* at width 10 exactly 95-99 move (10 -> 9), at width 20 exactly 98-99
|
|
167
|
+
* (20 -> 19); every other percent in 0-100 rounds as before.
|
|
168
|
+
* - `null` / `undefined` / non-finite renders an empty bar, matching the
|
|
169
|
+
* `percent === null ? 0 : ...` guard the `progress` renderers already carried.
|
|
170
|
+
* - Out-of-range input is clamped to 0-100 before rounding, so the count is
|
|
171
|
+
* always within `[0, width]` and a `'░'.repeat(width - filled)` can never
|
|
172
|
+
* be handed a negative count (the old inline form threw `RangeError` at
|
|
173
|
+
* 120%). A non-positive width yields 0.
|
|
174
|
+
*
|
|
175
|
+
* Callers wanting the glyph run call `renderProgressBar`; this is exported so
|
|
176
|
+
* the rounding rule can be pinned directly against the legacy curve.
|
|
177
|
+
*/
|
|
178
|
+
function progressBarFilledCells(percent, width) {
|
|
179
|
+
const cells = Number.isFinite(width) && width > 0 ? Math.floor(width) : 0;
|
|
180
|
+
if (cells === 0)
|
|
181
|
+
return 0;
|
|
182
|
+
if (typeof percent !== 'number' || !Number.isFinite(percent))
|
|
183
|
+
return 0;
|
|
184
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
185
|
+
if (clamped >= 100)
|
|
186
|
+
return cells;
|
|
187
|
+
// Scale by the WIDTH, not by 100 — this is cells-from-percent, not the
|
|
188
|
+
// completion-ratio derivation lint-completion-ratio-drift.cjs guards.
|
|
189
|
+
const rounded = Math.round((clamped / 100) * cells);
|
|
190
|
+
return Math.min(rounded, cells - 1);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Render the glyph run of a `width`-cell progress bar for `percent` —
|
|
194
|
+
* `'█'` for each filled cell, `'░'` for the rest, always exactly `width`
|
|
195
|
+
* glyphs (an empty string for a non-positive width). Brackets, the printed
|
|
196
|
+
* percent and any suffix stay with the caller; the fill rule is
|
|
197
|
+
* `progressBarFilledCells` (#4294).
|
|
198
|
+
*/
|
|
199
|
+
function renderProgressBar(percent, width) {
|
|
200
|
+
const filled = progressBarFilledCells(percent, width);
|
|
201
|
+
const cells = Number.isFinite(width) && width > 0 ? Math.floor(width) : 0;
|
|
202
|
+
return '█'.repeat(filled) + '░'.repeat(cells - filled);
|
|
203
|
+
}
|
|
@@ -45,10 +45,19 @@ const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhas
|
|
|
45
45
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
46
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
47
47
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
48
|
-
const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
48
|
+
const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
50
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
51
|
+
const { SCOPE } = planningScopeMod;
|
|
49
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module
|
|
50
53
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
51
54
|
const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, findMilestoneScopeHeadingLines } = roadmapParserMod;
|
|
55
|
+
// #4129: the single owner of "count the ROADMAP's milestone Complete rows"
|
|
56
|
+
// (pure computation, no I/O — no cycle on this path) for the intent-first
|
|
57
|
+
// progress counters the phase-complete transaction passes downstream.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
|
|
59
|
+
const phaseLifecycleMod = require("./phase-lifecycle.cjs");
|
|
60
|
+
const { deriveProgressFromRoadmap: deriveProgressFromRoadmapForIntent, clampPercent: clampPercentForIntent } = phaseLifecycleMod;
|
|
52
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
53
62
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
54
63
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
@@ -287,25 +296,26 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
287
296
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
288
297
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
289
298
|
baseExists = matchPhaseDirs(dirs, normalized).matches.length > 0;
|
|
290
|
-
const dirPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalized)}\\.(\\d+)`);
|
|
291
|
-
for (const dir of dirs) {
|
|
292
|
-
const match = dir.match(dirPattern);
|
|
293
|
-
if (match)
|
|
294
|
-
decimalSet.add(parseInt(match[1], 10));
|
|
295
|
-
}
|
|
296
299
|
}
|
|
297
300
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
298
301
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
299
302
|
try {
|
|
300
303
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
304
|
-
decimalSet.add(parseInt(pm[1], 10));
|
|
304
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, roadmapContent, normalized)) {
|
|
305
|
+
decimalSet.add(n);
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
catch {
|
|
308
|
-
|
|
309
|
+
// ROADMAP.md read failure is non-fatal — fall back to the directory-only
|
|
310
|
+
// scan (empty rawContent) so on-disk decimal directories are still counted.
|
|
311
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
|
|
312
|
+
decimalSet.add(n);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
|
|
318
|
+
decimalSet.add(n);
|
|
309
319
|
}
|
|
310
320
|
}
|
|
311
321
|
const existingDecimals = Array.from(decimalSet)
|
|
@@ -1061,12 +1071,24 @@ function assertDescriptionPreservesMilestoneScope(cwd, description, command) {
|
|
|
1061
1071
|
* before any directory does; milestone-scoping is wrong here because a number
|
|
1062
1072
|
* used under any milestone on another branch is still taken).
|
|
1063
1073
|
*
|
|
1074
|
+
* #4225 — the horizon must track the ALLOCATION scope. When the allocation is
|
|
1075
|
+
* workstream-scoped (`--ws`/`GSD_WORKSTREAM`, resolved into the env before
|
|
1076
|
+
* dispatch), the sibling's copy of the SAME workstream is what carries that
|
|
1077
|
+
* scope's independent numbering; the sibling's ROOT roadmap and phases/
|
|
1078
|
+
* belong to a different numbering universe (docs/FEATURES.md §51 REQ-WS-01 —
|
|
1079
|
+
* workstream state is isolated in `.planning/workstreams/{name}/`) and must
|
|
1080
|
+
* not contribute. `planningDir(wt, ws)` reuses the canonical resolver, so the
|
|
1081
|
+
* sibling scope matches the local scope's own resolution (env workstream plus
|
|
1082
|
+
* env project segment) by construction; `ws === null` (no workstream active)
|
|
1083
|
+
* keeps the #3849 root-scope horizon byte-for-byte.
|
|
1084
|
+
*
|
|
1064
1085
|
* Widen, never refuse: a missing `.planning/`, an unreadable sibling, a
|
|
1065
1086
|
* non-git cwd, or an unavailable git binary each leave `used` untouched —
|
|
1066
1087
|
* allocation then behaves exactly as it did before this horizon existed.
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1088
|
+
* A sibling that simply lacks the active workstream's directory is the same
|
|
1089
|
+
* fail-open case: it contributes nothing. Sentinels reuse the canonical
|
|
1090
|
+
* `isSentinelPhaseId`; the dir pattern is the same one the on-disk scan uses,
|
|
1091
|
+
* so decimal sub-phases (`411.1-foo`) are correctly not integers.
|
|
1070
1092
|
*/
|
|
1071
1093
|
function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
1072
1094
|
let porcelain;
|
|
@@ -1084,6 +1106,13 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1084
1106
|
catch {
|
|
1085
1107
|
return; // not a git repo / git unavailable — unchanged behavior
|
|
1086
1108
|
}
|
|
1109
|
+
// #4225: the env workstream, read once with planningDir's own discriminator
|
|
1110
|
+
// (`?? null` = deliberately no workstream — never re-derived per sibling).
|
|
1111
|
+
// A poisoned value would already have thrown at the local `planningDir(cwd)`
|
|
1112
|
+
// call every allocator makes before reaching this horizon; the per-sibling
|
|
1113
|
+
// try/catch below still keeps any resolution failure fail-open.
|
|
1114
|
+
const ws = process.env['GSD_WORKSTREAM'] ?? null;
|
|
1115
|
+
const siblingPlanningDir = (wt) => planningDir(wt, ws);
|
|
1087
1116
|
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
1088
1117
|
// Same header shape the allocators scan locally (#1729 tag tolerance).
|
|
1089
1118
|
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
@@ -1094,7 +1123,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1094
1123
|
if (!wt || node_path_1.default.resolve(wt) === node_path_1.default.resolve(cwd))
|
|
1095
1124
|
continue;
|
|
1096
1125
|
try {
|
|
1097
|
-
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(wt, '
|
|
1126
|
+
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(siblingPlanningDir(wt), 'phases'))) {
|
|
1098
1127
|
const match = entry.match(dirNumPattern);
|
|
1099
1128
|
if (!match)
|
|
1100
1129
|
continue;
|
|
@@ -1104,10 +1133,10 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1104
1133
|
}
|
|
1105
1134
|
}
|
|
1106
1135
|
catch {
|
|
1107
|
-
/* worktree has no .planning — normal, contributes nothing */
|
|
1136
|
+
/* worktree has no .planning (or no copy of this scope) — normal, contributes nothing */
|
|
1108
1137
|
}
|
|
1109
1138
|
try {
|
|
1110
|
-
const content = node_fs_1.default.readFileSync(node_path_1.default.join(wt, '
|
|
1139
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(siblingPlanningDir(wt), 'ROADMAP.md'), 'utf-8');
|
|
1111
1140
|
let m;
|
|
1112
1141
|
headerPattern.lastIndex = 0;
|
|
1113
1142
|
while ((m = headerPattern.exec(content)) !== null) {
|
|
@@ -1117,7 +1146,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1117
1146
|
}
|
|
1118
1147
|
}
|
|
1119
1148
|
catch {
|
|
1120
|
-
/* no roadmap in that worktree — normal, contributes nothing */
|
|
1149
|
+
/* no roadmap in that worktree (or scope) — normal, contributes nothing */
|
|
1121
1150
|
}
|
|
1122
1151
|
}
|
|
1123
1152
|
}
|
|
@@ -1343,7 +1372,63 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1343
1372
|
// found')` above, which terminates the process and never reaches here.
|
|
1344
1373
|
publishStateContract(cwd);
|
|
1345
1374
|
}
|
|
1346
|
-
|
|
1375
|
+
// #4569: scans all three representations of an existing decimal sub-phase
|
|
1376
|
+
// under `base` — on-disk `phases/` directories, `### Phase BASE.N:` headings,
|
|
1377
|
+
// and `- [ ] Phase BASE.N:` roadmap SUMMARY CHECKLIST bullets. A bullet-only
|
|
1378
|
+
// roadmap with no heading yet and no on-disk directory yet must still be
|
|
1379
|
+
// seen, or an allocator can silently reallocate an already-used decimal
|
|
1380
|
+
// number. Shared by `cmdPhaseInsert`'s normalized-base scan and its
|
|
1381
|
+
// sibling-allocation parent-base scan so the two never drift apart.
|
|
1382
|
+
function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
|
|
1383
|
+
const decimalSet = new Set();
|
|
1384
|
+
// #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
|
|
1385
|
+
// scan above — a missing phasesDir (no decimal sub-phases yet) is the
|
|
1386
|
+
// expected, silent case (empty decimalSet). A readdirSync failure once the
|
|
1387
|
+
// dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
|
|
1388
|
+
// `phase insert` proceed with an incomplete decimalSet and risk writing a
|
|
1389
|
+
// decimal phase number that collides with an existing on-disk directory
|
|
1390
|
+
// the scan simply never saw — surfaced loud instead, like the sibling.
|
|
1391
|
+
//
|
|
1392
|
+
// #4634 (lint-phase-enumeration-drift): routed through the canonical
|
|
1393
|
+
// PHYSICAL-set owner (`listAllPhaseDirs`, phase-locator.cts) instead of a
|
|
1394
|
+
// hand-rolled `readdirSync`. This scan — like its sibling `cmdPhaseNextDecimal`
|
|
1395
|
+
// and its caller `cmdPhaseInsert` (both exempted in the drift guard for the
|
|
1396
|
+
// same reason) — must see EVERY on-disk decimal sub-phase directory
|
|
1397
|
+
// regardless of the current milestone window, so `listMilestonePhaseDirs`
|
|
1398
|
+
// (windowed) is the wrong owner here; `includeSentinels: true` preserves this
|
|
1399
|
+
// function's pre-existing behavior of never sentinel-filtering (the decimal
|
|
1400
|
+
// regex below only ever matches `base.N`-shaped names, so sentinel inclusion
|
|
1401
|
+
// is a no-op either way).
|
|
1402
|
+
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
1403
|
+
const { value: dirs, scope } = listAllPhaseDirs(phasesDir, { includeSentinels: true });
|
|
1404
|
+
if (scope === SCOPE.UNREADABLE) {
|
|
1405
|
+
// The dir EXISTS but could not be read (EACCES/EIO) — a genuine anomaly,
|
|
1406
|
+
// not the expected empty-decimalSet case above. Surfaced loud, matching
|
|
1407
|
+
// this function's pre-migration `readdirSync` catch: swallowing it would
|
|
1408
|
+
// let `phase insert` proceed with an incomplete decimalSet and collide
|
|
1409
|
+
// with an existing on-disk decimal directory the scan never saw.
|
|
1410
|
+
error(`Failed to scan phase directories for existing decimal phases: unable to read ${phasesDir}`);
|
|
1411
|
+
}
|
|
1412
|
+
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(base)}\\.(\\d+)`);
|
|
1413
|
+
for (const dir of dirs) {
|
|
1414
|
+
const dm = dir.match(decimalPattern);
|
|
1415
|
+
if (dm)
|
|
1416
|
+
decimalSet.add(parseInt(dm[1], 10));
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
1420
|
+
let rmMatch;
|
|
1421
|
+
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
1422
|
+
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
1423
|
+
}
|
|
1424
|
+
const checklistDecimalPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
|
|
1425
|
+
let clMatch;
|
|
1426
|
+
while ((clMatch = checklistDecimalPattern.exec(rawContent)) !== null) {
|
|
1427
|
+
decimalSet.add(parseInt(clMatch[1], 10));
|
|
1428
|
+
}
|
|
1429
|
+
return decimalSet;
|
|
1430
|
+
}
|
|
1431
|
+
function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested') {
|
|
1347
1432
|
if (!afterPhase || !description) {
|
|
1348
1433
|
error('after-phase and description required for phase insert');
|
|
1349
1434
|
}
|
|
@@ -1373,43 +1458,20 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1373
1458
|
}
|
|
1374
1459
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1375
1460
|
const normalizedBase = normalizePhaseName(afterPhase);
|
|
1376
|
-
const decimalSet =
|
|
1377
|
-
// #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
|
|
1378
|
-
// scan above — a missing phasesDir (no decimal sub-phases yet) is the
|
|
1379
|
-
// expected, silent case (empty decimalSet). A readdirSync failure once the
|
|
1380
|
-
// dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
|
|
1381
|
-
// `phase insert` proceed with an incomplete decimalSet and risk writing a
|
|
1382
|
-
// decimal phase number that collides with an existing on-disk directory
|
|
1383
|
-
// the scan simply never saw — surfaced loud instead, like the sibling.
|
|
1384
|
-
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
1385
|
-
// Initialized (not just declared) so TS's definite-assignment check is
|
|
1386
|
-
// satisfied without relying on control-flow narrowing through error()'s
|
|
1387
|
-
// `never` return, which TS does not propagate through a destructured
|
|
1388
|
-
// module-property function reference — error() still halts the process
|
|
1389
|
-
// before `dirs` below is ever computed from this placeholder value.
|
|
1390
|
-
let entries = [];
|
|
1391
|
-
try {
|
|
1392
|
-
entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1393
|
-
}
|
|
1394
|
-
catch (e) {
|
|
1395
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1396
|
-
error(`Failed to scan phase directories for existing decimal phases: ${msg}`);
|
|
1397
|
-
}
|
|
1398
|
-
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1399
|
-
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalizedBase)}\\.(\\d+)`);
|
|
1400
|
-
for (const dir of dirs) {
|
|
1401
|
-
const dm = dir.match(decimalPattern);
|
|
1402
|
-
if (dm)
|
|
1403
|
-
decimalSet.add(parseInt(dm[1], 10));
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
1407
|
-
let rmMatch;
|
|
1408
|
-
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
1409
|
-
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
1410
|
-
}
|
|
1461
|
+
const decimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, normalizedBase);
|
|
1411
1462
|
const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
|
|
1412
|
-
|
|
1463
|
+
let _decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
1464
|
+
// #4569: sibling allocation joins afterPhase's PARENT level instead of nesting
|
|
1465
|
+
// one level deeper under afterPhase itself. A top-level phase (no existing
|
|
1466
|
+
// decimal segment) has no sibling level to join; nested is the only sensible
|
|
1467
|
+
// allocation, so we silently fall back for that case.
|
|
1468
|
+
const lastDotIndex = normalizedBase.lastIndexOf('.');
|
|
1469
|
+
if (allocation === 'sibling' && lastDotIndex !== -1) {
|
|
1470
|
+
const parentBase = normalizedBase.slice(0, lastDotIndex);
|
|
1471
|
+
const siblingDecimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, parentBase);
|
|
1472
|
+
const siblingNextDecimal = siblingDecimalSet.size === 0 ? 1 : Math.max(...siblingDecimalSet) + 1;
|
|
1473
|
+
_decimalPhase = `${parentBase}.${siblingNextDecimal}`;
|
|
1474
|
+
}
|
|
1413
1475
|
const insertConfig = loadConfig(cwd);
|
|
1414
1476
|
const projectCode = insertConfig.project_code || '';
|
|
1415
1477
|
const pfx = projectCode ? `${projectCode}-` : '';
|
|
@@ -3022,7 +3084,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3022
3084
|
// #2617: pass the project's runtime so the blocked-completion error below
|
|
3023
3085
|
// suggests the command surface this runtime actually installs
|
|
3024
3086
|
// ($gsd-… on Codex) rather than a hard-coded Claude-style string.
|
|
3025
|
-
const verificationStatus = readVerificationStatus(phaseFullDir, {
|
|
3087
|
+
const verificationStatus = readVerificationStatus(phaseFullDir, {
|
|
3088
|
+
runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
|
|
3089
|
+
convention: resolvePhaseIdConvention(cwd),
|
|
3090
|
+
});
|
|
3026
3091
|
// #3057 B3: the staleness check inside readVerificationStatus can itself
|
|
3027
3092
|
// fail (fs / scanPhasePlans / clock error), in which case `status` above
|
|
3028
3093
|
// was routed as if nothing were stale (unchanged fail-open routing) — but
|
|
@@ -3767,14 +3832,53 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3767
3832
|
const fmBody = frontmatterMod.stripFrontmatter(stateContent);
|
|
3768
3833
|
const bodyHasPhaseField = stateExtractField(fmBody, 'Current Phase') != null ||
|
|
3769
3834
|
stateExtractField(fmBody, 'Phase') != null;
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3835
|
+
// #4129: the POST-completion progress counters, derived from the very
|
|
3836
|
+
// ROADMAP this transaction just mutated (still in memory — it hits disk
|
|
3837
|
+
// only at writePlanningFileSet, AFTER this content was assembled).
|
|
3838
|
+
// buildStateFrontmatter's disk scan inside syncAndPreserveStateMd
|
|
3839
|
+
// reads the PRE-completion ROADMAP (and any stale-dated sibling
|
|
3840
|
+
// verification), so without this intent the persisted counter failed
|
|
3841
|
+
// to increment on the completing phase's own transaction. Routed
|
|
3842
|
+
// through the #2736 authoritativeFm seam's object direction: the
|
|
3843
|
+
// pre-preservation merge makes it the derived truth the ratchet
|
|
3844
|
+
// compares, and the post-preservation re-assert (completedOnlyRaise)
|
|
3845
|
+
// is a floor no preservation branch can drop below. clampPercent is
|
|
3846
|
+
// completePhaseCore's own percent formula (state-transition.cts),
|
|
3847
|
+
// reused so the frontmatter and the body `Progress:` line agree.
|
|
3848
|
+
const postCompletionRoadmapScope = roadmapContent !== null
|
|
3849
|
+
? extractCurrentMilestone(roadmapContent, cwd)
|
|
3850
|
+
: null;
|
|
3851
|
+
const postCompletionRoadmapProgress = postCompletionRoadmapScope !== null
|
|
3852
|
+
? deriveProgressFromRoadmapForIntent(postCompletionRoadmapScope)
|
|
3853
|
+
: null;
|
|
3854
|
+
const authoritativeProgress = postCompletionRoadmapProgress && postCompletionRoadmapProgress.completedPhases !== null
|
|
3855
|
+
? postCompletionRoadmapProgress.totalPhases !== null && postCompletionRoadmapProgress.totalPhases > 0
|
|
3856
|
+
? {
|
|
3857
|
+
completed_phases: postCompletionRoadmapProgress.completedPhases,
|
|
3858
|
+
percent: clampPercentForIntent(postCompletionRoadmapProgress.completedPhases, postCompletionRoadmapProgress.totalPhases),
|
|
3776
3859
|
}
|
|
3860
|
+
: { completed_phases: postCompletionRoadmapProgress.completedPhases }
|
|
3777
3861
|
: undefined;
|
|
3862
|
+
const authoritativeFm = authoritativeProgress
|
|
3863
|
+
? {
|
|
3864
|
+
...(nextPhaseDisplayName
|
|
3865
|
+
? bodyHasPhaseField || !nextPhaseNum
|
|
3866
|
+
? { current_phase_name: nextPhaseDisplayName }
|
|
3867
|
+
: {
|
|
3868
|
+
current_phase: String(nextPhaseNum),
|
|
3869
|
+
current_phase_name: nextPhaseDisplayName,
|
|
3870
|
+
}
|
|
3871
|
+
: {}),
|
|
3872
|
+
progress: authoritativeProgress,
|
|
3873
|
+
}
|
|
3874
|
+
: nextPhaseDisplayName
|
|
3875
|
+
? bodyHasPhaseField || !nextPhaseNum
|
|
3876
|
+
? { current_phase_name: nextPhaseDisplayName }
|
|
3877
|
+
: {
|
|
3878
|
+
current_phase: String(nextPhaseNum),
|
|
3879
|
+
current_phase_name: nextPhaseDisplayName,
|
|
3880
|
+
}
|
|
3881
|
+
: undefined;
|
|
3778
3882
|
// ADR-3408 §8.3 / #3469: this deliberately bypasses
|
|
3779
3883
|
// readModifyWriteStateMd (STATE.md is committed atomically with
|
|
3780
3884
|
// ROADMAP/REQUIREMENTS), so it calls the single write-seam
|