@opengsd/gsd-core 1.10.0 → 1.11.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-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -16,6 +16,7 @@ const ioMod = require("./io.cjs");
|
|
|
16
16
|
const { output, error } = ioMod;
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
18
|
const validate_cjs_1 = require("./validate.cjs");
|
|
19
|
+
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
21
|
const unusableInputMod = require("./unusable-input.cjs");
|
|
21
22
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
@@ -83,7 +84,7 @@ const UNTERMINATED_KEY_THRESHOLD = 2;
|
|
|
83
84
|
* has a false-positive class the other closes.
|
|
84
85
|
*/
|
|
85
86
|
function isFrontmatterShaped(region) {
|
|
86
|
-
const lines =
|
|
87
|
+
const lines = (0, text_lines_cjs_1.splitLines)(region).filter((line) => line.trim() !== '');
|
|
87
88
|
if (lines.length === 0)
|
|
88
89
|
return false;
|
|
89
90
|
return lines.every((line) => (/^\s*[a-zA-Z0-9_-]+:/.test(line) // key: value
|
|
@@ -91,6 +92,80 @@ function isFrontmatterShaped(region) {
|
|
|
91
92
|
|| /^\s+\S/.test(line) // indented continuation of a nested value
|
|
92
93
|
));
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* #3257: a Symbol-keyed channel that carries full-line (column-0 `#`) YAML
|
|
97
|
+
* comments through a parse → reconstruct round-trip. Comments are otherwise
|
|
98
|
+
* unrepresentable on the Frontmatter object (Record<string, ...>) and were
|
|
99
|
+
* silently dropped by reconstructFrontmatter. The Symbol is invisible to
|
|
100
|
+
* Object.entries / Object.keys / JSON.stringify / for-in, so every existing
|
|
101
|
+
* reader is unchanged; only reconstructFrontmatter reads it. Leading comments
|
|
102
|
+
* are attached to the top-level key that follows them; comments after the last
|
|
103
|
+
* key go to `trailing`. Only set when a comment is actually seen, so comment-less
|
|
104
|
+
* frontmatter parses byte-identically to before.
|
|
105
|
+
*/
|
|
106
|
+
const FULL_LINE_COMMENTS = Symbol('fullLineComments');
|
|
107
|
+
/**
|
|
108
|
+
* Unescape the interior of a YAML double-quoted scalar — the exact inverse of
|
|
109
|
+
* `escapeDoubleQuoted` (#3497). The writer has escaped `\`/`"`/`\n`/`\t`/`\r`/
|
|
110
|
+
* `\xHH` since #1779, but the reader only stripped the delimiters, so
|
|
111
|
+
* parse(serialize(x)) ≠ x for any quoted scalar carrying a `"` or `\`: each
|
|
112
|
+
* read-modify-write round-trip doubled the backslashes (b → 2b+1), growing a
|
|
113
|
+
* repeatedly-synced field — and its document — without bound until tooling
|
|
114
|
+
* OOMed. Recognized escapes decode per YAML double-quoted semantics; an
|
|
115
|
+
* unrecognized `\c` is kept literally (backslash + char), matching the
|
|
116
|
+
* strip-only behavior hand-authored files had before this fix.
|
|
117
|
+
*/
|
|
118
|
+
function unescapeDoubleQuoted(s) {
|
|
119
|
+
let out = '';
|
|
120
|
+
for (let i = 0; i < s.length; i++) {
|
|
121
|
+
const ch = s[i];
|
|
122
|
+
if (ch !== '\\' || i === s.length - 1) {
|
|
123
|
+
out += ch;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const next = s[++i];
|
|
127
|
+
if (next === '\\' || next === '"') {
|
|
128
|
+
out += next;
|
|
129
|
+
}
|
|
130
|
+
else if (next === 'n') {
|
|
131
|
+
out += '\n';
|
|
132
|
+
}
|
|
133
|
+
else if (next === 't') {
|
|
134
|
+
out += '\t';
|
|
135
|
+
}
|
|
136
|
+
else if (next === 'r') {
|
|
137
|
+
out += '\r';
|
|
138
|
+
}
|
|
139
|
+
else if (next === 'x') {
|
|
140
|
+
const hex = s.slice(i + 1, i + 3);
|
|
141
|
+
if (/^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
142
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
143
|
+
i += 2;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
out += '\\x'; // not \xHH — keep literally
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
out += '\\' + next; // unrecognized escape — keep literally
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Strip the quote delimiters off a parsed YAML scalar, un-escaping the interior
|
|
157
|
+
* when the scalar is double-quoted (#3497 — the parse-side complement of
|
|
158
|
+
* `escapeDoubleQuoted`). Single-quoted scalars keep the historical strip-only
|
|
159
|
+
* behavior (the writer never emits them; `''` → `'` folding is out of scope).
|
|
160
|
+
* A scalar wrapped in double quotes un-escapes; anything else keeps the exact
|
|
161
|
+
* prior delimiter-strip behavior, including a stray unpaired boundary quote.
|
|
162
|
+
*/
|
|
163
|
+
function parseQuotedScalar(value) {
|
|
164
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
165
|
+
return unescapeDoubleQuoted(value.slice(1, -1));
|
|
166
|
+
}
|
|
167
|
+
return value.replace(/^["']|["']$/g, '');
|
|
168
|
+
}
|
|
94
169
|
/**
|
|
95
170
|
* Parse one already-delimited YAML region into a Frontmatter object.
|
|
96
171
|
*
|
|
@@ -100,12 +175,20 @@ function isFrontmatterShaped(region) {
|
|
|
100
175
|
*/
|
|
101
176
|
function parseYamlRegion(yaml) {
|
|
102
177
|
const frontmatter = {};
|
|
103
|
-
const lines =
|
|
178
|
+
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
179
|
+
// #3257: pending column-0 full-line comments, attached to the next top-level key.
|
|
180
|
+
let pendingComments = [];
|
|
181
|
+
let commentChannel;
|
|
104
182
|
const stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
|
105
183
|
for (const line of lines) {
|
|
106
184
|
// Skip empty lines
|
|
107
185
|
if (line.trim() === '')
|
|
108
186
|
continue;
|
|
187
|
+
// #3257: capture column-0 full-line comments; attach them to the next top-level key.
|
|
188
|
+
if (/^#/.test(line)) {
|
|
189
|
+
pendingComments.push(line);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
109
192
|
// Calculate indentation (number of leading spaces)
|
|
110
193
|
const indentMatch = line.match(/^(\s*)/);
|
|
111
194
|
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
@@ -118,6 +201,13 @@ function parseYamlRegion(yaml) {
|
|
|
118
201
|
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
119
202
|
if (keyMatch) {
|
|
120
203
|
const key = keyMatch[2];
|
|
204
|
+
// #3257: attach any pending comments to this (top-level) key.
|
|
205
|
+
if (pendingComments.length) {
|
|
206
|
+
if (!commentChannel)
|
|
207
|
+
commentChannel = { leading: {}, trailing: [] };
|
|
208
|
+
commentChannel.leading[key] = pendingComments;
|
|
209
|
+
pendingComments = [];
|
|
210
|
+
}
|
|
121
211
|
const value = keyMatch[3].trim();
|
|
122
212
|
if (value === '' || value === '[') {
|
|
123
213
|
// Key with no value or opening bracket — could be nested object or array
|
|
@@ -134,13 +224,13 @@ function parseYamlRegion(yaml) {
|
|
|
134
224
|
}
|
|
135
225
|
else {
|
|
136
226
|
// Simple key: value
|
|
137
|
-
current.obj[key] = value
|
|
227
|
+
current.obj[key] = parseQuotedScalar(value);
|
|
138
228
|
current.key = null;
|
|
139
229
|
}
|
|
140
230
|
}
|
|
141
231
|
else if (line.trim().startsWith('- ')) {
|
|
142
232
|
// Array item
|
|
143
|
-
const itemValue = line.trim().slice(2)
|
|
233
|
+
const itemValue = parseQuotedScalar(line.trim().slice(2));
|
|
144
234
|
// If current context is an empty object, convert to array
|
|
145
235
|
if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
|
|
146
236
|
// Find the key in parent that points to this object and convert it
|
|
@@ -160,6 +250,15 @@ function parseYamlRegion(yaml) {
|
|
|
160
250
|
}
|
|
161
251
|
}
|
|
162
252
|
}
|
|
253
|
+
// #3257: trailing comments (after the last key) + attach the channel if any comment was seen.
|
|
254
|
+
if (pendingComments.length) {
|
|
255
|
+
if (!commentChannel)
|
|
256
|
+
commentChannel = { leading: {}, trailing: [] };
|
|
257
|
+
commentChannel.trailing = pendingComments;
|
|
258
|
+
}
|
|
259
|
+
if (commentChannel) {
|
|
260
|
+
frontmatter[FULL_LINE_COMMENTS] = commentChannel;
|
|
261
|
+
}
|
|
163
262
|
return frontmatter;
|
|
164
263
|
}
|
|
165
264
|
/**
|
|
@@ -273,9 +372,17 @@ function scalarNeedsDoubleQuoting(s) {
|
|
|
273
372
|
}
|
|
274
373
|
function reconstructFrontmatter(obj) {
|
|
275
374
|
const lines = [];
|
|
375
|
+
// #3257: read the full-line-comment channel (set by parseYamlRegion when comments
|
|
376
|
+
// were present). Object.entries skips the Symbol key, so the data loop is unchanged.
|
|
377
|
+
const commentChannel = obj[FULL_LINE_COMMENTS];
|
|
276
378
|
for (const [key, value] of Object.entries(obj)) {
|
|
277
379
|
if (value === null || value === undefined)
|
|
278
380
|
continue;
|
|
381
|
+
// #3257: re-emit this key's leading full-line comments before the key itself.
|
|
382
|
+
const leading = commentChannel?.leading[key];
|
|
383
|
+
if (leading)
|
|
384
|
+
for (const c of leading)
|
|
385
|
+
lines.push(c);
|
|
279
386
|
if (Array.isArray(value)) {
|
|
280
387
|
if (value.length === 0) {
|
|
281
388
|
lines.push(`${key}: []`);
|
|
@@ -348,8 +455,35 @@ function reconstructFrontmatter(obj) {
|
|
|
348
455
|
}
|
|
349
456
|
}
|
|
350
457
|
}
|
|
458
|
+
// #3257: re-emit any trailing full-line comments (those after the last key).
|
|
459
|
+
if (commentChannel?.trailing?.length) {
|
|
460
|
+
for (const c of commentChannel.trailing)
|
|
461
|
+
lines.push(c);
|
|
462
|
+
}
|
|
351
463
|
return lines.join('\n');
|
|
352
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* #3257: copy the full-line-comment channel from `source` onto `target`, filtering
|
|
467
|
+
* `leading` to keys still present in `target` (a deleted key's annotation goes with
|
|
468
|
+
* it — AC5). No-op when `source` carries no channel. Consumers that rebuild their
|
|
469
|
+
* target object fresh (syncStateFrontmatter builds derivedFm via buildStateFrontmatter
|
|
470
|
+
* and copies keys with Object.keys, which skips the Symbol) MUST call this before
|
|
471
|
+
* reconstructFrontmatter, or the channel parseYamlRegion attached to the extracted
|
|
472
|
+
* source is lost.
|
|
473
|
+
*/
|
|
474
|
+
function propagateCommentChannel(source, target) {
|
|
475
|
+
const channel = source[FULL_LINE_COMMENTS];
|
|
476
|
+
if (!channel)
|
|
477
|
+
return;
|
|
478
|
+
const filtered = { leading: {}, trailing: channel.trailing };
|
|
479
|
+
for (const [key, comments] of Object.entries(channel.leading)) {
|
|
480
|
+
if (key in target)
|
|
481
|
+
filtered.leading[key] = comments;
|
|
482
|
+
}
|
|
483
|
+
if (filtered.trailing.length || Object.keys(filtered.leading).length) {
|
|
484
|
+
target[FULL_LINE_COMMENTS] = filtered;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
353
487
|
/**
|
|
354
488
|
* Slice a frontmatter YAML body into per-top-level-key raw text segments. Each segment
|
|
355
489
|
* runs from a column-0 `key:` line through the line before the next column-0 key (or the
|
|
@@ -359,7 +493,7 @@ function reconstructFrontmatter(obj) {
|
|
|
359
493
|
* not modify (e.g. must_haves.artifacts / .prohibitions).
|
|
360
494
|
*/
|
|
361
495
|
function sliceTopLevelFrontmatterSegments(yaml) {
|
|
362
|
-
const lines =
|
|
496
|
+
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
363
497
|
const segments = [];
|
|
364
498
|
let current = null;
|
|
365
499
|
for (const line of lines) {
|
|
@@ -423,7 +557,7 @@ function spliceFrontmatter(content, newObj) {
|
|
|
423
557
|
// unrelated `must_haves` block. Keys absent from the original (genuinely new) are
|
|
424
558
|
// regenerated and appended; keys absent from `newObj` are preserved (never silently
|
|
425
559
|
// deleted by a set/merge).
|
|
426
|
-
const fmLines =
|
|
560
|
+
const fmLines = (0, text_lines_cjs_1.splitLines)(fmBlock);
|
|
427
561
|
const inner = fmLines.slice(1, -1).join('\n'); // drop the opening `---` and closing `---`
|
|
428
562
|
let originalParsed;
|
|
429
563
|
try {
|
|
@@ -504,27 +638,27 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
504
638
|
if (!fmMatch)
|
|
505
639
|
return [];
|
|
506
640
|
const yaml = fmMatch[1];
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
641
|
+
const yamlLines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
642
|
+
// Find must_haves: first to detect its indentation level. Split-then-scan
|
|
643
|
+
// (rather than a whole-string /m match) so a CRLF or blank-line boundary
|
|
644
|
+
// can never be absorbed into the indent capture (#3360) — see
|
|
645
|
+
// .gsd/phase/chore-3413-text-lines-seam/40-design.md.
|
|
646
|
+
const mustHavesLinePattern = /^(\s*)must_haves:\s*$/;
|
|
647
|
+
const mustHavesLineIndex = yamlLines.findIndex((line) => mustHavesLinePattern.test(line));
|
|
648
|
+
if (mustHavesLineIndex === -1)
|
|
510
649
|
return [];
|
|
511
|
-
const mustHavesIndent =
|
|
650
|
+
const mustHavesIndent = yamlLines[mustHavesLineIndex].match(/^(\s*)/)[1].length;
|
|
512
651
|
// Find the block (e.g., "truths:", "artifacts:", "key_links:") under must_haves
|
|
513
652
|
// It must be indented more than must_haves but we detect the actual indent dynamically
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
if (
|
|
653
|
+
const blockLinePattern = new RegExp(`^(\\s+)${blockName}:\\s*$`);
|
|
654
|
+
const blockLineIndex = yamlLines.findIndex((line) => blockLinePattern.test(line));
|
|
655
|
+
if (blockLineIndex === -1)
|
|
517
656
|
return [];
|
|
518
|
-
const blockIndent =
|
|
657
|
+
const blockIndent = yamlLines[blockLineIndex].match(/^(\s*)/)[1].length;
|
|
519
658
|
// The block must be nested under must_haves (more indented)
|
|
520
659
|
if (blockIndent <= mustHavesIndent)
|
|
521
660
|
return [];
|
|
522
|
-
|
|
523
|
-
const blockStart = yaml.indexOf(blockMatch[0]);
|
|
524
|
-
if (blockStart === -1)
|
|
525
|
-
return [];
|
|
526
|
-
const afterBlock = yaml.slice(blockStart);
|
|
527
|
-
const blockLines = afterBlock.split(/\r?\n/).slice(1); // skip the header line
|
|
661
|
+
const blockLines = yamlLines.slice(blockLineIndex + 1); // skip the header line
|
|
528
662
|
// List items are indented one level deeper than blockIndent
|
|
529
663
|
// Continuation KVs are indented one level deeper than list items
|
|
530
664
|
const items = [];
|
|
@@ -865,4 +999,5 @@ module.exports = {
|
|
|
865
999
|
cmdFrontmatterSet,
|
|
866
1000
|
cmdFrontmatterMerge,
|
|
867
1001
|
cmdFrontmatterValidate,
|
|
1002
|
+
propagateCommentChannel,
|
|
868
1003
|
};
|
|
@@ -24,14 +24,18 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
25
|
const io = require("./io.cjs");
|
|
26
26
|
const { output, error } = io;
|
|
27
|
-
|
|
28
|
-
const phaseId = require("./phase-id.cjs");
|
|
29
|
-
const { escapeRegex } = phaseId;
|
|
27
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
30
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
29
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
30
|
const { planningPaths, planningDir, findContextMdIn } = planningWorkspace;
|
|
33
31
|
const decisions_cjs_1 = require("./decisions.cjs");
|
|
34
32
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
35
|
+
const { scanPhasePlans } = planScanMod;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
38
|
+
const { scopeToPhase } = phaseIdMod;
|
|
35
39
|
/**
|
|
36
40
|
* Parse REQ-IDs from REQUIREMENTS.md content.
|
|
37
41
|
*
|
|
@@ -91,7 +95,7 @@ function parseRequirements(reqMd) {
|
|
|
91
95
|
}
|
|
92
96
|
function detectCoverage(items, planText) {
|
|
93
97
|
return items.map(it => {
|
|
94
|
-
const re = new RegExp('\\b' + escapeRegex(it.id) + '\\b');
|
|
98
|
+
const re = new RegExp('\\b' + (0, pattern_cjs_1.escapeRegex)(it.id) + '\\b');
|
|
95
99
|
return {
|
|
96
100
|
source: it.source,
|
|
97
101
|
item: it.id,
|
|
@@ -152,6 +156,38 @@ const PHASE_REQ_RANGE_RE = /^(.+-)(\d+)\.\.(.+-)(\d+)$/;
|
|
|
152
156
|
* against pathological input like `X-1..X-100000` ballooning the comparison set.
|
|
153
157
|
*/
|
|
154
158
|
const MAX_PHASE_REQ_RANGE = 1000;
|
|
159
|
+
/**
|
|
160
|
+
* Shape filter applied to `--phase-req-ids` tokens AFTER range expansion
|
|
161
|
+
* (#3189). ROADMAP `**Requirements:**` lines routinely carry prose trailing
|
|
162
|
+
* the real ID list — locked-decision annotations, ambiguity scores,
|
|
163
|
+
* prohibitions, dates — and the function's own contract says callers may pass
|
|
164
|
+
* that roadmap value through verbatim. Without a shape filter every prose word
|
|
165
|
+
* survives the whitespace split and is reported as an individually-missing
|
|
166
|
+
* requirement, drowning the real coverage signal.
|
|
167
|
+
*
|
|
168
|
+
* The filter is intentionally wider than `ID_PATTERN` (which mandates a hyphen
|
|
169
|
+
* and so would reject the hyphen-less `R1`..`R8` family real roadmaps use).
|
|
170
|
+
* Three load-bearing details (#3189):
|
|
171
|
+
*
|
|
172
|
+
* 1. The digit lookahead `(?=.*\d)` matters. Without it ALL-CAPS prose tokens
|
|
173
|
+
* that appear in annotations (`LOCKED`, `TBD`-as-prose, `NONE`-as-prose)
|
|
174
|
+
* would pass and still reach the report as fake IDs. Every real
|
|
175
|
+
* requirement ID carries a digit (`R1`, `SEL-01`, `P1-P3`).
|
|
176
|
+
* 2. It MUST run after `expandPhaseReqIdToken`. Filtering before would
|
|
177
|
+
* discard the range tokens themselves (`SEL-01..SEL-03` matches no
|
|
178
|
+
* single-ID shape), silently undoing the #1269 / #1419 range expansion.
|
|
179
|
+
* 3. It is NOT reusable as `ID_PATTERN`. `ID_PATTERN` mandates a hyphen,
|
|
180
|
+
* so it rejects `R1`..`R8` and would leave only range-shaped tokens
|
|
181
|
+
* standing — a silently-empty report, which is worse.
|
|
182
|
+
*
|
|
183
|
+
* Accepts both hyphen-less digit-bearing IDs (`R1`, `R8`) and prefix-hyphen
|
|
184
|
+
* IDs (`REQ-01`, `SEL-01`, `BACK-07`), including multi-segment prefixes
|
|
185
|
+
* (`REQ2-01`). Rejects prose (`LOCKED`), punctuation (`—`, `##`, `+`),
|
|
186
|
+
* dates, version-likes (`0.12;`), backtick fragments, and any token still
|
|
187
|
+
* containing `.` (so invalid range tokens returned literal by
|
|
188
|
+
* `expandPhaseReqIdToken` are dropped rather than surfaced as fake IDs).
|
|
189
|
+
*/
|
|
190
|
+
const PHASE_REQ_ID_SHAPE_RE = /^(?=.*\d)[A-Z][A-Z0-9]*(?:[-_][A-Za-z0-9]+)*$/;
|
|
155
191
|
/**
|
|
156
192
|
* Expand a single `--phase-req-ids` token in place. If it is a valid ascending
|
|
157
193
|
* same-prefix numeric range (`<PREFIX>-NN..<PREFIX>-MM`, identical prefix both
|
|
@@ -212,6 +248,15 @@ function expandPhaseReqIdToken(token) {
|
|
|
212
248
|
* spanning more than MAX_PHASE_REQ_RANGE IDs) stays literal — no partial
|
|
213
249
|
* expansion, no guessing.
|
|
214
250
|
*
|
|
251
|
+
* ID-shape filter (#3189): STRICTLY AFTER range expansion, every token is
|
|
252
|
+
* filtered through `PHASE_REQ_ID_SHAPE_RE`. ROADMAP `**Requirements:**` lines
|
|
253
|
+
* routinely carry prose trailing the real ID list (locked-decision annotations,
|
|
254
|
+
* ambiguity scores, prohibitions, dates); without the filter every prose word
|
|
255
|
+
* would be reported as an individually-missing requirement. Tokens that cannot
|
|
256
|
+
* be requirement IDs (prose, punctuation, dates, invalid range tokens left
|
|
257
|
+
* literal by `expandPhaseReqIdToken`) are dropped; an input whose every token
|
|
258
|
+
* is dropped collapses to `null` (skip semantics).
|
|
259
|
+
*
|
|
215
260
|
* Tolerates JSON-array-ish input (`["REQ-01","REQ-02"]`) since callers may pass
|
|
216
261
|
* the roadmap value through verbatim.
|
|
217
262
|
*/
|
|
@@ -229,7 +274,12 @@ function normalizePhaseReqIds(rawVal) {
|
|
|
229
274
|
const ids = v.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
|
|
230
275
|
// Expand range tokens (#1269) per-token AFTER the split, preserving input order.
|
|
231
276
|
const expanded = ids.flatMap(expandPhaseReqIdToken);
|
|
232
|
-
|
|
277
|
+
// #3189: drop tokens that cannot be requirement IDs (prose, punctuation,
|
|
278
|
+
// dates, invalid range tokens left literal by expandPhaseReqIdToken). MUST
|
|
279
|
+
// run after expand so valid ranges still expand (filtering before would
|
|
280
|
+
// discard `SEL-01..SEL-03` itself, undoing #1269 / #1419).
|
|
281
|
+
const filtered = expanded.filter(t => PHASE_REQ_ID_SHAPE_RE.test(t));
|
|
282
|
+
return filtered.length === 0 ? null : filtered;
|
|
233
283
|
}
|
|
234
284
|
function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
235
285
|
const phaseReqIds = normalizePhaseReqIds(options.phaseReqIds);
|
|
@@ -268,7 +318,13 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
268
318
|
phaseDirFiles = node_fs_1.default.readdirSync(absPhaseDir);
|
|
269
319
|
}
|
|
270
320
|
catch { /* unreadable */ }
|
|
271
|
-
|
|
321
|
+
// #3511-class: scope the raw listing to this phase dir before the
|
|
322
|
+
// phase-numbered -CONTEXT.md predicate. `phaseDirFiles` itself stays raw —
|
|
323
|
+
// it is also reused below only as a `.length > 0` guard ahead of
|
|
324
|
+
// scanPhasePlans's own plan-sequence-numbered enumeration, a different
|
|
325
|
+
// grammar that must not be scoped by phase number.
|
|
326
|
+
const scopedPhaseDirFiles = scopeToPhase(phaseDirFiles, node_path_1.default.basename(absPhaseDir));
|
|
327
|
+
const ctxFile = findContextMdIn(scopedPhaseDirFiles);
|
|
272
328
|
const ctxPath = ctxFile ? node_path_1.default.join(absPhaseDir, ctxFile) : null;
|
|
273
329
|
const ctxMd = ctxPath ? node_fs_1.default.readFileSync(ctxPath, 'utf-8') : '';
|
|
274
330
|
// Use extractDecisions so gap-checker can distinguish could-not-parse from none-present.
|
|
@@ -278,7 +334,12 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
278
334
|
let planText = '';
|
|
279
335
|
try {
|
|
280
336
|
if (phaseDirFiles.length > 0) {
|
|
281
|
-
|
|
337
|
+
// #3183 (lint-plan-count-drift): source the live plan-file list from
|
|
338
|
+
// the single owner (scanPhasePlans) instead of a local `-PLAN\.md$`
|
|
339
|
+
// filter on the already-read listing — picks up bare PLAN.md, nested
|
|
340
|
+
// plans/, and excludes superseded plans, none of which the prior
|
|
341
|
+
// root-only exact-suffix filter did.
|
|
342
|
+
const files = scanPhasePlans(absPhaseDir).planFiles;
|
|
282
343
|
planText = files.map(f => {
|
|
283
344
|
try {
|
|
284
345
|
return node_fs_1.default.readFileSync(node_path_1.default.join(absPhaseDir, f), 'utf-8');
|
|
@@ -41,6 +41,8 @@ exports.tryLocalBranch = tryLocalBranch;
|
|
|
41
41
|
exports.resolveBaseBranchDiagnostics = resolveBaseBranchDiagnostics;
|
|
42
42
|
exports.resolveBaseBranch = resolveBaseBranch;
|
|
43
43
|
exports.gitWorktreeInfoInternal = gitWorktreeInfoInternal;
|
|
44
|
+
exports.phaseStartCommit = phaseStartCommit;
|
|
45
|
+
exports.changedFilesSince = changedFilesSince;
|
|
44
46
|
exports.cmdGitBaseBranch = cmdGitBaseBranch;
|
|
45
47
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
46
48
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -244,6 +246,106 @@ function gitWorktreeInfoInternal(cwd, deps) {
|
|
|
244
246
|
return { inside: false, worktreeRoot: null };
|
|
245
247
|
}
|
|
246
248
|
}
|
|
249
|
+
// ─── Adapter 3: phase-start anchor + touched-file listing (issue #1953) ───────
|
|
250
|
+
/**
|
|
251
|
+
* Resolve the commit that ADDED `<phaseDir>/*-PLAN.md` — the anchor commit
|
|
252
|
+
* marking when the phase began (see `.gsd/phase/feat-1953-complexity-triggered-
|
|
253
|
+
* refactor/42-router-contract.md`, "Touched-file anchor"). `phaseDir` is a
|
|
254
|
+
* project-relative path (backslashes are normalized unconditionally before
|
|
255
|
+
* building the pathspec, never via `path.sep` — matches the repo's
|
|
256
|
+
* cross-platform path-normalization convention).
|
|
257
|
+
*
|
|
258
|
+
* Bounded (`timeout: 15_000`), degrades to `null` on any failure or when no
|
|
259
|
+
* such commit exists (a phase never planned through git, a shallow clone).
|
|
260
|
+
* Never throws.
|
|
261
|
+
*/
|
|
262
|
+
function phaseStartCommit(cwd, phaseDir, execGit) {
|
|
263
|
+
const git = execGit ?? shell_command_projection_cjs_1.execGit;
|
|
264
|
+
try {
|
|
265
|
+
const normalizedPhaseDir = phaseDir.replace(/\\/g, '/');
|
|
266
|
+
const pathspec = `${normalizedPhaseDir}/*-PLAN.md`;
|
|
267
|
+
const r = git(['log', '--format=%H', '--diff-filter=A', '-1', '--', pathspec], { cwd, timeout: 15_000 });
|
|
268
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
269
|
+
return null;
|
|
270
|
+
const sha = r.stdout.trim();
|
|
271
|
+
return sha || null;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Reject characters/sequences that have no legitimate use in a git revision
|
|
279
|
+
* expression reaching this module (a ref name, SHA, or `<ref>~N` / `<ref>^N`
|
|
280
|
+
* / `<ref>@{...}` navigation) but that a hostile `--since` value could use to
|
|
281
|
+
* confuse either the shell-out or a downstream reader: whitespace, ASCII
|
|
282
|
+
* control characters, and the option-shaped `?`, `*`, `[`, `\` characters
|
|
283
|
+
* that `git check-ref-format` also disallows in ref *names*. A leading `-`
|
|
284
|
+
* is rejected outright — that is the actual option-injection vector `--end-
|
|
285
|
+
* of-options` (below) already neutralizes, so this is belt-and-suspenders
|
|
286
|
+
* for older git. `..` is rejected because `sinceRef` is a single revision
|
|
287
|
+
* that this function itself turns into a range (`sinceRef..HEAD`); a
|
|
288
|
+
* `sinceRef` that already contains `..` can only produce a malformed or
|
|
289
|
+
* misleading range. A trailing `.lock` is rejected per `check-ref-format`.
|
|
290
|
+
*
|
|
291
|
+
* Deliberately NOT rejected: `~`, `^`, `:`, `@`, `{`, `}` — `check-ref-
|
|
292
|
+
* format` disallows these in a bare ref *name*, but this value is a git
|
|
293
|
+
* *revision expression*, and rejecting them would break entirely ordinary
|
|
294
|
+
* user input such as `HEAD~1`, `HEAD^`, or `main@{yesterday}`. None of
|
|
295
|
+
* these characters can reintroduce option parsing once `--end-of-options`
|
|
296
|
+
* is in effect, so allowing them costs nothing security-wise.
|
|
297
|
+
*/
|
|
298
|
+
function isSafeRevisionRef(ref) {
|
|
299
|
+
if (ref === '')
|
|
300
|
+
return false;
|
|
301
|
+
if (ref.startsWith('-'))
|
|
302
|
+
return false;
|
|
303
|
+
if (/[\x00-\x1f\x7f ?*[\\]/.test(ref))
|
|
304
|
+
return false;
|
|
305
|
+
if (ref.includes('..'))
|
|
306
|
+
return false;
|
|
307
|
+
if (ref.endsWith('.lock'))
|
|
308
|
+
return false;
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* List files changed between `sinceRef` and `HEAD`, NUL-delimited and
|
|
313
|
+
* quotepath-safe. Load-bearing details (see the router contract):
|
|
314
|
+
* - `-z` and `-c core.quotepath=false` avoid git's lossy quote-and-escape
|
|
315
|
+
* round-trip for non-ASCII paths;
|
|
316
|
+
* - splitting on `NUL` (never `\n`) tolerates a filename containing a real
|
|
317
|
+
* newline (git permits it);
|
|
318
|
+
* - `sinceRef` is validated by `isSafeRevisionRef` AND the revision-range
|
|
319
|
+
* argument is preceded by `--end-of-options`. A trailing `--` alone does
|
|
320
|
+
* NOT stop git from option-parsing an argument that appears BEFORE it —
|
|
321
|
+
* it only stops PATHSPEC interpretation of arguments AFTER it — so
|
|
322
|
+
* `--since '--output=/tmp/pwn'` would otherwise become the argument
|
|
323
|
+
* `--output=/tmp/pwn..HEAD`, which git accepts as an option and uses to
|
|
324
|
+
* redirect diff output to an attacker-chosen path. `--end-of-options`
|
|
325
|
+
* (git >= 2.24) is the correct fix: everything after it is parsed as a
|
|
326
|
+
* revision or path, never as an option, regardless of leading `-`.
|
|
327
|
+
*
|
|
328
|
+
* Bounded (`timeout: 15_000`), degrades to `null` when `sinceRef` fails
|
|
329
|
+
* validation or the underlying git call fails (non-zero exit, timeout, or
|
|
330
|
+
* spawn error) — never throws. An empty result set (no files changed
|
|
331
|
+
* between the two revisions) is a valid, non-null answer: `[]`.
|
|
332
|
+
*/
|
|
333
|
+
function changedFilesSince(cwd, sinceRef, execGit) {
|
|
334
|
+
if (!isSafeRevisionRef(sinceRef))
|
|
335
|
+
return null;
|
|
336
|
+
const git = execGit ?? shell_command_projection_cjs_1.execGit;
|
|
337
|
+
try {
|
|
338
|
+
const r = git(['-c', 'core.quotepath=false', 'diff', '--name-only', '-z', '--end-of-options', `${sinceRef}..HEAD`, '--'], { cwd, timeout: 15_000 });
|
|
339
|
+
if (r.exitCode !== 0)
|
|
340
|
+
return null;
|
|
341
|
+
if (!r.stdout)
|
|
342
|
+
return [];
|
|
343
|
+
return r.stdout.split('\0').filter((f) => f.length > 0);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
247
349
|
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
248
350
|
/**
|
|
249
351
|
* CLI command: `gsd-tools git base-branch`
|
|
@@ -27,6 +27,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
27
27
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
28
28
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
29
29
|
const clock_cjs_1 = require("./clock.cjs");
|
|
30
|
+
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
31
32
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
32
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -126,6 +127,12 @@ function parseTaskMustHaves(content) {
|
|
|
126
127
|
/**
|
|
127
128
|
* Read all task plan files from a GSD-2 tasks/ directory.
|
|
128
129
|
*/
|
|
130
|
+
// #3183 (ADR-3180 Decision 4(a) — bucket B, out of scope for the
|
|
131
|
+
// scanPhasePlans migration): this reads a FOREIGN GSD-2 legacy project's own
|
|
132
|
+
// `tasks/` directory convention (`T##-PLAN.md`) during a one-time import —
|
|
133
|
+
// it is not this project's `.planning/phases/<phase>/` layout at all, has no
|
|
134
|
+
// nested-plans/superseded-status concept, and scanPhasePlans's grammar
|
|
135
|
+
// (which is scoped to GSD's OWN phase directories) does not apply here.
|
|
129
136
|
function readTasksDir(tasksDir) {
|
|
130
137
|
if (!node_fs_1.default.existsSync(tasksDir))
|
|
131
138
|
return [];
|
|
@@ -294,7 +301,9 @@ function buildStateMd(phaseMap) {
|
|
|
294
301
|
const currentEntry = phaseMap.find(p => !p.slice.done);
|
|
295
302
|
const totalPhases = phaseMap.length;
|
|
296
303
|
const donePhases = phaseMap.filter(p => p.slice.done).length;
|
|
297
|
-
|
|
304
|
+
// ADR-3180 D7: one owner for completion percent. clampPercent's 100 ceiling is
|
|
305
|
+
// unreachable here (donePhases is a subset of totalPhases) — the value is unchanged.
|
|
306
|
+
const pct = (0, phase_lifecycle_cjs_1.clampPercent)(donePhases, totalPhases);
|
|
298
307
|
const currentPhaseNum = currentEntry ? zeroPad(currentEntry.phaseNum) : zeroPad(totalPhases);
|
|
299
308
|
const currentSlug = currentEntry ? slugify(currentEntry.slice.title) : 'complete';
|
|
300
309
|
const status = currentEntry ? 'Ready to plan' : 'All phases complete';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent Install rule (Phase 11, #3309, ADR-3180 §8.2/§8.3/§8.5).
|
|
4
|
+
*
|
|
5
|
+
* One code, W010, ported behavior-preserving from `cmdValidateHealth`'s
|
|
6
|
+
* agent-install block (`src/verify.cts:1992-2027`). That block wraps a
|
|
7
|
+
* single `checkAgentsInstalled(_slashRuntime, cwd)` call in a try/catch that
|
|
8
|
+
* swallows any thrown exception silently ("agent check is non-blocking",
|
|
9
|
+
* `verify.cts:2025-2027`) and then branches on the SAME subject —
|
|
10
|
+
* "agent installation is incomplete" — across four mutually exclusive
|
|
11
|
+
* combinations of `missing_agents`/`incomplete_agents`, firing at most one
|
|
12
|
+
* `addIssue('warning', 'W010', ...)` per call. Per this phase's design doc
|
|
13
|
+
* ("Rejected alternatives" §3), these four sites are confirmed to be one
|
|
14
|
+
* subject varying only in trigger detail, not four subjects — W010 stays a
|
|
15
|
+
* single code.
|
|
16
|
+
*
|
|
17
|
+
* `snapshot.agentInstall` (`src/planning-snapshot.cts`'s `buildAgentInstallField`)
|
|
18
|
+
* already performs the try/catch this rule used to need: `scope` is
|
|
19
|
+
* `UNREADABLE` only when the scan itself threw, mirroring
|
|
20
|
+
* `cmdValidateHealth`'s silent catch — this rule reproduces that silence by
|
|
21
|
+
* returning no diagnostic for `UNREADABLE`, rather than inventing a new,
|
|
22
|
+
* more severe 5th case the original never had.
|
|
23
|
+
*
|
|
24
|
+
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
|
|
25
|
+
* ("Rule table organization" — Agent installation group)
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic-types.cjs is an export= CommonJS module
|
|
28
|
+
const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
|
|
29
|
+
const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const planningScopeMod = require("../planning-scope.cjs");
|
|
32
|
+
const { SCOPE } = planningScopeMod;
|
|
33
|
+
const package_identity_cjs_1 = require("../package-identity.cjs");
|
|
34
|
+
/**
|
|
35
|
+
* `check(snapshot)` for W010 — see module header for the exact 4-way
|
|
36
|
+
* branching this ports from `verify.cts:1992-2027`, and its message/fix
|
|
37
|
+
* templates copied verbatim (only the interpolated `agentStatus.*` values
|
|
38
|
+
* differ per call).
|
|
39
|
+
*/
|
|
40
|
+
function checkAgentInstall(snapshot) {
|
|
41
|
+
const { value: status, scope } = snapshot.agentInstall;
|
|
42
|
+
// Mirrors verify.cts:2025-2027's try/catch around the checkAgentsInstalled
|
|
43
|
+
// call itself — a thrown scan is swallowed, not reported. `scope` here is
|
|
44
|
+
// UNREADABLE only in that same case (buildAgentInstallField's own catch).
|
|
45
|
+
if (scope === SCOPE.UNREADABLE)
|
|
46
|
+
return [];
|
|
47
|
+
if (status.agents_installed)
|
|
48
|
+
return [];
|
|
49
|
+
// verify.cts:1995 — zero agents installed at all.
|
|
50
|
+
if (status.installed_agents.length === 0) {
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
code: 'W010',
|
|
54
|
+
severity: SEVERITY.WARNING,
|
|
55
|
+
message: `No GSD agents found in ${status.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`,
|
|
56
|
+
remedy: adviseRemedy(`Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
// verify.cts:2002 — some agents incomplete (missing a generated file), zero fully missing.
|
|
61
|
+
if (status.incomplete_agents.length > 0 && status.missing_agents.length === 0) {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
code: 'W010',
|
|
65
|
+
severity: SEVERITY.WARNING,
|
|
66
|
+
message: `Incomplete agent installs (missing generated file): ${status.incomplete_agents.join(', ')} — affected workflows may fall back to general-purpose`,
|
|
67
|
+
remedy: adviseRemedy(`Re-run the GSD installer to complete the install: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
// verify.cts:2009 — both missing AND incomplete agents present.
|
|
72
|
+
if (status.incomplete_agents.length > 0) {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
code: 'W010',
|
|
76
|
+
severity: SEVERITY.WARNING,
|
|
77
|
+
message: `Missing ${status.missing_agents.length} GSD agents: ${status.missing_agents.join(', ')}; incomplete agent installs (missing generated file): ${status.incomplete_agents.join(', ')} — affected workflows will fall back to general-purpose`,
|
|
78
|
+
remedy: adviseRemedy(`Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
// verify.cts:2017 — agents missing only (no incomplete).
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
code: 'W010',
|
|
86
|
+
severity: SEVERITY.WARNING,
|
|
87
|
+
message: `Missing ${status.missing_agents.length} GSD agents: ${status.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
|
|
88
|
+
remedy: adviseRemedy(`Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const RULES = [
|
|
93
|
+
{
|
|
94
|
+
code: 'W010',
|
|
95
|
+
severity: SEVERITY.WARNING,
|
|
96
|
+
description: 'GSD agent installation missing or incomplete',
|
|
97
|
+
repairable: false,
|
|
98
|
+
check: checkAgentInstall,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
module.exports = { RULES };
|