@opengsd/gsd-core 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Milestone Release Tags — #5705 (epic #3628, Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* `gsd-tools milestone release-tags --map <file> [--dry-run]`: preflight and
|
|
6
|
+
* create annotated tags across the repositories listed in `sub_repos` from a
|
|
7
|
+
* caller-supplied map `[{ repo, tag, ref?, message?, skip? }]`.
|
|
8
|
+
*
|
|
9
|
+
* Contract (the result shape is read by Phases 2-3 of the epic, so it is fixed
|
|
10
|
+
* here and pinned by tests):
|
|
11
|
+
*
|
|
12
|
+
* { ok, phase: 'preflight' | 'apply' | 'dry_run',
|
|
13
|
+
* repos: { [key]: { status, tag?, commit?, reason?, error?,
|
|
14
|
+
* existing_commit?, requested_commit?, detached_head? } },
|
|
15
|
+
* created: string[], // "repo:tag" for every tag written
|
|
16
|
+
* errors: [{ repo?, tag?, reason, error? }] }
|
|
17
|
+
*
|
|
18
|
+
* `key` is the normalized repo path. When one repo receives more than one
|
|
19
|
+
* non-skipped entry (two tags), every entry of that repo is keyed `repo:tag`
|
|
20
|
+
* instead, so no result is overwritten.
|
|
21
|
+
*
|
|
22
|
+
* Every entry is preflighted before the first write, so a predictable failure
|
|
23
|
+
* (unknown repo, bad tag name, unresolved ref, conflicting tag) writes nothing
|
|
24
|
+
* anywhere. Tag creation itself is not transactional across repositories: a
|
|
25
|
+
* failure after the first write is reported with the list of tags already
|
|
26
|
+
* written (`created`), never rolled back, and a re-run with the same map
|
|
27
|
+
* reports those tags as `already_tagged` and retries only the failed ones.
|
|
28
|
+
*
|
|
29
|
+
* Nothing is pushed, no existing tag is ever overwritten, and every git call is
|
|
30
|
+
* an argv array run with the target repository as cwd under a 15 s bound.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RELEASE_TAG_REASONS = exports.MAX_RELEASE_TAG_ENTRIES = void 0;
|
|
37
|
+
exports.preflightReleaseTags = preflightReleaseTags;
|
|
38
|
+
exports.createReleaseTags = createReleaseTags;
|
|
39
|
+
exports.cmdMilestoneReleaseTags = cmdMilestoneReleaseTags;
|
|
40
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
43
|
+
const security_cjs_1 = require("./security.cjs");
|
|
44
|
+
const command_arg_projection_cjs_1 = require("./command-arg-projection.cjs");
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const ioMod = require("./io.cjs");
|
|
47
|
+
const { output, ERROR_REASON } = ioMod;
|
|
48
|
+
const error = ioMod.error;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const configLoaderMod = require("./config-loader.cjs");
|
|
51
|
+
const { loadConfig } = configLoaderMod;
|
|
52
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
53
|
+
/** Maximum entries accepted in one request. Exactly this many are accepted. */
|
|
54
|
+
exports.MAX_RELEASE_TAG_ENTRIES = 256;
|
|
55
|
+
/** Bound on every git subprocess (ms). */
|
|
56
|
+
const RELEASE_TAG_GIT_TIMEOUT_MS = 15000;
|
|
57
|
+
/** Largest `--map` file read from disk (bytes). */
|
|
58
|
+
const MAX_MAP_FILE_BYTES = 1_048_576;
|
|
59
|
+
/** Every reason code this module can emit — a closed set, pinned against CONTEXT.md. */
|
|
60
|
+
exports.RELEASE_TAG_REASONS = Object.freeze([
|
|
61
|
+
'no_sub_repos',
|
|
62
|
+
'invalid_request',
|
|
63
|
+
'too_many_entries',
|
|
64
|
+
'repo_not_configured',
|
|
65
|
+
'repo_path_invalid',
|
|
66
|
+
'repo_missing',
|
|
67
|
+
'not_a_git_repo',
|
|
68
|
+
'tag_invalid',
|
|
69
|
+
'ref_unresolved',
|
|
70
|
+
'tag_conflict',
|
|
71
|
+
'duplicate_entry',
|
|
72
|
+
'git_timeout',
|
|
73
|
+
'tag_create_failed',
|
|
74
|
+
]);
|
|
75
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
76
|
+
function newResult(phase) {
|
|
77
|
+
// Null prototype: a hostile repo key such as `__proto__` is plain data.
|
|
78
|
+
return { ok: true, phase, repos: Object.create(null), created: [], errors: [] };
|
|
79
|
+
}
|
|
80
|
+
function topLevelError(reason, message) {
|
|
81
|
+
const result = newResult('preflight');
|
|
82
|
+
result.ok = false;
|
|
83
|
+
result.errors.push({ reason, error: message });
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function isPlainObject(v) {
|
|
87
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a repository path to a forward-slash relative form, or null when
|
|
91
|
+
* it is absolute, contains a `..` segment (either separator), a NUL, or is
|
|
92
|
+
* empty. Backslashes are converted FIRST so `a\..\b` cannot slip past.
|
|
93
|
+
*/
|
|
94
|
+
function normalizeRepoPath(raw) {
|
|
95
|
+
const s = raw.replace(/\\/g, '/');
|
|
96
|
+
if (s === '' || s.includes('\0') || s.startsWith('/') || /^[A-Za-z]:/.test(s))
|
|
97
|
+
return null;
|
|
98
|
+
const segments = s.split('/');
|
|
99
|
+
if (segments.includes('..'))
|
|
100
|
+
return null;
|
|
101
|
+
const kept = segments.filter((seg) => seg !== '' && seg !== '.');
|
|
102
|
+
return kept.length === 0 ? null : kept.join('/');
|
|
103
|
+
}
|
|
104
|
+
function readSubRepos(cwd) {
|
|
105
|
+
const config = loadConfig(cwd);
|
|
106
|
+
const raw = config['sub_repos'];
|
|
107
|
+
const out = new Set();
|
|
108
|
+
if (!Array.isArray(raw))
|
|
109
|
+
return out;
|
|
110
|
+
for (const item of raw) {
|
|
111
|
+
if (typeof item !== 'string')
|
|
112
|
+
continue;
|
|
113
|
+
const n = normalizeRepoPath(item);
|
|
114
|
+
if (n !== null)
|
|
115
|
+
out.add(n);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/** Validate request shape. Returns a top-level failure, or null when well-formed. */
|
|
120
|
+
function validateRequest(entries) {
|
|
121
|
+
if (!Array.isArray(entries)) {
|
|
122
|
+
return topLevelError('invalid_request', 'request must be a JSON array of entry objects');
|
|
123
|
+
}
|
|
124
|
+
if (entries.length > exports.MAX_RELEASE_TAG_ENTRIES) {
|
|
125
|
+
return topLevelError('too_many_entries', `request has ${entries.length} entries; the limit is ${exports.MAX_RELEASE_TAG_ENTRIES}`);
|
|
126
|
+
}
|
|
127
|
+
const result = newResult('preflight');
|
|
128
|
+
entries.forEach((entry, index) => {
|
|
129
|
+
const where = `entry ${index}`;
|
|
130
|
+
if (!isPlainObject(entry)) {
|
|
131
|
+
result.errors.push({ reason: 'invalid_request', error: `${where}: must be an object` });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (typeof entry['repo'] !== 'string') {
|
|
135
|
+
result.errors.push({ reason: 'invalid_request', error: `${where}: "repo" must be a string` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const repo = entry['repo'];
|
|
139
|
+
if (entry['skip'] === true)
|
|
140
|
+
return;
|
|
141
|
+
if (entry['skip'] !== undefined && typeof entry['skip'] !== 'boolean') {
|
|
142
|
+
result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "skip" must be a boolean` });
|
|
143
|
+
}
|
|
144
|
+
if (typeof entry['tag'] !== 'string') {
|
|
145
|
+
result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "tag" must be a string` });
|
|
146
|
+
}
|
|
147
|
+
for (const field of ['ref', 'message']) {
|
|
148
|
+
if (entry[field] !== undefined && typeof entry[field] !== 'string') {
|
|
149
|
+
result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "${field}" must be a string` });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (result.errors.length > 0) {
|
|
154
|
+
result.ok = false;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
// ─── Preflight ────────────────────────────────────────────────────────────
|
|
160
|
+
function runPreflight(cwd, entries, options, phase) {
|
|
161
|
+
const shape = validateRequest(entries);
|
|
162
|
+
if (shape)
|
|
163
|
+
return { result: shape, plans: [] };
|
|
164
|
+
const list = entries;
|
|
165
|
+
const exec = options.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
166
|
+
const timeout = options.timeoutMs ?? RELEASE_TAG_GIT_TIMEOUT_MS;
|
|
167
|
+
const run = (dir, args) => exec(args, { cwd: dir, timeout });
|
|
168
|
+
const subRepos = readSubRepos(cwd);
|
|
169
|
+
if (subRepos.size === 0) {
|
|
170
|
+
return { result: topLevelError('no_sub_repos', 'no sub_repos configured in .planning/config.json'), plans: [] };
|
|
171
|
+
}
|
|
172
|
+
// A repo that receives more than one non-skipped entry is keyed `repo:tag`.
|
|
173
|
+
const perRepo = new Map();
|
|
174
|
+
for (const entry of list) {
|
|
175
|
+
if (entry.skip === true)
|
|
176
|
+
continue;
|
|
177
|
+
const k = normalizeRepoPath(entry.repo) ?? entry.repo;
|
|
178
|
+
perRepo.set(k, (perRepo.get(k) ?? 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
// A repo given both a skip entry and a tag entry is contradictory; the check is
|
|
181
|
+
// order-independent so preflight never reports `skipped` for a repo that gets tagged.
|
|
182
|
+
const skippedRepos = new Set();
|
|
183
|
+
for (const entry of list) {
|
|
184
|
+
if (entry.skip === true)
|
|
185
|
+
skippedRepos.add(normalizeRepoPath(entry.repo) ?? entry.repo);
|
|
186
|
+
}
|
|
187
|
+
const result = newResult(phase);
|
|
188
|
+
const plans = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
for (const entry of list) {
|
|
191
|
+
const rawRepo = entry.repo;
|
|
192
|
+
const normalized = normalizeRepoPath(rawRepo);
|
|
193
|
+
const repo = normalized ?? rawRepo;
|
|
194
|
+
if (entry.skip === true) {
|
|
195
|
+
const skipKey = `skip\0${repo}`;
|
|
196
|
+
if (seen.has(skipKey)) {
|
|
197
|
+
result.ok = false;
|
|
198
|
+
result.errors.push({ repo, reason: 'duplicate_entry' });
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
seen.add(skipKey);
|
|
202
|
+
// When the repo also has a tag entry, that entry records the duplicate_entry error
|
|
203
|
+
// under the same key; writing `skipped` here would overwrite it order-dependently.
|
|
204
|
+
if ((perRepo.get(repo) ?? 0) === 0)
|
|
205
|
+
result.repos[repo] = { status: 'skipped' };
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const tag = entry.tag;
|
|
210
|
+
const key = (perRepo.get(repo) ?? 0) > 1 ? `${repo}:${tag}` : repo;
|
|
211
|
+
const fail = (reason, extra = {}) => {
|
|
212
|
+
result.ok = false;
|
|
213
|
+
result.repos[key] = { status: 'error', tag, reason, ...extra };
|
|
214
|
+
result.errors.push({ repo, tag, reason, ...(extra.error ? { error: extra.error } : {}) });
|
|
215
|
+
};
|
|
216
|
+
if (skippedRepos.has(repo)) {
|
|
217
|
+
fail('duplicate_entry', { error: 'repo has both a skip entry and a tag entry' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (normalized === null || (0, security_cjs_1.tryWithinRoot)(normalized, cwd) === null) {
|
|
221
|
+
fail('repo_path_invalid', { error: 'repo path must be relative and stay inside the project root' });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!subRepos.has(normalized)) {
|
|
225
|
+
fail('repo_not_configured', { error: 'repo is not listed in sub_repos' });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const pairKey = `${normalized}\0${tag}`;
|
|
229
|
+
if (seen.has(pairKey)) {
|
|
230
|
+
result.ok = false;
|
|
231
|
+
result.errors.push({ repo, tag, reason: 'duplicate_entry' });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
seen.add(pairKey);
|
|
235
|
+
const dir = node_path_1.default.join(cwd, normalized);
|
|
236
|
+
let isDir = false;
|
|
237
|
+
try {
|
|
238
|
+
isDir = node_fs_1.default.statSync(dir).isDirectory();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
isDir = false;
|
|
242
|
+
}
|
|
243
|
+
if (!isDir) {
|
|
244
|
+
fail('repo_missing', { error: 'repo directory does not exist' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const top = run(dir, ['rev-parse', '--show-toplevel']);
|
|
248
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(top)) {
|
|
249
|
+
fail('git_timeout', { error: 'git rev-parse timed out' });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
let isOwnWorkTree = false;
|
|
253
|
+
if (top.exitCode === 0) {
|
|
254
|
+
try {
|
|
255
|
+
// `.native` expands Windows 8.3 short names (RUNNER~1) the way git's long-form path does.
|
|
256
|
+
isOwnWorkTree = node_fs_1.default.realpathSync.native(top.stdout) === node_fs_1.default.realpathSync.native(dir);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
isOwnWorkTree = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!isOwnWorkTree) {
|
|
263
|
+
fail('not_a_git_repo', { error: 'directory is not the root of a git work tree' });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (tag === '' || tag.startsWith('-') || tag.includes('\0')) {
|
|
267
|
+
fail('tag_invalid', { error: 'tag name is empty or starts with "-"' });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const fmt = run(dir, ['check-ref-format', `refs/tags/${tag}`]);
|
|
271
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(fmt)) {
|
|
272
|
+
fail('git_timeout', { error: 'git check-ref-format timed out' });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (fmt.exitCode !== 0) {
|
|
276
|
+
fail('tag_invalid', { error: 'tag name rejected by git check-ref-format' });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const refOmitted = entry.ref === undefined;
|
|
280
|
+
const ref = entry.ref ?? 'HEAD';
|
|
281
|
+
if (ref === '' || ref.startsWith('-') || ref.includes('\0')) {
|
|
282
|
+
fail('ref_unresolved', { error: 'ref is empty or starts with "-"' });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const resolved = run(dir, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
286
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(resolved)) {
|
|
287
|
+
fail('git_timeout', { error: 'git rev-parse timed out' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (resolved.exitCode !== 0 || resolved.stdout === '') {
|
|
291
|
+
fail('ref_unresolved', { error: `ref "${ref}" does not resolve to a commit` });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const commit = resolved.stdout;
|
|
295
|
+
let detached;
|
|
296
|
+
if (refOmitted) {
|
|
297
|
+
const head = run(dir, ['symbolic-ref', '-q', 'HEAD']);
|
|
298
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(head)) {
|
|
299
|
+
fail('git_timeout', { error: 'git symbolic-ref timed out' });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (head.exitCode === 1)
|
|
303
|
+
detached = true;
|
|
304
|
+
}
|
|
305
|
+
const existing = run(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{commit}`]);
|
|
306
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(existing)) {
|
|
307
|
+
fail('git_timeout', { error: 'git rev-parse timed out' });
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (existing.exitCode === 0 && existing.stdout !== '') {
|
|
311
|
+
if (existing.stdout === commit) {
|
|
312
|
+
result.repos[key] = { status: 'already_tagged', tag, commit, ...(detached ? { detached_head: true } : {}) };
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
fail('tag_conflict', {
|
|
316
|
+
error: 'tag already exists at a different commit',
|
|
317
|
+
existing_commit: existing.stdout,
|
|
318
|
+
requested_commit: commit,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const anyRef = run(dir, ['show-ref', '--verify', '--quiet', `refs/tags/${tag}`]);
|
|
324
|
+
if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(anyRef)) {
|
|
325
|
+
fail('git_timeout', { error: 'git show-ref timed out' });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (anyRef.exitCode === 0) {
|
|
329
|
+
fail('tag_conflict', {
|
|
330
|
+
error: 'tag already exists and does not point at a commit',
|
|
331
|
+
existing_commit: null,
|
|
332
|
+
requested_commit: commit,
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
result.repos[key] = { status: 'would_create', tag, commit, ...(detached ? { detached_head: true } : {}) };
|
|
337
|
+
plans.push({
|
|
338
|
+
key,
|
|
339
|
+
repo: normalized,
|
|
340
|
+
dir,
|
|
341
|
+
tag,
|
|
342
|
+
message: entry.message === undefined || entry.message === '' ? tag : entry.message,
|
|
343
|
+
commit,
|
|
344
|
+
...(detached ? { detached: true } : {}),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return { result, plans };
|
|
348
|
+
}
|
|
349
|
+
/** Validate a request without writing anything. */
|
|
350
|
+
function preflightReleaseTags(cwd, entries, options = {}) {
|
|
351
|
+
return runPreflight(cwd, entries, options, 'preflight').result;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Preflight, then (unless `dryRun`) create each annotated tag in its own repo.
|
|
355
|
+
* A preflight failure returns before any write. A creation failure does not
|
|
356
|
+
* stop the remaining repos; the result lists every tag already written.
|
|
357
|
+
*/
|
|
358
|
+
function createReleaseTags(cwd, entries, options = {}) {
|
|
359
|
+
const dryRun = options.dryRun === true;
|
|
360
|
+
const { result, plans } = runPreflight(cwd, entries, options, dryRun ? 'dry_run' : 'apply');
|
|
361
|
+
if (!result.ok) {
|
|
362
|
+
result.phase = 'preflight';
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
if (dryRun)
|
|
366
|
+
return result;
|
|
367
|
+
const exec = options.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
368
|
+
const timeout = options.timeoutMs ?? RELEASE_TAG_GIT_TIMEOUT_MS;
|
|
369
|
+
for (const plan of plans) {
|
|
370
|
+
const r = exec(['tag', '-a', '--message', plan.message, '--', plan.tag, plan.commit], { cwd: plan.dir, timeout });
|
|
371
|
+
if (r.exitCode === 0) {
|
|
372
|
+
result.repos[plan.key] = {
|
|
373
|
+
status: 'created',
|
|
374
|
+
tag: plan.tag,
|
|
375
|
+
commit: plan.commit,
|
|
376
|
+
...(plan.detached ? { detached_head: true } : {}),
|
|
377
|
+
};
|
|
378
|
+
result.created.push(`${plan.repo}:${plan.tag}`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const reason = (0, shell_command_projection_cjs_1.isSpawnTimeout)(r) ? 'git_timeout' : 'tag_create_failed';
|
|
382
|
+
const message = r.stderr || r.stdout || ((0, shell_command_projection_cjs_1.isSpawnTimeout)(r) ? 'git tag timed out' : 'git tag failed');
|
|
383
|
+
result.ok = false;
|
|
384
|
+
result.repos[plan.key] = { status: 'failed', tag: plan.tag, commit: plan.commit, reason, error: message };
|
|
385
|
+
result.errors.push({ repo: plan.repo, tag: plan.tag, reason, error: message });
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* `milestone release-tags --map <file> [--dry-run]`. `args` is the full argv
|
|
392
|
+
* tail (`['milestone', 'release-tags', ...flags]`). Prints the result as JSON
|
|
393
|
+
* and sets a non-zero exit code when `ok` is false.
|
|
394
|
+
*/
|
|
395
|
+
function cmdMilestoneReleaseTags(cwd, args, raw) {
|
|
396
|
+
const parsed = (0, command_arg_projection_cjs_1.parseNamedArgsOrExit)(args, { valueFlags: ['map'], booleanFlags: ['dry-run'], positionals: 2 }, error);
|
|
397
|
+
const mapArg = parsed['map'];
|
|
398
|
+
if (typeof mapArg !== 'string' || mapArg === '') {
|
|
399
|
+
error('--map <path-to-json-file> required for milestone release-tags', ERROR_REASON.USAGE);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// A relative --map is relative to the shell's working directory, not the project root.
|
|
403
|
+
const mapPath = node_path_1.default.resolve(process.cwd(), mapArg);
|
|
404
|
+
let mapStat = null;
|
|
405
|
+
let statError = '';
|
|
406
|
+
try {
|
|
407
|
+
mapStat = node_fs_1.default.statSync(mapPath);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
statError = e instanceof Error ? e.message : String(e);
|
|
411
|
+
}
|
|
412
|
+
if (mapStat === null) {
|
|
413
|
+
error(`cannot read --map file: ${statError}`, ERROR_REASON.USAGE);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// A FIFO or device reports size 0 and would bypass the size cap on read.
|
|
417
|
+
if (!mapStat.isFile()) {
|
|
418
|
+
error('--map must be a regular file', ERROR_REASON.USAGE);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (mapStat.size > MAX_MAP_FILE_BYTES) {
|
|
422
|
+
error(`--map file exceeds ${MAX_MAP_FILE_BYTES} byte limit (got ${mapStat.size})`, ERROR_REASON.USAGE);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
let text = '';
|
|
426
|
+
try {
|
|
427
|
+
text = node_fs_1.default.readFileSync(mapPath, 'utf-8');
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
error(`cannot read --map file: ${e instanceof Error ? e.message : String(e)}`, ERROR_REASON.USAGE);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const json = (0, security_cjs_1.safeJsonParse)(text, { maxLength: MAX_MAP_FILE_BYTES, label: '--map file' });
|
|
434
|
+
const result = json.ok
|
|
435
|
+
? createReleaseTags(cwd, json.value, { dryRun: parsed['dry-run'] === true })
|
|
436
|
+
: topLevelError('invalid_request', json.error);
|
|
437
|
+
output(result, raw);
|
|
438
|
+
if (!result.ok)
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
}
|
|
@@ -38,8 +38,9 @@ const stateContract = require("./state-contract.cjs");
|
|
|
38
38
|
const { publishStateContract } = stateContract;
|
|
39
39
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
40
|
const phaseIdMod = require("./phase-id.cjs");
|
|
41
|
-
const {
|
|
41
|
+
const { isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
|
|
42
42
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
43
|
+
const milestone_release_map_cjs_1 = require("./milestone-release-map.cjs");
|
|
43
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
45
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
45
46
|
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, } = roadmapParserMod;
|
|
@@ -54,7 +55,7 @@ const planScanMod = require("./plan-scan.cjs");
|
|
|
54
55
|
const { scanPhasePlans } = planScanMod;
|
|
55
56
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
56
57
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
57
|
-
const { listMilestonePhaseDirs } = phaseLocatorMod;
|
|
58
|
+
const { phaseDirectoryOptions, listMilestonePhaseDirs, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
|
|
58
59
|
const { planningPaths, resolvePhaseIdConvention } = planningWorkspace;
|
|
59
60
|
const { extractFrontmatter } = frontmatterMod;
|
|
60
61
|
// ADR-3408 §8.3 / #3469: `writeStateMd` gets sync and NO preservation — the
|
|
@@ -72,43 +73,6 @@ const { syncAndPreserveStateMd, withStateLock, readModifyWriteStateMd, assertVer
|
|
|
72
73
|
// the caller-supplied `--archive-version` override and the STATE.md-derived
|
|
73
74
|
// live-read value, so a crafted value cannot escape `.planning/milestones/`.
|
|
74
75
|
const ARCHIVE_VERSION_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
75
|
-
/**
|
|
76
|
-
* Scope an `updateTableCell` call to the `## Traceability` (or
|
|
77
|
-
* `## Traceability Status`) heading's own section — up to the next H1/H2
|
|
78
|
-
* heading — instead of handing it the WHOLE REQUIREMENTS.md content.
|
|
79
|
-
*
|
|
80
|
-
* F1 (#2245 review, BLOCKER): `updateTableCell` binds to the FIRST GFM table
|
|
81
|
-
* found in whatever text it is given. The shipped requirements template
|
|
82
|
-
* (gsd-core/templates/requirements.md) puts an `## Out of Scope` table
|
|
83
|
-
* (`| Feature | Reason |`, no `Status` column) BEFORE `## Traceability` — so
|
|
84
|
-
* an unscoped whole-file call targets the Out-of-Scope table instead, fails
|
|
85
|
-
* with `{ok:false, reason:'unknown column: Status'}`, and the real
|
|
86
|
-
* Traceability row is never flipped, while the checkbox surface still flips
|
|
87
|
-
* and the command reports success (the #2140 silent-divergence class one
|
|
88
|
-
* level deeper). Mirrors phase.cts's `editProgressHeadingSlice` scoping of
|
|
89
|
-
* `## Progress` writes to that heading's own slice.
|
|
90
|
-
*
|
|
91
|
-
* Falls back to running `updateTableCell` against the whole `text` when no
|
|
92
|
-
* `## Traceability` heading exists — matching the previous (unscoped)
|
|
93
|
-
* behaviour for a REQUIREMENTS.md whose traceability table sits under some
|
|
94
|
-
* other heading, or with no heading at all (never worse than before this fix).
|
|
95
|
-
*/
|
|
96
|
-
function updateTraceabilityCell(text, match, column, newValue) {
|
|
97
|
-
const headingMatch = text.match(/^##[ \t]+Traceability(?:[ \t]+Status)?\b/im);
|
|
98
|
-
if (!headingMatch || headingMatch.index === undefined) {
|
|
99
|
-
return (0, markdown_table_cjs_1.updateTableCell)(text, match, column, newValue);
|
|
100
|
-
}
|
|
101
|
-
const headingOffset = headingMatch.index;
|
|
102
|
-
const before = text.slice(0, headingOffset);
|
|
103
|
-
const fromHeading = text.slice(headingOffset);
|
|
104
|
-
const nextHeadingOffset = fromHeading.search(/\n#{1,2}[ \t]/);
|
|
105
|
-
const scoped = nextHeadingOffset >= 0 ? fromHeading.slice(0, nextHeadingOffset) : fromHeading;
|
|
106
|
-
const after = nextHeadingOffset >= 0 ? fromHeading.slice(nextHeadingOffset) : '';
|
|
107
|
-
const result = (0, markdown_table_cjs_1.updateTableCell)(scoped, match, column, newValue);
|
|
108
|
-
if (!result.ok)
|
|
109
|
-
return result;
|
|
110
|
-
return { ok: true, value: before + result.value + after };
|
|
111
|
-
}
|
|
112
76
|
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
113
77
|
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
|
114
78
|
error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
|
|
@@ -186,7 +150,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
186
150
|
// Complete" gate is folded into the newValue callback so one
|
|
187
151
|
// updateTableCell call both probes the current value and writes.
|
|
188
152
|
let tableHit = false;
|
|
189
|
-
const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
|
|
153
|
+
const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
|
|
190
154
|
// #2788: accept `Gaps Found` as a forward input too — `revert-phase` (the
|
|
191
155
|
// documented gaps_found response) leaves a row stranded at Gaps Found with
|
|
192
156
|
// no inverse; a genuinely-satisfied requirement must be able to reach
|
|
@@ -229,7 +193,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
229
193
|
// that genuinely exists. Probe via a no-op updateTableCell write (its own
|
|
230
194
|
// tolerant scan) instead of findTableWithColumns (whole-table parse gate).
|
|
231
195
|
let currentStatusCell = '';
|
|
232
|
-
const statusProbe = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
|
|
196
|
+
const statusProbe = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
|
|
233
197
|
currentStatusCell = current;
|
|
234
198
|
return current;
|
|
235
199
|
});
|
|
@@ -433,7 +397,7 @@ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
|
|
|
433
397
|
// "only flip Pending -> Complete" gate, in reverse).
|
|
434
398
|
const rowMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === reqId.toLowerCase();
|
|
435
399
|
let tableHit = false;
|
|
436
|
-
const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
|
|
400
|
+
const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
|
|
437
401
|
if (/^complete$/i.test(current.trim())) {
|
|
438
402
|
tableHit = true;
|
|
439
403
|
return ' Gaps Found ';
|
|
@@ -524,6 +488,41 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
524
488
|
const phasesDir = planningPaths(cwd).phases;
|
|
525
489
|
const today = clock_cjs_1.realClock.localToday();
|
|
526
490
|
const milestoneName = options.name || version;
|
|
491
|
+
// #5706 (epic #3628): validate the caller-supplied release map BEFORE any
|
|
492
|
+
// read-for-decision or write below, so a bad map leaves the tree untouched
|
|
493
|
+
// (no archive directory, no MILESTONES.md or STATE.md change). An archived
|
|
494
|
+
// map is a recorded value: a differing map for an already-archived version is
|
|
495
|
+
// refused rather than silently overwritten; an identical one is left as is.
|
|
496
|
+
let releaseMap = null;
|
|
497
|
+
let releaseMapFile = null;
|
|
498
|
+
let releaseMapNeedsWrite = false;
|
|
499
|
+
if (typeof options.releaseMap === 'string') {
|
|
500
|
+
const loaded = (0, milestone_release_map_cjs_1.loadReleaseMapFile)(node_path_1.default.resolve(cwd, options.releaseMap));
|
|
501
|
+
if (!loaded.ok) {
|
|
502
|
+
error(`milestone complete: invalid --release-map — nothing has been changed.\n${loaded.errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
releaseMap = loaded.map;
|
|
506
|
+
releaseMapFile = (0, milestone_release_map_cjs_1.releaseMapArchivePath)(archiveDir, version);
|
|
507
|
+
const serialized = (0, milestone_release_map_cjs_1.serializeReleaseMapArchive)(version, releaseMap);
|
|
508
|
+
let existingText = null;
|
|
509
|
+
try {
|
|
510
|
+
existingText = node_fs_1.default.readFileSync(releaseMapFile, 'utf-8');
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
if (e.code !== 'ENOENT') {
|
|
514
|
+
error(`milestone complete: ${node_path_1.default.basename(releaseMapFile)} already exists and cannot be read — nothing has been changed.`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (existingText !== null && existingText !== serialized) {
|
|
518
|
+
error(`milestone complete: ${node_path_1.default.basename(releaseMapFile)} already exists with a different release map; the archived map is a recorded value and is not overwritten — nothing has been changed.`);
|
|
519
|
+
}
|
|
520
|
+
releaseMapNeedsWrite = existingText === null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const releaseMapReport = releaseMap && releaseMapFile
|
|
524
|
+
? { path: node_path_1.default.relative(cwd, releaseMapFile).split(node_path_1.default.sep).join('/'), entries: releaseMap.length }
|
|
525
|
+
: null;
|
|
527
526
|
// ADR-3408 §8.5 / #3469: "liberal but visible" — when the write-seam
|
|
528
527
|
// composition's preservation stage restores a curated frontmatter value
|
|
529
528
|
// over a disagreeing freshly-derived one, that divergence is surfaced
|
|
@@ -549,7 +548,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
549
548
|
// `platformEnsureDir(archiveDir)` below — a refused run (scope not COMPLETE,
|
|
550
549
|
// no --force) must be a true no-op on disk, and creating the archive
|
|
551
550
|
// directory first left an empty directory behind even on refusal.
|
|
552
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
|
|
551
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd, version, undefined, undefined, phaseDirectoryOptions(cwd));
|
|
553
552
|
if (isDirInMilestone.missingExplicitVersion) {
|
|
554
553
|
error(`no phases found for milestone ${version} in ROADMAP.md`);
|
|
555
554
|
}
|
|
@@ -574,6 +573,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
574
573
|
`entries, even though the ROADMAP has phase entries elsewhere), so phase scoping ` +
|
|
575
574
|
`cannot be trusted for this destructive operation. Re-run with --force to override.`);
|
|
576
575
|
}
|
|
576
|
+
// #2761/#4304: resolve once for every phase-directory consumer in this
|
|
577
|
+
// command. The matcher opts in only for bracket; the milestone enumerator
|
|
578
|
+
// retains the full resolved value it already accepted.
|
|
579
|
+
const phaseConvention = resolvePhaseIdConvention(cwd);
|
|
577
580
|
// Guard: prevent marking complete when ROADMAP still lists phases that have
|
|
578
581
|
// no directory on disk (disk_status: no_directory). This catches the case
|
|
579
582
|
// where the active milestone was erroneously marked complete before phases
|
|
@@ -655,8 +658,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
655
658
|
// properly-scaffolded bracket phase reads as disk_status: 'no_directory'
|
|
656
659
|
// — turning a real bracket project's own phases into false "ROADMAP
|
|
657
660
|
// lists N unstarted phase(s)" failures on `milestone complete`.
|
|
658
|
-
const
|
|
659
|
-
const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET,
|
|
661
|
+
const directoryContext = phaseDirectoryOptions(cwd, undefined, scopedContent);
|
|
662
|
+
const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, phaseConvention, directoryContext);
|
|
660
663
|
const noDirectoryPhases = [];
|
|
661
664
|
let pm;
|
|
662
665
|
const phaseDirEntries = (() => {
|
|
@@ -680,12 +683,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
680
683
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this local check already covered both 0 and 999; now delegates to the single canonical owner.
|
|
681
684
|
if (isSentinelPhaseId(phaseNum))
|
|
682
685
|
continue;
|
|
683
|
-
const
|
|
686
|
+
const lookup = resolvePhaseDirectoryLookup(cwd, phaseNum, undefined, directoryContext);
|
|
684
687
|
// A phase has disk_status: 'no_directory' when no phase directory
|
|
685
688
|
// with a matching token exists on disk. Use the same matchPhaseDirs
|
|
686
689
|
// owner that roadmap.analyze uses to avoid false positives on decimal
|
|
687
|
-
// (2.1) and letter-suffix (12A) phase IDs. (#2528)
|
|
688
|
-
|
|
690
|
+
// (2.1) and letter-suffix (12A) phase IDs. (#2528) Under #4304 the
|
|
691
|
+
// current checkout's bracket convention and migration-window legacy
|
|
692
|
+
// fallback are both owned by the shared lookup adapter.
|
|
693
|
+
const hasDirectory = matchPhaseDirsForLookup(phaseDirEntries, lookup, undefined, directoryContext).matches.length > 0;
|
|
689
694
|
if (!hasDirectory) {
|
|
690
695
|
noDirectoryPhases.push(phaseNum);
|
|
691
696
|
}
|
|
@@ -715,7 +720,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
715
720
|
let totalPlans = 0;
|
|
716
721
|
let totalTasks = 0;
|
|
717
722
|
const accomplishments = [];
|
|
718
|
-
// #2761
|
|
723
|
+
// #2761: resolved ONCE above, ambiently (no explicit `ws` —
|
|
719
724
|
// this function has none of its own and already resolves everything else
|
|
720
725
|
// off `cwd` via planningPaths(cwd), matching the ambient-workstream
|
|
721
726
|
// contract `resolvePhaseIdConvention`/`planningDir` share), and threaded
|
|
@@ -730,7 +735,6 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
730
735
|
// the silent-inherit path without changing behavior for `null` /
|
|
731
736
|
// `milestone-prefixed` projects, whose resolved convention is the same
|
|
732
737
|
// value `undefined` would have lazily produced anyway.
|
|
733
|
-
const phaseConvention = resolvePhaseIdConvention(cwd);
|
|
734
738
|
// #3597 (ADR-3180 Decision 2): SINGLE resolution of "which phase
|
|
735
739
|
// directories belong to the current milestone" AND the SCOPE discriminator
|
|
736
740
|
// that resolution came from — shared verbatim by the read-only stats loop
|
|
@@ -860,6 +864,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
860
864
|
phases_archive_skipped: phasesArchiveSkippedForScope,
|
|
861
865
|
phases_archive_skip_reason: phasesArchiveSkipReason,
|
|
862
866
|
quick: quickDirsToArchive,
|
|
867
|
+
...(releaseMapReport ? { release_map: { target: releaseMapReport.path, entries: releaseMapReport.entries } } : {}),
|
|
863
868
|
},
|
|
864
869
|
would_update: {
|
|
865
870
|
milestones_md: node_path_1.default.relative(cwd, milestonesPath).split(node_path_1.default.sep).join('/'),
|
|
@@ -885,6 +890,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
885
890
|
// archive directory behind. Reaching this point means the run is
|
|
886
891
|
// committed to mutating.
|
|
887
892
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(archiveDir);
|
|
893
|
+
// #5706: archive the validated release map by recorded value.
|
|
894
|
+
if (releaseMap && releaseMapFile && releaseMapNeedsWrite) {
|
|
895
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(releaseMapFile, (0, milestone_release_map_cjs_1.serializeReleaseMapArchive)(version, releaseMap));
|
|
896
|
+
}
|
|
888
897
|
// Archive ROADMAP.md
|
|
889
898
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
890
899
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
@@ -909,7 +918,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
909
918
|
}
|
|
910
919
|
// Create/append MILESTONES.md entry
|
|
911
920
|
const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
|
|
912
|
-
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
|
|
921
|
+
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n${releaseMap ? `${(0, milestone_release_map_cjs_1.renderReleaseMapMarkdown)(releaseMap)}\n` : ''}---\n\n`;
|
|
913
922
|
// #3685: mirror requirementsUpdated's diff-tracking contract — the result
|
|
914
923
|
// below used to report `milestones_updated: true` hardcoded, never
|
|
915
924
|
// consulting whether the MILESTONES.md write actually changed anything.
|
|
@@ -1166,6 +1175,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
1166
1175
|
milestones_updated: milestonesUpdated,
|
|
1167
1176
|
state_updated: stateUpdated,
|
|
1168
1177
|
preservation_warnings: preservationWarnings,
|
|
1178
|
+
...(releaseMapReport ? { release_map: releaseMapReport } : {}),
|
|
1169
1179
|
};
|
|
1170
1180
|
output(result, raw);
|
|
1171
1181
|
// #3227 (design doc §40 row 26 / "Not-corruption" rule): a refreshed
|