forge-workflow 0.0.10 → 0.1.0-beta.3

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