forge-workflow 0.0.10 → 0.1.0-beta.3
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 +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -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/forge/TOOLCHAIN.md +670 -0
- 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 +121 -0
- package/docs/guides/SUPPORT.md +190 -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 +214 -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/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- 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 +155 -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/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -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 +139 -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-detect.js +60 -0
- package/lib/beads-nudge.js +91 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -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 +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -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 +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -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 +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- 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 +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -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/kernel/backing-issue.js +311 -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/owned-kernel.js +43 -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 +2540 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- 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/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -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 +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -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 +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2105 -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 +241 -20
- 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/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- 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 +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- 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 +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- 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.sh +22 -3
- 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 +49 -84
- 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 +50 -83
- 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/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-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- 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-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- 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} +44 -50
- 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} +132 -157
- 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/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- 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/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- 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/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- 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 -281
- 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-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- 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 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- 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,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const YAML = require('yaml');
|
|
8
|
+
|
|
9
|
+
const CANONICAL_SKILL = {
|
|
10
|
+
name: 'guard-rails-audit',
|
|
11
|
+
description: 'Use when asked to inspect Forge L1 rail enforcement, audit events, or protected path policy.',
|
|
12
|
+
body: [
|
|
13
|
+
'# Guard Rails Audit',
|
|
14
|
+
'',
|
|
15
|
+
'Check Forge L1 rails, audit-event coverage, and protected path policy before recommending changes.',
|
|
16
|
+
].join('\n'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MATCHING_PROMPT = 'Inspect Forge L1 rail enforcement and audit events for protected path policy.';
|
|
20
|
+
const NON_MATCHING_PROMPT = 'Draft release notes for a minor README copy edit.';
|
|
21
|
+
const FIXTURE_FILE = 'src/index.js';
|
|
22
|
+
|
|
23
|
+
const SOURCES = [
|
|
24
|
+
{
|
|
25
|
+
label: 'S1',
|
|
26
|
+
harness: 'claude-code',
|
|
27
|
+
url: 'https://code.claude.com/docs/en/skills',
|
|
28
|
+
summary: 'Claude Code project skills live under .claude/skills/<skill>/SKILL.md and use description metadata for automatic invocation.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'S2',
|
|
32
|
+
harness: 'cursor',
|
|
33
|
+
url: 'https://docs.cursor.com/en/context',
|
|
34
|
+
summary: 'Cursor Project Rules live under .cursor/rules as .mdc files; Agent Requested rules are description-driven.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'S3',
|
|
38
|
+
harness: 'codex-cli',
|
|
39
|
+
url: 'https://github.com/openai/codex/blob/main/codex-rs/skills/src/assets/samples/skill-creator/SKILL.md',
|
|
40
|
+
summary: 'Codex skills use SKILL.md frontmatter name and description to determine when the skill gets used.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'S4',
|
|
44
|
+
harness: 'forge',
|
|
45
|
+
url: 'lib/codex-skills.js',
|
|
46
|
+
summary: 'Forge packages repository Codex skills from .codex/skills for installation into $CODEX_HOME/skills.',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const HARNESS_TARGETS = [
|
|
51
|
+
{
|
|
52
|
+
harness: 'claude-code',
|
|
53
|
+
target: '.claude/skills/guard-rails-audit/SKILL.md',
|
|
54
|
+
explicitInvocation: '/guard-rails-audit',
|
|
55
|
+
sourceLabel: 'S1',
|
|
56
|
+
source: 'Claude Code skill directory target',
|
|
57
|
+
buildFrontmatter: () => ({
|
|
58
|
+
name: CANONICAL_SKILL.name,
|
|
59
|
+
description: CANONICAL_SKILL.description,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
harness: 'cursor',
|
|
64
|
+
target: '.cursor/rules/guard-rails-audit.mdc',
|
|
65
|
+
explicitInvocation: '/guard-rails-audit',
|
|
66
|
+
sourceLabel: 'S2',
|
|
67
|
+
source: 'Cursor project rule with documented description/globs/alwaysApply metadata',
|
|
68
|
+
buildFrontmatter: () => ({
|
|
69
|
+
description: CANONICAL_SKILL.description,
|
|
70
|
+
globs: null,
|
|
71
|
+
alwaysApply: false,
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
harness: 'codex-cli',
|
|
76
|
+
target: '.codex/skills/guard-rails-audit/SKILL.md',
|
|
77
|
+
explicitInvocation: '/guard-rails-audit',
|
|
78
|
+
sourceLabel: 'S4',
|
|
79
|
+
source: 'Forge repository Codex skills package surface; custom slash prompt files are intentionally not used',
|
|
80
|
+
buildFrontmatter: () => ({
|
|
81
|
+
name: CANONICAL_SKILL.name,
|
|
82
|
+
description: CANONICAL_SKILL.description,
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function buildMarkdown(frontmatter, body) {
|
|
88
|
+
return `---\n${YAML.stringify(frontmatter, {
|
|
89
|
+
lineWidth: 0,
|
|
90
|
+
defaultKeyType: 'PLAIN',
|
|
91
|
+
defaultStringType: 'PLAIN',
|
|
92
|
+
}).trimEnd()}\n---\n\n${body}\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseFrontmatter(content) {
|
|
96
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
97
|
+
if (!match) return {};
|
|
98
|
+
const parsed = YAML.parse(match[1]);
|
|
99
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writeFile(root, relativePath, content) {
|
|
103
|
+
const absolutePath = path.join(root, relativePath);
|
|
104
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
105
|
+
fs.writeFileSync(absolutePath, content);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isDirectoryEmpty(directory) {
|
|
109
|
+
return !fs.existsSync(directory) || fs.readdirSync(directory).length === 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function materializeFixture(root) {
|
|
113
|
+
if (!isDirectoryEmpty(root)) {
|
|
114
|
+
throw new Error(`fixture directory must be empty: ${root}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fs.mkdirSync(root, { recursive: true });
|
|
118
|
+
writeFile(root, FIXTURE_FILE, 'export function fixture() { return true; }\n');
|
|
119
|
+
|
|
120
|
+
for (const target of HARNESS_TARGETS) {
|
|
121
|
+
writeFile(
|
|
122
|
+
root,
|
|
123
|
+
target.target,
|
|
124
|
+
buildMarkdown(target.buildFrontmatter(), CANONICAL_SKILL.body),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { root, skill: CANONICAL_SKILL, targets: HARNESS_TARGETS };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function tokens(value) {
|
|
132
|
+
return String(value)
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.match(/[a-z0-9]+/g)
|
|
135
|
+
?.filter((token) => token.length >= 4) || [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function descriptionMatches(description, prompt) {
|
|
139
|
+
const promptTokens = new Set(tokens(prompt));
|
|
140
|
+
const triggerTokens = tokens(description)
|
|
141
|
+
.filter((token) => !['when', 'asked', 'inspect'].includes(token));
|
|
142
|
+
|
|
143
|
+
return triggerTokens.some((token) => promptTokens.has(token));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function cursorGlobMatches(globs, filePath) {
|
|
147
|
+
void filePath;
|
|
148
|
+
if (!Array.isArray(globs) || globs.length === 0) return true;
|
|
149
|
+
return globs.includes('**/*') || globs.includes(filePath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateHarness(root, target) {
|
|
153
|
+
const absolutePath = path.join(root, target.target);
|
|
154
|
+
const failures = [];
|
|
155
|
+
|
|
156
|
+
if (!fs.existsSync(absolutePath)) {
|
|
157
|
+
failures.push(`missing ${target.target}`);
|
|
158
|
+
return { ...target, passed: false, failures };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const frontmatter = parseFrontmatter(fs.readFileSync(absolutePath, 'utf8'));
|
|
162
|
+
const description = frontmatter.description;
|
|
163
|
+
const positiveMatch = descriptionMatches(description, MATCHING_PROMPT);
|
|
164
|
+
const negativeMatch = descriptionMatches(description, NON_MATCHING_PROMPT);
|
|
165
|
+
|
|
166
|
+
if (description !== CANONICAL_SKILL.description) {
|
|
167
|
+
failures.push('description does not match canonical skill description');
|
|
168
|
+
}
|
|
169
|
+
if (!positiveMatch) {
|
|
170
|
+
failures.push('matching prompt did not select the skill by description');
|
|
171
|
+
}
|
|
172
|
+
if (negativeMatch) {
|
|
173
|
+
failures.push('unrelated prompt selected the skill');
|
|
174
|
+
}
|
|
175
|
+
if (target.explicitInvocation !== `/${CANONICAL_SKILL.name}`) {
|
|
176
|
+
failures.push('explicit slash invocation does not match canonical skill name');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (target.harness === 'claude-code') {
|
|
180
|
+
if (frontmatter.name !== CANONICAL_SKILL.name) failures.push('Claude skill name is missing or incorrect');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (target.harness === 'cursor') {
|
|
184
|
+
if (frontmatter.alwaysApply !== false) failures.push('Cursor rule must keep alwaysApply: false for request-time selection');
|
|
185
|
+
if (!cursorGlobMatches(frontmatter.globs, FIXTURE_FILE)) failures.push(`Cursor globs do not include ${FIXTURE_FILE}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (target.harness === 'codex-cli') {
|
|
189
|
+
if (!target.target.startsWith('.codex/skills/') || target.target.includes('/prompts/')) {
|
|
190
|
+
failures.push('Codex target must use .codex/skills, not undocumented prompt/slash files');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...target,
|
|
196
|
+
passed: failures.length === 0,
|
|
197
|
+
descriptionMatched: positiveMatch,
|
|
198
|
+
negativePromptIgnored: !negativeMatch,
|
|
199
|
+
frontmatter,
|
|
200
|
+
failures,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function runParity(options = {}) {
|
|
205
|
+
const root = options.fixtureDir || fs.mkdtempSync(path.join(os.tmpdir(), 'forge-w0-harness-parity-'));
|
|
206
|
+
const cleanup = options.fixtureDir ? false : options.cleanup !== false;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
materializeFixture(root);
|
|
210
|
+
const harnesses = HARNESS_TARGETS.map((target) => validateHarness(root, target));
|
|
211
|
+
const passed = harnesses.every((result) => result.passed);
|
|
212
|
+
const feasibleCount = harnesses.filter((result) => result.passed).length;
|
|
213
|
+
const knownIssues = passed ? [] : [{
|
|
214
|
+
issue: 'Cross-harness skill render parity did not pass for all three target harnesses.',
|
|
215
|
+
d38: 'D38 kills v3 if two of Claude/Cursor/Codex cannot render a single skill correctly by the W3 checkpoint.',
|
|
216
|
+
feasibleCount,
|
|
217
|
+
}];
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
fixture: 'w0-skill-auto-invoke-parity',
|
|
221
|
+
fixtureRoot: root,
|
|
222
|
+
matchingPrompt: MATCHING_PROMPT,
|
|
223
|
+
nonMatchingPrompt: NON_MATCHING_PROMPT,
|
|
224
|
+
canonicalSkill: CANONICAL_SKILL,
|
|
225
|
+
harnesses,
|
|
226
|
+
sources: SOURCES,
|
|
227
|
+
proofBoundary: {
|
|
228
|
+
level: 'metadata-surface',
|
|
229
|
+
liveAgentInvocation: 'not-run',
|
|
230
|
+
codexRuntimeDiscovery: 'not-run',
|
|
231
|
+
reason: 'closed-source harness model invocation is outside this deterministic fixture; .codex/skills is the Forge repository packaging surface, not proof of direct Codex runtime discovery',
|
|
232
|
+
},
|
|
233
|
+
passed,
|
|
234
|
+
knownIssues,
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
if (cleanup) fs.rmSync(root, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function printText(result) {
|
|
242
|
+
console.log(`W0 skill auto-invoke parity: ${result.passed ? 'PASS' : 'FAIL'}`);
|
|
243
|
+
for (const harness of result.harnesses) {
|
|
244
|
+
const status = harness.passed ? 'PASS' : 'FAIL';
|
|
245
|
+
console.log(`${status} ${harness.harness} ${harness.target}`);
|
|
246
|
+
for (const failure of harness.failures) {
|
|
247
|
+
console.log(` - ${failure}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (result.knownIssues.length > 0) {
|
|
251
|
+
for (const issue of result.knownIssues) {
|
|
252
|
+
console.log(`Known issue: ${issue.issue} ${issue.d38}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readStringFlag(args, name) {
|
|
258
|
+
const index = args.indexOf(name);
|
|
259
|
+
if (index === -1) return undefined;
|
|
260
|
+
const value = args[index + 1];
|
|
261
|
+
if (!value || value.startsWith('-')) return undefined;
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function main() {
|
|
266
|
+
const args = process.argv.slice(2);
|
|
267
|
+
const result = runParity({ fixtureDir: readStringFlag(args, '--fixture-dir') });
|
|
268
|
+
|
|
269
|
+
if (args.includes('--json')) {
|
|
270
|
+
console.log(JSON.stringify(result));
|
|
271
|
+
} else {
|
|
272
|
+
printText(result);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!result.passed) process.exitCode = 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (require.main === module) {
|
|
279
|
+
main();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
CANONICAL_SKILL,
|
|
284
|
+
HARNESS_TARGETS,
|
|
285
|
+
MATCHING_PROMPT,
|
|
286
|
+
NON_MATCHING_PROMPT,
|
|
287
|
+
SOURCES,
|
|
288
|
+
descriptionMatches,
|
|
289
|
+
materializeFixture,
|
|
290
|
+
parseFrontmatter,
|
|
291
|
+
runParity,
|
|
292
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-commit auto-sync for the committed `.agents/skills` mirror.
|
|
6
|
+
*
|
|
7
|
+
* `.agents/skills` is Codex's repo-local discovery path and the ONE skill mirror
|
|
8
|
+
* committed to the repo (so a teammate who clones WITHOUT running `forge setup`
|
|
9
|
+
* still gets Forge skills/stages auto-discovered). It must stay byte-identical to
|
|
10
|
+
* the canonical `skills/` source, which the structural drift gate enforces.
|
|
11
|
+
*
|
|
12
|
+
* This hook removes the regen friction: whenever `skills/**` is staged, it
|
|
13
|
+
* regenerates `.agents/skills` from `skills/` (reusing the same
|
|
14
|
+
* `populateCodexRepoSkills` / `populateAgentSkills` sync the drift gate checks
|
|
15
|
+
* against) and re-stages it, so the committed mirror never drifts from canonical.
|
|
16
|
+
*
|
|
17
|
+
* It is intentionally scoped to `skills/**` via lefthook's `glob`, so it only runs
|
|
18
|
+
* when the canonical source actually changes.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const { execFileSync } = require('node:child_process');
|
|
23
|
+
const { populateCodexRepoSkills, resolveCodexRepoSkillsDir } = require('../lib/codex-skills');
|
|
24
|
+
|
|
25
|
+
function repoRoot() {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
}).trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return path.resolve(__dirname, '..');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function main() {
|
|
37
|
+
const root = repoRoot();
|
|
38
|
+
let written = [];
|
|
39
|
+
try {
|
|
40
|
+
// clean: true so skills removed from canonical are dropped from the mirror too.
|
|
41
|
+
({ written } = populateCodexRepoSkills({ sourceRoot: root, projectRoot: root, clean: true }));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`sync-agent-skills: failed to regenerate .agents/skills — ${error.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mirror = resolveCodexRepoSkillsDir(root);
|
|
48
|
+
try {
|
|
49
|
+
// Stage the regenerated mirror (and any deletions within it).
|
|
50
|
+
execFileSync('git', ['add', '--all', '--', mirror], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`sync-agent-skills: failed to stage .agents/skills — ${error.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`sync-agent-skills: .agents/skills in sync with skills/ (${written.length} skills)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/scripts/sync-utils.sh
CHANGED
|
@@ -13,6 +13,9 @@ fi
|
|
|
13
13
|
|
|
14
14
|
# Source shared sanitize library
|
|
15
15
|
_SU_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if [[ -f "$_SU_SCRIPT_DIR/bootstrap-windows-tools.sh" ]]; then
|
|
17
|
+
source "$_SU_SCRIPT_DIR/bootstrap-windows-tools.sh"
|
|
18
|
+
fi
|
|
16
19
|
source "$_SU_SCRIPT_DIR/lib/sanitize.sh"
|
|
17
20
|
|
|
18
21
|
# ── Session Identity ───────────────────────────────────────────────────
|
package/scripts/test-ci-shard.js
CHANGED
|
@@ -177,17 +177,23 @@ function getShardPlan(args, { allUnitTests, durationMap }) {
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
const junitPath = path.join(reportDir, `${label}.xml`);
|
|
183
|
-
const bunCommand = process.env.BUN_EXE || 'bun';
|
|
184
|
-
const result = spawnSync(bunCommand, [
|
|
180
|
+
function buildBunTestArgs({ junitPath, files }) {
|
|
181
|
+
return [
|
|
185
182
|
'test',
|
|
183
|
+
'--timeout',
|
|
184
|
+
'15000',
|
|
186
185
|
'--reporter=junit',
|
|
187
186
|
'--reporter-outfile',
|
|
188
187
|
junitPath,
|
|
189
188
|
...files,
|
|
190
|
-
]
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runTests(label, files) {
|
|
193
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
194
|
+
const junitPath = path.join(reportDir, `${label}.xml`);
|
|
195
|
+
const bunCommand = process.env.BUN_EXE || 'bun';
|
|
196
|
+
const result = spawnSync(bunCommand, buildBunTestArgs({ junitPath, files }), {
|
|
191
197
|
cwd: rootDir,
|
|
192
198
|
stdio: 'inherit',
|
|
193
199
|
shell: process.platform === 'win32',
|
|
@@ -226,6 +232,7 @@ if (require.main === module) {
|
|
|
226
232
|
module.exports = {
|
|
227
233
|
DEFAULT_SMOKE_FILES,
|
|
228
234
|
PROFILE_MAX_AGE_MS,
|
|
235
|
+
buildBunTestArgs,
|
|
229
236
|
createDurationMap,
|
|
230
237
|
ensureFilesExist,
|
|
231
238
|
getShardPlan,
|
package/scripts/test.js
CHANGED
|
@@ -23,15 +23,9 @@ const PACKAGE_LEVEL_PATHS = new Set([
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
const KNOWN_TARGETABLE_PREFIXES = [
|
|
26
|
-
'.claude/commands/',
|
|
27
26
|
'.cursor/',
|
|
28
|
-
'.
|
|
29
|
-
'.roo/',
|
|
30
|
-
'.kilocode/',
|
|
31
|
-
'.opencode/',
|
|
32
|
-
'.codex/skills/',
|
|
27
|
+
'.codex/',
|
|
33
28
|
'.forge/',
|
|
34
|
-
'.github/prompts/',
|
|
35
29
|
'.github/agentic-workflows/',
|
|
36
30
|
'.github/workflows/',
|
|
37
31
|
'test/',
|
|
@@ -45,6 +39,65 @@ const ALWAYS_RUN_RISK_TEST_TARGETS = [
|
|
|
45
39
|
|
|
46
40
|
const isWindows = process.platform === 'win32';
|
|
47
41
|
|
|
42
|
+
// Wall-clock ceiling for a single spawned test lane. Bun's per-test `--timeout`
|
|
43
|
+
// races a JS timer and CANNOT preempt a synchronous blocking spawn (e.g. an
|
|
44
|
+
// `execFileSync` of git/bash that hangs during git mid-push state). Without this
|
|
45
|
+
// ceiling such a hang blocks `forge push` indefinitely (observed ~50 min on
|
|
46
|
+
// Windows, issue 8aef79e8). This kills the lane process so the push fails fast
|
|
47
|
+
// instead of hanging forever.
|
|
48
|
+
//
|
|
49
|
+
// The default is a practical 5 minutes for a single TARGETED lane: those run a
|
|
50
|
+
// small, mapped subset that completes in seconds, so 5 min is comfortably a
|
|
51
|
+
// fail-fast ceiling (not the old 15-min wait that made `forge push` feel
|
|
52
|
+
// wedged). Raise it with FORGE_TEST_TIMEOUT_MS for slow machines.
|
|
53
|
+
const DEFAULT_TEST_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
54
|
+
|
|
55
|
+
// Wall-clock budget for the FULL-SUITE fallback lane (`scripts/test-full-suite.js`),
|
|
56
|
+
// which runs on package-level, unmapped, or zero-resolved changes. Unlike a
|
|
57
|
+
// targeted lane, a healthy full suite legitimately takes 5-10 min, so the 5-min
|
|
58
|
+
// fail-fast ceiling would kill a good-but-slow full run. Kept at 10 min to stay
|
|
59
|
+
// aligned with the local-validation budget (VALIDATION_COMMAND_TIMEOUT_MS = 600000
|
|
60
|
+
// in lib/commands/validate.js and its "long enough subprocess timeout for the
|
|
61
|
+
// full local suite" regression test). FORGE_TEST_TIMEOUT_MS still overrides.
|
|
62
|
+
const DEFAULT_FULL_SUITE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
63
|
+
|
|
64
|
+
// Conventional shell exit code for a command terminated by a timeout.
|
|
65
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads and validates the FORGE_TEST_TIMEOUT_MS override, if any.
|
|
69
|
+
*
|
|
70
|
+
* @param {NodeJS.ProcessEnv} env Environment to read the override from.
|
|
71
|
+
* @returns {number|null} The positive integer override, or null when unset/invalid.
|
|
72
|
+
*/
|
|
73
|
+
function readTimeoutOverride(env) {
|
|
74
|
+
const parsed = Number.parseInt(env.FORGE_TEST_TIMEOUT_MS, 10);
|
|
75
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves the wall-clock timeout for a targeted/e2e/edge-case lane, honoring
|
|
80
|
+
* FORGE_TEST_TIMEOUT_MS.
|
|
81
|
+
*
|
|
82
|
+
* @param {NodeJS.ProcessEnv} [env=process.env] Environment to read the override from.
|
|
83
|
+
* @returns {number} Timeout in milliseconds (defaults to DEFAULT_TEST_COMMAND_TIMEOUT_MS).
|
|
84
|
+
*/
|
|
85
|
+
function resolveCommandTimeoutMs(env = process.env) {
|
|
86
|
+
return readTimeoutOverride(env) ?? DEFAULT_TEST_COMMAND_TIMEOUT_MS;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the wall-clock budget for the full-suite fallback lane. An explicit
|
|
91
|
+
* FORGE_TEST_TIMEOUT_MS override wins; otherwise it uses the larger,
|
|
92
|
+
* validation-aligned budget so a healthy-but-slow full run is not failed fast.
|
|
93
|
+
*
|
|
94
|
+
* @param {NodeJS.ProcessEnv} [env=process.env] Environment to read the override from.
|
|
95
|
+
* @returns {number} Timeout in milliseconds (defaults to DEFAULT_FULL_SUITE_TIMEOUT_MS).
|
|
96
|
+
*/
|
|
97
|
+
function resolveFullSuiteTimeoutMs(env = process.env) {
|
|
98
|
+
return readTimeoutOverride(env) ?? DEFAULT_FULL_SUITE_TIMEOUT_MS;
|
|
99
|
+
}
|
|
100
|
+
|
|
48
101
|
/**
|
|
49
102
|
* @typedef {Object} TestExecutionPlan
|
|
50
103
|
* @property {string[]} changedFiles
|
|
@@ -101,7 +154,19 @@ function isKnownTargetablePath(file) {
|
|
|
101
154
|
return true;
|
|
102
155
|
}
|
|
103
156
|
|
|
104
|
-
if (file.
|
|
157
|
+
if (file === 'README.md'
|
|
158
|
+
|| file === 'bin/forge.js'
|
|
159
|
+
|| file === 'QUICKSTART.md'
|
|
160
|
+
|| file === 'CHANGELOG.md'
|
|
161
|
+
|| file === 'AGENTS.md'
|
|
162
|
+
|| file === 'DEVELOPMENT.md'
|
|
163
|
+
|| file === 'docs/INDEX.md'
|
|
164
|
+
|| file === 'docs/PROJECT_DESIGN.md'
|
|
165
|
+
|| file.startsWith('docs/forge/')
|
|
166
|
+
|| file.startsWith('docs/guides/')
|
|
167
|
+
|| file.startsWith('docs/plans/')
|
|
168
|
+
|| file.startsWith('docs/reference/')
|
|
169
|
+
|| file.startsWith('docs/work/')) {
|
|
105
170
|
return true;
|
|
106
171
|
}
|
|
107
172
|
|
|
@@ -257,6 +322,15 @@ function runCommand(command, args, options = {}, spawnSync = defaultSpawnSync) {
|
|
|
257
322
|
});
|
|
258
323
|
|
|
259
324
|
if (result.error) {
|
|
325
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
326
|
+
console.error('');
|
|
327
|
+
console.error('Test lane exceeded its wall-clock ceiling and was terminated.');
|
|
328
|
+
console.error('A single test likely hung (e.g. a spawned git/bash call that never returns).');
|
|
329
|
+
console.error('Adjust the ceiling with FORGE_TEST_TIMEOUT_MS. Failing the run instead of');
|
|
330
|
+
console.error('blocking the push. See issue 8aef79e8.');
|
|
331
|
+
console.error('');
|
|
332
|
+
return TIMEOUT_EXIT_CODE;
|
|
333
|
+
}
|
|
260
334
|
throw result.error;
|
|
261
335
|
}
|
|
262
336
|
|
|
@@ -276,30 +350,35 @@ function runTestExecutionPlan(plan, deps = {}) {
|
|
|
276
350
|
const env = deps.env || stripGitHookEnv(process.env);
|
|
277
351
|
const bunCommand = deps.bunCommand || env.BUN_EXE || process.env.BUN_EXE || 'bun';
|
|
278
352
|
const label = deps.label || 'tests';
|
|
353
|
+
const timeout = resolveCommandTimeoutMs(env);
|
|
354
|
+
const laneOptions = { env, killSignal: 'SIGKILL', timeout };
|
|
355
|
+
// The full-suite fallback gets a larger, validation-aligned budget so a
|
|
356
|
+
// healthy-but-slow full run is not failed fast by the targeted-lane ceiling.
|
|
357
|
+
const fullSuiteOptions = { env, killSignal: 'SIGKILL', timeout: resolveFullSuiteTimeoutMs(env) };
|
|
279
358
|
|
|
280
359
|
console.log(`Running ${label} (${pkgManager})...`);
|
|
281
360
|
|
|
282
361
|
try {
|
|
283
362
|
if (plan.runFullSuite) {
|
|
284
363
|
console.log(` Mode: full suite (${plan.reason})`);
|
|
285
|
-
const status = runCommand('node', ['scripts/test-full-suite.js'],
|
|
364
|
+
const status = runCommand('node', ['scripts/test-full-suite.js'], fullSuiteOptions, spawnSync);
|
|
286
365
|
if (status !== 0) return status;
|
|
287
366
|
} else if (plan.testTargets.length > 0) {
|
|
288
367
|
console.log(` Mode: targeted (${plan.testTargets.length} test file${plan.testTargets.length === 1 ? '' : 's'})`);
|
|
289
368
|
const command = pkgManager === 'bun' ? bunCommand : pkgManager;
|
|
290
|
-
const status = runCommand(command, ['run', 'test', ...plan.testTargets],
|
|
369
|
+
const status = runCommand(command, ['run', 'test', ...plan.testTargets], laneOptions, spawnSync);
|
|
291
370
|
if (status !== 0) return status;
|
|
292
371
|
}
|
|
293
372
|
|
|
294
373
|
if (plan.runE2E) {
|
|
295
374
|
console.log(' Extra: running affected e2e tests');
|
|
296
|
-
const status = runCommand(bunCommand, ['test', 'test/e2e/'],
|
|
375
|
+
const status = runCommand(bunCommand, ['test', '--timeout', '15000', 'test/e2e/'], laneOptions, spawnSync);
|
|
297
376
|
if (status !== 0) return status;
|
|
298
377
|
}
|
|
299
378
|
|
|
300
379
|
if (!plan.runFullSuite && plan.runTestEnv) {
|
|
301
380
|
console.log(' Extra: running affected edge-case tests');
|
|
302
|
-
const status = runCommand(bunCommand, ['test', 'test-env/'],
|
|
381
|
+
const status = runCommand(bunCommand, ['test', '--timeout', '15000', 'test-env/'], laneOptions, spawnSync);
|
|
303
382
|
if (status !== 0) return status;
|
|
304
383
|
}
|
|
305
384
|
|
|
@@ -348,9 +427,13 @@ if (require.main === module) {
|
|
|
348
427
|
|
|
349
428
|
module.exports = {
|
|
350
429
|
ALWAYS_RUN_RISK_TEST_TARGETS,
|
|
430
|
+
DEFAULT_FULL_SUITE_TIMEOUT_MS,
|
|
431
|
+
DEFAULT_TEST_COMMAND_TIMEOUT_MS,
|
|
351
432
|
buildTestExecutionPlan,
|
|
352
433
|
classifyPushTests,
|
|
353
434
|
detectPackageManager,
|
|
435
|
+
resolveCommandTimeoutMs,
|
|
436
|
+
resolveFullSuiteTimeoutMs,
|
|
354
437
|
runLocalValidationTests,
|
|
355
438
|
runPrePushTests,
|
|
356
439
|
runTestExecutionPlan,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claim-safety
|
|
3
|
+
description: >
|
|
4
|
+
Claim a Forge issue and then PROVE you hold the live lease before you touch it, using `forge
|
|
5
|
+
issue owns <id>` (exit 0 iff you hold the single unexpired lease). Use this whenever winning
|
|
6
|
+
the claim matters: right after `forge claim`, before you `dev`/edit/`close`/`release` a
|
|
7
|
+
claimed issue, when two agents or a subagent fan-out contend for the same work, or before
|
|
8
|
+
any irreversible step. A claim's `ok:true` does NOT prove you won — duplicate replays return
|
|
9
|
+
it and expired leases get reclaimed; only `owns` proves it, so re-verify before
|
|
10
|
+
close/release. Trigger on "claim this issue safely", "did I actually win the lease",
|
|
11
|
+
"verify/prove ownership", "claim conflict", "two agents grabbed the same issue", "check I
|
|
12
|
+
still own it before closing", or before ANY mutation of a claimed issue. NOT for plain
|
|
13
|
+
single-issue create/update/close/comment with no ownership question (that is issue-basics),
|
|
14
|
+
and NOT for read-only selecting or ranking the next ready issue without claiming (that is
|
|
15
|
+
triage-ready).
|
|
16
|
+
allowed-tools: Read, Bash(forge:*)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Claim safety — claim, then prove you own the lease
|
|
20
|
+
|
|
21
|
+
Claiming is not owning. The Forge kernel keys a claim's idempotency on
|
|
22
|
+
`claim.create:<issue_id>:<actor>`, so a **same-key duplicate replay returns
|
|
23
|
+
`ok:true`** echoing the *current* call's `claim_id`. A genuine cross-actor
|
|
24
|
+
conflict returns `ok:false` (the partial-UNIQUE active-lease index guarantees one
|
|
25
|
+
lease) — but `ok:true` **alone does not prove sole ownership**. A live lease can
|
|
26
|
+
also be **reclaimed on expiry** (an expired lease is superseded by the next
|
|
27
|
+
claimant). Therefore every worker MUST verify ownership before mutating a claimed
|
|
28
|
+
issue, and RE-verify before `close`/`release`.
|
|
29
|
+
|
|
30
|
+
This is a reusable, standalone procedure: any skill or agent that mutates a
|
|
31
|
+
claimed issue embeds it (it is NOT folded into one orchestrator).
|
|
32
|
+
|
|
33
|
+
## The verification primitive
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
forge issue owns <id> # exit 0 iff YOU hold the live lease; non-zero otherwise
|
|
37
|
+
forge issue owns <id> --json # { ok:true, data:{ owned, claimed_by, expired, actor, expires_at } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`owns` resolves your actor the same way the kernel does — `FORGE_ACTOR` →
|
|
41
|
+
`FORGE_SESSION_ID` → default `forge` — then reports `owned:true` iff you hold the
|
|
42
|
+
**single active claim** AND that lease **has not expired**. It exits `0` when you
|
|
43
|
+
own it and non-zero (conflict, code 4) when you do not, with a clear "you do not
|
|
44
|
+
own the lease for `<id>` (held by `<actor>`)" message. It is a strict READ — it
|
|
45
|
+
never mutates kernel state. (`data.claimed_by` is lease-derived; there is no
|
|
46
|
+
`claims` command.)
|
|
47
|
+
|
|
48
|
+
## Procedure
|
|
49
|
+
|
|
50
|
+
1. **Select** work — `forge issue ready --json` (see the `triage-ready` skill).
|
|
51
|
+
Never claim epics/decisions or defer-windowed items.
|
|
52
|
+
2. **Claim** — `FORGE_ACTOR=<your-actor> forge claim <id>`. Always run under a
|
|
53
|
+
distinct actor so a losing claim reaches the conflict guard instead of
|
|
54
|
+
collapsing to a shared-actor duplicate.
|
|
55
|
+
3. **Prove ownership BEFORE working** — `forge issue owns <id>`.
|
|
56
|
+
- **exit 0 (OWNED)** → the lease is yours; proceed to work.
|
|
57
|
+
- **non-zero (NOT OWNED)** → you lost the race, or your claim collapsed to a
|
|
58
|
+
foreign/duplicate-collapsed claim. Do NOT work the issue. **Reselect** via
|
|
59
|
+
`forge issue ready --json` and start over at step 1.
|
|
60
|
+
4. **Work** the issue (`dev`/edit/etc.).
|
|
61
|
+
5. **RE-verify before `close`/`release`** — a live lease can be reclaimed on
|
|
62
|
+
expiry while you worked. Run `forge issue owns <id>` again:
|
|
63
|
+
- **OWNED** → `forge release check --target <release-ref> --json` (release-readiness; omit `--target` to use the project default), then
|
|
64
|
+
`forge close <id> --reason "…"` or `forge release <id>`.
|
|
65
|
+
- **NOT OWNED** → the lease was reclaimed (likely expired). Do NOT close/
|
|
66
|
+
release someone else's lease; reselect and, if the work is still needed,
|
|
67
|
+
re-claim and reconcile.
|
|
68
|
+
|
|
69
|
+
## Contract (what each result means)
|
|
70
|
+
|
|
71
|
+
| `forge claim` result | Meaning | Action |
|
|
72
|
+
|----------------------|---------|--------|
|
|
73
|
+
| `ok:false` (conflict, code 4) | A live lease is held by another actor | Reselect (`ready`) |
|
|
74
|
+
| `ok:true` | Provisionally yours — **not proof** (duplicate replays also return ok:true) | **Run `forge issue owns <id>`** |
|
|
75
|
+
|
|
76
|
+
| `forge issue owns` | Meaning | Action |
|
|
77
|
+
|--------------------|---------|--------|
|
|
78
|
+
| exit 0, `owned:true` | You hold the live, unexpired lease | Work / close / release |
|
|
79
|
+
| non-zero, `owned:false` | Someone else holds it, or your lease expired | Reselect; never mutate |
|
|
80
|
+
|
|
81
|
+
## Fork points
|
|
82
|
+
|
|
83
|
+
- **Actor source** — how `<your-actor>` is derived (`FORGE_ACTOR` explicit id →
|
|
84
|
+
`FORGE_SESSION_ID` → default `forge`). Use a distinct per-agent actor so
|
|
85
|
+
contending claims reach the conflict guard rather than collapsing to a duplicate.
|
|
86
|
+
- **Expiry / lease TTL** — whether claims carry an `expires_at` and how long;
|
|
87
|
+
`owns` treats an expired lease as NOT owned (it can be reclaimed).
|
|
88
|
+
- **Re-verify cadence** — verify after claim and again before `close`/`release`;
|
|
89
|
+
a longer task may re-verify more often (e.g. before each irreversible step).
|
|
90
|
+
- **Reselection policy** — on NOT-OWNED, how the next item is chosen (ranking /
|
|
91
|
+
filters live in the `triage-ready` skill).
|
|
92
|
+
- **Fail-closed posture** — with no usable clock/state, treat ownership as NOT
|
|
93
|
+
proven (mirror the readiness model's "no usable clock ⇒ not workable").
|
|
94
|
+
|
|
95
|
+
## Reliability notes
|
|
96
|
+
|
|
97
|
+
- A genuine cross-actor conflict returns `ok:false` — there is no phantom
|
|
98
|
+
`ok:true`-on-conflict. The real hazards are (a) a duplicate replay's `ok:true`
|
|
99
|
+
and (b) expiry-driven reclaim. `owns` closes both.
|
|
100
|
+
- Full multi-agent safety depends on the actor-identity kernel fix (distinct
|
|
101
|
+
actors per agent, kernel `d71a824b`): without distinct actors, two agents
|
|
102
|
+
share one idempotency key and `owns` cannot tell them apart.
|