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.
Files changed (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. 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
- * Scaffold GitHub-Beads sync files into a target project.
114
- * Copies workflow, config, and mapping template — never overwrites existing files.
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 created = [];
122
- const skipped = [];
123
-
124
- // Sync script modules that workflows depend on at runtime
125
- const syncScripts = [
126
- 'config.mjs', 'mapping.mjs', 'comment.mjs', 'github-api.mjs',
127
- 'sanitize.mjs', 'run-bd.mjs', 'label-mapper.mjs', 'index.mjs',
128
- 'reverse-sync.mjs', 'reverse-sync-cli.mjs',
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
  };
@@ -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
- let resolvedPath = null;
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
- resolvedPath = result.stdout.trim().split(/\r?\n/)[0].trim();
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(resolvedPath || command, args, execOptions);
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
- const dependencyId = dependency?.depends_on_id;
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
- const directDependents = issue.dependent_count ?? dependentsMap.get(issue.id)?.length ?? 0;
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
- const dependents = dependentsMap.get(issue.id) ?? [];
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 readBeadsSnapshot(projectRoot) {
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' && Number(issue.dependency_count || 0) === 0),
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
  };