@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
|
@@ -890,6 +890,7 @@ const VIOLATION_KINDS = Object.freeze({
|
|
|
890
890
|
LEGACY_READ_TAG: 'legacy_read_tag',
|
|
891
891
|
READ_TAG_GATE_MISSING: 'read_tag_gate_missing',
|
|
892
892
|
UNMATCHED_CONSUMER_TOKEN: 'unmatched_consumer_token',
|
|
893
|
+
UNRESOLVED_REFERENCE_INCLUDE: 'unresolved_reference_include',
|
|
893
894
|
});
|
|
894
895
|
|
|
895
896
|
/**
|
|
@@ -925,6 +926,8 @@ const REMEDIES = Object.freeze({
|
|
|
925
926
|
"add the <required_reading> MUST-Read gate clause to this agent's instructions (see docs/AGENTS.md)",
|
|
926
927
|
unmatched_consumer_token:
|
|
927
928
|
'declare the producing agent and marker in the registry, or remove the dispatch — nothing emits this token',
|
|
929
|
+
unresolved_reference_include:
|
|
930
|
+
'fix the gsd-core/references/ pointer so it names an existing reference file whole and ends at the name (whitespace, end of line, or closing punctuation)',
|
|
928
931
|
});
|
|
929
932
|
|
|
930
933
|
/**
|
|
@@ -174,6 +174,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
174
174
|
// entry).
|
|
175
175
|
'tests/learnings.test.cjs': ['docs/FEATURES.md'],
|
|
176
176
|
'tests/analyze-dependencies.test.cjs': ['docs/COMMANDS.md'],
|
|
177
|
+
'tests/auto-select-attribute.test.cjs': ['docs/reference/plan-md.md'],
|
|
177
178
|
'tests/autonomous-converge.test.cjs': [
|
|
178
179
|
'docs/COMMANDS.md',
|
|
179
180
|
'docs/how-to/run-phases-autonomously.md',
|
|
@@ -272,6 +273,32 @@ const DOCS_GUARD_TESTS = {
|
|
|
272
273
|
// seeding, so a content edit to any of them (e.g. renaming a landmark
|
|
273
274
|
// heading/string a hostile-input test targets) can change this test's
|
|
274
275
|
// fixture assumptions.
|
|
276
|
+
// #4728: pins the retirement of Gemini CLI prose from every localized
|
|
277
|
+
// how-to/ARCHITECTURE/USER-GUIDE/CONFIGURATION/context-monitor mirror, plus
|
|
278
|
+
// the PRESERVE (Antigravity) and MODEL-AXIS (Gemini 2.5 Pro) negative-space
|
|
279
|
+
// checks in the same locales.
|
|
280
|
+
'tests/gemini-runtime-removed.test.cjs': [
|
|
281
|
+
'docs/ja-JP/how-to/install-on-your-runtime.md',
|
|
282
|
+
'docs/ko-KR/how-to/install-on-your-runtime.md',
|
|
283
|
+
'docs/pt-BR/how-to/install-on-your-runtime.md',
|
|
284
|
+
'docs/zh-CN/how-to/install-on-your-runtime.md',
|
|
285
|
+
'docs/ja-JP/ARCHITECTURE.md',
|
|
286
|
+
'docs/ko-KR/ARCHITECTURE.md',
|
|
287
|
+
'docs/pt-BR/ARCHITECTURE.md',
|
|
288
|
+
'docs/zh-CN/ARCHITECTURE.md',
|
|
289
|
+
'docs/ja-JP/USER-GUIDE.md',
|
|
290
|
+
'docs/ko-KR/USER-GUIDE.md',
|
|
291
|
+
'docs/pt-BR/USER-GUIDE.md',
|
|
292
|
+
'docs/zh-CN/USER-GUIDE.md',
|
|
293
|
+
'docs/ja-JP/CONFIGURATION.md',
|
|
294
|
+
'docs/ko-KR/CONFIGURATION.md',
|
|
295
|
+
'docs/pt-BR/CONFIGURATION.md',
|
|
296
|
+
'docs/zh-CN/CONFIGURATION.md',
|
|
297
|
+
'docs/ja-JP/context-monitor.md',
|
|
298
|
+
'docs/ko-KR/context-monitor.md',
|
|
299
|
+
'docs/pt-BR/context-monitor.md',
|
|
300
|
+
'docs/zh-CN/context-monitor.md',
|
|
301
|
+
],
|
|
275
302
|
'tests/gen-state-md-docs.test.cjs': [
|
|
276
303
|
'docs/reference/state-md.md',
|
|
277
304
|
'docs/ja-JP/reference/state-md.md',
|
|
@@ -376,6 +403,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
376
403
|
'docs/how-to/plan-a-phase.md',
|
|
377
404
|
'docs/AGENTS.md',
|
|
378
405
|
],
|
|
406
|
+
'tests/ui-interaction-capture.test.cjs': ['docs/CONFIGURATION.md'],
|
|
379
407
|
'tests/ui-spec-inventory-provenance.test.cjs': [
|
|
380
408
|
'docs/FEATURES.md',
|
|
381
409
|
'docs/how-to/design-a-ui-phase.md',
|
|
@@ -78,6 +78,23 @@ const ROLE_TO_AGENT = {
|
|
|
78
78
|
verifier: 'gsd-verifier',
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// #4740 — Role → family mapping. Three families: orchestration, planning,
|
|
82
|
+
// execution. This constrains what a workflow may DECLARE in agent-roles for
|
|
83
|
+
// a given step (admissibility), a separate concern from ROLE_TO_AGENT's
|
|
84
|
+
// agent-file presence check above.
|
|
85
|
+
const ROLE_FAMILY = {
|
|
86
|
+
orchestrator: 'orchestration',
|
|
87
|
+
researcher: 'planning', planner: 'planning', checker: 'planning',
|
|
88
|
+
executor: 'execution', verifier: 'execution',
|
|
89
|
+
};
|
|
90
|
+
const EXPECTED_FAMILY_BY_STEP = {
|
|
91
|
+
discuss: 'orchestration',
|
|
92
|
+
plan: 'planning',
|
|
93
|
+
execute: 'execution',
|
|
94
|
+
verify: 'orchestration',
|
|
95
|
+
ship: 'orchestration',
|
|
96
|
+
};
|
|
97
|
+
|
|
81
98
|
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
82
99
|
|
|
83
100
|
/**
|
|
@@ -228,6 +245,52 @@ function crossCheckRoles(content, agentRoles, fileName) {
|
|
|
228
245
|
return errors;
|
|
229
246
|
}
|
|
230
247
|
|
|
248
|
+
// ─── Cross-check: declared roles vs. their permitted family (#4740) ──────────
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* For a step, verify every role declared in agentRoles belongs to that step's
|
|
252
|
+
* expected family (orchestration / planning / execution).
|
|
253
|
+
*
|
|
254
|
+
* Unlike `assertPointsCoverage`'s `if (!expected) continue // caught
|
|
255
|
+
* elsewhere` guard, an unknown step here fails CLOSED: for points
|
|
256
|
+
* there is a second net (the canonical-set and duplicate checks), but nothing
|
|
257
|
+
* else in the repo validates role families, so failing open on an unknown
|
|
258
|
+
* step would make it the one input that silently bypasses this gate — the
|
|
259
|
+
* "unknown resolving to a safe known" failure mode this check exists to close.
|
|
260
|
+
*
|
|
261
|
+
* Pure: never sorts, de-dupes, or otherwise mutates `agentRoles` — the same
|
|
262
|
+
* array `buildContract` puts into the generated contract.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} step The step named in the marker block.
|
|
265
|
+
* @param {string[]} agentRoles Roles declared in the block.
|
|
266
|
+
* @param {string} fileName For error messages.
|
|
267
|
+
* @returns {string[]} Array of error strings; empty = OK.
|
|
268
|
+
*/
|
|
269
|
+
function crossCheckRoleFamilies(step, agentRoles, fileName) {
|
|
270
|
+
const expectedFamily = EXPECTED_FAMILY_BY_STEP[step];
|
|
271
|
+
if (!expectedFamily) {
|
|
272
|
+
return [fileName + ': step "' + step + '" has no entry in EXPECTED_FAMILY_BY_STEP'];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const errors = [];
|
|
276
|
+
for (const role of agentRoles) {
|
|
277
|
+
const family = ROLE_FAMILY[role];
|
|
278
|
+
if (!family) {
|
|
279
|
+
errors.push(
|
|
280
|
+
fileName + ': declared agent-role "' + role + '" has no entry in ROLE_FAMILY mapping',
|
|
281
|
+
);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (family !== expectedFamily) {
|
|
285
|
+
errors.push(
|
|
286
|
+
fileName + ': declared agent-role "' + role + '" (family "' + family +
|
|
287
|
+
'") is not permitted at step "' + step + '" (expected family "' + expectedFamily + '")',
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return errors;
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
// ─── 12-points coverage assertion ────────────────────────────────────────────
|
|
232
295
|
|
|
233
296
|
/**
|
|
@@ -331,6 +394,10 @@ function buildContract(workflowsDir) {
|
|
|
331
394
|
const roleErrors = crossCheckRoles(content, entry.agentRoles, file);
|
|
332
395
|
allErrors.push(...roleErrors);
|
|
333
396
|
|
|
397
|
+
// Cross-check role families (#4740)
|
|
398
|
+
const roleFamilyErrors = crossCheckRoleFamilies(entry.step, entry.agentRoles, file);
|
|
399
|
+
allErrors.push(...roleFamilyErrors);
|
|
400
|
+
|
|
334
401
|
for (const auxiliary of auxiliaryHosts) {
|
|
335
402
|
let auxiliaryContent;
|
|
336
403
|
try {
|
|
@@ -667,6 +734,7 @@ function getWiredLoopPoints(repoRoot) {
|
|
|
667
734
|
module.exports = {
|
|
668
735
|
parseLoopHostBlock,
|
|
669
736
|
crossCheckRoles,
|
|
737
|
+
crossCheckRoleFamilies,
|
|
670
738
|
assertPointsCoverage,
|
|
671
739
|
buildContract,
|
|
672
740
|
serializeContract,
|
|
@@ -676,6 +744,7 @@ module.exports = {
|
|
|
676
744
|
CANONICAL_POINTS,
|
|
677
745
|
EXPECTED_POINTS_BY_STEP,
|
|
678
746
|
ROLE_TO_AGENT,
|
|
747
|
+
ROLE_FAMILY,
|
|
679
748
|
scanWiredPoints,
|
|
680
749
|
getWiredLoopPoints,
|
|
681
750
|
coveredKindsInRegion,
|
|
@@ -32,6 +32,7 @@ module.exports = {
|
|
|
32
32
|
"tests/changeset-parse.test.cjs",
|
|
33
33
|
"tests/check-contract-drift.test.cjs",
|
|
34
34
|
"tests/check-predicate.test.cjs",
|
|
35
|
+
"tests/check-ui-plan-gate.test.cjs",
|
|
35
36
|
"tests/check-ui-safety-gate.test.cjs",
|
|
36
37
|
"tests/check-update-config-dir.test.cjs",
|
|
37
38
|
"tests/chunked-planning-parallel.test.cjs",
|
|
@@ -52,6 +53,7 @@ module.exports = {
|
|
|
52
53
|
"tests/config-loader.test.cjs",
|
|
53
54
|
"tests/config-schema.property.test.cjs",
|
|
54
55
|
"tests/config.test.cjs",
|
|
56
|
+
"tests/configured-entrypoint-validation.test.cjs",
|
|
55
57
|
"tests/contributor-standards.test.cjs",
|
|
56
58
|
"tests/core-utils.test.cjs",
|
|
57
59
|
"tests/cursor-subagent-isolation.test.cjs",
|
|
@@ -73,6 +75,7 @@ module.exports = {
|
|
|
73
75
|
"tests/features-index-gate.test.cjs",
|
|
74
76
|
"tests/frontmatter.unit.test.cjs",
|
|
75
77
|
"tests/gap-checker.property.test.cjs",
|
|
78
|
+
"tests/gemini-runtime-removed.test.cjs",
|
|
76
79
|
"tests/gen-context-index.test.cjs",
|
|
77
80
|
"tests/gen-section-manifest.test.cjs",
|
|
78
81
|
"tests/git-base-branch.test.cjs",
|
|
@@ -83,6 +86,7 @@ module.exports = {
|
|
|
83
86
|
"tests/gsd-statusline.test.cjs",
|
|
84
87
|
"tests/gsd-tools-path-refs.test.cjs",
|
|
85
88
|
"tests/gsd-validate-commit-crash-policy.test.cjs",
|
|
89
|
+
"tests/gsd-validate-commit-sigpipe.test.cjs",
|
|
86
90
|
"tests/gsd-write-guard.test.cjs",
|
|
87
91
|
"tests/health-diagnostic-rules/worktree-health.test.cjs",
|
|
88
92
|
"tests/health-diagnostic.test.cjs",
|
|
@@ -111,6 +115,7 @@ module.exports = {
|
|
|
111
115
|
"tests/issue-version-gate.test.cjs",
|
|
112
116
|
"tests/kimi-upgrades.test.cjs",
|
|
113
117
|
"tests/lint-docs-command-form.test.cjs",
|
|
118
|
+
"tests/lint-retired-runtime-name.test.cjs",
|
|
114
119
|
"tests/lint-workflow-shellcheck-fetch.test.cjs",
|
|
115
120
|
"tests/list-seeds.test.cjs",
|
|
116
121
|
"tests/markdown-sectionizer.test.cjs",
|
|
@@ -125,6 +130,7 @@ module.exports = {
|
|
|
125
130
|
"tests/no-posix-mode-bit-assert.rule.test.cjs",
|
|
126
131
|
"tests/no-private-binary-resolution.rule.test.cjs",
|
|
127
132
|
"tests/no-unguarded-nonportable-exec.rule.test.cjs",
|
|
133
|
+
"tests/nsegment-phase-grammar.test.cjs",
|
|
128
134
|
"tests/observability/event.test.cjs",
|
|
129
135
|
"tests/onboard-command.test.cjs",
|
|
130
136
|
"tests/opencode-plugin-adapter.test.cjs",
|
|
@@ -138,6 +144,7 @@ module.exports = {
|
|
|
138
144
|
"tests/phase-locator.test.cjs",
|
|
139
145
|
"tests/phase.test.cjs",
|
|
140
146
|
"tests/plan-count-single-owner.test.cjs",
|
|
147
|
+
"tests/plan-document.test.cjs",
|
|
141
148
|
"tests/plan-phase-stall-detection.test.cjs",
|
|
142
149
|
"tests/plan-review-convergence.test.cjs",
|
|
143
150
|
"tests/planning-inspect.test.cjs",
|
|
@@ -154,6 +161,7 @@ module.exports = {
|
|
|
154
161
|
"tests/process-seam.test.cjs",
|
|
155
162
|
"tests/profile-output.test.cjs",
|
|
156
163
|
"tests/profile-pipeline.test.cjs",
|
|
164
|
+
"tests/project-root.test.cjs",
|
|
157
165
|
"tests/quick-batch.test.cjs",
|
|
158
166
|
"tests/quick-branching.test.cjs",
|
|
159
167
|
"tests/quick-research.test.cjs",
|
|
@@ -173,21 +181,27 @@ module.exports = {
|
|
|
173
181
|
"tests/roadmap-mode-field.test.cjs",
|
|
174
182
|
"tests/roadmap-parser.test.cjs",
|
|
175
183
|
"tests/roadmap-phase-fallback.test.cjs",
|
|
184
|
+
"tests/roadmap-upgrade-bracket.test.cjs",
|
|
176
185
|
"tests/roadmap.test.cjs",
|
|
186
|
+
"tests/run-tests-harness.test.cjs",
|
|
177
187
|
"tests/runtime-artifact-layout.test.cjs",
|
|
178
188
|
"tests/runtime-identity.test.cjs",
|
|
179
189
|
"tests/runtime-launcher-parity.test.cjs",
|
|
190
|
+
"tests/runtime-name-policy.test.cjs",
|
|
180
191
|
"tests/security.test.cjs",
|
|
181
192
|
"tests/settings-jsonc.test.cjs",
|
|
182
193
|
"tests/shared-hooks-dir-resolution.test.cjs",
|
|
183
194
|
"tests/shell-command-projection-dispatch.test.cjs",
|
|
195
|
+
"tests/shipped-reference-cites.test.cjs",
|
|
184
196
|
"tests/spawn-liveness-banner.test.cjs",
|
|
185
197
|
"tests/state-document.test.cjs",
|
|
186
198
|
"tests/state-transition.test.cjs",
|
|
187
199
|
"tests/state.test.cjs",
|
|
188
200
|
"tests/todos-workstream-scope.test.cjs",
|
|
189
201
|
"tests/tracer-bullet.test.cjs",
|
|
202
|
+
"tests/uat-predicate.test.cjs",
|
|
190
203
|
"tests/uat.test.cjs",
|
|
204
|
+
"tests/ui-interaction-capture.test.cjs",
|
|
191
205
|
"tests/ui-safety-gate.test.cjs",
|
|
192
206
|
"tests/ui-spec-inventory-provenance.test.cjs",
|
|
193
207
|
"tests/unreachable-guard-drift.test.cjs",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// Durability, not `--test-reporter-destination` (#3889 root cause): a
|
|
17
17
|
// reporter that YIELDS strings has them piped by Node into a
|
|
18
18
|
// `fs.WriteStream` targeting the destination path, and that stream buffers.
|
|
19
|
-
// The parent's
|
|
20
|
-
//
|
|
19
|
+
// The parent's per-chunk timeout (run-tests.cjs's runChunk) kills the child
|
|
20
|
+
// on a hang, and a hard kill (TerminateProcess on Windows, SIGKILL on POSIX)
|
|
21
|
+
// is uncatchable and gives the process zero chance to flush — so a
|
|
21
22
|
// yield-based reporter can lose every event still sitting in the stream's
|
|
22
23
|
// buffer, which is exactly the case this feature exists to diagnose (proven
|
|
23
24
|
// live: a chunk killed at 2006ms produced a `killed after 2006ms` line from
|
|
@@ -27,6 +27,7 @@ module.exports = {
|
|
|
27
27
|
"tests/capability-source.test.cjs",
|
|
28
28
|
"tests/capability-state.test.cjs",
|
|
29
29
|
"tests/changeset-new.test.cjs",
|
|
30
|
+
"tests/check-contract-drift.test.cjs",
|
|
30
31
|
"tests/check-env.test.cjs",
|
|
31
32
|
"tests/check-gap-analysis-plan-post-e2e.test.cjs",
|
|
32
33
|
"tests/check-glossary-refs.test.cjs",
|
|
@@ -42,6 +43,7 @@ module.exports = {
|
|
|
42
43
|
"tests/claude-md.test.cjs",
|
|
43
44
|
"tests/cline-install.test.cjs",
|
|
44
45
|
"tests/close-phase-todos-padded-resolves.test.cjs",
|
|
46
|
+
"tests/code-review-fix-pipeline-regression.test.cjs",
|
|
45
47
|
"tests/code-review-pipeline-regression.test.cjs",
|
|
46
48
|
"tests/code-review-tier3-files-override-scoping.test.cjs",
|
|
47
49
|
"tests/code-review.test.cjs",
|
|
@@ -62,6 +64,7 @@ module.exports = {
|
|
|
62
64
|
"tests/config-loader.test.cjs",
|
|
63
65
|
"tests/config-schema.property.test.cjs",
|
|
64
66
|
"tests/config.test.cjs",
|
|
67
|
+
"tests/configured-entrypoint-validation.test.cjs",
|
|
65
68
|
"tests/copilot-install.test.cjs",
|
|
66
69
|
"tests/copilot-upgrades.test.cjs",
|
|
67
70
|
"tests/core-utils.test.cjs",
|
|
@@ -107,6 +110,7 @@ module.exports = {
|
|
|
107
110
|
"tests/gsd-mcp-server-bin.test.cjs",
|
|
108
111
|
"tests/gsd-statusline.test.cjs",
|
|
109
112
|
"tests/gsd-validate-commit-crash-policy.test.cjs",
|
|
113
|
+
"tests/gsd-validate-commit-sigpipe.test.cjs",
|
|
110
114
|
"tests/gsd-write-guard.test.cjs",
|
|
111
115
|
"tests/health-diagnostic-rules/config-validation.test.cjs",
|
|
112
116
|
"tests/health-diagnostic-rules/worktree-health.test.cjs",
|
|
@@ -141,6 +145,7 @@ module.exports = {
|
|
|
141
145
|
"tests/kimi-agent-converter.test.cjs",
|
|
142
146
|
"tests/kimi-upgrades.test.cjs",
|
|
143
147
|
"tests/kimi-variant-disambiguation.test.cjs",
|
|
148
|
+
"tests/lint-phase-arg-assignment.test.cjs",
|
|
144
149
|
"tests/lint-workflow-shellcheck-fetch.test.cjs",
|
|
145
150
|
"tests/live-config-guard.test.cjs",
|
|
146
151
|
"tests/lockfile-cve-audit.test.cjs",
|
|
@@ -231,6 +236,7 @@ module.exports = {
|
|
|
231
236
|
"tests/reviewer-step-dispatch.test.cjs",
|
|
232
237
|
"tests/revision-remediation-binding.test.cjs",
|
|
233
238
|
"tests/roadmap-parser.test.cjs",
|
|
239
|
+
"tests/roadmap-upgrade-bracket.test.cjs",
|
|
234
240
|
"tests/roadmap.test.cjs",
|
|
235
241
|
"tests/run-tests-harness.test.cjs",
|
|
236
242
|
"tests/run-tests-temp-root.test.cjs",
|
|
@@ -248,12 +254,17 @@ module.exports = {
|
|
|
248
254
|
"tests/shell-command-projection-dispatch.test.cjs",
|
|
249
255
|
"tests/shell-command-projection-path-sep.test.cjs",
|
|
250
256
|
"tests/ship-notes-wedged-pr.test.cjs",
|
|
257
|
+
"tests/shipped-reference-cites.test.cjs",
|
|
251
258
|
"tests/skill-manifest.test.cjs",
|
|
252
259
|
"tests/slug-derivation-drift-guard.test.cjs",
|
|
253
260
|
"tests/state-todos-render.test.cjs",
|
|
254
261
|
"tests/state.test.cjs",
|
|
255
262
|
"tests/teams-status.test.cjs",
|
|
263
|
+
"tests/test-gate-watch-mode.test.cjs",
|
|
256
264
|
"tests/todos-workstream-scope.test.cjs",
|
|
265
|
+
"tests/ui-interaction-capture.test.cjs",
|
|
266
|
+
"tests/undo-commit-selection-4465.test.cjs",
|
|
267
|
+
"tests/undo-commit-selection.test.cjs",
|
|
257
268
|
"tests/unreachable-guard-drift.test.cjs",
|
|
258
269
|
"tests/unreachable-shell-guard.test.cjs",
|
|
259
270
|
"tests/unusable-input.test.cjs",
|
|
@@ -68,6 +68,7 @@ const DOCS_GUARD_EXEMPT_BASELINE = [
|
|
|
68
68
|
'lint-allow-test-rule-refs.test.cjs',
|
|
69
69
|
'lint-docs-command-form.test.cjs',
|
|
70
70
|
'lint-docs-required.test.cjs',
|
|
71
|
+
'lint-retired-runtime-name.test.cjs',
|
|
71
72
|
'manifest-version-sync.test.cjs',
|
|
72
73
|
'milestone-archive.test.cjs',
|
|
73
74
|
'model-resolver.test.cjs',
|
|
@@ -178,13 +179,39 @@ const DOCS_GUARD_EXEMPT_DOCS_PATHS = {
|
|
|
178
179
|
'docs/COMMANDS.md', 'docs/USER-GUIDE.md', 'docs/adr', 'docs/adr/0001-foo.md', 'docs/adr/0099-new.md',
|
|
179
180
|
'docs/agents', 'docs/agents/triage-labels.md',
|
|
180
181
|
],
|
|
182
|
+
// #4729: `writeFile('docs/...', ...)` calls WRITE fabricated fixture
|
|
183
|
+
// content into a throwaway mkdtemp repo; the file never reads real shipped
|
|
184
|
+
// docs/ content — every path here is fixture data for the guard under
|
|
185
|
+
// test.
|
|
186
|
+
// Re-confirmed: every path below is WRITTEN as fabricated fixture content
|
|
187
|
+
// into a throwaway mkdtemp repo; none reads real shipped docs. The a*/b*/c*/
|
|
188
|
+
// d* entries are the adversarial-regression fixtures (dialect-plus-runtime-
|
|
189
|
+
// word, adjacency laundering, version laundering, bare-marker abuse).
|
|
190
|
+
'lint-retired-runtime-name.test.cjs': [
|
|
191
|
+
'docs/adr', 'docs/adr/999-retired-runtime-record.md', 'docs/filler/note-',
|
|
192
|
+
'docs/guides/a1.md', 'docs/guides/a2.md', 'docs/guides/b1.md',
|
|
193
|
+
'docs/guides/b2.md', 'docs/guides/b3.md', 'docs/guides/c3.md',
|
|
194
|
+
'docs/guides/c4.md', 'docs/guides/config-homes.md', 'docs/guides/d1.md',
|
|
195
|
+
'docs/guides/hooks.md', 'docs/guides/one.md', 'docs/guides/runtime-claim.md',
|
|
196
|
+
'docs/guides/setup.md', 'docs/history/migration-note.md',
|
|
197
|
+
'docs/ja-JP/guides/runtimes.md', 'docs/pt-BR/guides/a3.md',
|
|
198
|
+
'docs/pt-BR/guides/hooks.md', 'docs/pt-BR/guides/policy.md',
|
|
199
|
+
'docs/reference/client-models.md', 'docs/reference/models.md',
|
|
200
|
+
'docs/zh-CN/guides/a4.md', 'docs/zh-CN/reference/models.md',
|
|
201
|
+
],
|
|
181
202
|
'manifest-version-sync.test.cjs': [],
|
|
182
203
|
// #3884: re-confirmed — the added docs/CLI-TOOLS.md:458 reference is the
|
|
183
204
|
// same class as the existing docs/TESTING-SUITES.md one (a placement-note
|
|
184
205
|
// / explanatory comment citing documented CLI behavior for context, never
|
|
185
206
|
// a read target); the exemption's premise still holds for both.
|
|
186
207
|
'milestone-archive.test.cjs': ['docs/CLI-TOOLS.md', 'docs/TESTING-SUITES.md'],
|
|
187
|
-
|
|
208
|
+
// #4505: additionally cites docs/features/dynamic-routing-with-failure-tier-escalation.md
|
|
209
|
+
// in explanatory comments, quoting the documented first-spawn contract the new rows
|
|
210
|
+
// assert against; the file never reads that (or any) docs/ file — every read it makes
|
|
211
|
+
// targets a tmpdir .planning fixture.
|
|
212
|
+
'model-resolver.test.cjs': [
|
|
213
|
+
'docs/TESTING-SUITES.md', 'docs/features/dynamic-routing-with-failure-tier-escalation.md',
|
|
214
|
+
],
|
|
188
215
|
'new-project-mvp-prompt.test.cjs': ['docs/CONFIGURATION.md'],
|
|
189
216
|
'onboard-command.test.cjs': ['docs/adr/0001-runtime.md'],
|
|
190
217
|
'opencode-command-dir-plural.test.cjs': ['docs/commands'],
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-phase-arg-assignment.cjs — a workflow that READS `${PHASE_ARG}` must
|
|
6
|
+
* also SAY where it comes from (#4777).
|
|
7
|
+
*
|
|
8
|
+
* ## Why
|
|
9
|
+
*
|
|
10
|
+
* Seven workflows shipped an `init.phase-op "${PHASE_ARG}"` call with no
|
|
11
|
+
* assignment anywhere in the file and no instruction to derive one. The
|
|
12
|
+
* variable expanded to the empty string on every run, `init.phase-op ""`
|
|
13
|
+
* resolved `phase_found: false` unconditionally, and each workflow fell
|
|
14
|
+
* through to its own "phase not executed" exit with a blank phase number —
|
|
15
|
+
* so `/gsd:secure-phase 4` reported that phase *nothing* was not executed.
|
|
16
|
+
* Two of them had carried the defect since the files were created in March.
|
|
17
|
+
*
|
|
18
|
+
* Nothing caught it because the working pattern lived in a *different* file:
|
|
19
|
+
* `verify-work.md` derives the value correctly, and a copy of an init block
|
|
20
|
+
* that leaves the derivation line behind is indistinguishable, on review,
|
|
21
|
+
* from one that never needed it.
|
|
22
|
+
*
|
|
23
|
+
* ## The rule
|
|
24
|
+
*
|
|
25
|
+
* A workflow whose bash blocks read `$PHASE_ARG` must carry one of:
|
|
26
|
+
*
|
|
27
|
+
* 1. a **shell assignment** matching one of the frozen `CANONICAL_FORMS`
|
|
28
|
+
* below, textually before the first read; or
|
|
29
|
+
* 2. a **prose derivation step** — a line outside the bash blocks that
|
|
30
|
+
* tells the model to parse `$ARGUMENTS` into `$PHASE_ARG`
|
|
31
|
+
* (`execute-phase.md`: "First positional token → `PHASE_ARG`"); or
|
|
32
|
+
* 3. an entry in `EXEMPT`, with a reason.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the forms are frozen
|
|
35
|
+
*
|
|
36
|
+
* Requiring *an* assignment would be satisfied by a seventh hand-rolled sed
|
|
37
|
+
* pipeline, which is how the two grammars in this tree already drifted apart.
|
|
38
|
+
* The rule therefore pins the exact text. There are three legitimate ways a
|
|
39
|
+
* workflow learns its phase — `$ARGUMENTS` positional, `--phase N`, and a
|
|
40
|
+
* positional `$1` passed by a skill dispatch — and each has exactly one
|
|
41
|
+
* spelling. A new grammar is a deliberate one-line addition here, reviewed
|
|
42
|
+
* once, rather than a copy that diverges silently.
|
|
43
|
+
*
|
|
44
|
+
* Detection only: this lint never edits a workflow.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
50
|
+
|
|
51
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
52
|
+
const DEFAULT_ROOT = path.join(ROOT, 'gsd-core', 'workflows');
|
|
53
|
+
const SECTIONIZER_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'markdown-sectionizer.cjs');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fenced-block extraction goes through the Markdown Sectionizer seam
|
|
57
|
+
* (ADR-1372) rather than a local fence regex — `local/no-adhoc-markdown-parsing`
|
|
58
|
+
* enforces that, and it is right to: a `~~~` block, an indented fence, or a
|
|
59
|
+
* nested fence inside another would each be misread by a hand-rolled pattern,
|
|
60
|
+
* and this rule's whole job is to know which lines are shell.
|
|
61
|
+
*/
|
|
62
|
+
function loadSectionizer() {
|
|
63
|
+
try {
|
|
64
|
+
return require(SECTIONIZER_PATH);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new ExitError(
|
|
67
|
+
1,
|
|
68
|
+
'lint-phase-arg-assignment: cannot load the markdown-sectionizer seam at ' +
|
|
69
|
+
`${path.relative(ROOT, SECTIONIZER_PATH)} — run 'npm run build:lib' first (${e.message})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SHELL_INFO_STRINGS = new Set(['bash', 'sh', 'shell']);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The only accepted spellings of a PHASE_ARG assignment, keyed by grammar.
|
|
78
|
+
* Compared after collapsing runs of whitespace, so indentation inside a
|
|
79
|
+
* block is free but the pipeline itself is not.
|
|
80
|
+
*
|
|
81
|
+
* Known limitation (#4777 review): neither `positional` nor `phaseFlag`
|
|
82
|
+
* strips a surrounding quote — `positional('4 --ws "my team"')` leaves
|
|
83
|
+
* `--ws`/`my`/`team` tokens in PHASE_ARG instead of stripping the flag, and
|
|
84
|
+
* `phaseFlag('--phase "4"')` yields an empty PHASE_ARG, reproducing this
|
|
85
|
+
* issue's own defect for that input shape. Every `argument-hint` in the
|
|
86
|
+
* repo documents unquoted usage as primary, and `positional` is copied
|
|
87
|
+
* verbatim from verify-work.md's pre-existing shipped form rather than
|
|
88
|
+
* "improved" here — hardening the regex would mean changing production
|
|
89
|
+
* bash text in all 7 shipped workflows this exact-match check compares
|
|
90
|
+
* against. See tests/lint-phase-arg-assignment.test.cjs's dedicated
|
|
91
|
+
* describe block for a live repro.
|
|
92
|
+
*/
|
|
93
|
+
const CANONICAL_FORMS = Object.freeze({
|
|
94
|
+
/** `/gsd:secure-phase 4` — the phase is the whole argument string, minus a `--ws <name>` pair. */
|
|
95
|
+
positional:
|
|
96
|
+
'PHASE_ARG=$(echo "$ARGUMENTS" | sed -E \'s/--ws[[:space:]]+[A-Za-z0-9._-]+//g\' | xargs)',
|
|
97
|
+
/** `/gsd:review --phase 4 --codex` — the phase is the value of a named flag. */
|
|
98
|
+
phaseFlag:
|
|
99
|
+
'PHASE_ARG=$(echo "$ARGUMENTS" | sed -nE \'s/.*--phase[[:space:]]+([A-Za-z0-9._-]+).*/\\1/p\')',
|
|
100
|
+
/** Skill dispatch passes the phase as `$1` (`Skill(skill="gsd-code-review", args="4")`). */
|
|
101
|
+
firstPositional: 'PHASE_ARG="${1}"',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Files that read `$PHASE_ARG` and legitimately do not assign it. Each entry
|
|
106
|
+
* is checked for staleness: one whose file stopped reading the variable, or
|
|
107
|
+
* started assigning it, fails just as loudly as a missing assignment — an
|
|
108
|
+
* exemption nobody can remove is how a list like this becomes a place to
|
|
109
|
+
* hide the next defect.
|
|
110
|
+
*/
|
|
111
|
+
const EXEMPT = new Map([
|
|
112
|
+
[
|
|
113
|
+
'code-review/steps/dispatch-fix.md',
|
|
114
|
+
'step fragment of code-review.md, which assigns PHASE_ARG="${1}" before dispatching it',
|
|
115
|
+
],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const READ_RE = /\$\{?PHASE_ARG\b/;
|
|
119
|
+
const ASSIGN_RE = /^[ \t]*PHASE_ARG=(?!=).*$/gm;
|
|
120
|
+
/**
|
|
121
|
+
* A prose derivation step: a non-bash line that routes `$ARGUMENTS` into
|
|
122
|
+
* `PHASE_ARG`. Matches both shapes in the tree — an arrow ("First positional
|
|
123
|
+
* token → `PHASE_ARG`") and a "store as `$PHASE_ARG`" instruction.
|
|
124
|
+
*/
|
|
125
|
+
const PROSE_ASSIGN_RE = /(?:→|->|store as)\s*`?\$?\{?PHASE_ARG/;
|
|
126
|
+
|
|
127
|
+
/** Collapse whitespace runs so indentation does not decide the verdict. */
|
|
128
|
+
function normalize(line) {
|
|
129
|
+
return line.trim().replace(/\s+/g, ' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const CANONICAL_SET = new Set(Object.values(CANONICAL_FORMS).map(normalize));
|
|
133
|
+
|
|
134
|
+
/** Every `*.md` under `dir`, recursively, as paths relative to `dir`. */
|
|
135
|
+
function collectWorkflows(dir) {
|
|
136
|
+
const out = [];
|
|
137
|
+
const walk = (current, prefix) => {
|
|
138
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
139
|
+
const abs = path.join(current, entry.name);
|
|
140
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
141
|
+
if (entry.isDirectory()) walk(abs, rel);
|
|
142
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(rel);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
walk(dir, '');
|
|
146
|
+
return out.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classify one workflow's PHASE_ARG handling.
|
|
151
|
+
*
|
|
152
|
+
* `reads` counts bash-block reads only: `${PHASE_ARG}` inside prose is a
|
|
153
|
+
* mention (an error message being described, an example invocation), not a
|
|
154
|
+
* consumption, and requiring an assignment for those would make the rule fire
|
|
155
|
+
* on documentation.
|
|
156
|
+
*/
|
|
157
|
+
function inspect(text, sectionizer = loadSectionizer()) {
|
|
158
|
+
const lines = text.split('\n');
|
|
159
|
+
const shell = sectionizer
|
|
160
|
+
.scanFencedBlocks(lines)
|
|
161
|
+
.filter((b) => SHELL_INFO_STRINGS.has(b.infoString.trim().split(/\s+/)[0]))
|
|
162
|
+
// closeLineIdx === -1 marks an unterminated fence: take the rest of the
|
|
163
|
+
// file rather than dropping the block, so a workflow whose last fence is
|
|
164
|
+
// missing its closer is still scanned instead of silently exempted.
|
|
165
|
+
.map((b) => lines.slice(b.openLineIdx + 1, b.closeLineIdx === -1 ? lines.length : b.closeLineIdx).join('\n'))
|
|
166
|
+
.join('\n');
|
|
167
|
+
|
|
168
|
+
const assignments = [...shell.matchAll(ASSIGN_RE)].map((m) => m[0]);
|
|
169
|
+
const firstAssignIdx = shell.search(/^[ \t]*PHASE_ARG=(?!=)/m);
|
|
170
|
+
const firstReadIdx = shell.search(READ_RE);
|
|
171
|
+
|
|
172
|
+
// The prose search deliberately runs over the WHOLE file rather than the
|
|
173
|
+
// non-bash remainder: a derivation step is a prose line, and stripping
|
|
174
|
+
// blocks first would only add a way for the two to disagree.
|
|
175
|
+
const hasProse = text.split('\n').some((line) => PROSE_ASSIGN_RE.test(line));
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
reads: firstReadIdx !== -1,
|
|
179
|
+
assignments,
|
|
180
|
+
nonCanonical: assignments.filter((a) => !CANONICAL_SET.has(normalize(a))),
|
|
181
|
+
// An assignment that lands after the first read is the same defect with
|
|
182
|
+
// extra steps — the read still expands to nothing.
|
|
183
|
+
assignsBeforeRead: firstAssignIdx !== -1 && firstReadIdx !== -1 && firstAssignIdx <= firstReadIdx,
|
|
184
|
+
hasProse,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Scan a workflows directory. Returns `{ scanned, violations, staleExemptions }`. */
|
|
189
|
+
function scan(rootDir = DEFAULT_ROOT, exempt = EXEMPT) {
|
|
190
|
+
const violations = [];
|
|
191
|
+
const seenExempt = new Set();
|
|
192
|
+
const sectionizer = loadSectionizer();
|
|
193
|
+
let scanned = 0;
|
|
194
|
+
|
|
195
|
+
for (const rel of collectWorkflows(rootDir)) {
|
|
196
|
+
const report = inspect(fs.readFileSync(path.join(rootDir, rel), 'utf8'), sectionizer);
|
|
197
|
+
if (!report.reads && report.assignments.length === 0) continue;
|
|
198
|
+
scanned += 1;
|
|
199
|
+
|
|
200
|
+
// Decide the verdict FIRST, then consult the exemption list. Marking an
|
|
201
|
+
// entry used before knowing whether the file would actually fail lets an
|
|
202
|
+
// exemption outlive its reason: the file gets fixed, the entry stays, and
|
|
203
|
+
// it silently pre-forgives the NEXT unassigned read in that same file.
|
|
204
|
+
let violation = null;
|
|
205
|
+
if (report.nonCanonical.length > 0) {
|
|
206
|
+
violation = { file: rel, kind: 'non-canonical', detail: normalize(report.nonCanonical[0]) };
|
|
207
|
+
} else if (!report.reads) {
|
|
208
|
+
violation = null;
|
|
209
|
+
} else if (report.assignments.length === 0 && !report.hasProse) {
|
|
210
|
+
violation = { file: rel, kind: 'unassigned', detail: 'reads ${PHASE_ARG} with no assignment and no derivation step' };
|
|
211
|
+
} else if (report.assignments.length > 0 && !report.assignsBeforeRead) {
|
|
212
|
+
violation = { file: rel, kind: 'late-assignment', detail: 'PHASE_ARG is assigned after its first use' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!violation) continue;
|
|
216
|
+
if (exempt.has(rel)) { seenExempt.add(rel); continue; }
|
|
217
|
+
violations.push(violation);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const staleExemptions = [...exempt.keys()]
|
|
221
|
+
.filter((rel) => !seenExempt.has(rel))
|
|
222
|
+
.map((rel) => ({ file: rel, reason: exempt.get(rel) }));
|
|
223
|
+
|
|
224
|
+
return { scanned, violations, staleExemptions };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function main() {
|
|
228
|
+
const { scanned, violations, staleExemptions } = scan();
|
|
229
|
+
const parts = [];
|
|
230
|
+
|
|
231
|
+
if (violations.length > 0) {
|
|
232
|
+
parts.push(
|
|
233
|
+
'lint-phase-arg-assignment: these workflows do not say where ${PHASE_ARG} comes from (#4777).\n' +
|
|
234
|
+
'An unassigned PHASE_ARG expands to the empty string, init.phase-op resolves\n' +
|
|
235
|
+
'phase_found:false for every invocation, and the workflow exits reporting that a blank\n' +
|
|
236
|
+
'phase was not executed. Add one of the canonical forms in this script — or, for a model-\n' +
|
|
237
|
+
'derived value, a prose derivation step — before the first read:\n' +
|
|
238
|
+
violations.map((v) => ` gsd-core/workflows/${v.file} [${v.kind}] ${v.detail}`).join('\n'),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (staleExemptions.length > 0) {
|
|
242
|
+
parts.push(
|
|
243
|
+
'lint-phase-arg-assignment: these EXEMPT entries are stale — delete them so the next\n' +
|
|
244
|
+
'unassigned read in those files is caught rather than silently pre-forgiven:\n' +
|
|
245
|
+
staleExemptions.map((s) => ` ${s.file}: ${s.reason}`).join('\n'),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (parts.length > 0) throw new ExitError(1, parts.join('\n\n'));
|
|
249
|
+
|
|
250
|
+
console.log(
|
|
251
|
+
`ok lint-phase-arg-assignment: ${scanned} workflow(s) checked, ${EXEMPT.size} exemption(s) all still needed`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { CANONICAL_FORMS, EXEMPT, DEFAULT_ROOT, inspect, scan };
|
|
256
|
+
|
|
257
|
+
if (require.main === module) runMain(main);
|