forge-workflow 0.0.9 → 0.1.0-beta.2
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/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { execFileSync } = require('node:child_process');
|
|
6
|
+
const babelParser = require('@babel/parser');
|
|
7
|
+
|
|
8
|
+
const SUPPORTED_TARGET = '0.1.0';
|
|
9
|
+
const AUDIT_ARTIFACT = 'docs/work/2026-06-06-kernel-backlog-memory-roadmap/bd-call-site-kill-list.md';
|
|
10
|
+
|
|
11
|
+
const GROUPS = ['command', 'runtime', 'docs', 'skills', 'hooks'];
|
|
12
|
+
|
|
13
|
+
const STATIC_ROOT_DOC_FILES = [
|
|
14
|
+
'AGENTS.md',
|
|
15
|
+
'README.md',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Instruction/rules roots always scanned for lingering `bd` references, independent
|
|
19
|
+
// of plugin manifests. `.claude/rules` was dropped from claude.plugin.json (so setup
|
|
20
|
+
// no longer creates an empty, never-populated dir), but a project may still carry
|
|
21
|
+
// `.claude/rules/*.md`, so the bd-retirement audit must keep scanning it.
|
|
22
|
+
const STATIC_INSTRUCTION_ROOTS = [
|
|
23
|
+
'.claude/rules',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const FORGE_SKILLS_PACK_ROOTS = [
|
|
27
|
+
'packages/skills/forge-plugin',
|
|
28
|
+
'packages/skills/forge-workflow',
|
|
29
|
+
'packages/skills/forge',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const STATIC_SCAN_ROOTS = [
|
|
33
|
+
'lefthook.yml',
|
|
34
|
+
'bin',
|
|
35
|
+
'lib',
|
|
36
|
+
'scripts',
|
|
37
|
+
'.forge',
|
|
38
|
+
'.github/workflows',
|
|
39
|
+
'packages/skills/README.md',
|
|
40
|
+
...FORGE_SKILLS_PACK_ROOTS,
|
|
41
|
+
'docs/PROJECT_DESIGN.md',
|
|
42
|
+
'docs/guides',
|
|
43
|
+
'docs/reference',
|
|
44
|
+
'docs/work/2026-06-06-kernel-backlog-memory-roadmap',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const SKIP_DIR_NAMES = new Set([
|
|
48
|
+
'.git',
|
|
49
|
+
'node_modules',
|
|
50
|
+
'coverage',
|
|
51
|
+
'dist',
|
|
52
|
+
'build',
|
|
53
|
+
'.worktrees',
|
|
54
|
+
'test',
|
|
55
|
+
'tests',
|
|
56
|
+
'test-env',
|
|
57
|
+
'test-results',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const SKIP_RELATIVE_PREFIXES = [
|
|
61
|
+
'.claude/worktrees/',
|
|
62
|
+
'packages/skills/node_modules/',
|
|
63
|
+
'packages/skills/test/',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const SKIP_RELATIVE_FILES = new Set([
|
|
67
|
+
AUDIT_ARTIFACT,
|
|
68
|
+
'.forge/pr-body.md',
|
|
69
|
+
'lib/release-readiness.js',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const TEXT_EXTENSIONS = new Set([
|
|
73
|
+
'.cjs',
|
|
74
|
+
'.js',
|
|
75
|
+
'.json',
|
|
76
|
+
'.md',
|
|
77
|
+
'.mdc',
|
|
78
|
+
'.mjs',
|
|
79
|
+
'.sh',
|
|
80
|
+
'.toml',
|
|
81
|
+
'.txt',
|
|
82
|
+
'.yaml',
|
|
83
|
+
'.yml',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const D20_HOT_PATH_FILES = new Set([
|
|
87
|
+
'lib/commands/_issue.js',
|
|
88
|
+
'lib/commands/prime.js',
|
|
89
|
+
'lib/commands/orient.js',
|
|
90
|
+
'lib/commands/recap.js',
|
|
91
|
+
'lib/commands/remember.js',
|
|
92
|
+
'lib/commands/recall.js',
|
|
93
|
+
'lib/commands/sync.js',
|
|
94
|
+
'lib/commands/worktree.js',
|
|
95
|
+
'lib/commands/setup.js',
|
|
96
|
+
'lib/project-memory.js',
|
|
97
|
+
'lib/workflow/state-manager.js',
|
|
98
|
+
'scripts/preflight.sh',
|
|
99
|
+
'scripts/smart-status.sh',
|
|
100
|
+
'scripts/smart-status-score.js',
|
|
101
|
+
'scripts/smart-status-sessions.js',
|
|
102
|
+
'AGENTS.md',
|
|
103
|
+
'lefthook.yml',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const D20_HOT_PATH_PREFIXES = [
|
|
107
|
+
'scripts/forge-team/',
|
|
108
|
+
'.forge/hooks/',
|
|
109
|
+
'.github/workflows/',
|
|
110
|
+
...FORGE_SKILLS_PACK_ROOTS.map(root => `${root}/`),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const REQUIRED_ISSUE_SUBCOMMANDS = [
|
|
114
|
+
'ready',
|
|
115
|
+
'list',
|
|
116
|
+
'show',
|
|
117
|
+
'search',
|
|
118
|
+
'stats',
|
|
119
|
+
'create',
|
|
120
|
+
'update',
|
|
121
|
+
'close',
|
|
122
|
+
'comment',
|
|
123
|
+
'dep',
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const REQUIRED_ISSUE_DEP_ACTIONS = ['add', 'remove'];
|
|
127
|
+
|
|
128
|
+
const REQUIRED_FORGE_SKILLS = [
|
|
129
|
+
{ name: 'ready', commandPatterns: [/forge ready/, /forge issue ready/] },
|
|
130
|
+
{ name: 'show', commandPatterns: [/forge show/, /forge issue show/] },
|
|
131
|
+
{ name: 'claim', commandPatterns: [/forge claim/] },
|
|
132
|
+
{ name: 'comment', commandPatterns: [/forge comment/, /forge issue comment/] },
|
|
133
|
+
{ name: 'close', commandPatterns: [/forge close/, /forge issue close/] },
|
|
134
|
+
{ name: 'recap', commandPatterns: [/forge recap/] },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const REQUIRED_CLAIM_RELEASE_COMMANDS = [
|
|
138
|
+
{
|
|
139
|
+
name: 'claim',
|
|
140
|
+
path: 'lib/commands/claim.js',
|
|
141
|
+
// claim delegates to the SHARED issue dispatch (runIssueSubcommand) so the
|
|
142
|
+
// check-after-write verify loop (gate.issue_verify) covers it — that
|
|
143
|
+
// delegation is accepted as kernel evidence alongside a direct
|
|
144
|
+
// runIssueOperation('claim') dispatch.
|
|
145
|
+
contractPatterns: [
|
|
146
|
+
/forge claim <id>/,
|
|
147
|
+
/runIssueSubcommand\(['"]claim['"]|runIssueOperation\(['"]claim['"]|operation:\s*['"]claim['"]/,
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'release',
|
|
152
|
+
path: 'lib/commands/release.js',
|
|
153
|
+
contractPatterns: [
|
|
154
|
+
/forge release <id>/,
|
|
155
|
+
/runIssueOperation\(['"]release['"]|operation:\s*['"]release['"]/,
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const FORGE_SKILLS_PACK_CANDIDATES = FORGE_SKILLS_PACK_ROOTS.map(root => ({
|
|
161
|
+
manifest: `${root}/manifest.json`,
|
|
162
|
+
root,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
const FRESH_CLONE_ACCEPTANCE_CANDIDATES = [
|
|
166
|
+
'test/e2e/fresh-clone-no-beads.test.js',
|
|
167
|
+
'test/e2e/fresh-clone-no-beads-acceptance.test.js',
|
|
168
|
+
'test/release/fresh-clone-no-beads.test.js',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const FRESH_CLONE_ACCEPTANCE_REQUIREMENTS = [
|
|
172
|
+
{
|
|
173
|
+
name: 'enabled acceptance test',
|
|
174
|
+
matches: hasEnabledAcceptanceTest,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'fresh clone setup',
|
|
178
|
+
patterns: [/fresh[-\s]?clone/i, /git\s+clone/i, /git\s*\(\s*\[\s*['"]clone['"]/i],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'no Beads/Dolt environment',
|
|
182
|
+
patterns: [/no[-\s]?beads/i, /without\s+beads/i, /withoutTools\s*\([^)]*['"]bd['"][^)]*['"]dolt['"]/i],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'forge prime',
|
|
186
|
+
patterns: [/forge\s+prime/i, /forge\s*\(\s*\[\s*['"]prime['"]/i],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'forge issue ready --json',
|
|
190
|
+
patterns: [
|
|
191
|
+
/forge\s+issue\s+ready\b[^\r\n]*--json/i,
|
|
192
|
+
/forge\s*\(\s*\[\s*['"]issue['"]\s*,\s*['"]ready['"][^\]]*['"]--json['"]/i,
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'forge claim',
|
|
197
|
+
patterns: [/forge\s+claim/i, /forge\s*\(\s*\[\s*['"]claim['"]/i],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'forge comment',
|
|
201
|
+
patterns: [/forge\s+(?:issue\s+)?comment/i, /forge\s*\(\s*\[\s*['"]comment['"]/i],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'forge close',
|
|
205
|
+
patterns: [/forge\s+(?:issue\s+)?close/i, /forge\s*\(\s*\[\s*['"]close['"]/i],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'forge recap',
|
|
209
|
+
patterns: [/forge\s+recap/i, /forge\s*\(\s*\[\s*['"]recap['"]/i],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'zero bd invocations',
|
|
213
|
+
patterns: [
|
|
214
|
+
/expect\s*\(\s*bdInvocations\s*\)\s*\.\s*(?:toEqual|toStrictEqual)\s*\(\s*\[\s*\]\s*\)/,
|
|
215
|
+
/expect\s*\(\s*bdInvocations\s*\)\s*\.\s*toHaveLength\s*\(\s*0\s*\)/,
|
|
216
|
+
/expect\s*\(\s*bdInvocations\s*\.\s*length\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(\s*0\s*\)/,
|
|
217
|
+
/assert\.(?:equal|strictEqual)\s*\(\s*bdInvocations\.length\s*,\s*0\s*\)/,
|
|
218
|
+
/assert\.deepStrictEqual\s*\(\s*bdInvocations\s*,\s*\[\s*\]\s*\)/,
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const REQUIRED_KERNEL_ADAPTER_OPERATIONS = [
|
|
224
|
+
...REQUIRED_ISSUE_SUBCOMMANDS,
|
|
225
|
+
...REQUIRED_CLAIM_RELEASE_COMMANDS.map(command => command.name),
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
function toRepoPath(filePath) {
|
|
229
|
+
return filePath.replaceAll(path.sep, '/');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeRepoPath(filePath) {
|
|
233
|
+
return toRepoPath(path.normalize(filePath)).replace(/^\.\//, '');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isSafeRepoPath(projectRoot, relativePath) {
|
|
237
|
+
return (
|
|
238
|
+
typeof relativePath === 'string' &&
|
|
239
|
+
relativePath.length > 0 &&
|
|
240
|
+
!path.isAbsolute(relativePath) &&
|
|
241
|
+
isWithinProject(projectRoot, relativePath)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function absolutePath(projectRoot, relativePath) {
|
|
246
|
+
return path.join(projectRoot, ...relativePath.split('/'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isWithinProject(projectRoot, relativePath) {
|
|
250
|
+
const root = path.resolve(projectRoot);
|
|
251
|
+
const resolved = path.resolve(projectRoot, relativePath);
|
|
252
|
+
const relative = path.relative(root, resolved);
|
|
253
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shouldSkipRelativePath(relativePath) {
|
|
257
|
+
if (SKIP_RELATIVE_FILES.has(relativePath)) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return SKIP_RELATIVE_PREFIXES.some(prefix => relativePath.startsWith(prefix));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function safeNormalizeRepoPath(projectRoot, filePath) {
|
|
265
|
+
if (typeof filePath !== 'string' || filePath.length === 0) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const relativePath = normalizeRepoPath(filePath);
|
|
269
|
+
return isSafeRepoPath(projectRoot, relativePath) ? relativePath : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readAgentPluginManifests(projectRoot) {
|
|
273
|
+
const agentsDir = absolutePath(projectRoot, 'lib/agents');
|
|
274
|
+
if (!fs.existsSync(agentsDir)) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
279
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.plugin.json'))
|
|
280
|
+
.flatMap(entry => {
|
|
281
|
+
try {
|
|
282
|
+
return [JSON.parse(fs.readFileSync(path.join(agentsDir, entry.name), 'utf8'))];
|
|
283
|
+
} catch (_error) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function collectAgentInstructionSurfaces(projectRoot) {
|
|
290
|
+
const rootDocFiles = [];
|
|
291
|
+
const instructionRoots = [];
|
|
292
|
+
const skillRoots = [];
|
|
293
|
+
|
|
294
|
+
for (const manifest of readAgentPluginManifests(projectRoot)) {
|
|
295
|
+
const rootConfig = safeNormalizeRepoPath(projectRoot, manifest.files?.rootConfig);
|
|
296
|
+
if (rootConfig) {
|
|
297
|
+
rootDocFiles.push(rootConfig);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [name, directory] of Object.entries(manifest.directories || {})) {
|
|
301
|
+
const normalized = safeNormalizeRepoPath(projectRoot, directory);
|
|
302
|
+
if (!normalized) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (name === 'skills') {
|
|
307
|
+
skillRoots.push(normalized);
|
|
308
|
+
} else {
|
|
309
|
+
instructionRoots.push(normalized);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
instructionRoots: [...new Set([...STATIC_INSTRUCTION_ROOTS, ...instructionRoots])],
|
|
316
|
+
rootDocFiles: [...new Set([...STATIC_ROOT_DOC_FILES, ...rootDocFiles])],
|
|
317
|
+
skillRoots: [...new Set(skillRoots)],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getRootDocFiles(projectRoot) {
|
|
322
|
+
return new Set(collectAgentInstructionSurfaces(projectRoot).rootDocFiles);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getInstructionPrefixes(projectRoot) {
|
|
326
|
+
return collectAgentInstructionSurfaces(projectRoot).instructionRoots.map(root => `${root}/`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getSkillPrefixes(projectRoot) {
|
|
330
|
+
return collectAgentInstructionSurfaces(projectRoot).skillRoots.map(root => `${root}/`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getDefaultScanRoots(projectRoot) {
|
|
334
|
+
const surfaces = collectAgentInstructionSurfaces(projectRoot);
|
|
335
|
+
return [
|
|
336
|
+
...new Set([
|
|
337
|
+
...surfaces.rootDocFiles,
|
|
338
|
+
...surfaces.skillRoots,
|
|
339
|
+
...surfaces.instructionRoots,
|
|
340
|
+
...STATIC_SCAN_ROOTS,
|
|
341
|
+
]),
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isTextFile(relativePath, projectRoot) {
|
|
346
|
+
const basename = path.basename(relativePath);
|
|
347
|
+
if (basename.endsWith('.test.js')) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (getRootDocFiles(projectRoot).has(relativePath) || basename === 'lefthook.yml') {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return TEXT_EXTENSIONS.has(path.extname(relativePath));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function walkFiles(projectRoot, relativeRoot) {
|
|
357
|
+
const rootPath = absolutePath(projectRoot, relativeRoot);
|
|
358
|
+
if (!fs.existsSync(rootPath)) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const stat = fs.statSync(rootPath);
|
|
363
|
+
if (stat.isFile()) {
|
|
364
|
+
const normalized = toRepoPath(relativeRoot);
|
|
365
|
+
return isTextFile(normalized, projectRoot) && !shouldSkipRelativePath(normalized) ? [normalized] : [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const files = [];
|
|
369
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true })
|
|
370
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
371
|
+
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
if (entry.isDirectory() && SKIP_DIR_NAMES.has(entry.name)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const relativePath = toRepoPath(path.join(relativeRoot, entry.name));
|
|
378
|
+
if (shouldSkipRelativePath(relativePath)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (entry.isDirectory()) {
|
|
383
|
+
files.push(...walkFiles(projectRoot, relativePath));
|
|
384
|
+
} else if (entry.isFile() && isTextFile(relativePath, projectRoot)) {
|
|
385
|
+
files.push(relativePath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return files;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function readSyncManifestScanRoots(projectRoot) {
|
|
393
|
+
const manifestPath = absolutePath(projectRoot, '.forge/sync-manifest.json');
|
|
394
|
+
if (!fs.existsSync(manifestPath)) {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
400
|
+
if (!Array.isArray(manifest.files)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
return manifest.files
|
|
404
|
+
.filter(file => typeof file === 'string')
|
|
405
|
+
.map(normalizeRepoPath)
|
|
406
|
+
.filter(file => (
|
|
407
|
+
file.length > 0 &&
|
|
408
|
+
isSafeRepoPath(projectRoot, file) &&
|
|
409
|
+
isTextFile(file, projectRoot)
|
|
410
|
+
));
|
|
411
|
+
} catch (_error) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getScanRoots(projectRoot, options = {}) {
|
|
417
|
+
const roots = options.scanRoots || getDefaultScanRoots(projectRoot);
|
|
418
|
+
return [
|
|
419
|
+
...new Set([
|
|
420
|
+
...roots,
|
|
421
|
+
...readSyncManifestScanRoots(projectRoot),
|
|
422
|
+
]),
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isInstructionPath(relativePath, projectRoot) {
|
|
427
|
+
return getInstructionPrefixes(projectRoot).some(prefix => relativePath.startsWith(prefix));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function classifyGroup(relativePath, projectRoot) {
|
|
431
|
+
if (
|
|
432
|
+
relativePath === 'lefthook.yml' ||
|
|
433
|
+
relativePath.startsWith('.forge/hooks/') ||
|
|
434
|
+
relativePath.startsWith('.github/workflows/')
|
|
435
|
+
) {
|
|
436
|
+
return 'hooks';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
getSkillPrefixes(projectRoot).some(prefix => relativePath.startsWith(prefix)) ||
|
|
441
|
+
relativePath.startsWith('packages/skills/')
|
|
442
|
+
) {
|
|
443
|
+
return 'skills';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (relativePath.startsWith('lib/commands/') || relativePath.startsWith('bin/')) {
|
|
447
|
+
return 'command';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
relativePath.startsWith('docs/') ||
|
|
452
|
+
getRootDocFiles(projectRoot).has(relativePath) ||
|
|
453
|
+
isInstructionPath(relativePath, projectRoot)
|
|
454
|
+
) {
|
|
455
|
+
return 'docs';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return 'runtime';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function findBdTerms(line) {
|
|
462
|
+
const terms = [];
|
|
463
|
+
if (/\bbd\b/i.test(line)) terms.push('bd');
|
|
464
|
+
if (/\.beads\b/i.test(line)) terms.push('.beads');
|
|
465
|
+
if (/\bdolt\b/i.test(line)) terms.push('dolt');
|
|
466
|
+
return [...new Set(terms)];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function newGroupedAudit() {
|
|
470
|
+
const groups = {};
|
|
471
|
+
for (const group of GROUPS) {
|
|
472
|
+
groups[group] = { count: 0, fileCount: 0, files: [] };
|
|
473
|
+
}
|
|
474
|
+
return groups;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function addCallSites(groups, group, relativePath, lineEntries) {
|
|
478
|
+
if (lineEntries.length === 0) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const target = groups[group];
|
|
483
|
+
target.count += lineEntries.length;
|
|
484
|
+
target.fileCount += 1;
|
|
485
|
+
target.files.push({
|
|
486
|
+
path: relativePath,
|
|
487
|
+
count: lineEntries.length,
|
|
488
|
+
lines: lineEntries,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Git-tracked repo files (repo-relative, forward-slash), or null when this isn't a git
|
|
493
|
+
// repo (then callers fall back to the raw walk). Restricting the bd-audit to tracked files
|
|
494
|
+
// makes the census deterministic under a dirty/parallel working tree: an untracked temp
|
|
495
|
+
// file a concurrent test drops under a scan root no longer perturbs it (issue 1d4077d8).
|
|
496
|
+
function trackedRepoFiles(projectRoot) {
|
|
497
|
+
let output;
|
|
498
|
+
try {
|
|
499
|
+
// NOSONAR S4036 — `git` is resolved from PATH by design (cross-platform CLI; the repo
|
|
500
|
+
// spawns git this way throughout and cannot hardcode a portable absolute path).
|
|
501
|
+
output = execFileSync('git', ['-C', projectRoot, 'ls-files', '-z'], {
|
|
502
|
+
encoding: 'utf8',
|
|
503
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
504
|
+
});
|
|
505
|
+
} catch (error) {
|
|
506
|
+
// Fall back to the raw walk ONLY for a genuine non-git tree (which legitimately has no
|
|
507
|
+
// tracked set). Any OTHER git/exec failure must NOT silently reintroduce the untracked-
|
|
508
|
+
// scan nondeterminism this fix removes — surface it instead of masking it.
|
|
509
|
+
if (/not a git repository/i.test(String(error && error.stderr))) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
const tracked = new Set();
|
|
515
|
+
for (const entry of output.split('\0')) {
|
|
516
|
+
if (entry) {
|
|
517
|
+
tracked.add(normalizeRepoPath(entry));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return tracked;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function auditBdCallSites(projectRoot, options = {}) {
|
|
524
|
+
const roots = getScanRoots(projectRoot, options);
|
|
525
|
+
// Only audit git-tracked files, so untracked working-tree pollution (e.g. a concurrent
|
|
526
|
+
// test's temp files under a scan root) can't perturb the census (issue 1d4077d8).
|
|
527
|
+
const tracked = trackedRepoFiles(projectRoot);
|
|
528
|
+
const files = [...new Set(roots.flatMap(root => walkFiles(projectRoot, root)))]
|
|
529
|
+
.filter(relativePath => !tracked || tracked.has(normalizeRepoPath(relativePath)))
|
|
530
|
+
.sort((left, right) => left.localeCompare(right));
|
|
531
|
+
const groups = newGroupedAudit();
|
|
532
|
+
|
|
533
|
+
for (const relativePath of files) {
|
|
534
|
+
const fullPath = absolutePath(projectRoot, relativePath);
|
|
535
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
536
|
+
const lineEntries = [];
|
|
537
|
+
const lines = content.split(/\r?\n/);
|
|
538
|
+
|
|
539
|
+
for (let index = 0; index < lines.length; index++) {
|
|
540
|
+
const terms = findBdTerms(lines[index]);
|
|
541
|
+
if (terms.length > 0) {
|
|
542
|
+
lineEntries.push({
|
|
543
|
+
line: index + 1,
|
|
544
|
+
terms,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
addCallSites(groups, classifyGroup(relativePath, projectRoot), relativePath, lineEntries);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
artifact: AUDIT_ARTIFACT,
|
|
554
|
+
groups,
|
|
555
|
+
totalCount: GROUPS.reduce((total, group) => total + groups[group].count, 0),
|
|
556
|
+
totalFiles: GROUPS.reduce((total, group) => total + groups[group].fileCount, 0),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function readRepoFile(projectRoot, relativePath) {
|
|
561
|
+
const fullPath = absolutePath(projectRoot, relativePath);
|
|
562
|
+
return fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function parseScriptAst(source) {
|
|
566
|
+
try {
|
|
567
|
+
return babelParser.parse(source, { sourceType: 'script' });
|
|
568
|
+
} catch (_error) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isFunctionLikeNode(node) {
|
|
574
|
+
return node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function collectTopLevelCommandBindings(ast) {
|
|
578
|
+
const bindings = {
|
|
579
|
+
functions: new Set(),
|
|
580
|
+
strings: new Map(),
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
for (const node of ast?.program?.body || []) {
|
|
584
|
+
if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
|
585
|
+
bindings.functions.add(node.id.name);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (node.type !== 'VariableDeclaration') {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for (const declaration of node.declarations) {
|
|
594
|
+
if (declaration.id?.type !== 'Identifier') {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (isFunctionLikeNode(declaration.init)) {
|
|
599
|
+
bindings.functions.add(declaration.id.name);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const stringValue = stringLiteralValue(declaration.init);
|
|
603
|
+
if (typeof stringValue === 'string') {
|
|
604
|
+
bindings.strings.set(declaration.id.name, stringValue);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return bindings;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function isModuleExportsMember(node) {
|
|
613
|
+
return node?.type === 'MemberExpression' &&
|
|
614
|
+
node.object?.type === 'Identifier' &&
|
|
615
|
+
node.object.name === 'module' &&
|
|
616
|
+
!node.computed &&
|
|
617
|
+
node.property?.type === 'Identifier' &&
|
|
618
|
+
node.property.name === 'exports';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function findModuleExportsObject(ast) {
|
|
622
|
+
for (const node of ast?.program?.body || []) {
|
|
623
|
+
if (
|
|
624
|
+
node.type === 'ExpressionStatement' &&
|
|
625
|
+
node.expression?.type === 'AssignmentExpression' &&
|
|
626
|
+
isModuleExportsMember(node.expression.left) &&
|
|
627
|
+
node.expression.right?.type === 'ObjectExpression'
|
|
628
|
+
) {
|
|
629
|
+
return node.expression.right;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function findObjectMember(node, name) {
|
|
637
|
+
if (node?.type !== 'ObjectExpression') {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return node.properties.find(property => objectPropertyName(property) === name) || null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resolvedStringValue(node, stringBindings) {
|
|
645
|
+
const literalValue = stringLiteralValue(node);
|
|
646
|
+
if (typeof literalValue === 'string') {
|
|
647
|
+
return literalValue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (node?.type === 'Identifier') {
|
|
651
|
+
return stringBindings.get(node.name) || null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function hasRegistryCommandHandler(property, functionBindings) {
|
|
658
|
+
if (!property) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (property.type === 'ObjectMethod') {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (isFunctionLikeNode(property.value)) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return property.value?.type === 'Identifier' && functionBindings.has(property.value.name);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Mirror the CLI registry contract without executing future command modules during the gate.
|
|
674
|
+
function commandModuleHasRegistryContract(projectRoot, name) {
|
|
675
|
+
const source = readRepoFile(projectRoot, `lib/commands/${name}.js`);
|
|
676
|
+
if (!source) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const ast = parseScriptAst(source);
|
|
681
|
+
if (!ast) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const bindings = collectTopLevelCommandBindings(ast);
|
|
686
|
+
const exportedObject = findModuleExportsObject(ast);
|
|
687
|
+
const exportedName = resolvedStringValue(findObjectMember(exportedObject, 'name')?.value, bindings.strings);
|
|
688
|
+
const description = resolvedStringValue(findObjectMember(exportedObject, 'description')?.value, bindings.strings);
|
|
689
|
+
|
|
690
|
+
return exportedName === name &&
|
|
691
|
+
typeof description === 'string' &&
|
|
692
|
+
description.trim().length > 0 &&
|
|
693
|
+
hasRegistryCommandHandler(findObjectMember(exportedObject, 'handler'), bindings.functions);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function isD20HotPath(relativePath, projectRoot) {
|
|
697
|
+
const surfaces = collectAgentInstructionSurfaces(projectRoot);
|
|
698
|
+
const hotPathFiles = new Set([
|
|
699
|
+
...D20_HOT_PATH_FILES,
|
|
700
|
+
...surfaces.rootDocFiles,
|
|
701
|
+
]);
|
|
702
|
+
const hotPathPrefixes = [
|
|
703
|
+
...D20_HOT_PATH_PREFIXES,
|
|
704
|
+
...surfaces.instructionRoots.map(root => `${root}/`),
|
|
705
|
+
...surfaces.skillRoots.map(root => `${root}/`),
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
return hotPathFiles.has(relativePath) ||
|
|
709
|
+
hotPathPrefixes.some(prefix => relativePath.startsWith(prefix));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function flattenHotPathEvidence(audit, projectRoot) {
|
|
713
|
+
const files = [];
|
|
714
|
+
for (const group of GROUPS) {
|
|
715
|
+
files.push(...audit.groups[group].files);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return files
|
|
719
|
+
.filter(file => isD20HotPath(file.path, projectRoot))
|
|
720
|
+
.filter(Boolean)
|
|
721
|
+
.map(file => ({
|
|
722
|
+
path: file.path,
|
|
723
|
+
count: file.count,
|
|
724
|
+
lines: file.lines.map(line => line.line),
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function objectPropertyName(property) {
|
|
729
|
+
return property.key?.name || property.key?.value;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function memberPropertyName(memberExpression) {
|
|
733
|
+
return memberExpression.property?.name || memberExpression.property?.value;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function memberObjectName(memberExpression) {
|
|
737
|
+
return memberExpression.object?.name || memberExpression.object?.value;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function calleeInfo(callee) {
|
|
741
|
+
if (callee?.type === 'Identifier') {
|
|
742
|
+
return { name: callee.name, objectName: null, propertyName: null };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (callee?.type === 'MemberExpression') {
|
|
746
|
+
return {
|
|
747
|
+
name: null,
|
|
748
|
+
objectName: memberObjectName(callee),
|
|
749
|
+
propertyName: memberPropertyName(callee),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return { name: null, objectName: null, propertyName: null };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function hasEnabledAcceptanceTest(source) {
|
|
757
|
+
return extractEnabledTestSources(source).length > 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function extractEnabledTestSources(source) {
|
|
761
|
+
const ast = parseScriptAst(source);
|
|
762
|
+
if (!ast) {
|
|
763
|
+
return /\b(?:test|it)\s*\(/.test(source) &&
|
|
764
|
+
!/\b(?:describe|test|it)\s*\.\s*(?:skip|todo)\s*\(/.test(source)
|
|
765
|
+
? [source]
|
|
766
|
+
: [];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const tests = [];
|
|
770
|
+
|
|
771
|
+
const visit = (node, disabled) => {
|
|
772
|
+
if (!node || typeof node !== 'object') {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let nextDisabled = disabled;
|
|
777
|
+
if (node.type === 'CallExpression') {
|
|
778
|
+
const { name, objectName, propertyName } = calleeInfo(node.callee);
|
|
779
|
+
const skippedCall = ['describe', 'test', 'it'].includes(objectName) &&
|
|
780
|
+
['skip', 'todo'].includes(propertyName);
|
|
781
|
+
nextDisabled = disabled || skippedCall;
|
|
782
|
+
|
|
783
|
+
const directEnabledTest = ['test', 'it'].includes(name);
|
|
784
|
+
const memberEnabledTest = ['test', 'it'].includes(objectName) &&
|
|
785
|
+
propertyName &&
|
|
786
|
+
!['skip', 'todo'].includes(propertyName);
|
|
787
|
+
|
|
788
|
+
if (!nextDisabled && (directEnabledTest || memberEnabledTest)) {
|
|
789
|
+
tests.push(source.slice(node.start, node.end));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
for (const value of Object.values(node)) {
|
|
794
|
+
if (Array.isArray(value)) {
|
|
795
|
+
for (const child of value) {
|
|
796
|
+
visit(child, nextDisabled);
|
|
797
|
+
}
|
|
798
|
+
} else if (value?.type) {
|
|
799
|
+
visit(value, nextDisabled);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
visit(ast, false);
|
|
805
|
+
return tests;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function parseSubcommandsObject(source) {
|
|
809
|
+
try {
|
|
810
|
+
const ast = babelParser.parse(source, { sourceType: 'script' });
|
|
811
|
+
const declaration = ast.program.body
|
|
812
|
+
.filter(node => node.type === 'VariableDeclaration')
|
|
813
|
+
.flatMap(node => node.declarations)
|
|
814
|
+
.find(node => node.id?.name === 'SUBCOMMANDS' && node.init?.type === 'ObjectExpression');
|
|
815
|
+
return declaration?.init || null;
|
|
816
|
+
} catch (_error) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function extractIssueSubcommands(source) {
|
|
822
|
+
const subcommands = parseSubcommandsObject(source);
|
|
823
|
+
if (!subcommands) {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return subcommands.properties
|
|
828
|
+
.map(objectPropertyName)
|
|
829
|
+
.filter(name => typeof name === 'string');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function extractIssueDepActions(source) {
|
|
833
|
+
const subcommands = parseSubcommandsObject(source);
|
|
834
|
+
const depProperty = subcommands?.properties.find(property => objectPropertyName(property) === 'dep');
|
|
835
|
+
const actionsProperty = findObjectMember(depProperty?.value, 'actions');
|
|
836
|
+
const actionNames = actionsProperty?.value?.type === 'ObjectExpression'
|
|
837
|
+
? actionsProperty.value.properties.map(objectPropertyName)
|
|
838
|
+
: [];
|
|
839
|
+
return REQUIRED_ISSUE_DEP_ACTIONS.filter(action => actionNames.includes(action));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function walkAst(node, visitor) {
|
|
843
|
+
if (!node || typeof node !== 'object') {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
visitor(node);
|
|
848
|
+
|
|
849
|
+
for (const value of Object.values(node)) {
|
|
850
|
+
if (Array.isArray(value)) {
|
|
851
|
+
value.forEach(child => walkAst(child, visitor));
|
|
852
|
+
} else if (value && typeof value === 'object' && typeof value.type === 'string') {
|
|
853
|
+
walkAst(value, visitor);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function stringLiteralValue(node) {
|
|
859
|
+
if (node?.type === 'StringLiteral') {
|
|
860
|
+
return node.value;
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function booleanLiteralValue(node) {
|
|
866
|
+
if (node?.type === 'BooleanLiteral') {
|
|
867
|
+
return node.value;
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function canProvideKernelBroker(node) {
|
|
873
|
+
if (!node) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (node.type === 'NullLiteral') {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
if (node.type === 'Identifier') {
|
|
880
|
+
return node.name !== 'undefined';
|
|
881
|
+
}
|
|
882
|
+
if (
|
|
883
|
+
node.type === 'BooleanLiteral' ||
|
|
884
|
+
node.type === 'StringLiteral' ||
|
|
885
|
+
node.type === 'NumericLiteral'
|
|
886
|
+
) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function objectHasKernelIssueOption(node) {
|
|
893
|
+
if (node?.type !== 'ObjectExpression') {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return node.properties.some(property => {
|
|
898
|
+
const name = objectPropertyName(property);
|
|
899
|
+
return (
|
|
900
|
+
(name === 'issueBackend' && stringLiteralValue(property.value) === 'kernel') ||
|
|
901
|
+
(name === 'useKernelBroker' && booleanLiteralValue(property.value) === true) ||
|
|
902
|
+
(name === 'kernelBroker' && canProvideKernelBroker(property.value))
|
|
903
|
+
);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function callHasKernelIssueOptions(node) {
|
|
908
|
+
return node.arguments.slice(3).some(objectHasKernelIssueOption);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function isRunIssueOperationCall(node) {
|
|
912
|
+
return node?.type === 'CallExpression' &&
|
|
913
|
+
node.callee?.type === 'Identifier' &&
|
|
914
|
+
node.callee.name === 'runIssueOperation';
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function collectIssueOperationCoverage(source) {
|
|
918
|
+
const coverage = {
|
|
919
|
+
kernelDynamic: false,
|
|
920
|
+
kernelOperations: new Set(),
|
|
921
|
+
nonKernelDynamic: false,
|
|
922
|
+
nonKernelOperations: new Set(),
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
const ast = babelParser.parse(source, { sourceType: 'script' });
|
|
927
|
+
walkAst(ast, node => {
|
|
928
|
+
if (!isRunIssueOperationCall(node)) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const operation = stringLiteralValue(node.arguments[0]);
|
|
933
|
+
const target = callHasKernelIssueOptions(node)
|
|
934
|
+
? { dynamic: 'kernelDynamic', operations: coverage.kernelOperations }
|
|
935
|
+
: { dynamic: 'nonKernelDynamic', operations: coverage.nonKernelOperations };
|
|
936
|
+
if (operation) {
|
|
937
|
+
target.operations.add(operation);
|
|
938
|
+
} else {
|
|
939
|
+
coverage[target.dynamic] = true;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
} catch (_error) {
|
|
943
|
+
// Leave coverage empty on parse failures.
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return coverage;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function sourceHasOnlyKernelBackedIssueOperations(source, operations) {
|
|
950
|
+
try {
|
|
951
|
+
const ast = babelParser.parse(source, { sourceType: 'script' });
|
|
952
|
+
const reachableRoots = collectHandlerReachableRoots(ast);
|
|
953
|
+
if (reachableRoots.length === 0) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
let found = false;
|
|
958
|
+
let hasNonKernelMatch = false;
|
|
959
|
+
for (const root of reachableRoots) {
|
|
960
|
+
walkExecutableNodes(root, node => {
|
|
961
|
+
if (!isRunIssueOperationCall(node)) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const operation = stringLiteralValue(node.arguments[0]);
|
|
966
|
+
const matchesOperation = operation ? operations.has(operation) : true;
|
|
967
|
+
if (!matchesOperation) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (callHasKernelIssueOptions(node)) {
|
|
972
|
+
found = true;
|
|
973
|
+
} else {
|
|
974
|
+
hasNonKernelMatch = true;
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return found && !hasNonKernelMatch;
|
|
980
|
+
} catch (_error) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function sourceHasIssueSurfaceKernelOptions(source) {
|
|
986
|
+
const coverage = collectIssueOperationCoverage(source);
|
|
987
|
+
if (coverage.nonKernelDynamic) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (REQUIRED_ISSUE_SUBCOMMANDS.some(command => coverage.nonKernelOperations.has(command))) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return coverage.kernelDynamic ||
|
|
996
|
+
REQUIRED_ISSUE_SUBCOMMANDS.every(command => coverage.kernelOperations.has(command));
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function findModuleExportsAssignment(ast) {
|
|
1000
|
+
for (const node of ast?.program?.body || []) {
|
|
1001
|
+
if (
|
|
1002
|
+
node.type === 'ExpressionStatement' &&
|
|
1003
|
+
node.expression?.type === 'AssignmentExpression' &&
|
|
1004
|
+
isModuleExportsMember(node.expression.left)
|
|
1005
|
+
) {
|
|
1006
|
+
return node.expression.right;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function isRequireCallOf(node, requestPaths) {
|
|
1014
|
+
return node?.type === 'CallExpression' &&
|
|
1015
|
+
node.callee?.type === 'Identifier' &&
|
|
1016
|
+
node.callee.name === 'require' &&
|
|
1017
|
+
requestPaths.includes(stringLiteralValue(node.arguments?.[0]));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function extractRequireBindingNames(declaration, requestPaths) {
|
|
1021
|
+
if (!isRequireCallOf(declaration.init, requestPaths)) {
|
|
1022
|
+
return [];
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (declaration.id?.type === 'Identifier') {
|
|
1026
|
+
return [declaration.id.name];
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (declaration.id?.type === 'ObjectPattern') {
|
|
1030
|
+
return declaration.id.properties
|
|
1031
|
+
.filter(property => property.type === 'ObjectProperty' && property.value?.type === 'Identifier')
|
|
1032
|
+
.map(property => property.value.name);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function requireLocalNames(ast, requestPaths) {
|
|
1039
|
+
const names = new Set();
|
|
1040
|
+
for (const node of ast?.program?.body || []) {
|
|
1041
|
+
if (node.type !== 'VariableDeclaration') {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for (const declaration of node.declarations) {
|
|
1046
|
+
for (const name of extractRequireBindingNames(declaration, requestPaths)) {
|
|
1047
|
+
names.add(name);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return names;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function collectTopLevelFunctions(ast) {
|
|
1056
|
+
const functions = new Map();
|
|
1057
|
+
for (const node of ast?.program?.body || []) {
|
|
1058
|
+
if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
|
1059
|
+
functions.set(node.id.name, node);
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (node.type === 'VariableDeclaration') {
|
|
1064
|
+
for (const declaration of node.declarations) {
|
|
1065
|
+
if (declaration.id?.type === 'Identifier' && isFunctionLikeNode(declaration.init)) {
|
|
1066
|
+
functions.set(declaration.id.name, declaration.init);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return functions;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function exportedHandlerNode(ast) {
|
|
1076
|
+
const exportsObject = findModuleExportsObject(ast);
|
|
1077
|
+
const handlerProperty = findObjectMember(exportsObject, 'handler');
|
|
1078
|
+
if (!handlerProperty) {
|
|
1079
|
+
return { node: null, identifierName: null };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (handlerProperty.type === 'ObjectMethod') {
|
|
1083
|
+
return { node: handlerProperty, identifierName: null };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (isFunctionLikeNode(handlerProperty.value)) {
|
|
1087
|
+
return { node: handlerProperty.value, identifierName: null };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (handlerProperty.value?.type === 'Identifier') {
|
|
1091
|
+
return { node: null, identifierName: handlerProperty.value.name };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return { node: null, identifierName: null };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function isFunctionNode(node) {
|
|
1098
|
+
return node?.type === 'FunctionDeclaration' ||
|
|
1099
|
+
node?.type === 'FunctionExpression' ||
|
|
1100
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
1101
|
+
node?.type === 'ObjectMethod' ||
|
|
1102
|
+
node?.type === 'ClassMethod';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Walk a root node visiting its own executable body but NOT descending into
|
|
1106
|
+
// nested function definitions. Dead/unused inner closures therefore contribute
|
|
1107
|
+
// no evidence; code in another function only counts when that function is itself
|
|
1108
|
+
// reached through a real call (see collectHandlerReachableRoots).
|
|
1109
|
+
function walkExecutableNodes(root, visitor) {
|
|
1110
|
+
const visit = node => {
|
|
1111
|
+
if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
visitor(node);
|
|
1115
|
+
if (isFunctionNode(node) && node !== root) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
for (const value of Object.values(node)) {
|
|
1119
|
+
if (Array.isArray(value)) {
|
|
1120
|
+
value.forEach(visit);
|
|
1121
|
+
} else {
|
|
1122
|
+
visit(value);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
visit(root);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Collect the function bodies actually reachable from the exported `handler`, so evidence
|
|
1130
|
+
// scans ignore unused helpers that never run when `forge <command>` dispatches.
|
|
1131
|
+
function collectHandlerReachableRoots(ast) {
|
|
1132
|
+
const functions = collectTopLevelFunctions(ast);
|
|
1133
|
+
const { node, identifierName } = exportedHandlerNode(ast);
|
|
1134
|
+
|
|
1135
|
+
const visited = new Set();
|
|
1136
|
+
const roots = [];
|
|
1137
|
+
const queue = [];
|
|
1138
|
+
if (node) {
|
|
1139
|
+
queue.push(node);
|
|
1140
|
+
} else if (identifierName && functions.has(identifierName)) {
|
|
1141
|
+
queue.push(functions.get(identifierName));
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
while (queue.length > 0) {
|
|
1145
|
+
const current = queue.shift();
|
|
1146
|
+
if (!current || visited.has(current)) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
visited.add(current);
|
|
1150
|
+
roots.push(current);
|
|
1151
|
+
|
|
1152
|
+
walkExecutableNodes(current, inner => {
|
|
1153
|
+
if (inner.type === 'CallExpression' && inner.callee?.type === 'Identifier') {
|
|
1154
|
+
const target = functions.get(inner.callee.name);
|
|
1155
|
+
if (target && !visited.has(target)) {
|
|
1156
|
+
queue.push(target);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return roots;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function publicIssueHandlerDelegatesToSurface(source) {
|
|
1166
|
+
if (!source) {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let ast;
|
|
1171
|
+
try {
|
|
1172
|
+
ast = babelParser.parse(source, { sourceType: 'script' });
|
|
1173
|
+
} catch (_error) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const surfaceBindings = requireLocalNames(ast, ['./_issue', './_issue.js']);
|
|
1178
|
+
if (surfaceBindings.size === 0) {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const exportsValue = findModuleExportsAssignment(ast);
|
|
1183
|
+
if (!exportsValue) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const roots = exportsValue.type === 'ObjectExpression'
|
|
1188
|
+
? collectHandlerReachableRoots(ast)
|
|
1189
|
+
: [exportsValue];
|
|
1190
|
+
if (roots.length === 0) {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
let delegates = false;
|
|
1195
|
+
for (const root of roots) {
|
|
1196
|
+
walkExecutableNodes(root, node => {
|
|
1197
|
+
if (node.type !== 'CallExpression') {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
// Delegation requires actually CALLING a binding from ./_issue (e.g.
|
|
1201
|
+
// dispatch(...) or createIssueCommand()), not merely referencing it —
|
|
1202
|
+
// `void dispatch` / `typeof dispatch` must not count as delegation.
|
|
1203
|
+
const callee = node.callee;
|
|
1204
|
+
if (callee?.type === 'Identifier' && surfaceBindings.has(callee.name)) {
|
|
1205
|
+
delegates = true;
|
|
1206
|
+
} else if (
|
|
1207
|
+
callee?.type === 'MemberExpression' &&
|
|
1208
|
+
callee.object?.type === 'Identifier' &&
|
|
1209
|
+
surfaceBindings.has(callee.object.name)
|
|
1210
|
+
) {
|
|
1211
|
+
delegates = true;
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return delegates;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function stripJsComments(code) {
|
|
1220
|
+
let ast;
|
|
1221
|
+
try {
|
|
1222
|
+
ast = babelParser.parse(code, { sourceType: 'script' });
|
|
1223
|
+
} catch (_error) {
|
|
1224
|
+
return code;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const comments = ast.comments || [];
|
|
1228
|
+
if (comments.length === 0) {
|
|
1229
|
+
return code;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const chars = [...code];
|
|
1233
|
+
for (const comment of comments) {
|
|
1234
|
+
for (let index = comment.start; index < comment.end && index < chars.length; index += 1) {
|
|
1235
|
+
if (chars[index] !== '\n' && chars[index] !== '\r') {
|
|
1236
|
+
chars[index] = ' ';
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return chars.join('');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function extractKernelAdapterOperations(projectRoot) {
|
|
1245
|
+
const source = readRepoFile(projectRoot, 'lib/adapters/kernel-issue-adapter.js');
|
|
1246
|
+
if (!source) {
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const operations = new Set();
|
|
1251
|
+
try {
|
|
1252
|
+
const ast = babelParser.parse(source, { sourceType: 'script' });
|
|
1253
|
+
walkAst(ast, node => {
|
|
1254
|
+
if (
|
|
1255
|
+
node.type === 'VariableDeclarator' &&
|
|
1256
|
+
node.id?.name === 'KERNEL_ISSUE_OPERATIONS' &&
|
|
1257
|
+
node.init?.type === 'CallExpression' &&
|
|
1258
|
+
node.init.arguments[0]?.type === 'ObjectExpression'
|
|
1259
|
+
) {
|
|
1260
|
+
for (const property of node.init.arguments[0].properties) {
|
|
1261
|
+
const name = objectPropertyName(property);
|
|
1262
|
+
const value = stringLiteralValue(property.value);
|
|
1263
|
+
if (typeof name === 'string') operations.add(name);
|
|
1264
|
+
if (typeof value === 'string') operations.add(value);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (node.type === 'ClassMethod' && node.key) {
|
|
1269
|
+
const name = objectPropertyName(node);
|
|
1270
|
+
if (typeof name === 'string') {
|
|
1271
|
+
operations.add(name);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
} catch (_error) {
|
|
1276
|
+
return [];
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return [...operations];
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function kernelAdapterSupportsOperation(operations, operation) {
|
|
1283
|
+
return operations.has(operation) || (operation === 'show' && operations.has('read'));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function missingKernelAdapterOperations(projectRoot) {
|
|
1287
|
+
const operations = new Set(extractKernelAdapterOperations(projectRoot));
|
|
1288
|
+
return REQUIRED_KERNEL_ADAPTER_OPERATIONS.filter(operation =>
|
|
1289
|
+
!kernelAdapterSupportsOperation(operations, operation)
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function hasKernelBackedIssueSurface(projectRoot) {
|
|
1294
|
+
const source = readRepoFile(projectRoot, 'lib/commands/_issue.js');
|
|
1295
|
+
const issueCommandSource = readRepoFile(projectRoot, 'lib/commands/issue.js');
|
|
1296
|
+
const registryValid = commandModuleHasRegistryContract(projectRoot, 'issue');
|
|
1297
|
+
const delegatesToSurface = publicIssueHandlerDelegatesToSurface(issueCommandSource);
|
|
1298
|
+
const missingAdapterOperations = missingKernelAdapterOperations(projectRoot);
|
|
1299
|
+
if (!source) {
|
|
1300
|
+
return {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
missingSubcommands: REQUIRED_ISSUE_SUBCOMMANDS,
|
|
1303
|
+
missingDepActions: REQUIRED_ISSUE_DEP_ACTIONS,
|
|
1304
|
+
missingAdapterOperations,
|
|
1305
|
+
missingKernelEvidence: true,
|
|
1306
|
+
beadsBacked: true,
|
|
1307
|
+
registryValid,
|
|
1308
|
+
delegatesToSurface,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const subcommands = extractIssueSubcommands(source);
|
|
1313
|
+
const missingSubcommands = REQUIRED_ISSUE_SUBCOMMANDS.filter(command => !subcommands.includes(command));
|
|
1314
|
+
const depActions = extractIssueDepActions(source);
|
|
1315
|
+
const missingDepActions = REQUIRED_ISSUE_DEP_ACTIONS.filter(action => !depActions.includes(action));
|
|
1316
|
+
const missingKernelEvidence = !defaultIssueBackendIsKernel(projectRoot) &&
|
|
1317
|
+
!sourceHasIssueSurfaceKernelOptions(source);
|
|
1318
|
+
const beadsBacked = isBeadsBackedCommandSource(source) ||
|
|
1319
|
+
isBeadsBackedCommandSource(issueCommandSource);
|
|
1320
|
+
|
|
1321
|
+
return {
|
|
1322
|
+
ok: missingSubcommands.length === 0 &&
|
|
1323
|
+
missingDepActions.length === 0 &&
|
|
1324
|
+
missingAdapterOperations.length === 0 &&
|
|
1325
|
+
!missingKernelEvidence &&
|
|
1326
|
+
!beadsBacked &&
|
|
1327
|
+
registryValid &&
|
|
1328
|
+
delegatesToSurface,
|
|
1329
|
+
missingSubcommands,
|
|
1330
|
+
missingDepActions,
|
|
1331
|
+
missingAdapterOperations,
|
|
1332
|
+
missingKernelEvidence,
|
|
1333
|
+
beadsBacked,
|
|
1334
|
+
registryValid,
|
|
1335
|
+
delegatesToSurface,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function isBeadsBackedCommandSource(source) {
|
|
1340
|
+
return /makeAliasCommand|buildBdArgs|Beads issue|bd-|\.beads/.test(source) ||
|
|
1341
|
+
sourceHasDirectBdInvocation(source);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function callCalleeName(callee) {
|
|
1345
|
+
if (callee?.type === 'Identifier') {
|
|
1346
|
+
return callee.name;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (callee?.type === 'MemberExpression') {
|
|
1350
|
+
return memberPropertyName(callee);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function shellCommandStartsWithBdText(value) {
|
|
1357
|
+
return typeof value === 'string' && /^\s*bd(?:\s|$|\.exe\b|\.cmd\b)/i.test(value);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function staticStringPrefix(node) {
|
|
1361
|
+
const literal = stringLiteralValue(node);
|
|
1362
|
+
if (typeof literal === 'string') {
|
|
1363
|
+
return literal;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (node?.type === 'TemplateLiteral') {
|
|
1367
|
+
return node.quasis[0]?.value?.cooked ?? node.quasis[0]?.value?.raw ?? null;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
|
1371
|
+
return staticStringPrefix(node.left);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function collectBdCommandAliases(ast) {
|
|
1378
|
+
const exact = new Set();
|
|
1379
|
+
const shell = new Set();
|
|
1380
|
+
|
|
1381
|
+
walkAst(ast, node => {
|
|
1382
|
+
if (
|
|
1383
|
+
node.type === 'VariableDeclarator' &&
|
|
1384
|
+
node.id?.type === 'Identifier'
|
|
1385
|
+
) {
|
|
1386
|
+
if (stringLiteralValue(node.init) === 'bd') {
|
|
1387
|
+
exact.add(node.id.name);
|
|
1388
|
+
shell.add(node.id.name);
|
|
1389
|
+
} else if (shellCommandStartsWithBdText(staticStringPrefix(node.init))) {
|
|
1390
|
+
shell.add(node.id.name);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
return { exact, shell };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function isBdCommandArgument(node, aliases) {
|
|
1399
|
+
return stringLiteralValue(node) === 'bd' ||
|
|
1400
|
+
(node?.type === 'Identifier' && aliases.has(node.name));
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function isBdShellCommandArgument(node, aliases) {
|
|
1404
|
+
if (shellCommandStartsWithBdText(staticStringPrefix(node))) {
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (node?.type === 'Identifier' && aliases.has(node.name)) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
|
1413
|
+
return isBdShellCommandArgument(node.left, aliases);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function sourceHasDirectBdInvocation(source) {
|
|
1420
|
+
const ast = parseScriptAst(source);
|
|
1421
|
+
if (!ast) {
|
|
1422
|
+
return /(?:exec|execSync|execFile|execFileSync|spawn|spawnSync)\s*\(\s*['"]\s*bd(?:\s|['"]|\.exe\b|\.cmd\b)/.test(source);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const aliases = collectBdCommandAliases(ast);
|
|
1426
|
+
let found = false;
|
|
1427
|
+
walkAst(ast, node => {
|
|
1428
|
+
if (found || node.type !== 'CallExpression') {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const calleeName = callCalleeName(node.callee);
|
|
1433
|
+
if (
|
|
1434
|
+
['exec', 'execSync'].includes(calleeName) &&
|
|
1435
|
+
isBdShellCommandArgument(node.arguments[0], aliases.shell)
|
|
1436
|
+
) {
|
|
1437
|
+
found = true;
|
|
1438
|
+
} else if (
|
|
1439
|
+
['execFile', 'execFileSync', 'spawn', 'spawnSync'].includes(calleeName) &&
|
|
1440
|
+
isBdCommandArgument(node.arguments[0], aliases.exact)
|
|
1441
|
+
) {
|
|
1442
|
+
found = true;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
return found;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function defaultIssueBackendIsKernel(projectRoot) {
|
|
1450
|
+
const source = readRepoFile(projectRoot, 'lib/forge-issues.js');
|
|
1451
|
+
const ast = parseScriptAst(source);
|
|
1452
|
+
if (!ast) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
let foundKernelDefault = false;
|
|
1457
|
+
walkAst(ast, node => {
|
|
1458
|
+
if (foundKernelDefault) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const body = createIssueServiceBody(node);
|
|
1463
|
+
if (!body) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
walkAst(body, child => {
|
|
1468
|
+
if (
|
|
1469
|
+
!foundKernelDefault &&
|
|
1470
|
+
child.type === 'VariableDeclarator' &&
|
|
1471
|
+
child.id?.type === 'Identifier' &&
|
|
1472
|
+
child.id.name === 'resolvedBackend'
|
|
1473
|
+
) {
|
|
1474
|
+
foundKernelDefault = expressionDefaultsToKernelBackend(child.init);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
return foundKernelDefault;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function createIssueServiceBody(node) {
|
|
1483
|
+
if (node?.type === 'FunctionDeclaration' && node.id?.name === 'createIssueService') {
|
|
1484
|
+
return node.body;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (
|
|
1488
|
+
node?.type === 'VariableDeclarator' &&
|
|
1489
|
+
node.id?.type === 'Identifier' &&
|
|
1490
|
+
node.id.name === 'createIssueService' &&
|
|
1491
|
+
isFunctionLikeNode(node.init)
|
|
1492
|
+
) {
|
|
1493
|
+
return node.init.body;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function expressionDefaultsToKernelBackend(node) {
|
|
1500
|
+
for (const operand of flattenLogicalFallbackOperands(node)) {
|
|
1501
|
+
if (isCreateBeadsIssueBackendCall(operand)) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
if (isCreateKernelIssueBackendCall(operand)) {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function flattenLogicalFallbackOperands(node) {
|
|
1513
|
+
if (node?.type === 'LogicalExpression' && ['||', '??'].includes(node.operator)) {
|
|
1514
|
+
return [
|
|
1515
|
+
...flattenLogicalFallbackOperands(node.left),
|
|
1516
|
+
...flattenLogicalFallbackOperands(node.right),
|
|
1517
|
+
];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return [node];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function isCreateKernelIssueBackendCall(node) {
|
|
1524
|
+
return node?.type === 'CallExpression' &&
|
|
1525
|
+
node.callee?.type === 'Identifier' &&
|
|
1526
|
+
node.callee.name === 'createKernelIssueBackend';
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function isCreateBeadsIssueBackendCall(node) {
|
|
1530
|
+
return node?.type === 'CallExpression' &&
|
|
1531
|
+
node.callee?.type === 'Identifier' &&
|
|
1532
|
+
node.callee.name === 'createBeadsIssueBackend';
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function hasKernelBackedClaimReleaseSurface(projectRoot) {
|
|
1536
|
+
const missingCommands = [];
|
|
1537
|
+
const beadsBackedCommands = [];
|
|
1538
|
+
const nonKernelCommands = [];
|
|
1539
|
+
const defaultKernelBackend = defaultIssueBackendIsKernel(projectRoot);
|
|
1540
|
+
|
|
1541
|
+
for (const command of REQUIRED_CLAIM_RELEASE_COMMANDS) {
|
|
1542
|
+
const source = readRepoFile(projectRoot, command.path);
|
|
1543
|
+
if (!source || !command.contractPatterns.every(pattern => pattern.test(source))) {
|
|
1544
|
+
missingCommands.push(command.name);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (!commandModuleHasRegistryContract(projectRoot, command.name)) {
|
|
1549
|
+
missingCommands.push(command.name);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (isBeadsBackedCommandSource(source)) {
|
|
1553
|
+
beadsBackedCommands.push(command.name);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (!defaultKernelBackend && !sourceHasOnlyKernelBackedIssueOperations(source, new Set([command.name]))) {
|
|
1558
|
+
nonKernelCommands.push(command.name);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return {
|
|
1563
|
+
ok: missingCommands.length === 0 && beadsBackedCommands.length === 0 && nonKernelCommands.length === 0,
|
|
1564
|
+
missingCommands,
|
|
1565
|
+
beadsBackedCommands,
|
|
1566
|
+
nonKernelCommands,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function hasIssueScopedRecap(projectRoot) {
|
|
1571
|
+
const source = readRepoFile(projectRoot, 'lib/commands/recap.js');
|
|
1572
|
+
return commandModuleHasRegistryContract(projectRoot, 'recap') &&
|
|
1573
|
+
/forge recap <issue>/.test(source) &&
|
|
1574
|
+
/issue/i.test(source) &&
|
|
1575
|
+
!/\.beads|buildRecap\(projectRoot/.test(source);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function hasForgeSkillsPack(projectRoot) {
|
|
1579
|
+
return FORGE_SKILLS_PACK_CANDIDATES.some(candidate =>
|
|
1580
|
+
fs.existsSync(absolutePath(projectRoot, candidate.manifest)) &&
|
|
1581
|
+
manifestDeclaresSkills(projectRoot, candidate.manifest) &&
|
|
1582
|
+
REQUIRED_FORGE_SKILLS.every(skill => hasCliWrapperSkill(projectRoot, candidate.root, skill))
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function collectJsonStringsAndKeys(value) {
|
|
1587
|
+
if (Array.isArray(value)) {
|
|
1588
|
+
return value.flatMap(collectJsonStringsAndKeys);
|
|
1589
|
+
}
|
|
1590
|
+
if (value && typeof value === 'object') {
|
|
1591
|
+
return Object.entries(value).flatMap(([key, child]) => [
|
|
1592
|
+
key,
|
|
1593
|
+
...collectJsonStringsAndKeys(child),
|
|
1594
|
+
]);
|
|
1595
|
+
}
|
|
1596
|
+
return typeof value === 'string' ? [value] : [];
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function manifestDeclaresSkills(projectRoot, manifestPath) {
|
|
1600
|
+
try {
|
|
1601
|
+
const manifest = JSON.parse(fs.readFileSync(absolutePath(projectRoot, manifestPath), 'utf8'));
|
|
1602
|
+
const manifestTerms = collectJsonStringsAndKeys(manifest);
|
|
1603
|
+
return REQUIRED_FORGE_SKILLS.every(skill =>
|
|
1604
|
+
manifestTerms.some(term => term === skill.name || term.endsWith(`/${skill.name}`))
|
|
1605
|
+
);
|
|
1606
|
+
} catch (_error) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function hasCliWrapperSkill(projectRoot, skillRoot, skill) {
|
|
1612
|
+
const candidates = [
|
|
1613
|
+
`${skillRoot}/skills/${skill.name}/SKILL.md`,
|
|
1614
|
+
`${skillRoot}/${skill.name}/SKILL.md`,
|
|
1615
|
+
`${skillRoot}/skills/${skill.name}.md`,
|
|
1616
|
+
];
|
|
1617
|
+
|
|
1618
|
+
return candidates.some(relativePath => {
|
|
1619
|
+
const fullPath = absolutePath(projectRoot, relativePath);
|
|
1620
|
+
if (!fs.existsSync(fullPath)) {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const source = fs.readFileSync(fullPath, 'utf8');
|
|
1625
|
+
return skill.commandPatterns.some(pattern => pattern.test(source));
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function acceptanceRequirementSatisfied(requirement, source) {
|
|
1630
|
+
if (typeof requirement.matches === 'function') {
|
|
1631
|
+
return requirement.matches(source);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
return requirement.patterns.some(pattern => pattern.test(source));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function missingFreshCloneAcceptanceCoverage(source) {
|
|
1638
|
+
const enabledTestSources = extractEnabledTestSources(source);
|
|
1639
|
+
if (enabledTestSources.length === 0) {
|
|
1640
|
+
return FRESH_CLONE_ACCEPTANCE_REQUIREMENTS.map(requirement => requirement.name);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const workflowRequirements = FRESH_CLONE_ACCEPTANCE_REQUIREMENTS
|
|
1644
|
+
.filter(requirement => requirement.name !== 'enabled acceptance test');
|
|
1645
|
+
const candidateMissing = enabledTestSources
|
|
1646
|
+
.map(testSource => stripJsComments(testSource))
|
|
1647
|
+
.map(executableSource => workflowRequirements
|
|
1648
|
+
.filter(requirement => !acceptanceRequirementSatisfied(requirement, executableSource))
|
|
1649
|
+
.map(requirement => requirement.name))
|
|
1650
|
+
.sort((left, right) => left.length - right.length);
|
|
1651
|
+
|
|
1652
|
+
return candidateMissing[0] || [];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function freshCloneAcceptanceStatus(projectRoot) {
|
|
1656
|
+
const inspected = [];
|
|
1657
|
+
|
|
1658
|
+
for (const relativePath of FRESH_CLONE_ACCEPTANCE_CANDIDATES) {
|
|
1659
|
+
const fullPath = absolutePath(projectRoot, relativePath);
|
|
1660
|
+
if (!fs.existsSync(fullPath)) {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const missing = missingFreshCloneAcceptanceCoverage(fs.readFileSync(fullPath, 'utf8'));
|
|
1665
|
+
if (missing.length === 0) {
|
|
1666
|
+
return {
|
|
1667
|
+
ok: true,
|
|
1668
|
+
path: relativePath,
|
|
1669
|
+
missing,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
inspected.push({ path: relativePath, missing });
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (inspected.length === 0) {
|
|
1676
|
+
return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
path: FRESH_CLONE_ACCEPTANCE_CANDIDATES[0],
|
|
1679
|
+
missing: FRESH_CLONE_ACCEPTANCE_REQUIREMENTS.map(requirement => requirement.name),
|
|
1680
|
+
reason: 'missing',
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
inspected.sort((left, right) => left.missing.length - right.missing.length);
|
|
1685
|
+
return {
|
|
1686
|
+
ok: false,
|
|
1687
|
+
path: inspected[0].path,
|
|
1688
|
+
missing: inspected[0].missing,
|
|
1689
|
+
reason: 'incomplete',
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Canonicalize the kill-list for the currency comparison: drop the per-file
|
|
1694
|
+
// `- lines: …` detail so the check is line-number-INSENSITIVE. The rendered
|
|
1695
|
+
// artifact keeps those line numbers as an informational aid, but they are
|
|
1696
|
+
// volatile — any PR that edits a scanned file shifts them, which would
|
|
1697
|
+
// otherwise mark the artifact "stale" and red the release-readiness gate on
|
|
1698
|
+
// every unrelated PR. The comparison therefore keeps only the stable signal:
|
|
1699
|
+
// the summary table plus each file's path and bd-term COUNT. A real change in
|
|
1700
|
+
// call-site counts (adding/removing bd usage) still trips staleness; a pure
|
|
1701
|
+
// line shift no longer does.
|
|
1702
|
+
function canonicalizeAuditArtifact(markdown) {
|
|
1703
|
+
return markdown
|
|
1704
|
+
.replace(/\r\n/g, '\n')
|
|
1705
|
+
.split('\n')
|
|
1706
|
+
.filter(line => !/^\s*- lines:/.test(line))
|
|
1707
|
+
.join('\n');
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function isAuditArtifactCurrent(projectRoot, audit) {
|
|
1711
|
+
const artifactPath = absolutePath(projectRoot, AUDIT_ARTIFACT);
|
|
1712
|
+
if (!fs.existsSync(artifactPath)) {
|
|
1713
|
+
return {
|
|
1714
|
+
ok: false,
|
|
1715
|
+
reason: 'missing',
|
|
1716
|
+
evidence: missingArtifactEvidence(AUDIT_ARTIFACT),
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const expected = canonicalizeAuditArtifact(renderBdCallSiteAuditMarkdown(audit));
|
|
1721
|
+
const actual = canonicalizeAuditArtifact(fs.readFileSync(artifactPath, 'utf8'));
|
|
1722
|
+
if (actual !== expected) {
|
|
1723
|
+
return {
|
|
1724
|
+
ok: false,
|
|
1725
|
+
reason: 'stale',
|
|
1726
|
+
evidence: [{ path: AUDIT_ARTIFACT }],
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return { ok: true, reason: 'current', evidence: [] };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function missingArtifactEvidence(relativePath) {
|
|
1734
|
+
return [{ path: relativePath, missing: true }];
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function hotPathBlocker(audit, projectRoot) {
|
|
1738
|
+
const hotPathEvidence = flattenHotPathEvidence(audit, projectRoot);
|
|
1739
|
+
if (hotPathEvidence.length > 0) {
|
|
1740
|
+
return {
|
|
1741
|
+
id: 'bd-hot-path-issue-commands',
|
|
1742
|
+
title: 'bd in D20 hot-path surfaces',
|
|
1743
|
+
detail: 'Remove direct bd, .beads, and Dolt access from issue, sync, worktree, setup, preflight, smart-status, forge-team, hook, and agent-instruction hot paths before claiming Beads retirement.',
|
|
1744
|
+
evidence: hotPathEvidence,
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function auditArtifactBlocker(projectRoot, audit) {
|
|
1752
|
+
const auditArtifact = isAuditArtifactCurrent(projectRoot, audit);
|
|
1753
|
+
if (!auditArtifact.ok) {
|
|
1754
|
+
return {
|
|
1755
|
+
id: 'd20-audit-artifact-current',
|
|
1756
|
+
title: 'D20 bd call-site kill-list artifact is not current',
|
|
1757
|
+
detail: `Regenerate ${AUDIT_ARTIFACT}; current status: ${auditArtifact.reason}.`,
|
|
1758
|
+
evidence: auditArtifact.evidence,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function formatIssueSurfaceDetail(issueSurface, claimReleaseSurface) {
|
|
1766
|
+
const issueParts = [
|
|
1767
|
+
`Required issue subcommands: ${REQUIRED_ISSUE_SUBCOMMANDS.join(', ')}. Missing today: ${issueSurface.missingSubcommands.join(', ') || 'none'}`,
|
|
1768
|
+
`missing issue dep actions: ${issueSurface.missingDepActions.map(action => `dep ${action}`).join(', ') || 'none'}`,
|
|
1769
|
+
`missing Kernel adapter operations: ${issueSurface.missingAdapterOperations.join(', ') || 'none'}`,
|
|
1770
|
+
`issue command registry-valid: ${issueSurface.registryValid ? 'yes' : 'no'}`,
|
|
1771
|
+
`issue command delegates to issue surface: ${issueSurface.delegatesToSurface ? 'yes' : 'no'}`,
|
|
1772
|
+
`Kernel evidence missing for issue surface: ${issueSurface.missingKernelEvidence ? 'yes' : 'no'}`,
|
|
1773
|
+
`Beads-backed issue surface: ${issueSurface.beadsBacked ? 'yes' : 'no'}`,
|
|
1774
|
+
];
|
|
1775
|
+
const claimReleaseParts = [
|
|
1776
|
+
`Required claim/release commands: claim, release. Missing claim/release today: ${claimReleaseSurface.missingCommands.join(', ') || 'none'}`,
|
|
1777
|
+
`Beads-backed claim/release: ${claimReleaseSurface.beadsBackedCommands.join(', ') || 'none'}`,
|
|
1778
|
+
`Kernel evidence missing for claim/release: ${claimReleaseSurface.nonKernelCommands.join(', ') || 'none'}`,
|
|
1779
|
+
];
|
|
1780
|
+
return `${issueParts.join('; ')}. ${claimReleaseParts.join('; ')}.`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function issueSurfaceBlocker(projectRoot) {
|
|
1784
|
+
const issueSurface = hasKernelBackedIssueSurface(projectRoot);
|
|
1785
|
+
const claimReleaseSurface = hasKernelBackedClaimReleaseSurface(projectRoot);
|
|
1786
|
+
if (!issueSurface.ok || !claimReleaseSurface.ok) {
|
|
1787
|
+
return {
|
|
1788
|
+
id: 'kernel-backed-forge-issue',
|
|
1789
|
+
title: 'missing Kernel-backed forge issue command set',
|
|
1790
|
+
detail: formatIssueSurfaceDetail(issueSurface, claimReleaseSurface),
|
|
1791
|
+
evidence: [
|
|
1792
|
+
{ path: 'lib/commands/issue.js' },
|
|
1793
|
+
{ path: 'lib/commands/_issue.js' },
|
|
1794
|
+
{ path: 'lib/adapters/kernel-issue-adapter.js' },
|
|
1795
|
+
...REQUIRED_CLAIM_RELEASE_COMMANDS.map(command => ({ path: command.path })),
|
|
1796
|
+
],
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function primeBlocker(projectRoot) {
|
|
1804
|
+
if (!commandModuleHasRegistryContract(projectRoot, 'prime')) {
|
|
1805
|
+
return {
|
|
1806
|
+
id: 'forge-prime',
|
|
1807
|
+
title: 'missing forge prime',
|
|
1808
|
+
detail: 'D22 requires a registered forge prime command that replaces bd prime and emits bounded orientation.',
|
|
1809
|
+
evidence: missingArtifactEvidence('lib/commands/prime.js'),
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function orientRecapBlocker(projectRoot) {
|
|
1817
|
+
const hasOrient = commandModuleHasRegistryContract(projectRoot, 'orient');
|
|
1818
|
+
const recapIssueScoped = hasIssueScopedRecap(projectRoot);
|
|
1819
|
+
if (!hasOrient || !recapIssueScoped) {
|
|
1820
|
+
return {
|
|
1821
|
+
id: 'forge-orient-issue-recap',
|
|
1822
|
+
title: 'missing forge orient / issue-scoped forge recap',
|
|
1823
|
+
detail: `forge orient registered: ${hasOrient ? 'yes' : 'no'}; issue-scoped forge recap present: ${recapIssueScoped ? 'yes' : 'no'}.`,
|
|
1824
|
+
evidence: [
|
|
1825
|
+
...(hasOrient ? [] : missingArtifactEvidence('lib/commands/orient.js')),
|
|
1826
|
+
...(recapIssueScoped ? [] : [{ path: 'lib/commands/recap.js' }]),
|
|
1827
|
+
],
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function skillsPackBlocker(projectRoot) {
|
|
1835
|
+
if (!hasForgeSkillsPack(projectRoot)) {
|
|
1836
|
+
return {
|
|
1837
|
+
id: 'forge-skills-pack',
|
|
1838
|
+
title: 'missing Forge skills pack',
|
|
1839
|
+
detail: `D22 requires a Forge plugin skills pack with CLI-wrapper skills: ${REQUIRED_FORGE_SKILLS.map(skill => skill.name).join(', ')}.`,
|
|
1840
|
+
evidence: missingArtifactEvidence('packages/skills/forge-plugin/manifest.json'),
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function rememberRecallBlocker(projectRoot) {
|
|
1848
|
+
const hasRemember = commandModuleHasRegistryContract(projectRoot, 'remember');
|
|
1849
|
+
const hasRecall = commandModuleHasRegistryContract(projectRoot, 'recall');
|
|
1850
|
+
if (!hasRemember || !hasRecall) {
|
|
1851
|
+
return {
|
|
1852
|
+
id: 'forge-remember-recall',
|
|
1853
|
+
title: 'missing forge remember / forge recall',
|
|
1854
|
+
detail: `forge remember registered: ${hasRemember ? 'yes' : 'no'}; forge recall registered: ${hasRecall ? 'yes' : 'no'}.`,
|
|
1855
|
+
evidence: [
|
|
1856
|
+
...(hasRemember ? [] : missingArtifactEvidence('lib/commands/remember.js')),
|
|
1857
|
+
...(hasRecall ? [] : missingArtifactEvidence('lib/commands/recall.js')),
|
|
1858
|
+
],
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// Surfaces that could re-model pre-merge as a universal stage/command, each paired
|
|
1866
|
+
// with the pattern that flags real stage/command residue WITHOUT tripping on the
|
|
1867
|
+
// legacy read-path name maps we intentionally keep (so historical
|
|
1868
|
+
// `currentStage='premerge'` state still round-trips):
|
|
1869
|
+
// - stage/command emitters (the AGENTS.md generator, status.js) are matched on the
|
|
1870
|
+
// SLASH form `/premerge`, so the legacy `premerge:` map keys (no slash) are tolerated;
|
|
1871
|
+
// - the pure stage taxonomies (plugin-catalog, recommend, capability matrix) are
|
|
1872
|
+
// matched on any `premerge`, since none of them carry a legacy read-path.
|
|
1873
|
+
function premergeEmbeddedGateStatus(projectRoot) {
|
|
1874
|
+
const slashCommand = /\/premerge/;
|
|
1875
|
+
const anyPremerge = /premerge/i;
|
|
1876
|
+
const checks = [
|
|
1877
|
+
['lib/workflow/stages.js', /\bpremerge\b/],
|
|
1878
|
+
['lib/workflow-profiles.js', /['"]\/premerge['"]/],
|
|
1879
|
+
['AGENTS.md', anyPremerge],
|
|
1880
|
+
['lib/agents-config.js', slashCommand],
|
|
1881
|
+
['lib/plugin-catalog.js', anyPremerge],
|
|
1882
|
+
['lib/commands/recommend.js', anyPremerge],
|
|
1883
|
+
['lib/harness-capability-matrix.js', anyPremerge],
|
|
1884
|
+
['lib/commands/status.js', slashCommand],
|
|
1885
|
+
];
|
|
1886
|
+
return checks.map(([relPath, pattern]) => ({
|
|
1887
|
+
path: relPath,
|
|
1888
|
+
present: pattern.test(readRepoFile(projectRoot, relPath)),
|
|
1889
|
+
}));
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function premergeEmbeddedGateBlocker(projectRoot) {
|
|
1893
|
+
const evidence = premergeEmbeddedGateStatus(projectRoot)
|
|
1894
|
+
.filter(item => item.present)
|
|
1895
|
+
.map(item => ({ path: item.path }));
|
|
1896
|
+
|
|
1897
|
+
if (evidence.length === 0) {
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return {
|
|
1902
|
+
id: 'premerge-embedded-gate',
|
|
1903
|
+
title: 'pre-merge still modeled as a universal stage',
|
|
1904
|
+
detail: 'The roadmap requires pre-merge behavior to become a task-type gate/checkpoint embedded in existing stages, not a universal standalone workflow stage.',
|
|
1905
|
+
evidence,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function freshCloneBlocker(projectRoot) {
|
|
1910
|
+
const status = freshCloneAcceptanceStatus(projectRoot);
|
|
1911
|
+
if (!status.ok) {
|
|
1912
|
+
const presentDetail = status.reason === 'missing' ? 'candidate test present: no' : `candidate test present: yes (${status.path})`;
|
|
1913
|
+
return {
|
|
1914
|
+
id: 'fresh-clone-no-beads-acceptance',
|
|
1915
|
+
title: 'missing fresh-clone no-Beads acceptance test',
|
|
1916
|
+
detail: `D22 acceptance requires a fresh clone with no Beads/Dolt installed to prime, run forge issue ready --json, claim, comment, close, and recap with zero bd invocations. ${presentDetail}; missing acceptance coverage: ${status.missing.join(', ')}.`,
|
|
1917
|
+
evidence: status.reason === 'missing' ? missingArtifactEvidence(status.path) : [{ path: status.path }],
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function buildBlockers(projectRoot, audit) {
|
|
1925
|
+
return [
|
|
1926
|
+
hotPathBlocker(audit, projectRoot),
|
|
1927
|
+
auditArtifactBlocker(projectRoot, audit),
|
|
1928
|
+
issueSurfaceBlocker(projectRoot),
|
|
1929
|
+
primeBlocker(projectRoot),
|
|
1930
|
+
orientRecapBlocker(projectRoot),
|
|
1931
|
+
skillsPackBlocker(projectRoot),
|
|
1932
|
+
rememberRecallBlocker(projectRoot),
|
|
1933
|
+
premergeEmbeddedGateBlocker(projectRoot),
|
|
1934
|
+
freshCloneBlocker(projectRoot),
|
|
1935
|
+
].filter(Boolean);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function buildReadinessReport(projectRoot, options = {}) {
|
|
1939
|
+
const target = options.target || SUPPORTED_TARGET;
|
|
1940
|
+
// Accept prerelease variants (0.1.0-beta.1, 0.1.0-rc.1, …) against their base version's
|
|
1941
|
+
// readiness profile — a beta's readiness content is identical to its release.
|
|
1942
|
+
const baseTarget = String(target).split('-')[0];
|
|
1943
|
+
if (baseTarget !== SUPPORTED_TARGET) {
|
|
1944
|
+
return {
|
|
1945
|
+
success: false,
|
|
1946
|
+
target,
|
|
1947
|
+
blockers: [{
|
|
1948
|
+
id: 'unsupported-target',
|
|
1949
|
+
title: 'unsupported release readiness target',
|
|
1950
|
+
detail: `Unsupported release readiness target: ${target}. Supported target: ${SUPPORTED_TARGET} (prerelease suffixes like -beta.1 are accepted).`,
|
|
1951
|
+
evidence: [],
|
|
1952
|
+
}],
|
|
1953
|
+
audit: auditBdCallSites(projectRoot, options),
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const audit = auditBdCallSites(projectRoot, options);
|
|
1958
|
+
const blockers = buildBlockers(projectRoot, audit);
|
|
1959
|
+
|
|
1960
|
+
return {
|
|
1961
|
+
success: blockers.length === 0,
|
|
1962
|
+
target,
|
|
1963
|
+
blockers,
|
|
1964
|
+
audit,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function formatEvidenceItem(item) {
|
|
1969
|
+
if (item.missing) {
|
|
1970
|
+
return ` - missing ${item.path}`;
|
|
1971
|
+
}
|
|
1972
|
+
if (Array.isArray(item.lines)) {
|
|
1973
|
+
return ` - ${item.path}: ${item.count} call sites on lines ${item.lines.join(', ')}`;
|
|
1974
|
+
}
|
|
1975
|
+
return ` - ${item.path}`;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function formatEvidence(evidence = []) {
|
|
1979
|
+
if (evidence.length === 0) {
|
|
1980
|
+
return [];
|
|
1981
|
+
}
|
|
1982
|
+
return [' Evidence:', ...evidence.map(formatEvidenceItem)];
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function renderBlocker(blocker) {
|
|
1986
|
+
return [
|
|
1987
|
+
`- [FAIL] ${blocker.title}`,
|
|
1988
|
+
` id: ${blocker.id}`,
|
|
1989
|
+
` ${blocker.detail}`,
|
|
1990
|
+
...formatEvidence(blocker.evidence),
|
|
1991
|
+
];
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function renderBlockerSection(blockers) {
|
|
1995
|
+
if (blockers.length === 0) {
|
|
1996
|
+
return ['No blockers found.'];
|
|
1997
|
+
}
|
|
1998
|
+
return [
|
|
1999
|
+
`Blockers (${blockers.length}):`,
|
|
2000
|
+
...blockers.flatMap(renderBlocker),
|
|
2001
|
+
];
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function renderAuditSummary(report) {
|
|
2005
|
+
return [
|
|
2006
|
+
'',
|
|
2007
|
+
'D20 bd call-site audit:',
|
|
2008
|
+
...GROUPS.map(group => {
|
|
2009
|
+
const summary = report.audit.groups[group];
|
|
2010
|
+
return `- ${group}: ${summary.count} call sites in ${summary.fileCount} files`;
|
|
2011
|
+
}),
|
|
2012
|
+
`- artifact: ${report.audit.artifact}`,
|
|
2013
|
+
];
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function renderReadinessReport(report) {
|
|
2017
|
+
const lines = [
|
|
2018
|
+
`Forge release readiness check: ${report.target}`,
|
|
2019
|
+
`Result: ${report.success ? 'PASS' : 'FAIL'}`,
|
|
2020
|
+
'',
|
|
2021
|
+
...renderBlockerSection(report.blockers),
|
|
2022
|
+
...renderAuditSummary(report),
|
|
2023
|
+
];
|
|
2024
|
+
|
|
2025
|
+
return `${lines.join('\n')}\n`;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function renderLineList(lines) {
|
|
2029
|
+
return lines
|
|
2030
|
+
.map(entry => `${entry.line} (${entry.terms.join(', ')})`)
|
|
2031
|
+
.join(', ');
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function renderBdCallSiteAuditMarkdown(audit) {
|
|
2035
|
+
const header = [
|
|
2036
|
+
'# D20 bd Call-Site Kill List',
|
|
2037
|
+
'',
|
|
2038
|
+
'Generated by `forge release check --target 0.1.0`.',
|
|
2039
|
+
'Purpose: tracked migration artifact for D20 so later PRs can remove Beads/Dolt hot-path usage against a concrete checklist.',
|
|
2040
|
+
'',
|
|
2041
|
+
'## Summary',
|
|
2042
|
+
'',
|
|
2043
|
+
'| Group | Call sites | Files |',
|
|
2044
|
+
'| --- | ---: | ---: |',
|
|
2045
|
+
];
|
|
2046
|
+
|
|
2047
|
+
const summaryRows = GROUPS.map(group => {
|
|
2048
|
+
const summary = audit.groups[group];
|
|
2049
|
+
return `| ${group} | ${summary.count} | ${summary.fileCount} |`;
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
return `${[
|
|
2053
|
+
...header,
|
|
2054
|
+
...summaryRows,
|
|
2055
|
+
...GROUPS.flatMap(group => renderAuditGroup(audit, group)),
|
|
2056
|
+
'',
|
|
2057
|
+
].join('\n')}`;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function renderAuditGroup(audit, group) {
|
|
2061
|
+
const files = audit.groups[group].files;
|
|
2062
|
+
if (files.length === 0) {
|
|
2063
|
+
return ['', `## ${group}`, '', 'No current call sites in this group.'];
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
return [
|
|
2067
|
+
'',
|
|
2068
|
+
`## ${group}`,
|
|
2069
|
+
'',
|
|
2070
|
+
...files.flatMap(file => [
|
|
2071
|
+
`- [ ] ${file.path} (${file.count})`,
|
|
2072
|
+
` - lines: ${renderLineList(file.lines)}`,
|
|
2073
|
+
]),
|
|
2074
|
+
];
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
module.exports = {
|
|
2078
|
+
AUDIT_ARTIFACT,
|
|
2079
|
+
GROUPS,
|
|
2080
|
+
SUPPORTED_TARGET,
|
|
2081
|
+
auditBdCallSites,
|
|
2082
|
+
buildReadinessReport,
|
|
2083
|
+
canonicalizeAuditArtifact,
|
|
2084
|
+
renderBdCallSiteAuditMarkdown,
|
|
2085
|
+
renderReadinessReport,
|
|
2086
|
+
// Exposed for the premerge-de-stage certification tests.
|
|
2087
|
+
premergeEmbeddedGateStatus,
|
|
2088
|
+
premergeEmbeddedGateBlocker,
|
|
2089
|
+
};
|