@opengsd/gsd-core 1.13.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-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-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 +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- 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 +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- 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 +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- 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 +9 -1
- 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/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- 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/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- 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 +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/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 +6 -5
- 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 +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- 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/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- 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 +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- 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/sequential-root-pin.md +35 -0
- 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 +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- 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 +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- 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 +52 -5
- 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 +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- 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 +39 -209
- 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 +209 -0
- 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 +45 -187
- 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 +132 -20
- 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 +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- 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 +22 -9
- 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 +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- 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 +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -269,5 +269,53 @@ function parsePlanDocument(content, planPath = '') {
|
|
|
269
269
|
taskCount: tasks.length,
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
|
-
const planDocument = { TASK_KIND, parsePlanDocument, planIdFromFile };
|
|
272
|
+
const planDocument = { TASK_KIND, parsePlanDocument, planIdFromFile, extractThreatRegisterIds };
|
|
273
|
+
/**
|
|
274
|
+
* #4683 — first-cell IDs of the STRIDE register rows inside every
|
|
275
|
+
* `<threat_model>` block. The register is a markdown table (the
|
|
276
|
+
* `<threat_model>` template in agents/gsd-planner.md): one row per threat,
|
|
277
|
+
* first cell `T-{phase}-NN` — decimal phases included — or the reserved
|
|
278
|
+
* `T-{phase}-SC` supply-chain row. Only digit-suffixed IDs match: `-SC` is
|
|
279
|
+
* deliberately shared by EVERY plan in a phase (planner rule "Keep
|
|
280
|
+
* `T-{phase}-SC` in `<threat_model>`"), so it can never be a uniqueness
|
|
281
|
+
* violation. IDs in prose or non-threat tables never count; only register
|
|
282
|
+
* rows inside a threat_model block do. One entry per matched row, in document
|
|
283
|
+
* order — deciding that the same ID in two plans is a collision is the
|
|
284
|
+
* aggregator's question (init.cts), not the per-document parser's.
|
|
285
|
+
*
|
|
286
|
+
* Knowingly unmatched residual classes (#4683 review, accepted): lowercase
|
|
287
|
+
* `t-47-01`, letter suffixes (`T-47-05A`), annotated first cells
|
|
288
|
+
* (`| T-47-06 (revised) |`), IDs in non-first cells, and an unterminated
|
|
289
|
+
* `<threat_model>` block all yield no claim. All are off-template shapes — the
|
|
290
|
+
* planner template fixes the row grammar — so the residual risk is silent
|
|
291
|
+
* under-detection, never a false hard-stop.
|
|
292
|
+
*/
|
|
293
|
+
const THREAT_MODEL_BLOCK_RE = /<threat_model>([\s\S]*?)<\/threat_model>/gi;
|
|
294
|
+
const THREAT_REGISTER_ROW_RE = /^[^\S\n]*\|[^\S\n]*(T-\d+(?:\.\d+)?-\d+)[^\S\n]*\|/;
|
|
295
|
+
function extractThreatRegisterIds(content) {
|
|
296
|
+
// Fenced code blocks are prose, not registers (#4683 review MAJOR): a plan
|
|
297
|
+
// that QUOTES an existing register — exactly what the gap-closure flow tells
|
|
298
|
+
// the planner to read — must not have its quoted IDs counted as claims, or
|
|
299
|
+
// the execute-phase gate would hard-stop a correct phase. Same line-toggling
|
|
300
|
+
// idiom as the deferred-scope scan in phase.cts.
|
|
301
|
+
const lines = [];
|
|
302
|
+
let inFence = false;
|
|
303
|
+
for (const line of content.split(/\r?\n/)) {
|
|
304
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
305
|
+
inFence = !inFence;
|
|
306
|
+
lines.push('');
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
lines.push(inFence ? '' : line);
|
|
310
|
+
}
|
|
311
|
+
const ids = [];
|
|
312
|
+
for (const blockMatch of lines.join('\n').matchAll(THREAT_MODEL_BLOCK_RE)) {
|
|
313
|
+
for (const line of blockMatch[1].split('\n')) {
|
|
314
|
+
const row = line.match(THREAT_REGISTER_ROW_RE);
|
|
315
|
+
if (row)
|
|
316
|
+
ids.push(row[1]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return ids;
|
|
320
|
+
}
|
|
273
321
|
module.exports = planDocument;
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Planning Document — the parse -> mutate -> serialize seam for a `.planning/`
|
|
4
|
+
* root artifact BODY (ADR-4910, epic #4906 Phase 1, #4917).
|
|
5
|
+
*
|
|
6
|
+
* Composes the existing structural seams — never reimplements them:
|
|
7
|
+
* - `markdown-sectionizer.cjs` (`tokenizeHeadings`, `collectSections`,
|
|
8
|
+
* `scanFencedBlocks`, `scanInlineCodeSpans`) for headings/sections and
|
|
9
|
+
* fence/inline-code awareness.
|
|
10
|
+
* - `markdown-table.cjs` (`splitTableRow`, `isDelimiterRow`,
|
|
11
|
+
* `parseMarkdownTable`) for GFM table detection and validation.
|
|
12
|
+
* - `artifacts.cjs` (`isCanonicalPlanningFile`) for the artifact-kind gate.
|
|
13
|
+
*
|
|
14
|
+
* This phase migrates NO call site — it is purely additive (ADR-4910 §7).
|
|
15
|
+
* Only `boldField` nodes are writable; `table`/`checklist` nodes parse and
|
|
16
|
+
* read only. Phase 3 (#4958) checked its own evidence (#4736, #4793) and
|
|
17
|
+
* found neither needed a table/checklist writer here — see ADR-4910's
|
|
18
|
+
* 2026-09-24 amendment. A writer for either kind is unclaimed until a real
|
|
19
|
+
* call site names it.
|
|
20
|
+
*
|
|
21
|
+
* Hyrum's Law commitment (row 3 of the design's behaviour table): `serialize`
|
|
22
|
+
* with zero staged edits returns `doc.source` BYTE-IDENTICAL — never a
|
|
23
|
+
* re-render (#4499's root cause). Every byte outside an edited `valueSpan` is
|
|
24
|
+
* the ORIGINAL source, spliced, never regenerated.
|
|
25
|
+
*
|
|
26
|
+
* ADR-457 build-at-publish: source in src/planning-document.cts, compiled to
|
|
27
|
+
* gsd-core/bin/lib/planning-document.cjs (gitignored).
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.PLANNING_ARTIFACTS = void 0;
|
|
34
|
+
exports.parsePlanningDoc = parsePlanningDoc;
|
|
35
|
+
exports.findField = findField;
|
|
36
|
+
exports.readNode = readNode;
|
|
37
|
+
exports.setFieldValue = setFieldValue;
|
|
38
|
+
exports.hasUnreadableNodes = hasUnreadableNodes;
|
|
39
|
+
exports.serialize = serialize;
|
|
40
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
41
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
42
|
+
const artifacts_cjs_1 = require("./artifacts.cjs");
|
|
43
|
+
// `frontmatter.cts` uses `export =` (CJS-style single export object), so it
|
|
44
|
+
// is imported as a default import (esModuleInterop), not a named import.
|
|
45
|
+
const frontmatter_cjs_1 = __importDefault(require("./frontmatter.cjs"));
|
|
46
|
+
const { frontmatterRegion } = frontmatter_cjs_1.default;
|
|
47
|
+
/**
|
|
48
|
+
* Canonical `.planning/` root artifact basenames this seam recognises,
|
|
49
|
+
* derived from the SAME registry `isCanonicalPlanningFile` consults
|
|
50
|
+
* (`artifacts.cts`'s `CANONICAL_EXACT`) — never a second, independently
|
|
51
|
+
* maintained list.
|
|
52
|
+
*
|
|
53
|
+
* Filtered to `.md` names only: `CANONICAL_EXACT` also carries non-markdown
|
|
54
|
+
* artifacts (`config.json`, `state.json`, `milestone.lock`, …) that this
|
|
55
|
+
* parser has no grammar for. Handing that JSON/lock content to the markdown
|
|
56
|
+
* parser below returns a successful EMPTY document (`nodes: []`), which reads
|
|
57
|
+
* as "this document records nothing" when the truth is "wrong kind entirely"
|
|
58
|
+
* — the empty-vs-error confusion #4917 / ADR-4910 §5 exists to eliminate. Do
|
|
59
|
+
* NOT remove this filter to "restore" the full registry.
|
|
60
|
+
*/
|
|
61
|
+
exports.PLANNING_ARTIFACTS = Object.freeze(Array.from(artifacts_cjs_1.CANONICAL_EXACT).filter((name) => name.endsWith('.md')));
|
|
62
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
63
|
+
let nodeCounter = 0;
|
|
64
|
+
function mintId(kind) {
|
|
65
|
+
nodeCounter += 1;
|
|
66
|
+
return `${kind}-${nodeCounter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
67
|
+
}
|
|
68
|
+
function splitLinesInfo(source) {
|
|
69
|
+
const out = [];
|
|
70
|
+
let offset = 0;
|
|
71
|
+
const rawLines = source.split('\n');
|
|
72
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
73
|
+
const raw = rawLines[i];
|
|
74
|
+
const hasCR = raw.endsWith('\r');
|
|
75
|
+
const text = hasCR ? raw.slice(0, -1) : raw;
|
|
76
|
+
out.push({ text, start: offset, end: offset + text.length });
|
|
77
|
+
offset += raw.length + 1; // +1 for the '\n' split on ('\r' already counted in raw.length)
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Locate the frontmatter block, if any, by COMPOSING `frontmatter.cts`'s
|
|
83
|
+
* `frontmatterRegion` — the fence-detection grammar (byte-0 rule, BOM strip,
|
|
84
|
+
* `\n---` search, CR handling) lives there, once, and this seam never
|
|
85
|
+
* re-derives it (ADR-4910 Decision 1).
|
|
86
|
+
*
|
|
87
|
+
* `frontmatterRegion` reports the YAML body's own bounds (`region`,
|
|
88
|
+
* `terminated`, and the possibly BOM-stripped `content`), not this seam's
|
|
89
|
+
* `Span` shape (an absolute byte range into the UNSTRIPPED `source`,
|
|
90
|
+
* inclusive of both fences). This adapter translates one into the other by
|
|
91
|
+
* reading ONLY the two boundary characters `frontmatterRegion` already
|
|
92
|
+
* anchored (whether the YAML end / closing fence sit on a CRLF line) — it
|
|
93
|
+
* does not re-scan for the fences themselves.
|
|
94
|
+
*/
|
|
95
|
+
function findFrontmatterSpan(source) {
|
|
96
|
+
const found = frontmatterRegion(source);
|
|
97
|
+
if (!found)
|
|
98
|
+
return null;
|
|
99
|
+
// `found.content` may be `source` with a single leading BOM stripped;
|
|
100
|
+
// every offset below is relative to `found.content`, so translate back to
|
|
101
|
+
// `source` coordinates by the same delta.
|
|
102
|
+
const bomDelta = source.length - found.content.length;
|
|
103
|
+
const content = found.content;
|
|
104
|
+
if (!found.terminated) {
|
|
105
|
+
return { span: { start: bomDelta, end: bomDelta + content.length }, terminated: false };
|
|
106
|
+
}
|
|
107
|
+
// `frontmatterRegion` already did fence DETECTION — `found` being non-null
|
|
108
|
+
// and `terminated` IS that result. It reports only the YAML body's bounds
|
|
109
|
+
// (`region`), not an absolute span, so recover the closing fence's end
|
|
110
|
+
// from `region`'s length. The one thing still read directly here is the
|
|
111
|
+
// opening fence's fixed-width line ending (`\n` vs `\r\n`), needed to
|
|
112
|
+
// translate `region`'s length into a `content` offset — not a re-scan for
|
|
113
|
+
// the fence itself.
|
|
114
|
+
const headerEnd = content.startsWith('---\r\n') ? 5 : 4;
|
|
115
|
+
const yamlEnd = headerEnd + found.region.length;
|
|
116
|
+
const closingLineStart = content[yamlEnd] === '\r' ? yamlEnd + 1 : yamlEnd;
|
|
117
|
+
const fenceLineStart = closingLineStart + 1;
|
|
118
|
+
let fenceEnd = fenceLineStart + 3;
|
|
119
|
+
if (content[fenceEnd] === '\r')
|
|
120
|
+
fenceEnd += 1;
|
|
121
|
+
return { span: { start: bomDelta, end: bomDelta + fenceEnd }, terminated: true };
|
|
122
|
+
}
|
|
123
|
+
/** Build the set of 0-based line indices that fall inside a fenced code
|
|
124
|
+
* block (opening/closing delimiter lines included), so `**Label:**`/table/
|
|
125
|
+
* checklist scanning never treats fenced content as a node (rows 9/14). */
|
|
126
|
+
function fencedLineIndices(lines) {
|
|
127
|
+
const raw = lines.map((l) => l.text);
|
|
128
|
+
const blocks = (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(raw);
|
|
129
|
+
const set = new Set();
|
|
130
|
+
for (const b of blocks) {
|
|
131
|
+
const end = b.closeLineIdx === -1 ? raw.length - 1 : b.closeLineIdx;
|
|
132
|
+
for (let i = b.openLineIdx; i <= end; i++)
|
|
133
|
+
set.add(i);
|
|
134
|
+
}
|
|
135
|
+
return set;
|
|
136
|
+
}
|
|
137
|
+
/** Matches both shipped bold-field spellings: colon-inside (`**Label:**`,
|
|
138
|
+
* the original grammar) and colon-outside (`**Label**:`, the canonical form
|
|
139
|
+
* used throughout `templates/roadmap.md`). Each alternative's trailing
|
|
140
|
+
* marker is exactly 3 characters (`:**` or `**:`), so `token.slice(2, -3)`
|
|
141
|
+
* in `parseBoldFieldLine` strips the leading `**` and the spelling-specific
|
|
142
|
+
* trailing marker identically for both, yielding the same `label` either
|
|
143
|
+
* way. Deliberately excludes a bare unbolded `Label:` form — see Phase 1's
|
|
144
|
+
* prose-vs-field disambiguation design. */
|
|
145
|
+
const BOLD_FIELD_RE = /^(\s*)(\*\*[^*\r\n]+(?::\*\*|\*\*:))([ \t]*)([^\r\n]*)$/;
|
|
146
|
+
/** Boundary marking a hand-written trailing annotation on a field line —
|
|
147
|
+
* the token owner must never destroy prose past this separator. */
|
|
148
|
+
const TRAILING_SEPARATOR_RE = / — /;
|
|
149
|
+
function parseBoldFieldLine(line) {
|
|
150
|
+
const m = BOLD_FIELD_RE.exec(line.text);
|
|
151
|
+
if (!m)
|
|
152
|
+
return null;
|
|
153
|
+
const [, leading, token, spacing, rest] = m;
|
|
154
|
+
const labelStart = line.start + leading.length;
|
|
155
|
+
const labelSpan = { start: labelStart, end: labelStart + token.length };
|
|
156
|
+
const label = token.slice(2, -3);
|
|
157
|
+
const restStart = labelSpan.end + spacing.length;
|
|
158
|
+
const sepMatch = TRAILING_SEPARATOR_RE.exec(rest);
|
|
159
|
+
const valueRaw = sepMatch ? rest.slice(0, sepMatch.index) : rest;
|
|
160
|
+
const trimmedValue = valueRaw.replace(/\s+$/, '');
|
|
161
|
+
const valueSpan = { start: restStart, end: restStart + trimmedValue.length };
|
|
162
|
+
const trailingSpan = { start: valueSpan.end, end: line.end };
|
|
163
|
+
return {
|
|
164
|
+
kind: 'boldField',
|
|
165
|
+
id: mintId('boldField'),
|
|
166
|
+
span: { start: labelSpan.start, end: line.end },
|
|
167
|
+
error: null,
|
|
168
|
+
label,
|
|
169
|
+
labelSpan,
|
|
170
|
+
valueSpan,
|
|
171
|
+
trailingSpan,
|
|
172
|
+
value: trimmedValue,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** A checklist line is one whose SOLE bullet, per `iterateBullets` (the same
|
|
176
|
+
* grammar the repo's other bullet consumers use), is a checkbox marker, OR
|
|
177
|
+
* whose bullet TEXT begins with a task-list marker.
|
|
178
|
+
*
|
|
179
|
+
* `iterateBullets` owns bullet *structure* — is this a bullet, where does its
|
|
180
|
+
* text start — and continues to own that here unchanged. It only classifies
|
|
181
|
+
* `-`-prefixed bullets as `checkbox-checked`/`checkbox-unchecked`; GFM also
|
|
182
|
+
* permits `*` and `+` as bullet markers, and `* [ ] x` / `+ [x] y` are valid
|
|
183
|
+
* GFM task-list items that `iterateBullets` reports as plain `dash`-family
|
|
184
|
+
* bullets with the `[ ]`/`[x]` left in the bullet's own text. Widening
|
|
185
|
+
* `iterateBullets` itself is forbidden by ADR-2143 §2's extend-never-mutate
|
|
186
|
+
* lock (inherited by this epic), so the task-list-marker interpretation is
|
|
187
|
+
* layered on here, over the bullet's already-extracted text — never by
|
|
188
|
+
* re-scanning the raw line with a new hand-rolled regex.
|
|
189
|
+
*
|
|
190
|
+
* Known limit inherited from `iterateBullets`, not introduced here:
|
|
191
|
+
* `-\t[ ] text` (a tab between the marker and the text) is not recognised as
|
|
192
|
+
* a bullet at all, so it can never become a checklist line. That is a
|
|
193
|
+
* pre-existing `markdown-sectionizer` boundary affecting every consumer of
|
|
194
|
+
* `iterateBullets`, and fixing it would mean altering the locked seam. */
|
|
195
|
+
function isChecklistLine(text) {
|
|
196
|
+
const items = (0, markdown_sectionizer_cjs_1.iterateBullets)(text);
|
|
197
|
+
if (items.length !== 1)
|
|
198
|
+
return false;
|
|
199
|
+
const item = items[0];
|
|
200
|
+
if (item.marker === 'checkbox-checked' || item.marker === 'checkbox-unchecked')
|
|
201
|
+
return true;
|
|
202
|
+
return /^\[[ xX]\] /.test(item.text);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Scan the document body (everything outside the frontmatter block and
|
|
206
|
+
* outside fenced code) for `boldField`, `table`, and `checklist` nodes, in
|
|
207
|
+
* document order.
|
|
208
|
+
*/
|
|
209
|
+
function scanBodyNodes(source, lines, frontmatterEnd) {
|
|
210
|
+
const fenced = fencedLineIndices(lines);
|
|
211
|
+
const nodes = [];
|
|
212
|
+
let i = 0;
|
|
213
|
+
while (i < lines.length) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
if (fenced.has(i) || line.start < frontmatterEnd) {
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const trimmed = line.text.trim();
|
|
220
|
+
// Table: a pipe-shaped header line followed by a valid delimiter row.
|
|
221
|
+
if (trimmed.startsWith('|') && trimmed.indexOf('|', 1) !== -1 && i + 1 < lines.length) {
|
|
222
|
+
const delimiterLine = lines[i + 1];
|
|
223
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(delimiterLine.text);
|
|
224
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(line.text);
|
|
225
|
+
if (delimiterLine.text.trim().startsWith('|')
|
|
226
|
+
&& (0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells)
|
|
227
|
+
&& delimiterCells.length === headerCells.length
|
|
228
|
+
&& !fenced.has(i + 1)) {
|
|
229
|
+
let last = i + 1;
|
|
230
|
+
while (last + 1 < lines.length && lines[last + 1].text.trim().startsWith('|') && !fenced.has(last + 1)) {
|
|
231
|
+
last += 1;
|
|
232
|
+
}
|
|
233
|
+
const span = { start: line.start, end: lines[last].end };
|
|
234
|
+
const tableText = source.slice(span.start, span.end);
|
|
235
|
+
const parsed = (0, markdown_table_cjs_1.parseMarkdownTable)(tableText);
|
|
236
|
+
nodes.push(parsed.ok
|
|
237
|
+
? {
|
|
238
|
+
kind: 'table',
|
|
239
|
+
id: mintId('table'),
|
|
240
|
+
span,
|
|
241
|
+
error: null,
|
|
242
|
+
columns: parsed.value.columns,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
kind: 'table',
|
|
246
|
+
id: mintId('table'),
|
|
247
|
+
span,
|
|
248
|
+
error: { reason: parsed.reason, span },
|
|
249
|
+
columns: null,
|
|
250
|
+
});
|
|
251
|
+
i = last + 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Checklist: a contiguous run of checkbox-bullet lines.
|
|
256
|
+
if (isChecklistLine(line.text)) {
|
|
257
|
+
let last = i;
|
|
258
|
+
let count = 0;
|
|
259
|
+
while (last < lines.length && !fenced.has(last) && isChecklistLine(lines[last].text)) {
|
|
260
|
+
count += 1;
|
|
261
|
+
last += 1;
|
|
262
|
+
}
|
|
263
|
+
last -= 1;
|
|
264
|
+
const span = { start: line.start, end: lines[last].end };
|
|
265
|
+
nodes.push({ kind: 'checklist', id: mintId('checklist'), span, error: null, items: count });
|
|
266
|
+
i = last + 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Bold field.
|
|
270
|
+
const field = parseBoldFieldLine(line);
|
|
271
|
+
if (field) {
|
|
272
|
+
nodes.push(field);
|
|
273
|
+
i += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
i += 1;
|
|
277
|
+
}
|
|
278
|
+
return nodes;
|
|
279
|
+
}
|
|
280
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* Parse `source` (the raw text of a `.planning/` root artifact) into a
|
|
283
|
+
* `PlanningDoc`. Document-level `Result` failure is reserved for: `artifact`
|
|
284
|
+
* not a recognised planning artifact kind, `source` not a readable string, or
|
|
285
|
+
* an opened-but-never-closed frontmatter fence (ADR-4910 §5's reservation).
|
|
286
|
+
* A malformed SUB-structure (a ragged table, say) never fails the whole
|
|
287
|
+
* document — it is recorded as that one node's `error`, and every sibling
|
|
288
|
+
* node stays readable (row 7). `nodes: []` on a genuinely empty document is
|
|
289
|
+
* success, not an error (row 15).
|
|
290
|
+
*/
|
|
291
|
+
function parsePlanningDoc(source, artifact) {
|
|
292
|
+
if (typeof source !== 'string') {
|
|
293
|
+
return { ok: false, reason: 'unreadable: source is not a string' };
|
|
294
|
+
}
|
|
295
|
+
if (typeof artifact !== 'string' ||
|
|
296
|
+
!(0, artifacts_cjs_1.isCanonicalPlanningFile)(artifact) ||
|
|
297
|
+
!exports.PLANNING_ARTIFACTS.includes(artifact)) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
reason: `not a markdown planning document (artifact: ${String(artifact)})`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const nodes = [];
|
|
304
|
+
let frontmatterEnd = 0;
|
|
305
|
+
const fm = findFrontmatterSpan(source);
|
|
306
|
+
if (fm) {
|
|
307
|
+
if (!fm.terminated) {
|
|
308
|
+
return { ok: false, reason: 'no frontmatter terminator' };
|
|
309
|
+
}
|
|
310
|
+
nodes.push({ kind: 'frontmatter', id: mintId('frontmatter'), span: fm.span, error: null });
|
|
311
|
+
frontmatterEnd = fm.span.end;
|
|
312
|
+
}
|
|
313
|
+
if (source.length === 0) {
|
|
314
|
+
return { ok: true, value: { source, artifact, nodes: [], staged: new Map() } };
|
|
315
|
+
}
|
|
316
|
+
const lines = splitLinesInfo(source);
|
|
317
|
+
// Sections: one per heading, in document order — every heading is its own
|
|
318
|
+
// boundary (`collectSections(source, () => true)`), so a nested `####`
|
|
319
|
+
// still gets its own SectionNode rather than being folded into its parent.
|
|
320
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(source);
|
|
321
|
+
if (headings.length > 0) {
|
|
322
|
+
const sections = (0, markdown_sectionizer_cjs_1.collectSections)(source, () => true);
|
|
323
|
+
for (const s of sections) {
|
|
324
|
+
nodes.push({
|
|
325
|
+
kind: 'section',
|
|
326
|
+
id: mintId('section'),
|
|
327
|
+
span: { start: s.heading.offset, end: s.bodyEnd },
|
|
328
|
+
error: null,
|
|
329
|
+
heading: s.heading.text,
|
|
330
|
+
level: s.heading.level,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
nodes.push(...scanBodyNodes(source, lines, frontmatterEnd));
|
|
335
|
+
nodes.sort((a, b) => a.span.start - b.span.start);
|
|
336
|
+
return { ok: true, value: { source, artifact, nodes, staged: new Map() } };
|
|
337
|
+
}
|
|
338
|
+
/** Find the id of the (first, document-order) `boldField` node whose label
|
|
339
|
+
* exactly matches `label`, or `null` when none does. */
|
|
340
|
+
function findField(doc, label) {
|
|
341
|
+
for (const n of doc.nodes) {
|
|
342
|
+
if (n.kind === 'boldField' && n.label === label)
|
|
343
|
+
return n.id;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
/** Read a node by id. Node-scoped failure only — an unknown id or a node
|
|
348
|
+
* that failed to parse never throws. */
|
|
349
|
+
function readNode(doc, id) {
|
|
350
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
351
|
+
if (!node) {
|
|
352
|
+
return { ok: false, reason: 'unknown node id', span: { start: 0, end: 0 } };
|
|
353
|
+
}
|
|
354
|
+
if (node.error) {
|
|
355
|
+
return { ok: false, reason: node.error.reason, span: node.error.span };
|
|
356
|
+
}
|
|
357
|
+
if (node.kind === 'boldField') {
|
|
358
|
+
return { ok: true, value: doc.staged.get(id) ?? node.value };
|
|
359
|
+
}
|
|
360
|
+
return { ok: true, value: doc.source.slice(node.span.start, node.span.end) };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Stage a new value for a `boldField` node, returning a NEW `PlanningDoc`
|
|
364
|
+
* (immutable — `doc` itself is never mutated). Refuses an id this doc did
|
|
365
|
+
* not mint, and refuses any node kind other than `boldField` — only the
|
|
366
|
+
* `valueSpan` is ever writable this phase (ADR-4910 §1).
|
|
367
|
+
*/
|
|
368
|
+
function setFieldValue(doc, id, value) {
|
|
369
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
370
|
+
if (!node) {
|
|
371
|
+
return { ok: false, reason: 'unknown node id' };
|
|
372
|
+
}
|
|
373
|
+
if (node.kind !== 'boldField') {
|
|
374
|
+
return { ok: false, reason: `node kind '${node.kind}' is not writable this phase` };
|
|
375
|
+
}
|
|
376
|
+
// #4917 / ADR-4910 Decision 2 & 4: a boldField's token boundary is a LINE
|
|
377
|
+
// boundary, not just an offset range — a value containing \n or \r escapes
|
|
378
|
+
// the field's own span and reparses as sibling structure (a forged field)
|
|
379
|
+
// once spliced back into the source. Decision 4 licenses refusal for any
|
|
380
|
+
// value the grammar cannot represent; Phase 3 may widen this to escaping,
|
|
381
|
+
// but Phase 1 refuses outright. Do not remove this as an over-restriction.
|
|
382
|
+
if (/[\r\n]/.test(value)) {
|
|
383
|
+
return { ok: false, reason: 'field value must not contain a line break (\\r or \\n)' };
|
|
384
|
+
}
|
|
385
|
+
// #4917 / ADR-4910 Decision 4: "a value that cannot be represented in the
|
|
386
|
+
// grammar is refused by the writer, with a report." This is a GENERAL
|
|
387
|
+
// round-trip representability check, not a blacklist of forbidden
|
|
388
|
+
// substrings — the `\r`/`\n` guard above is a narrower special case kept
|
|
389
|
+
// for its clearer message, but THIS check is the backstop. It rebuilds the
|
|
390
|
+
// line exactly as it would be written (existing leading/label/spacing +
|
|
391
|
+
// the new value + the existing trailing text) and re-parses that line
|
|
392
|
+
// through the SAME `parseBoldFieldLine` grammar the reader uses. If the
|
|
393
|
+
// value the grammar reads back is not byte-identical to what the caller
|
|
394
|
+
// staged, the grammar cannot represent this value (e.g. it contains the
|
|
395
|
+
// ` — ` trailing-separator token, which would silently reclassify the
|
|
396
|
+
// rest of the value as trailing prose) and the write is refused. Do NOT
|
|
397
|
+
// replace this with a list of forbidden characters/substrings — the next
|
|
398
|
+
// separator the grammar grows would silently slip past a blacklist.
|
|
399
|
+
const leadingText = doc.source.slice(node.span.start, node.labelSpan.start);
|
|
400
|
+
const tokenText = doc.source.slice(node.labelSpan.start, node.labelSpan.end);
|
|
401
|
+
const spacingText = doc.source.slice(node.labelSpan.end, node.valueSpan.start);
|
|
402
|
+
const trailingText = doc.source.slice(node.trailingSpan.start, node.trailingSpan.end);
|
|
403
|
+
const candidateLine = `${leadingText}${tokenText}${spacingText}${value}${trailingText}`;
|
|
404
|
+
const candidateInfo = { text: candidateLine, start: 0, end: candidateLine.length };
|
|
405
|
+
const reparsed = parseBoldFieldLine(candidateInfo);
|
|
406
|
+
if (!reparsed || reparsed.value !== value) {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
reason: 'field value is not representable in the boldField grammar (would not round-trip)',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const staged = new Map(doc.staged);
|
|
413
|
+
staged.set(id, value);
|
|
414
|
+
return { ok: true, value: { source: doc.source, artifact: doc.artifact, nodes: doc.nodes, staged } };
|
|
415
|
+
}
|
|
416
|
+
/** True when any node in `doc` failed to parse. */
|
|
417
|
+
function hasUnreadableNodes(doc) {
|
|
418
|
+
return doc.nodes.some((n) => n.error !== null);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Splice every staged edit into `doc.source` and return the resulting text.
|
|
422
|
+
* With zero staged edits, returns `doc.source` BYTE-IDENTICAL — never a
|
|
423
|
+
* re-render (row 3). Refuses outright — even with zero staged edits — when
|
|
424
|
+
* `hasUnreadableNodes(doc)` is true (the ADR-4910 amendment): `serialize`
|
|
425
|
+
* re-emits the WHOLE document, so the refusal is document-scoped, not
|
|
426
|
+
* mutation-scoped.
|
|
427
|
+
*/
|
|
428
|
+
function serialize(doc) {
|
|
429
|
+
if (hasUnreadableNodes(doc)) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
reason: 'unreadable-nodes',
|
|
433
|
+
nodes: doc.nodes
|
|
434
|
+
.filter((n) => n.error !== null)
|
|
435
|
+
.map((n) => ({ id: n.id, kind: n.kind, span: n.error.span, reason: n.error.reason })),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (doc.staged.size === 0) {
|
|
439
|
+
return { ok: true, value: doc.source };
|
|
440
|
+
}
|
|
441
|
+
const edits = [];
|
|
442
|
+
for (const [id, value] of doc.staged) {
|
|
443
|
+
const node = doc.nodes.find((n) => n.id === id);
|
|
444
|
+
if (!node || node.kind !== 'boldField')
|
|
445
|
+
continue; // unreachable: setFieldValue already gated this
|
|
446
|
+
edits.push({ start: node.valueSpan.start, end: node.valueSpan.end, value });
|
|
447
|
+
}
|
|
448
|
+
edits.sort((a, b) => a.start - b.start);
|
|
449
|
+
let out = '';
|
|
450
|
+
let cursor = 0;
|
|
451
|
+
for (const e of edits) {
|
|
452
|
+
out += doc.source.slice(cursor, e.start) + e.value;
|
|
453
|
+
cursor = e.end;
|
|
454
|
+
}
|
|
455
|
+
out += doc.source.slice(cursor);
|
|
456
|
+
return { ok: true, value: out };
|
|
457
|
+
}
|
|
458
|
+
// Consumers: require('../gsd-core/bin/lib/planning-document.cjs')
|
|
459
|
+
// Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
|
|
@@ -94,6 +94,9 @@ const { parseMarkdownTable, matchTableSchema } = markdownTable;
|
|
|
94
94
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
95
95
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
96
96
|
const { normalizeLineEndings } = coreUtilsMod;
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
98
|
+
const securityMod = require("./security.cjs");
|
|
99
|
+
const { tryWithinRoot, PathAcceptance, isContainedIn } = securityMod;
|
|
97
100
|
/**
|
|
98
101
|
* The wire schema version. A consumer MUST reject any value other than this
|
|
99
102
|
* one rather than best-effort-parsing an unknown shape.
|
|
@@ -184,32 +187,45 @@ function toPosix(value) {
|
|
|
184
187
|
* directory like `.planning-evil/` that merely shares a string prefix. Pure
|
|
185
188
|
* string comparison, no I/O — callers own their own `fs.realpathSync` call
|
|
186
189
|
* (and its own not-found/broken-symlink handling).
|
|
190
|
+
*
|
|
191
|
+
* NOT an independent containment implementation — it is the comparison step
|
|
192
|
+
* of one, and that comparison now comes from `security.cts`'s exported
|
|
193
|
+
* `isContainedIn` rather than being redeclared here. `readDocument` below
|
|
194
|
+
* realpaths target and root itself (to keep its own exists-vs-escaped
|
|
195
|
+
* tri-state) and calls `isContainedIn` directly; `isPathContained` gets its
|
|
196
|
+
* containment DECISION from the canonical `tryWithinRoot` predicate instead
|
|
197
|
+
* (ADR-4650 decision 6) and never called this comparison directly. Every
|
|
198
|
+
* caller owns its own resolution.
|
|
187
199
|
*/
|
|
188
|
-
function isWithinRoot(resolvedTarget, resolvedRoot) {
|
|
189
|
-
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep);
|
|
190
|
-
}
|
|
191
200
|
/**
|
|
192
|
-
* Containment check for a path (file OR directory)
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* `
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
+
* Containment check for a path (file OR directory), used where the caller
|
|
202
|
+
* does not need to distinguish "target vanished / broken symlink" from
|
|
203
|
+
* "target resolved but escapes root" — both degrade the same way at every
|
|
204
|
+
* call site that uses this (an escaped or unresolvable phase directory is
|
|
205
|
+
* treated identically to an unreadable one). `readDocument` below needs
|
|
206
|
+
* that distinction for its own exists/readable tri-state, so it keeps its
|
|
207
|
+
* own inline `realpathSync` calls and calls `isContainedIn` directly instead
|
|
208
|
+
* of this wrapper.
|
|
209
|
+
*
|
|
210
|
+
* The containment DECISION comes from the canonical `tryWithinRoot`
|
|
211
|
+
* predicate (ADR-4650 decision 6: a wrapper may decide HOW to degrade,
|
|
212
|
+
* never WHETHER a path is contained). Must-exist stays this module's OWN
|
|
213
|
+
* degradation condition, applied after: `tryWithinRoot` deliberately accepts
|
|
214
|
+
* a not-yet-created path under the root (ancestor-walk realpath), but every
|
|
215
|
+
* caller of `isPathContained` guards an `fs` read that is about to happen
|
|
216
|
+
* against an already-existing directory, so a vanished/unresolvable path
|
|
217
|
+
* must still degrade the same as an escaped one.
|
|
201
218
|
*/
|
|
202
219
|
function isPathContained(target, root) {
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
if (tryWithinRoot(target, root, PathAcceptance.AbsoluteInsideRoot) === null)
|
|
221
|
+
return false;
|
|
205
222
|
try {
|
|
206
|
-
|
|
207
|
-
realRoot = node_fs_1.default.realpathSync(root);
|
|
223
|
+
node_fs_1.default.realpathSync(target);
|
|
208
224
|
}
|
|
209
225
|
catch {
|
|
210
226
|
return false;
|
|
211
227
|
}
|
|
212
|
-
return
|
|
228
|
+
return true;
|
|
213
229
|
}
|
|
214
230
|
function readDocument(filePath, root) {
|
|
215
231
|
let stat;
|
|
@@ -234,7 +250,7 @@ function readDocument(filePath, root) {
|
|
|
234
250
|
// here — the same non-answer `readDocument` already gives "not exists".
|
|
235
251
|
return { text: null, exists: false, readable: false };
|
|
236
252
|
}
|
|
237
|
-
if (!
|
|
253
|
+
if (!isContainedIn(realTarget, realRoot)) {
|
|
238
254
|
return { text: null, exists: true, readable: false };
|
|
239
255
|
}
|
|
240
256
|
try {
|
|
@@ -901,7 +917,24 @@ function extractDependencyTokens(sectionBody) {
|
|
|
901
917
|
const m = DEPENDS_ON_LINE_RE.exec(sectionBody);
|
|
902
918
|
if (!m)
|
|
903
919
|
return [];
|
|
904
|
-
|
|
920
|
+
// #4764: phase REFERENCES, not digit runs — the same prose-anchored grammar
|
|
921
|
+
// init.manager's dep_phases extraction uses (owner: phase-id.cts's
|
|
922
|
+
// PHASE_DEP_REF_SOURCE). The whole-field token scrape this replaces pulled
|
|
923
|
+
// calendar dates, git shas and ledger ids in as dependencies. The grammar's
|
|
924
|
+
// capture group 1 already excludes the "Phase(s)" anchor word, so no
|
|
925
|
+
// prefix-strip literal is needed here. Self-exclusion (init.manager drops
|
|
926
|
+
// the row's own number) is deliberately NOT applied: this reader has no row
|
|
927
|
+
// context at the extraction site and reports informationally, it does not
|
|
928
|
+
// gate.
|
|
929
|
+
const refRe = new RegExp(phaseIdMod.PHASE_DEP_REF_SOURCE, 'gi');
|
|
930
|
+
const tokenRe = new RegExp(phaseIdMod.PHASE_NUMBER_TOKEN_SOURCE, 'g');
|
|
931
|
+
const tokens = [];
|
|
932
|
+
let refMatch;
|
|
933
|
+
while ((refMatch = refRe.exec(m[1])) !== null) {
|
|
934
|
+
for (const t of refMatch[1].matchAll(tokenRe))
|
|
935
|
+
tokens.push(t[0]);
|
|
936
|
+
}
|
|
937
|
+
return sortedUnique(tokens);
|
|
905
938
|
}
|
|
906
939
|
/**
|
|
907
940
|
* This phase's own ROADMAP.md section body — milestone-scoped via the SAME
|