forge-workflow 0.0.10 → 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 +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- 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 +461 -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 +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/{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 +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 +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- 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 +17 -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 +0 -1
- 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 +838 -972
- 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 +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 +307 -100
- 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 +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 +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/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 +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/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 +99 -497
- 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 +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 +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- 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-coordinator.sh +3 -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
package/lib/setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const { cleanupDeprecatedSyncFiles } = require('./deprecated-sync-cleanup');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Save setup state to .forge/setup-state.json
|
|
@@ -110,76 +111,36 @@ async function markStepComplete(projectPath, stepName) {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
114
|
+
* Remove generated GitHub-Beads sync scaffold from existing installs.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} projectPath - Target project root
|
|
117
|
+
* @returns {Promise<{removed: string[], skipped: string[]}>}
|
|
118
|
+
*/
|
|
119
|
+
async function cleanupDeprecatedGithubBeadsSync(projectPath, options = {}) {
|
|
120
|
+
return cleanupDeprecatedSyncFiles(projectPath, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Deprecated compatibility shim for the removed GitHub-Beads sync scaffold.
|
|
125
|
+
*
|
|
126
|
+
* Performs cleanup of generated legacy sync artifacts only. User-owned files
|
|
127
|
+
* at matching paths are preserved unless their content matches known generated
|
|
128
|
+
* templates.
|
|
115
129
|
*
|
|
116
130
|
* @param {string} projectPath - Target project root
|
|
117
131
|
* @param {string} pkgDir - Forge package directory (source of template files)
|
|
118
|
-
* @returns {Promise<{created: string[], skipped: string[]}>}
|
|
132
|
+
* @returns {Promise<{created: string[], skipped: string[], removed: string[], deprecated: boolean, message: string}>}
|
|
119
133
|
*/
|
|
120
134
|
async function scaffoldGithubBeadsSync(projectPath, pkgDir) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const files = [
|
|
132
|
-
// Phase 1: GitHub → Beads
|
|
133
|
-
{
|
|
134
|
-
src: path.join(pkgDir, '.github', 'workflows', 'github-to-beads.yml'),
|
|
135
|
-
dest: path.join('.github', 'workflows', 'github-to-beads.yml'),
|
|
136
|
-
},
|
|
137
|
-
// Phase 2: Beads → GitHub
|
|
138
|
-
{
|
|
139
|
-
src: path.join(pkgDir, '.github', 'workflows', 'beads-to-github.yml'),
|
|
140
|
-
dest: path.join('.github', 'workflows', 'beads-to-github.yml'),
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
src: path.join(pkgDir, 'scripts', 'github-beads-sync.config.json'),
|
|
144
|
-
dest: path.join('scripts', 'github-beads-sync.config.json'),
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
src: null, // generated in-place (empty mapping template)
|
|
148
|
-
dest: path.join('.github', 'beads-mapping.json'),
|
|
149
|
-
content: '{}',
|
|
150
|
-
},
|
|
151
|
-
// Sync script modules
|
|
152
|
-
...syncScripts.map((name) => ({
|
|
153
|
-
src: path.join(pkgDir, 'scripts', 'github-beads-sync', name),
|
|
154
|
-
dest: path.join('scripts', 'github-beads-sync', name),
|
|
155
|
-
})),
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
for (const file of files) {
|
|
159
|
-
const destPath = path.join(projectPath, file.dest);
|
|
160
|
-
|
|
161
|
-
// Never overwrite existing files — preserve user customizations
|
|
162
|
-
if (fs.existsSync(destPath)) {
|
|
163
|
-
skipped.push(file.dest);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Ensure parent directory exists
|
|
168
|
-
const destDir = path.dirname(destPath);
|
|
169
|
-
await fs.promises.mkdir(destDir, { recursive: true });
|
|
170
|
-
|
|
171
|
-
if (file.src) {
|
|
172
|
-
// Copy from template
|
|
173
|
-
await fs.promises.copyFile(file.src, destPath);
|
|
174
|
-
} else {
|
|
175
|
-
// Write generated content
|
|
176
|
-
await fs.promises.writeFile(destPath, file.content, 'utf-8');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
created.push(file.dest);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return { created, skipped };
|
|
135
|
+
const cleanup = await cleanupDeprecatedGithubBeadsSync(projectPath, { packageDir: pkgDir });
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
created: [],
|
|
139
|
+
skipped: [],
|
|
140
|
+
removed: cleanup.removed,
|
|
141
|
+
deprecated: true,
|
|
142
|
+
message: 'GitHub-Beads sync scaffolding is deprecated; future GitHub issue sync must use Forge Kernel/server authority.'
|
|
143
|
+
};
|
|
183
144
|
}
|
|
184
145
|
|
|
185
146
|
module.exports = {
|
|
@@ -188,5 +149,6 @@ module.exports = {
|
|
|
188
149
|
isSetupComplete,
|
|
189
150
|
getNextStep,
|
|
190
151
|
markStepComplete,
|
|
152
|
+
cleanupDeprecatedGithubBeadsSync,
|
|
191
153
|
scaffoldGithubBeadsSync
|
|
192
154
|
};
|
package/lib/shell-utils.js
CHANGED
|
@@ -5,10 +5,62 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
// Extensions Windows can spawn directly via CreateProcess. Anything else that
|
|
11
|
+
// `where.exe` returns — the extensionless POSIX shim (`npm`) or the batch shim
|
|
12
|
+
// (`npm.cmd`) — cannot be handed to execFileSync: the former is a bash script
|
|
13
|
+
// (spawnSync ENOENT, kernel issue 9997d516) and patched Node refuses .cmd/.bat
|
|
14
|
+
// without shell:true (CVE-2024-27980).
|
|
15
|
+
const WINDOWS_DIRECT_SPAWN_EXTS = new Set(['.exe', '.com']);
|
|
16
|
+
|
|
17
|
+
// Conservative allowlist for tokens executed through cmd.exe. Blocks every cmd
|
|
18
|
+
// metacharacter (& | < > ^ % ! " ' ` ( ) ; and whitespace) so shell execution
|
|
19
|
+
// cannot be turned into injection even if a caller ever passes dynamic input.
|
|
20
|
+
const SAFE_SHELL_TOKEN = /^[A-Za-z0-9@_+=:,./\\-]+$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Throw when any token is unsafe to pass through cmd.exe.
|
|
24
|
+
* @param {string} command - The command name
|
|
25
|
+
* @param {string[]} args - Command arguments
|
|
26
|
+
*/
|
|
27
|
+
function assertShellSafeTokens(command, args) {
|
|
28
|
+
for (const token of [command, ...args]) {
|
|
29
|
+
if (typeof token !== 'string' || !SAFE_SHELL_TOKEN.test(token)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Refusing to run "${command}" via shell: unsafe token ${JSON.stringify(token)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decide how to spawn a command on Windows from `where.exe` candidates.
|
|
39
|
+
* Prefers a directly spawnable .exe/.com — including when the caller already
|
|
40
|
+
* passed an absolute .exe/.com path (which `where.exe` may not re-enumerate);
|
|
41
|
+
* otherwise runs the BARE command name through cmd.exe (shell:true), which
|
|
42
|
+
* resolves npm.cmd/npx.cmd/lefthook.cmd via PATH/PATHEXT — so paths with spaces
|
|
43
|
+
* never hit shell parsing or the shell-safe token allowlist.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} command - Original command name or absolute path
|
|
46
|
+
* @param {string[]} candidates - Resolved paths from `where.exe`, best first
|
|
47
|
+
* @returns {{ file: string, shell: boolean }}
|
|
48
|
+
*/
|
|
49
|
+
function resolveWindowsSpawnSpec(command, candidates) {
|
|
50
|
+
const direct = [command, ...candidates].find(
|
|
51
|
+
(candidate) => WINDOWS_DIRECT_SPAWN_EXTS.has(path.extname(candidate).toLowerCase())
|
|
52
|
+
);
|
|
53
|
+
if (direct) {
|
|
54
|
+
return { file: direct, shell: false };
|
|
55
|
+
}
|
|
56
|
+
return { file: command, shell: true };
|
|
57
|
+
}
|
|
8
58
|
|
|
9
59
|
/**
|
|
10
60
|
* Securely execute a command with PATH validation.
|
|
11
61
|
* Mitigates SonarCloud S4036: Ensures executables are from trusted locations.
|
|
62
|
+
* On Windows, handles npm/npx/lefthook-style cmd shims that cannot be spawned
|
|
63
|
+
* directly (see resolveWindowsSpawnSpec).
|
|
12
64
|
* @param {string} command - The command to execute
|
|
13
65
|
* @param {string[]} [args=[]] - Command arguments
|
|
14
66
|
* @param {object} [options={}] - execFileSync options
|
|
@@ -18,16 +70,16 @@ function secureExecFileSync(command, args = [], options = {}) {
|
|
|
18
70
|
const {
|
|
19
71
|
_execFileSync = execFileSync,
|
|
20
72
|
_spawnSync = spawnSync,
|
|
73
|
+
_platform = process.platform,
|
|
21
74
|
...execOptions
|
|
22
75
|
} = options;
|
|
23
76
|
|
|
24
|
-
|
|
77
|
+
const isWindows = _platform === 'win32';
|
|
78
|
+
const pathResolver = isWindows ? 'where.exe' : 'which';
|
|
25
79
|
|
|
80
|
+
let candidates = [];
|
|
26
81
|
try {
|
|
27
82
|
// Resolve command's full path to validate it's in a trusted location
|
|
28
|
-
const isWindows = process.platform === 'win32';
|
|
29
|
-
const pathResolver = isWindows ? 'where.exe' : 'which';
|
|
30
|
-
|
|
31
83
|
const result = _spawnSync(pathResolver, [command], {
|
|
32
84
|
encoding: 'utf8',
|
|
33
85
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
@@ -35,14 +87,32 @@ function secureExecFileSync(command, args = [], options = {}) {
|
|
|
35
87
|
|
|
36
88
|
if (result.status === 0 && result.stdout) {
|
|
37
89
|
// Handle both CRLF (Windows) and LF (Unix) line endings
|
|
38
|
-
|
|
90
|
+
candidates = result.stdout
|
|
91
|
+
.trim()
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.map((line) => line.trim())
|
|
94
|
+
.filter(Boolean);
|
|
39
95
|
}
|
|
40
96
|
} catch (_err) { // NOSONAR - S2486: Intentionally ignored; falls back to direct command execution below
|
|
41
97
|
}
|
|
42
98
|
|
|
99
|
+
if (isWindows) {
|
|
100
|
+
const spec = resolveWindowsSpawnSpec(command, candidates);
|
|
101
|
+
if (spec.shell) {
|
|
102
|
+
assertShellSafeTokens(command, args);
|
|
103
|
+
// Node 22 (DEP0190) deprecates passing an args array together with shell:true.
|
|
104
|
+
// assertShellSafeTokens has already guaranteed every token is free of shell
|
|
105
|
+
// metacharacters, so fold them into a single command line and pass no args array —
|
|
106
|
+
// identical execution through cmd.exe (npm.cmd/npx.cmd shims), no deprecation warning.
|
|
107
|
+
const commandLine = [command, ...args].join(' ');
|
|
108
|
+
return _execFileSync(commandLine, [], { ...execOptions, shell: true });
|
|
109
|
+
}
|
|
110
|
+
return _execFileSync(spec.file, args, execOptions);
|
|
111
|
+
}
|
|
112
|
+
|
|
43
113
|
// Fall back only when resolution failed. If execution of the resolved binary
|
|
44
114
|
// throws, propagate that error instead of retrying with the unresolved name.
|
|
45
|
-
return _execFileSync(
|
|
115
|
+
return _execFileSync(candidates[0] || command, args, execOptions);
|
|
46
116
|
}
|
|
47
117
|
|
|
48
118
|
module.exports = {
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module skills-sync
|
|
3
|
+
*
|
|
4
|
+
* Skills-only surface population + drift detection for Forge.
|
|
5
|
+
*
|
|
6
|
+
* Canonical source of truth is the committed root `skills/` directory — each
|
|
7
|
+
* `skills/<name>/SKILL.md` (plus any sibling files) is an auto-surfacing skill.
|
|
8
|
+
* Agent harness skill directories (`.agents/skills/`, `.claude/skills/`,
|
|
9
|
+
* `.codex/skills/`, `.cursor/skills/`, `.hermes/skills/`) are GENERATED from that
|
|
10
|
+
* canonical source. `.agents/skills` is Codex's documented repo-local discovery
|
|
11
|
+
* path (scanned from cwd up to the repo root) and is the ONE mirror committed to
|
|
12
|
+
* the repo (kept byte-identical by a pre-commit sync hook + the drift gate), so a
|
|
13
|
+
* teammate who clones the repo WITHOUT running `forge setup` still gets the Forge
|
|
14
|
+
* skills/stages auto-discovered. The other mirrors (including `.codex/skills`) are
|
|
15
|
+
* gitignored and populated at setup only.
|
|
16
|
+
*
|
|
17
|
+
* This module is the Forge-internal port of the `@forge/skills` CLI sync step.
|
|
18
|
+
* Forge cannot depend on `@forge/skills` at runtime (the `packages/` workspace is
|
|
19
|
+
* excluded from the published npm package), so the minimal population + check
|
|
20
|
+
* logic lives here. It mirrors the CLI's whole-directory copy semantics so that
|
|
21
|
+
* `skills sync --check` and this module agree on what "in sync" means.
|
|
22
|
+
*
|
|
23
|
+
* Design constraints (locked in kernel-skill-surface-design.md §6):
|
|
24
|
+
* - Registry-independent: never requires `.skills/.registry.json` (the registry
|
|
25
|
+
* is gitignored and absent on a fresh checkout).
|
|
26
|
+
* - AGENTS.md-safe: never writes AGENTS.md (it is the protected workflow contract).
|
|
27
|
+
* - Existing-mirror scoped: `checkSkillsSync` only validates agent skill dirs
|
|
28
|
+
* that already exist on disk. The gitignored, setup-populated mirrors
|
|
29
|
+
* (`.claude/skills`, `.codex/skills`, `.cursor/skills`, `.hermes/skills`) are
|
|
30
|
+
* absent on a clean checkout and are correctly skipped; `.agents/skills` is the
|
|
31
|
+
* committed mirror that is always present and thus always enforced against the
|
|
32
|
+
* canonical source, which is what the gate guarantees.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
|
|
38
|
+
/** Canonical skills live under this directory at the repo/package root. */
|
|
39
|
+
const CANONICAL_SKILLS_DIR = 'skills';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Standard agent harness skill directories, relative to a project root.
|
|
43
|
+
* Order is stable for deterministic reporting.
|
|
44
|
+
*/
|
|
45
|
+
const AGENT_SKILL_DIRS = [
|
|
46
|
+
'.agents/skills',
|
|
47
|
+
'.claude/skills',
|
|
48
|
+
'.codex/skills',
|
|
49
|
+
'.cursor/skills',
|
|
50
|
+
'.hermes/skills',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Skill name must be a safe single path segment (no traversal). */
|
|
54
|
+
function isValidSkillName(name) {
|
|
55
|
+
return (
|
|
56
|
+
typeof name === 'string' &&
|
|
57
|
+
name.length > 0 &&
|
|
58
|
+
name !== '.' &&
|
|
59
|
+
name !== '..' &&
|
|
60
|
+
!name.includes('/') &&
|
|
61
|
+
!name.includes('\\') &&
|
|
62
|
+
!name.includes('\0')
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List canonical skills under `<sourceRoot>/skills`.
|
|
68
|
+
*
|
|
69
|
+
* A skill is any direct subdirectory that contains a `SKILL.md` file.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} sourceRoot - Directory containing the canonical `skills/` dir.
|
|
72
|
+
* @param {object} [options]
|
|
73
|
+
* @param {Set<string>|string[]} [options.only] - Restrict to these skill names.
|
|
74
|
+
* @returns {{name: string, sourcePath: string}[]} Sorted list of skills.
|
|
75
|
+
*/
|
|
76
|
+
function listCanonicalSkills(sourceRoot, options = {}) {
|
|
77
|
+
const skillsDir = path.join(sourceRoot, CANONICAL_SKILLS_DIR);
|
|
78
|
+
if (!fs.existsSync(skillsDir)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const only = options.only ? new Set(options.only) : null;
|
|
83
|
+
const skills = [];
|
|
84
|
+
|
|
85
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
86
|
+
if (!entry.isDirectory()) continue;
|
|
87
|
+
if (!isValidSkillName(entry.name)) continue;
|
|
88
|
+
if (only && !only.has(entry.name)) continue;
|
|
89
|
+
|
|
90
|
+
const sourcePath = path.join(skillsDir, entry.name);
|
|
91
|
+
if (!fs.existsSync(path.join(sourcePath, 'SKILL.md'))) continue;
|
|
92
|
+
|
|
93
|
+
skills.push({ name: entry.name, sourcePath });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
97
|
+
return skills;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List all files under a directory as paths relative to that directory,
|
|
102
|
+
* using forward slashes for stable cross-platform comparison.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} dir - Directory to walk.
|
|
105
|
+
* @returns {string[]} Sorted relative file paths.
|
|
106
|
+
*/
|
|
107
|
+
function listFilesRecursive(dir) {
|
|
108
|
+
const out = [];
|
|
109
|
+
|
|
110
|
+
function walk(current, prefix) {
|
|
111
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
112
|
+
const abs = path.join(current, entry.name);
|
|
113
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
walk(abs, rel);
|
|
116
|
+
} else if (entry.isFile()) {
|
|
117
|
+
out.push(rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (fs.existsSync(dir)) {
|
|
123
|
+
walk(dir, '');
|
|
124
|
+
}
|
|
125
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Normalize line endings so Windows CRLF working-tree state is not false drift. */
|
|
130
|
+
function normalizeContent(buffer) {
|
|
131
|
+
return buffer.toString('utf8').replace(/\r\n/g, '\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remove a symlink sitting at a target path so a real directory can be written
|
|
136
|
+
* in its place. Only ever removes the link entry itself — never a real
|
|
137
|
+
* directory (with content) and never the symlink's target.
|
|
138
|
+
*
|
|
139
|
+
* Guards against the exact cruft that broke local skill-sync: a pre-existing
|
|
140
|
+
* DANGLING symlink at a skill dir path, which makes `mkdirSync`/copy fail.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} targetPath - Path that may hold a (possibly dangling) symlink.
|
|
143
|
+
* @returns {boolean} True if a symlink was cleared, false otherwise.
|
|
144
|
+
*/
|
|
145
|
+
function clearSymlinkAtPath(targetPath) {
|
|
146
|
+
let linkStat;
|
|
147
|
+
try {
|
|
148
|
+
// lstat does NOT follow the link, so a dangling symlink is still detected.
|
|
149
|
+
linkStat = fs.lstatSync(targetPath);
|
|
150
|
+
} catch {
|
|
151
|
+
return false; // nothing at this path
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!linkStat.isSymbolicLink()) {
|
|
155
|
+
return false; // a real file/dir — leave it for the normal copy path
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remove ONLY the link. Neither unlink nor rm follow a symlink's target, so
|
|
159
|
+
// the (possibly dangling) target's contents are never touched.
|
|
160
|
+
try {
|
|
161
|
+
fs.unlinkSync(targetPath);
|
|
162
|
+
} catch {
|
|
163
|
+
// Windows directory symlinks/junctions can reject unlink; rmSync removes the
|
|
164
|
+
// link entry itself without recursing into the target.
|
|
165
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Copy a single skill directory (recursively) from source to target,
|
|
172
|
+
* overwriting existing files. Byte-for-byte copy (matches CLI cpSync).
|
|
173
|
+
*
|
|
174
|
+
* @param {string} sourcePath - Source skill directory.
|
|
175
|
+
* @param {string} targetPath - Target skill directory.
|
|
176
|
+
*/
|
|
177
|
+
function copySkillDir(sourcePath, targetPath) {
|
|
178
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
179
|
+
for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) {
|
|
180
|
+
const src = path.join(sourcePath, entry.name);
|
|
181
|
+
const dest = path.join(targetPath, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
copySkillDir(src, dest);
|
|
184
|
+
} else if (entry.isFile()) {
|
|
185
|
+
fs.copyFileSync(src, dest);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Populate an agent harness skills directory from the canonical source.
|
|
192
|
+
*
|
|
193
|
+
* Writes `<targetSkillsDir>/<name>/...` for every canonical skill. Existing
|
|
194
|
+
* skills are overwritten; pre-existing unrelated skill dirs are left untouched
|
|
195
|
+
* unless `clean` is set.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} params
|
|
198
|
+
* @param {string} params.sourceRoot - Root containing the canonical `skills/` dir.
|
|
199
|
+
* @param {string} params.targetSkillsDir - Absolute path to the agent skills dir.
|
|
200
|
+
* @param {Set<string>|string[]} [params.only] - Restrict to these skill names.
|
|
201
|
+
* @param {boolean} [params.clean=false] - Remove canonical-managed stale dirs first.
|
|
202
|
+
* @returns {{written: string[]}} Names of skills written.
|
|
203
|
+
*/
|
|
204
|
+
function populateAgentSkills({ sourceRoot, targetSkillsDir, only, clean = false }) {
|
|
205
|
+
const skills = listCanonicalSkills(sourceRoot, { only });
|
|
206
|
+
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
if (clean) {
|
|
209
|
+
// Keep-set is the FULL canonical set, not the `only`-filtered subset, so a
|
|
210
|
+
// partial sync (only: [...]) never deletes other canonical skills' dirs.
|
|
211
|
+
const canonicalNames = new Set(listCanonicalSkills(sourceRoot).map((s) => s.name));
|
|
212
|
+
for (const entry of fs.readdirSync(targetSkillsDir, { withFileTypes: true })) {
|
|
213
|
+
if (entry.isDirectory() && !canonicalNames.has(entry.name)) {
|
|
214
|
+
fs.rmSync(path.join(targetSkillsDir, entry.name), { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const written = [];
|
|
220
|
+
for (const skill of skills) {
|
|
221
|
+
const targetPath = path.join(targetSkillsDir, skill.name);
|
|
222
|
+
// Defensive: clear a pre-existing (possibly dangling) symlink so the copy
|
|
223
|
+
// doesn't fail. Only symlinks are removed — real dirs are overwritten in place.
|
|
224
|
+
clearSymlinkAtPath(targetPath);
|
|
225
|
+
copySkillDir(skill.sourcePath, targetPath);
|
|
226
|
+
written.push(skill.name);
|
|
227
|
+
}
|
|
228
|
+
return { written };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compare a canonical skill directory against a target copy.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} sourcePath - Canonical skill dir.
|
|
235
|
+
* @param {string} targetPath - Target (generated) skill dir.
|
|
236
|
+
* @returns {{file: string, status: 'missing'|'changed'|'extra'}[]} Drift entries.
|
|
237
|
+
*/
|
|
238
|
+
function diffSkillDir(sourcePath, targetPath) {
|
|
239
|
+
const drift = [];
|
|
240
|
+
const sourceFiles = new Set(listFilesRecursive(sourcePath));
|
|
241
|
+
const targetFiles = new Set(listFilesRecursive(targetPath));
|
|
242
|
+
|
|
243
|
+
for (const rel of sourceFiles) {
|
|
244
|
+
if (!targetFiles.has(rel)) {
|
|
245
|
+
drift.push({ file: rel, status: 'missing' });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const a = normalizeContent(fs.readFileSync(path.join(sourcePath, rel)));
|
|
249
|
+
const b = normalizeContent(fs.readFileSync(path.join(targetPath, rel)));
|
|
250
|
+
if (a !== b) {
|
|
251
|
+
drift.push({ file: rel, status: 'changed' });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const rel of targetFiles) {
|
|
256
|
+
if (!sourceFiles.has(rel)) {
|
|
257
|
+
drift.push({ file: rel, status: 'extra' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return drift;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check that existing agent skill mirrors match the canonical source.
|
|
266
|
+
*
|
|
267
|
+
* Only agent skill dirs that already EXIST under `repoRoot` are validated. The
|
|
268
|
+
* gitignored, setup-populated mirrors (`.claude/skills`, `.codex/skills`, etc.)
|
|
269
|
+
* are absent on a clean checkout and correctly skipped; the committed
|
|
270
|
+
* `.agents/skills` mirror is always present and always enforced against the
|
|
271
|
+
* canonical source. Any mirror present locally must match canonical.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} params
|
|
274
|
+
* @param {string} params.repoRoot - Repo root (contains `skills/` + agent dirs).
|
|
275
|
+
* @param {Set<string>|string[]} [params.only] - Restrict canonical set.
|
|
276
|
+
* @param {string[]} [params.agentSkillDirs] - Override the agent dirs to scan.
|
|
277
|
+
* @returns {{inSync: boolean, checkedAgents: string[], drift: object[]}}
|
|
278
|
+
*/
|
|
279
|
+
function checkSkillsSync({ repoRoot, only, agentSkillDirs = AGENT_SKILL_DIRS }) {
|
|
280
|
+
const skills = listCanonicalSkills(repoRoot, { only });
|
|
281
|
+
const canonicalByName = new Map(skills.map((s) => [s.name, s]));
|
|
282
|
+
const drift = [];
|
|
283
|
+
const checkedAgents = [];
|
|
284
|
+
|
|
285
|
+
for (const rel of agentSkillDirs) {
|
|
286
|
+
const agentDir = path.join(repoRoot, rel);
|
|
287
|
+
if (!fs.existsSync(agentDir)) continue; // populated at setup time; absence ≠ drift
|
|
288
|
+
checkedAgents.push(rel);
|
|
289
|
+
|
|
290
|
+
const targetNames = fs
|
|
291
|
+
.readdirSync(agentDir, { withFileTypes: true })
|
|
292
|
+
.filter((e) => e.isDirectory())
|
|
293
|
+
.map((e) => e.name);
|
|
294
|
+
|
|
295
|
+
// Skills present canonically but missing/changed in the mirror.
|
|
296
|
+
for (const skill of skills) {
|
|
297
|
+
const targetPath = path.join(agentDir, skill.name);
|
|
298
|
+
// A regular file at the skill path is corruption, not a valid mirror:
|
|
299
|
+
// treat it as drift instead of letting diffSkillDir() throw ENOTDIR.
|
|
300
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
301
|
+
drift.push({ agent: rel, skill: skill.name, file: 'SKILL.md', status: 'missing' });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
for (const entry of diffSkillDir(skill.sourcePath, targetPath)) {
|
|
305
|
+
drift.push({ agent: rel, skill: skill.name, ...entry });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Skill dirs in the mirror with no canonical source = stale.
|
|
310
|
+
for (const name of targetNames) {
|
|
311
|
+
if (!canonicalByName.has(name)) {
|
|
312
|
+
drift.push({ agent: rel, skill: name, file: '*', status: 'stale' });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { inSync: drift.length === 0, checkedAgents, drift };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
CANONICAL_SKILLS_DIR,
|
|
322
|
+
AGENT_SKILL_DIRS,
|
|
323
|
+
isValidSkillName,
|
|
324
|
+
listCanonicalSkills,
|
|
325
|
+
listFilesRecursive,
|
|
326
|
+
clearSymlinkAtPath,
|
|
327
|
+
populateAgentSkills,
|
|
328
|
+
diffSkillDir,
|
|
329
|
+
checkSkillsSync,
|
|
330
|
+
};
|
|
@@ -104,7 +104,12 @@ function buildDependentsMap(issues) {
|
|
|
104
104
|
|
|
105
105
|
for (const issue of issues) {
|
|
106
106
|
for (const dependency of issue.dependencies ?? []) {
|
|
107
|
-
|
|
107
|
+
// The Kernel emits `dependencies` as bare id strings; the legacy shape
|
|
108
|
+
// used `{ depends_on_id }` objects. Accept both so the reverse-graph is
|
|
109
|
+
// reconstructable even when an issue arrives without a `dependents` array.
|
|
110
|
+
const dependencyId = typeof dependency === 'string'
|
|
111
|
+
? dependency
|
|
112
|
+
: dependency?.depends_on_id;
|
|
108
113
|
if (!dependencyId) {
|
|
109
114
|
continue;
|
|
110
115
|
}
|
|
@@ -139,13 +144,22 @@ function scoreIssues(issues, options = {}) {
|
|
|
139
144
|
return [...issues]
|
|
140
145
|
.map((issue) => {
|
|
141
146
|
const priorityWeight = getPriorityWeight(issue.priority);
|
|
142
|
-
|
|
147
|
+
// Prefer an explicit count, then the Kernel's authoritative `dependents`
|
|
148
|
+
// reverse-graph array, then the locally-reconstructed map (legacy shape).
|
|
149
|
+
const directDependents = issue.dependent_count
|
|
150
|
+
?? (Array.isArray(issue.dependents) ? issue.dependents.length : undefined)
|
|
151
|
+
?? dependentsMap.get(issue.id)?.length
|
|
152
|
+
?? 0;
|
|
143
153
|
const unblockChain = Math.max(directDependents + 1, 1);
|
|
144
154
|
const typeWeight = getTypeWeight(issue.type);
|
|
145
155
|
const statusBoost = getStatusBoost(issue.status);
|
|
146
156
|
const epicProximity = getEpicProximity(issue, epicStats);
|
|
147
157
|
const stalenessBoost = getStalenessBoost(issue.updated_at, now);
|
|
148
|
-
|
|
158
|
+
// The Kernel supplies `dependents` directly (reverse graph); fall back to
|
|
159
|
+
// the reconstructed map only for the legacy { depends_on_id } shape.
|
|
160
|
+
const dependents = Array.isArray(issue.dependents)
|
|
161
|
+
? issue.dependents
|
|
162
|
+
: (dependentsMap.get(issue.id) ?? []);
|
|
149
163
|
const score = priorityWeight
|
|
150
164
|
* unblockChain
|
|
151
165
|
* typeWeight
|
|
@@ -67,6 +67,8 @@ function isAssignedToDeveloper(issue, developer) {
|
|
|
67
67
|
return developer.name !== '' && issue.created_by === developer.name;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const DEFAULT_STALE_AFTER_DAYS = 14;
|
|
71
|
+
|
|
70
72
|
function parseTimestampOrZero(value) {
|
|
71
73
|
if (!value) {
|
|
72
74
|
return 0;
|
|
@@ -82,16 +84,55 @@ function sortByUpdatedAtDesc(left, right) {
|
|
|
82
84
|
return rightTime - leftTime;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function
|
|
87
|
+
function hasUnresolvedDependencies(issue) {
|
|
88
|
+
return Number(issue.dependency_count || 0) > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isOpenIssue(issue) {
|
|
92
|
+
return issue.status === 'open' || issue.status === 'in_progress';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isStale(issue, now, staleAfterDays) {
|
|
96
|
+
if (!isOpenIssue(issue)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const updatedAt = parseTimestampOrZero(issue.updated_at);
|
|
101
|
+
if (updatedAt === 0) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const staleMs = staleAfterDays * 24 * 60 * 60 * 1000;
|
|
106
|
+
return now.getTime() - updatedAt >= staleMs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readBeadsSnapshot(projectRoot, options = {}) {
|
|
86
110
|
const developer = getDeveloperIdentity(projectRoot);
|
|
87
111
|
const issues = readIssues(projectRoot);
|
|
112
|
+
const parsedNow = options.now instanceof Date
|
|
113
|
+
? options.now
|
|
114
|
+
: (typeof options.now === 'string' ? new Date(options.now) : null);
|
|
115
|
+
const now = parsedNow instanceof Date && !Number.isNaN(parsedNow.getTime())
|
|
116
|
+
? parsedNow
|
|
117
|
+
: new Date();
|
|
118
|
+
const parsedStaleAfterDays = Number(options.staleAfterDays);
|
|
119
|
+
const staleAfterDays = Number.isFinite(parsedStaleAfterDays)
|
|
120
|
+
? parsedStaleAfterDays
|
|
121
|
+
: DEFAULT_STALE_AFTER_DAYS;
|
|
88
122
|
|
|
89
123
|
return {
|
|
90
124
|
developer,
|
|
91
125
|
issues,
|
|
126
|
+
active: issues.filter(issue => issue.status === 'in_progress').sort(sortByUpdatedAtDesc),
|
|
92
127
|
activeAssigned: issues.filter(issue => issue.status === 'in_progress' && isAssignedToDeveloper(issue, developer)),
|
|
93
|
-
ready: issues.filter(issue => issue.status === 'open' &&
|
|
128
|
+
ready: issues.filter(issue => issue.status === 'open' && !hasUnresolvedDependencies(issue)),
|
|
129
|
+
blocked: issues.filter(issue => isOpenIssue(issue) && hasUnresolvedDependencies(issue)).sort(sortByUpdatedAtDesc),
|
|
130
|
+
stale: issues.filter(issue => isStale(issue, now, staleAfterDays)).sort(sortByUpdatedAtDesc),
|
|
94
131
|
recentCompleted: issues.filter(issue => issue.status === 'closed').sort(sortByUpdatedAtDesc),
|
|
132
|
+
limits: [
|
|
133
|
+
'Uses local Beads issues.jsonl only.',
|
|
134
|
+
'Does not read GitHub review, CI, project, or sync freshness state.',
|
|
135
|
+
],
|
|
95
136
|
};
|
|
96
137
|
}
|
|
97
138
|
|
|
@@ -99,4 +140,6 @@ module.exports = {
|
|
|
99
140
|
getDeveloperIdentity,
|
|
100
141
|
isAssignedToDeveloper,
|
|
101
142
|
readBeadsSnapshot,
|
|
143
|
+
hasUnresolvedDependencies,
|
|
144
|
+
isStale,
|
|
102
145
|
};
|