@opengsd/gsd-core 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Undo Commit Selection — issue #4661 (absorbed into epic #4906, Phase 5).
|
|
4
|
+
*
|
|
5
|
+
* Pure scope-matching logic for `/gsd:undo --phase N` / `--plan N-M`, replacing
|
|
6
|
+
* the two hand-rolled `git log --oneline | grep -E ...` pipelines that used to
|
|
7
|
+
* live in `gsd-core/workflows/undo.md`. Those greps were unanchored substring
|
|
8
|
+
* matches against a live regex built from unvalidated user input — four
|
|
9
|
+
* documented bug classes (see #4661):
|
|
10
|
+
*
|
|
11
|
+
* 1. The id was interpolated raw into an ERE: `.` and `+` are live
|
|
12
|
+
* metacharacters there, so a malformed or merely differently-shaped id
|
|
13
|
+
* silently matched the WRONG phase.
|
|
14
|
+
* 2. The grep was unanchored, so a commit that only MENTIONS a scope
|
|
15
|
+
* (`docs(99-01): explain feat(03-01): commit convention`) was wrongly
|
|
16
|
+
* selected as a DECLARATION of it.
|
|
17
|
+
* 3. Phase-mode's `\):` suffix and plan-mode's missing suffix disagreed on
|
|
18
|
+
* the same commit (`feat(03-01)!: breaking change`).
|
|
19
|
+
* 4. The "obvious" fix — anchoring `git log --oneline`'s record start —
|
|
20
|
+
* is a trap: it silently drops `fixup! ...` / `Revert "..."` wrapper
|
|
21
|
+
* commits (a worse failure, a silent partial revert) and returns zero
|
|
22
|
+
* matches under `color.ui=always` (ANSI codes precede the sha).
|
|
23
|
+
*
|
|
24
|
+
* This module closes all four by parsing each commit subject through the
|
|
25
|
+
* SAME anchored conventional-commit grammar the changelog/PR-title gate uses
|
|
26
|
+
* (`HEADER_RE`, reused verbatim from `scripts/release-notes/conventional-title.cjs`
|
|
27
|
+
* — see the require() below), then comparing the extracted scope to the
|
|
28
|
+
* caller-supplied target id by exact STRING EQUALITY. No regex is built from
|
|
29
|
+
* user input at any point past `validatePhaseNumber` (src/security.cts),
|
|
30
|
+
* which the CLI caller (`gsd-tools.cjs query select-revert-commits`) runs
|
|
31
|
+
* BEFORE this module ever sees the id.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately no `fs`/`child_process` in this file: it operates purely on
|
|
34
|
+
* `{sha, subject}` records the caller already fetched (e.g. via `git log
|
|
35
|
+
* --format=%H%x00%s`), so it is unit-testable without touching git or the
|
|
36
|
+
* filesystem.
|
|
37
|
+
*
|
|
38
|
+
* KNOWN LIMIT (documented, not a defect): `unwrapSubject` unwraps ONE level
|
|
39
|
+
* of a `fixup! `/`squash! `/`Revert "..."` wrapper. A doubly-wrapped subject
|
|
40
|
+
* (e.g. `Revert "fixup! feat(03-01): work"`) is treated as unparseable (no
|
|
41
|
+
* declared scope) after that single unwrap — it is NOT selected, never
|
|
42
|
+
* mis-selected. This bounds the module to the wrapper shapes git itself
|
|
43
|
+
* produces (`fixup!`/`squash!` from `--fixup`/`--squash`, `Revert "..."` from
|
|
44
|
+
* `git revert`), each of which wraps a single ordinary commit subject, not
|
|
45
|
+
* another wrapper.
|
|
46
|
+
*/
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.selectCommitsByDeclaredScope = selectCommitsByDeclaredScope;
|
|
52
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
53
|
+
// Reuse the exact HEADER_RE the changelog classifier and PR-title CI gate
|
|
54
|
+
// already depend on (#1549) — never fork a second copy of this grammar. A
|
|
55
|
+
// bare `require()` (not a TS `import ... = require(...)`) is used
|
|
56
|
+
// deliberately: `scripts/` sits outside this build's `rootDir` ("src", see
|
|
57
|
+
// tsconfig.build.json) and carries no `.d.ts`, so a type-checked import-equals
|
|
58
|
+
// would fail module resolution (TS7016) under `strict`/`noImplicitAny`. A
|
|
59
|
+
// plain `require()` resolves through Node's ambient `NodeRequire` type
|
|
60
|
+
// (`(id: string) => any`) instead, sidestepping that check entirely — the
|
|
61
|
+
// cast below is the only place the resulting shape is asserted.
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
63
|
+
const conventionalTitle = require(node_path_1.default.resolve(__dirname, '..', '..', '..', 'scripts', 'release-notes', 'conventional-title.cjs'));
|
|
64
|
+
const { HEADER_RE } = conventionalTitle;
|
|
65
|
+
// ─── Subject unwrapping (Known limit: exactly ONE level) ──────────────────
|
|
66
|
+
const FIXUP_PREFIX = 'fixup! ';
|
|
67
|
+
const SQUASH_PREFIX = 'squash! ';
|
|
68
|
+
const REVERT_RE = /^Revert "(.*)"$/;
|
|
69
|
+
/**
|
|
70
|
+
* Strip exactly one `fixup! `/`squash! `/`Revert "..."` wrapper layer off a
|
|
71
|
+
* commit subject, returning the (possibly) unwrapped remainder. Never
|
|
72
|
+
* recurses — a doubly-wrapped subject is returned with only its outermost
|
|
73
|
+
* layer removed, which is intentionally still unparseable by `HEADER_RE` (see
|
|
74
|
+
* module doc comment).
|
|
75
|
+
*/
|
|
76
|
+
function unwrapSubject(subject) {
|
|
77
|
+
if (subject.startsWith(FIXUP_PREFIX))
|
|
78
|
+
return subject.slice(FIXUP_PREFIX.length);
|
|
79
|
+
if (subject.startsWith(SQUASH_PREFIX))
|
|
80
|
+
return subject.slice(SQUASH_PREFIX.length);
|
|
81
|
+
const revertMatch = REVERT_RE.exec(subject);
|
|
82
|
+
if (revertMatch)
|
|
83
|
+
return revertMatch[1];
|
|
84
|
+
return subject;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse a (possibly wrapped) commit subject into its declared conventional-
|
|
88
|
+
* commit scope, or `null` when the subject carries no parseable scope: no
|
|
89
|
+
* clean `type[(scope)][!]:` header at all, or a header with no `(scope)`
|
|
90
|
+
* group. Never regex-matches against untrusted `targetId` — this function
|
|
91
|
+
* only ever consumes the commit's OWN subject text.
|
|
92
|
+
*/
|
|
93
|
+
function parseDeclaredScope(subject) {
|
|
94
|
+
const unwrapped = unwrapSubject(subject);
|
|
95
|
+
const m = HEADER_RE.exec(unwrapped);
|
|
96
|
+
if (!m)
|
|
97
|
+
return null;
|
|
98
|
+
const scopeWithParens = m[2]; // e.g. "(03-01)" — includes parens, or undefined
|
|
99
|
+
if (!scopeWithParens)
|
|
100
|
+
return null;
|
|
101
|
+
// HEADER_RE's scope group is `\([^)]*\)` — always at least the two paren
|
|
102
|
+
// characters when present, so slicing them off is safe unconditionally.
|
|
103
|
+
return scopeWithParens.slice(1, -1);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Select commits whose DECLARED conventional-commit scope identifies
|
|
107
|
+
* `targetId`, by exact string equality (`'plan'` mode) or exact-or-phase-
|
|
108
|
+
* prefixed equality (`'phase'` mode). `targetId` is assumed ALREADY
|
|
109
|
+
* VALIDATED/NORMALIZED by the caller (e.g. via `validatePhaseNumber` from
|
|
110
|
+
* `src/security.cts`) — this function performs no validation of its own, and
|
|
111
|
+
* never builds a regex from it (string equality / `startsWith` only), so a
|
|
112
|
+
* malformed or metacharacter-laden `targetId` cannot cause a wildcard-style
|
|
113
|
+
* over-match the way the retired grep pipelines could.
|
|
114
|
+
*/
|
|
115
|
+
function selectCommitsByDeclaredScope(commits, targetId, mode = 'plan') {
|
|
116
|
+
const phasePrefix = `${targetId}-`;
|
|
117
|
+
const classified = commits.map(({ sha, subject }) => {
|
|
118
|
+
const parsedScope = parseDeclaredScope(subject);
|
|
119
|
+
let matched = false;
|
|
120
|
+
if (parsedScope !== null) {
|
|
121
|
+
matched = mode === 'phase'
|
|
122
|
+
? parsedScope === targetId || parsedScope.startsWith(phasePrefix)
|
|
123
|
+
: parsedScope === targetId;
|
|
124
|
+
}
|
|
125
|
+
return { sha, subject, parsedScope, matched };
|
|
126
|
+
});
|
|
127
|
+
const selected = classified
|
|
128
|
+
.filter((c) => c.matched)
|
|
129
|
+
.map(({ sha, subject }) => ({ sha, subject }));
|
|
130
|
+
return { selected, classified };
|
|
131
|
+
}
|
|
@@ -51,7 +51,7 @@ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
|
51
51
|
const security_cjs_1 = require("./security.cjs");
|
|
52
52
|
const { output, error } = io;
|
|
53
53
|
const { extractPhaseToken, scopeToPhase } = phaseId;
|
|
54
|
-
const { extractFrontmatter } = frontmatterMod;
|
|
54
|
+
const { extractFrontmatter, FRONTMATTER_UNPARSEABLE } = frontmatterMod;
|
|
55
55
|
const { normalizeLineEndings } = coreUtilsMod;
|
|
56
56
|
const { SCOPE } = planningScopeMod;
|
|
57
57
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -102,8 +102,14 @@ const VERIFICATION_ROUTING_TABLE = {
|
|
|
102
102
|
},
|
|
103
103
|
stale: {
|
|
104
104
|
status: 'stale',
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
// #4682: staleness means covered source files changed after the verifier
|
|
106
|
+
// last ran — the only remedy is re-running the verifier.
|
|
107
|
+
// /gsd-verify-work never rewrites VERIFICATION.md, so advising it from
|
|
108
|
+
// here was an advice loop. execute-phase resumes at the verification
|
|
109
|
+
// gates and re-runs the verifier (its resume tree routes a stale report
|
|
110
|
+
// to re-verification), which regenerates VERIFICATION.md and its digest.
|
|
111
|
+
next_action: 'Verification is stale — covered source files changed after the verifier last ran. Re-run execute-phase for this phase: it resumes at the verification gates and re-runs the verifier, regenerating VERIFICATION.md and its digest. verify-work alone cannot refresh a stale report.',
|
|
112
|
+
next_command: 'execute-phase',
|
|
107
113
|
},
|
|
108
114
|
// INTERNAL SENTINEL: constructed when no *-VERIFICATION.md file exists or when
|
|
109
115
|
// the file has no parseable frontmatter status. Never emitted by the verifier.
|
|
@@ -112,6 +118,15 @@ const VERIFICATION_ROUTING_TABLE = {
|
|
|
112
118
|
next_action: 'No verification report found — the verify step never completed. Running execute-phase is safe here: it resumes at the verification gates and does not re-run plans that already have a SUMMARY.md (see #2868).',
|
|
113
119
|
next_command: 'execute-phase',
|
|
114
120
|
},
|
|
121
|
+
// #4806: the report EXISTS but its frontmatter is not parseable YAML —
|
|
122
|
+
// fundamentally different from "missing" (the verify step DID run; re-running
|
|
123
|
+
// execute-phase cannot fix a YAML typo). Consumers treat any non-'passed'
|
|
124
|
+
// status as blocking, so this value fails safe while telling the truth.
|
|
125
|
+
unparseable: {
|
|
126
|
+
status: 'unparseable',
|
|
127
|
+
next_action: "The *-VERIFICATION.md frontmatter is not parseable YAML — fix the syntax error in the report itself. Re-running execute-phase cannot fix a YAML typo in an existing report.",
|
|
128
|
+
next_command: '',
|
|
129
|
+
},
|
|
115
130
|
// INTERNAL SENTINEL: constructed when the file has a status value not in
|
|
116
131
|
// VERIFIER_STATUSES. Never emitted by the verifier.
|
|
117
132
|
unknown: {
|
|
@@ -174,8 +189,95 @@ function canonicalizeCoveredFiles(files) {
|
|
|
174
189
|
* Bump on any change to the digest's input shape (path list, hashing order,
|
|
175
190
|
* per-file hash algorithm) so an old stored digest can never collide with a
|
|
176
191
|
* differently-computed new one — a version mismatch is just a mismatch.
|
|
192
|
+
*
|
|
193
|
+
* Version history:
|
|
194
|
+
* v1 (#4155) — every covered path's whole bytes, uniformly.
|
|
195
|
+
* v2 (#4623) — repo-wide planning documents (`isSharedPlanningDoc`) are
|
|
196
|
+
* excluded from the hash by construction.
|
|
197
|
+
*
|
|
198
|
+
* A stored digest names its own version (`v<N>:sha256:…`), and
|
|
199
|
+
* `readVerificationStatus` recomputes under the STORED version rather than
|
|
200
|
+
* this constant — so bumping it does not flip every already-verified phase
|
|
201
|
+
* to `stale` on upgrade. A legacy v1 report keeps v1 semantics, shared
|
|
202
|
+
* documents included, until it is re-fingerprinted; only a version outside
|
|
203
|
+
* `KNOWN_FINGERPRINT_VERSIONS` is unrecomputable and fails closed.
|
|
177
204
|
*/
|
|
178
|
-
const FINGERPRINT_VERSION =
|
|
205
|
+
const FINGERPRINT_VERSION = 2;
|
|
206
|
+
const KNOWN_FINGERPRINT_VERSIONS = new Set([1, 2]);
|
|
207
|
+
/**
|
|
208
|
+
* #4623: the planning roots whose DIRECT children are repo-wide planning
|
|
209
|
+
* documents, as project-root-relative posix paths. Always `.planning`; plus
|
|
210
|
+
* the phase's OWN planning root when a phase directory is known — the parent
|
|
211
|
+
* of its `phases/` directory, which is how `planningDir` lays out every
|
|
212
|
+
* scope (`.planning`, `.planning/<project>`, `.planning/workstreams/<ws>`,
|
|
213
|
+
* `.planning/<project>/workstreams/<ws>`; `planning-workspace.cts`). Derived
|
|
214
|
+
* from the phase's position rather than from a list of layouts so a
|
|
215
|
+
* workstream-scoped `ROADMAP.md` is recognised without this function
|
|
216
|
+
* knowing what a workstream is, and so `.planning/research/notes.md` is
|
|
217
|
+
* NOT mistaken for one — lexically the two are indistinguishable from
|
|
218
|
+
* `.planning/<project>/ROADMAP.md`. A phase directory that does not sit
|
|
219
|
+
* under the project root (unit fixtures at a bare tmpdir) contributes no
|
|
220
|
+
* extra root.
|
|
221
|
+
*/
|
|
222
|
+
function sharedPlanningRoots(projectRoot, phaseDir) {
|
|
223
|
+
const roots = ['.planning'];
|
|
224
|
+
if (phaseDir) {
|
|
225
|
+
const phasesDir = node_path_1.default.dirname(node_path_1.default.resolve(phaseDir));
|
|
226
|
+
const planningDir = node_path_1.default.dirname(phasesDir);
|
|
227
|
+
const rel = normalizeRel(node_path_1.default.relative(node_path_1.default.resolve(projectRoot), planningDir));
|
|
228
|
+
// Two structural checks, both load-bearing: the phase dir's PARENT must be
|
|
229
|
+
// the `phases/` directory `planningDir` lays every scope out with, and the
|
|
230
|
+
// derived root must sit inside `.planning/`. Without them any accepted
|
|
231
|
+
// directory — `<root>/src/phases/01-fake` — would nominate `src` as a
|
|
232
|
+
// planning root and silently drop real implementation evidence from the
|
|
233
|
+
// digest (found by the cross-AI review of this change). A shape that fails
|
|
234
|
+
// either check contributes no extra root; `.planning` itself is already
|
|
235
|
+
// present.
|
|
236
|
+
if (node_path_1.default.basename(phasesDir) === 'phases' &&
|
|
237
|
+
rel.startsWith('.planning/') &&
|
|
238
|
+
!rel.includes('/../') &&
|
|
239
|
+
!roots.includes(rel)) {
|
|
240
|
+
roots.push(rel);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return roots;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* #4623: a covered path names a repo-wide planning document when it sits
|
|
247
|
+
* DIRECTLY under one of `sharedPlanningRoots` — `ROADMAP.md`,
|
|
248
|
+
* `REQUIREMENTS.md`, `STATE.md`, `PROJECT.md`, `MILESTONES.md`,
|
|
249
|
+
* `config.json`, … — as opposed to a phase's own artifacts under
|
|
250
|
+
* `<root>/phases/<phase>/` or a research note under `.planning/research/`.
|
|
251
|
+
* Every phase rewrites these as ordinary bookkeeping (a roadmap checkbox, a
|
|
252
|
+
* requirement's traceability cell, STATE.md's position), so hashing their
|
|
253
|
+
* whole bytes into one phase's digest coupled every phase's staleness to
|
|
254
|
+
* every other phase's close — and to its OWN close, since `phase.complete`
|
|
255
|
+
* and `requirements mark-complete` write them after the verifier has
|
|
256
|
+
* already run.
|
|
257
|
+
*
|
|
258
|
+
* Defined by position, not by a name list, so the set cannot drift as new
|
|
259
|
+
* top-level planning documents appear (the tree already carries a dozen).
|
|
260
|
+
* `rel` is expected posix-normalized (`canonicalizeCoveredFiles`), so a
|
|
261
|
+
* `./.planning/ROADMAP.md` spelling has already collapsed to the bare form.
|
|
262
|
+
*/
|
|
263
|
+
function isSharedPlanningDoc(rel, roots = ['.planning']) {
|
|
264
|
+
if (rel === '' || rel.endsWith('/'))
|
|
265
|
+
return false;
|
|
266
|
+
return roots.includes(node_path_1.default.posix.dirname(rel));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* #4623: the fingerprint version a stored `covered_digest` was computed
|
|
270
|
+
* under, or `null` when the prefix is absent, malformed, or names a version
|
|
271
|
+
* this build cannot recompute (an unknown version is a mismatch by
|
|
272
|
+
* construction — the fail-closed shape `FINGERPRINT_VERSION`'s doc promises).
|
|
273
|
+
*/
|
|
274
|
+
function parseFingerprintVersion(digest) {
|
|
275
|
+
const m = /^v(\d+):sha256:/.exec(digest);
|
|
276
|
+
if (!m)
|
|
277
|
+
return null;
|
|
278
|
+
const version = Number(m[1]);
|
|
279
|
+
return KNOWN_FINGERPRINT_VERSIONS.has(version) ? version : null;
|
|
280
|
+
}
|
|
179
281
|
/**
|
|
180
282
|
* #4155: recompute the deterministic content fingerprint over a verifier's
|
|
181
283
|
* declared covered-input set (phase PLAN/SUMMARY, mapped requirements,
|
|
@@ -216,14 +318,23 @@ const FINGERPRINT_VERSION = 1;
|
|
|
216
318
|
* confinement work (against `projectRoot`, the correct boundary for this
|
|
217
319
|
* data), so no security property is lost by bypassing a narrower seam here.
|
|
218
320
|
*/
|
|
219
|
-
function computeCoveredDigest(projectRoot, coveredFiles) {
|
|
321
|
+
function computeCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_VERSION, opts = {}) {
|
|
322
|
+
// #4623: `version` selects the input shape to hash under — the CURRENT
|
|
323
|
+
// one for a fresh fingerprint (the CLI verb), or the STORED one when
|
|
324
|
+
// `readVerificationStatus` recomputes against a report's own digest.
|
|
325
|
+
// `opts.phaseDir` lets v2 recognise the phase's own planning root
|
|
326
|
+
// (`sharedPlanningRoots`); without it only `.planning/` itself is shared.
|
|
327
|
+
if (!KNOWN_FINGERPRINT_VERSIONS.has(version))
|
|
328
|
+
return null;
|
|
220
329
|
const uniqueSorted = canonicalizeCoveredFiles(coveredFiles);
|
|
221
330
|
if (uniqueSorted.length === 0)
|
|
222
331
|
return null;
|
|
332
|
+
const sharedRoots = version >= 2 ? sharedPlanningRoots(projectRoot, opts.phaseDir) : [];
|
|
333
|
+
let hashed = 0;
|
|
223
334
|
// Canonicalize the root ONCE — every candidate's realpath is checked against
|
|
224
335
|
// this, not the possibly-symlinked `projectRoot` argument itself. Always via
|
|
225
336
|
// the REAL fs, never fsImpl: `projectRoot` is a trusted anchor the CALLER
|
|
226
|
-
// derived (
|
|
337
|
+
// derived (resolveProjectRoot), not attacker-influenced covered-input data —
|
|
227
338
|
// routing it through a caller-scoped containment seam (e.g. #4155's
|
|
228
339
|
// containmentEnforcingVerificationFs, confined to `.planning/`, a proper
|
|
229
340
|
// SUBSET of `projectRoot`) would reject the root itself and fail every
|
|
@@ -262,6 +373,15 @@ function computeCoveredDigest(projectRoot, coveredFiles) {
|
|
|
262
373
|
const st = node_fs_1.default.statSync(real);
|
|
263
374
|
if (!st.isFile())
|
|
264
375
|
return null;
|
|
376
|
+
// #4623 (v2+): a repo-wide planning document is VALIDATED exactly as
|
|
377
|
+
// every other covered path — confined, present, a regular file; the
|
|
378
|
+
// fail-closed contract above is unchanged — but its bytes contribute
|
|
379
|
+
// nothing to the digest. It may stay declared in `covered_files` (the
|
|
380
|
+
// verifier's instructions long said to list the mapped requirement,
|
|
381
|
+
// and every report already written does); its bookkeeping churn can
|
|
382
|
+
// no longer read as drift.
|
|
383
|
+
if (isSharedPlanningDoc(rel, sharedRoots))
|
|
384
|
+
continue;
|
|
265
385
|
bytes = node_fs_1.default.readFileSync(real);
|
|
266
386
|
}
|
|
267
387
|
catch {
|
|
@@ -269,12 +389,19 @@ function computeCoveredDigest(projectRoot, coveredFiles) {
|
|
|
269
389
|
}
|
|
270
390
|
const fileHash = node_crypto_1.default.createHash('sha256').update(bytes).digest('hex');
|
|
271
391
|
parts.push(`${rel}\n${fileHash}\n`);
|
|
392
|
+
hashed++;
|
|
272
393
|
}
|
|
394
|
+
// #4623 (v2+): a declaration made ONLY of shared planning documents has no
|
|
395
|
+
// evidence in it at all — a constant digest over the header would satisfy
|
|
396
|
+
// the fingerprint pair while grounding the verification in nothing. Fail
|
|
397
|
+
// closed, the same way an empty declaration does.
|
|
398
|
+
if (version >= 2 && hashed === 0)
|
|
399
|
+
return null;
|
|
273
400
|
const aggregate = node_crypto_1.default
|
|
274
401
|
.createHash('sha256')
|
|
275
|
-
.update(`v${
|
|
402
|
+
.update(`v${version}\n${parts.join('')}`, 'utf-8')
|
|
276
403
|
.digest('hex');
|
|
277
|
-
return `v${
|
|
404
|
+
return `v${version}:sha256:${aggregate}`;
|
|
278
405
|
}
|
|
279
406
|
/**
|
|
280
407
|
* #4155: the content fingerprint only recomputes digests for paths the
|
|
@@ -687,6 +814,16 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
687
814
|
// same root cause as the false-clean class fixed elsewhere in #3707-CR.
|
|
688
815
|
const content = normalizeLineEndings(fsImpl.readFileSync(filePath, 'utf-8'));
|
|
689
816
|
fm = extractFrontmatter(content, filePath);
|
|
817
|
+
// #4806: an unparseable frontmatter block is NOT "missing" — the file
|
|
818
|
+
// exists and verification ran. Report a distinct status so the caller is
|
|
819
|
+
// sent to fix the YAML, not to re-run execute-phase.
|
|
820
|
+
if (fm[FRONTMATTER_UNPARSEABLE] === true) {
|
|
821
|
+
return {
|
|
822
|
+
status: 'unparseable',
|
|
823
|
+
next_action: "The *-VERIFICATION.md frontmatter is not parseable YAML — fix the syntax error in the report itself. Re-running execute-phase cannot fix a YAML typo in an existing report.",
|
|
824
|
+
next_command: '',
|
|
825
|
+
};
|
|
826
|
+
}
|
|
690
827
|
const statusVal = fm['status'];
|
|
691
828
|
// status is always a scalar string in a well-formed VERIFICATION.md frontmatter;
|
|
692
829
|
// only accept string values — arrays and objects are not valid status values.
|
|
@@ -741,9 +878,19 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
741
878
|
// short-circuit in turn: the live-directory re-scan (for a plan/summary
|
|
742
879
|
// added AFTER verification and never declared in covered_files) only
|
|
743
880
|
// runs once the digest itself has already matched.
|
|
881
|
+
//
|
|
882
|
+
// #4623: recompute under the STORED digest's own version, not the
|
|
883
|
+
// current constant — a v1 report written before the shared-document
|
|
884
|
+
// exclusion keeps v1 semantics rather than going stale on upgrade. An
|
|
885
|
+
// unknown version parses to `null`, which `computeCoveredDigest`
|
|
886
|
+
// refuses (returns `null`), so the compare below fails closed.
|
|
887
|
+
const storedVersion = hasWellFormedFingerprint && typeof coveredDigestVal === 'string'
|
|
888
|
+
? parseFingerprintVersion(coveredDigestVal)
|
|
889
|
+
: null;
|
|
744
890
|
isStale =
|
|
745
891
|
!hasWellFormedFingerprint ||
|
|
746
|
-
|
|
892
|
+
storedVersion === null ||
|
|
893
|
+
computeCoveredDigest((0, project_root_cjs_1.resolveProjectRoot)(phaseDir), coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal ||
|
|
747
894
|
!allCurrentArtifactsCovered(phaseDir, coveredFilesVal);
|
|
748
895
|
}
|
|
749
896
|
else {
|
|
@@ -761,7 +908,10 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
761
908
|
return {
|
|
762
909
|
status: entry.status,
|
|
763
910
|
next_action: entry.next_action,
|
|
764
|
-
|
|
911
|
+
// #4682: execute-phase resumes at the verification gates and re-runs
|
|
912
|
+
// the verifier, regenerating VERIFICATION.md and its digest — the same
|
|
913
|
+
// routing the `missing` sentinel has used since #2868.
|
|
914
|
+
next_command: projectNextCommand('execute-phase', runtime, phaseArg),
|
|
765
915
|
};
|
|
766
916
|
}
|
|
767
917
|
// 3. Route — exclude internal sentinels from raw-file lookup (they are
|
|
@@ -898,6 +1048,69 @@ function cmdVerificationResolveFile(cwd, phaseDirArg, raw) {
|
|
|
898
1048
|
}
|
|
899
1049
|
output({ verification_file: verificationPath }, raw, verificationPath);
|
|
900
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* #4623: parse the argv tokens after `verification.fingerprint <phaseDir>`
|
|
1053
|
+
* into a covered-file list. The router hands over a raw positional slice,
|
|
1054
|
+
* so every `--files`-style form other `gsd-tools` verbs accept (`commit
|
|
1055
|
+
* --files a b`, `docs/CLI-TOOLS.md`) used to reach `computeCoveredDigest`
|
|
1056
|
+
* with the literal token `--files` — or an unsplit `"a,b"` — as a covered
|
|
1057
|
+
* path, and the whole command failed closed with "a covered file is
|
|
1058
|
+
* missing, unreadable, or escapes the project root". On the reporting
|
|
1059
|
+
* project that message convinced two people the digest was permanently
|
|
1060
|
+
* unrecomputable.
|
|
1061
|
+
*
|
|
1062
|
+
* Accepted, all equivalent and freely mixed:
|
|
1063
|
+
* - bare positionals `a b` (the documented form, unchanged)
|
|
1064
|
+
* - a single flag `--files a`
|
|
1065
|
+
* - a comma-separated value `--files a,b` (also `--files=a,b`)
|
|
1066
|
+
* - a repeated flag `--files a --files b`
|
|
1067
|
+
*
|
|
1068
|
+
* Only a `--files` VALUE is comma-split: a bare positional keeps its bytes,
|
|
1069
|
+
* so the documented form's behaviour on a comma-bearing filename is
|
|
1070
|
+
* unchanged. Any other `--flag` is an explicit usage error, never a path —
|
|
1071
|
+
* a mis-typed flag must not fail as "file missing" again. (`--raw` never
|
|
1072
|
+
* reaches here; the CLI entry point splices it out before routing.)
|
|
1073
|
+
*/
|
|
1074
|
+
function parseFingerprintFileArgs(tokens) {
|
|
1075
|
+
const files = [];
|
|
1076
|
+
const EMPTY_VALUE = '--files requires at least one path for verification.fingerprint (a path, or a comma-separated list)';
|
|
1077
|
+
const splitList = (value) => value
|
|
1078
|
+
.split(',')
|
|
1079
|
+
.map((s) => s.trim())
|
|
1080
|
+
.filter((s) => s.length > 0);
|
|
1081
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1082
|
+
const token = tokens[i];
|
|
1083
|
+
if (token === '--files') {
|
|
1084
|
+
const value = tokens[i + 1];
|
|
1085
|
+
if (value === undefined || value.startsWith('--')) {
|
|
1086
|
+
return { error: '--files requires a value for verification.fingerprint (a path, or a comma-separated list)' };
|
|
1087
|
+
}
|
|
1088
|
+
const list = splitList(value);
|
|
1089
|
+
// An empty or all-comma value is a usage error, never a silent no-op —
|
|
1090
|
+
// the caller would otherwise meet the generic zero-files error and go
|
|
1091
|
+
// looking for a missing path.
|
|
1092
|
+
if (list.length === 0)
|
|
1093
|
+
return { error: EMPTY_VALUE };
|
|
1094
|
+
files.push(...list);
|
|
1095
|
+
i++;
|
|
1096
|
+
}
|
|
1097
|
+
else if (token.startsWith('--files=')) {
|
|
1098
|
+
const list = splitList(token.slice('--files='.length));
|
|
1099
|
+
if (list.length === 0)
|
|
1100
|
+
return { error: EMPTY_VALUE };
|
|
1101
|
+
files.push(...list);
|
|
1102
|
+
}
|
|
1103
|
+
else if (token.startsWith('--')) {
|
|
1104
|
+
return {
|
|
1105
|
+
error: `unknown flag ${token} for verification.fingerprint (covered files are bare positionals or --files <a[,b]>, repeatable)`,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
files.push(token);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return { files };
|
|
1113
|
+
}
|
|
901
1114
|
/**
|
|
902
1115
|
* CLI command handler (#4155): compute the covered-input fingerprint the
|
|
903
1116
|
* verifier embeds in VERIFICATION.md frontmatter (`covered_files`,
|
|
@@ -913,7 +1126,13 @@ function cmdVerificationResolveFile(cwd, phaseDirArg, raw) {
|
|
|
913
1126
|
* @param cwd - Current working directory.
|
|
914
1127
|
* @param phaseDirArg - Phase directory path (absolute or relative to cwd);
|
|
915
1128
|
* its project root is the base covered paths resolve against.
|
|
916
|
-
*
|
|
1129
|
+
* Must be an existing directory (#4623): with the
|
|
1130
|
+
* phase dir omitted, the first covered file used to be
|
|
1131
|
+
* taken as the phase dir and the rest hashed — a
|
|
1132
|
+
* plausible digest over the wrong set, at exit 0.
|
|
1133
|
+
* @param fileArgs - The argv tokens after the phase dir, parsed by
|
|
1134
|
+
* `parseFingerprintFileArgs`: covered-input paths
|
|
1135
|
+
* relative to the project root, bare or via `--files`.
|
|
917
1136
|
* @param raw - Whether to emit raw (non-JSON) output: just the
|
|
918
1137
|
* `covered_digest` string, so `VAR=$(gsd_run query
|
|
919
1138
|
* verification.fingerprint "$PHASE_DIR" ... --raw)` is
|
|
@@ -921,17 +1140,34 @@ function cmdVerificationResolveFile(cwd, phaseDirArg, raw) {
|
|
|
921
1140
|
* from the caller's own input list in that mode, so
|
|
922
1141
|
* only the computed digest needs a raw form.
|
|
923
1142
|
*/
|
|
924
|
-
function cmdVerificationFingerprint(cwd, phaseDirArg,
|
|
1143
|
+
function cmdVerificationFingerprint(cwd, phaseDirArg, fileArgs, raw) {
|
|
925
1144
|
if (!phaseDirArg) {
|
|
926
1145
|
error('phase directory required for verification.fingerprint');
|
|
927
1146
|
return;
|
|
928
1147
|
}
|
|
1148
|
+
const phaseDir = node_path_1.default.resolve(cwd, phaseDirArg);
|
|
1149
|
+
let phaseDirIsDir = false;
|
|
1150
|
+
try {
|
|
1151
|
+
phaseDirIsDir = node_fs_1.default.statSync(phaseDir).isDirectory();
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
// not found → not a directory
|
|
1155
|
+
}
|
|
1156
|
+
if (!phaseDirIsDir) {
|
|
1157
|
+
error(`phase directory not found: ${phaseDirArg} — verification.fingerprint takes the phase directory first, then the covered files`);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const parsed = parseFingerprintFileArgs(fileArgs);
|
|
1161
|
+
if ('error' in parsed) {
|
|
1162
|
+
error(parsed.error);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const files = parsed.files;
|
|
929
1166
|
if (files.length === 0) {
|
|
930
1167
|
error('at least one covered file required for verification.fingerprint');
|
|
931
1168
|
return;
|
|
932
1169
|
}
|
|
933
|
-
const
|
|
934
|
-
const projectRoot = (0, project_root_cjs_1.findProjectRoot)(phaseDir);
|
|
1170
|
+
const projectRoot = (0, project_root_cjs_1.resolveProjectRoot)(phaseDir);
|
|
935
1171
|
// canonicalizeCoveredFiles here is for the emitted `covered_files` field —
|
|
936
1172
|
// computeCoveredDigest canonicalizes its own `coveredFiles` argument
|
|
937
1173
|
// internally too (it must, for callers like readVerificationStatus that
|
|
@@ -939,8 +1175,21 @@ function cmdVerificationFingerprint(cwd, phaseDirArg, files, raw) {
|
|
|
939
1175
|
// already-canonical list keeps that internal pass a cheap no-op rather
|
|
940
1176
|
// than a second meaningfully different canonicalization.
|
|
941
1177
|
const uniqueSorted = canonicalizeCoveredFiles(files);
|
|
942
|
-
const digest = computeCoveredDigest(projectRoot, uniqueSorted);
|
|
1178
|
+
const digest = computeCoveredDigest(projectRoot, uniqueSorted, FINGERPRINT_VERSION, { phaseDir });
|
|
943
1179
|
if (digest === null) {
|
|
1180
|
+
// #4623: name the one null that is NOT a bad path — a declaration made
|
|
1181
|
+
// only of shared planning documents hashes nothing under v2, and the
|
|
1182
|
+
// generic message below would send the caller looking for a missing file
|
|
1183
|
+
// that is not missing. Discriminated AFTER the v2 attempt, and only when a
|
|
1184
|
+
// v1 pass over the same list (which hashes, and therefore validates, every
|
|
1185
|
+
// path) succeeds: an all-shared list with a missing or directory member is
|
|
1186
|
+
// a bad path first, and gets the generic message.
|
|
1187
|
+
const sharedRoots = sharedPlanningRoots(projectRoot, phaseDir);
|
|
1188
|
+
if (uniqueSorted.every((f) => isSharedPlanningDoc(f, sharedRoots)) &&
|
|
1189
|
+
computeCoveredDigest(projectRoot, uniqueSorted, 1) !== null) {
|
|
1190
|
+
error(`could not compute fingerprint — every covered file is a repo-wide planning document (direct children of ${sharedRoots.join(', ')} never enter the digest); declare the phase's own artifacts and implementation files`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
944
1193
|
error('could not compute fingerprint — a covered file is missing, unreadable, or escapes the project root');
|
|
945
1194
|
return;
|
|
946
1195
|
}
|
|
@@ -958,5 +1207,9 @@ module.exports = {
|
|
|
958
1207
|
cmdVerificationStatus,
|
|
959
1208
|
cmdVerificationResolveFile,
|
|
960
1209
|
computeCoveredDigest,
|
|
1210
|
+
sharedPlanningRoots,
|
|
1211
|
+
isSharedPlanningDoc,
|
|
1212
|
+
parseFingerprintVersion,
|
|
1213
|
+
parseFingerprintFileArgs,
|
|
961
1214
|
cmdVerificationFingerprint,
|
|
962
1215
|
};
|
|
@@ -32,6 +32,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
32
32
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
33
33
|
const node_path_1 = __importDefault(require("node:path"));
|
|
34
34
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
35
|
+
const security_cjs_1 = require("./security.cjs");
|
|
35
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
36
37
|
const phaseIdMod = require("./phase-id.cjs");
|
|
37
38
|
const { stripProjectCodePrefix, extractPhaseToken, comparePhaseNum } = phaseIdMod;
|
|
@@ -380,6 +381,25 @@ function splitSegments(cmd) {
|
|
|
380
381
|
.map(s => s.trim())
|
|
381
382
|
.filter(s => s.length > 0);
|
|
382
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Decode entity-escaped ampersands (`&` → `&`) — #4730, the grounding
|
|
386
|
+
* gate's share of the #3611 defect. Planners emit `<automated>` bodies with
|
|
387
|
+
* `&&` as the chain operator, and the executing agent reads the
|
|
388
|
+
* decoded (rendered) form. Without this decode, `splitSegments` cuts
|
|
389
|
+
* `&&` at its semicolons and a `cd` target absorbs the trailing
|
|
390
|
+
* `&` fragment (`rawTarget: "src &"`), reporting an existing directory
|
|
391
|
+
* as a `missing_dir` blocker. Applied to the command text inside
|
|
392
|
+
* `resolveVerifyCommandTarget` — before any segment splitting or target
|
|
393
|
+
* resolution, after `result.command` has captured the text verbatim — so the
|
|
394
|
+
* escaped and literal forms of the same command produce identical verdicts.
|
|
395
|
+
* Kept module-private beside `splitSegments`, mirroring the sibling
|
|
396
|
+
* `src/verify.cts`'s own private `decodeEntityAmps` (#3611): the two gates
|
|
397
|
+
* are separate modules that independently parse `<automated>` text, and
|
|
398
|
+
* neither imports the other.
|
|
399
|
+
*/
|
|
400
|
+
function decodeEntityAmps(s) {
|
|
401
|
+
return s.replace(/&/g, '&');
|
|
402
|
+
}
|
|
383
403
|
/** Strip a single matching pair of surrounding quotes, if present. */
|
|
384
404
|
function stripQuotes(s) {
|
|
385
405
|
if (s.length >= 2) {
|
|
@@ -438,6 +458,16 @@ function stripLeadingDotSlash(s) {
|
|
|
438
458
|
* without touching the filesystem. A climb that names a concrete sibling (e.g.
|
|
439
459
|
* `cd ../../frontend`, the exact #2401 shape) still names something checkable
|
|
440
460
|
* and falls through to the normal filesystem probe below.
|
|
461
|
+
*
|
|
462
|
+
* An ABSOLUTE target outside the project root takes the same `outside_root`
|
|
463
|
+
* exit (#4767). It is more ambiguous across worktrees, not less: it is pinned
|
|
464
|
+
* to exactly one checkout, and when a planner copies the orchestrator's cwd
|
|
465
|
+
* into `<automated>` that checkout is the main tree — so under worktree
|
|
466
|
+
* isolation the command exists, runs, and passes against code the worktree
|
|
467
|
+
* changed and the main tree did not. Existence is therefore not evidence for
|
|
468
|
+
* an absolute target outside the root, and the filesystem is not consulted.
|
|
469
|
+
* Containment goes through `tryWithinRootLexical` (#4636) — lexical because the
|
|
470
|
+
* probe is read-only and must not depend on the target existing.
|
|
441
471
|
*/
|
|
442
472
|
function isPureAncestorClimb(rel) {
|
|
443
473
|
if (rel.length === 0)
|
|
@@ -501,7 +531,10 @@ function resolveVerifyCommandTarget(command, options) {
|
|
|
501
531
|
if (typeof command !== 'string')
|
|
502
532
|
return result;
|
|
503
533
|
result.command = command;
|
|
504
|
-
|
|
534
|
+
// #4730: decode entity-escaped ampersands BEFORE segment splitting (see
|
|
535
|
+
// decodeEntityAmps) so the chain operator is visible to the splitter and
|
|
536
|
+
// the verdict matches what the executing agent's decoded form grounds to.
|
|
537
|
+
const trimmed = decodeEntityAmps(command.trim());
|
|
505
538
|
if (trimmed === '')
|
|
506
539
|
return result;
|
|
507
540
|
// Nyquist "MISSING — Wave 0 must create …" sentinel; Dimension 8 owns it.
|
|
@@ -550,7 +583,18 @@ function resolveVerifyCommandTarget(command, options) {
|
|
|
550
583
|
result.form = form;
|
|
551
584
|
result.rawTarget = rawTarget;
|
|
552
585
|
result.target = target;
|
|
553
|
-
if (
|
|
586
|
+
if (isAbs) {
|
|
587
|
+
// #4767: an absolute target outside projectRoot is `outside_root`, same
|
|
588
|
+
// as the bare climb — see isPureAncestorClimb's doc for why existence is
|
|
589
|
+
// not evidence here and the filesystem is deliberately not consulted.
|
|
590
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(target, base) === null) {
|
|
591
|
+
result.status = 'ok';
|
|
592
|
+
result.severity = 'warning';
|
|
593
|
+
result.reason = 'outside_root';
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
554
598
|
const rel = node_path_1.default.relative(base, target);
|
|
555
599
|
if (isPureAncestorClimb(rel)) {
|
|
556
600
|
result.status = 'ok';
|