forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
package/lib/pr-pull.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * @module pr-pull
29
29
  */
30
30
 
31
- const { isFailed, isGreen, runShepherdPass } = require('./pr-shepherd');
31
+ const { isFailed, isGreen } = require('./pr-shepherd');
32
32
  const { fenceUntrusted } = require('./untrusted-content');
33
33
 
34
34
  /** Token caps that keep the payload bounded regardless of PR size. */
@@ -680,9 +680,12 @@ function buildPullPayload({
680
680
 
681
681
  return {
682
682
  ...prField(pr),
683
+ // DEPRECATED: `state` is the legacy decision-pass ladder, kept only for
684
+ // back-compat. It is now a PROJECTION of `verdict` (via verdictToLegacyState),
685
+ // never independently computed. Consumers MUST read `verdict` — the single,
686
+ // trustworthy, fail-closed merge vocabulary (never false-clean). Full removal
687
+ // of `state` is tracked as a follow-up kernel issue.
683
688
  state,
684
- // `verdict` is the trustworthy, fail-closed merge signal (never false-clean);
685
- // `state` remains the legacy decision-pass state for back-compat.
686
689
  ...(verdict ? { verdict } : {}),
687
690
  ...(evidence ? { evidence } : {}),
688
691
  summary,
@@ -884,9 +887,14 @@ function rankConflict(v) {
884
887
  return null;
885
888
  }
886
889
 
887
- /** Rank 3: branch behind base. */
890
+ /** Rank 3: branch behind base. Gate on GitHub's actual blocking state
891
+ * (mergeStateStatus=BEHIND — set only when branch protection requires branches be
892
+ * up to date), NOT a raw compareCommits behind-count: a count>0 is often stale or
893
+ * non-blocking (strict-up-to-date not required), so escalating on it falsely
894
+ * labels a mergeable PR `behind` (issue 5291f2d2). The count is still surfaced in
895
+ * `evidence.behind`/blockers as WHY — it just no longer drives the verdict. */
888
896
  function rankBehind(v) {
889
- return (v.mss === 'BEHIND' || v.behind > 0) ? 'BEHIND' : null;
897
+ return (v.mss === 'BEHIND') ? 'BEHIND' : null;
890
898
  }
891
899
 
892
900
  /** Rank 4: checks — failing/missing/skipped/pending REQUIRED checks, a failing
@@ -956,6 +964,84 @@ function computeVerdict(input) {
956
964
  return { verdict: rankClean(v), evidence: v.evidence };
957
965
  }
958
966
 
967
+ /**
968
+ * The canonical verdict enum — every value `computeVerdict` can return, highest
969
+ * priority first (mirrors VERDICT_RANKS + rankClean). This is the SINGLE source
970
+ * for the `pr-verdict:*` label set the pr-monitor workflow reconciles, so the
971
+ * label and `forge shepherd <pr> --pull --json` can never drift: both are this
972
+ * same verdict.
973
+ */
974
+ const MERGE_VERDICTS = [
975
+ 'UNKNOWN', 'BLOCKED-CONFLICT', 'BEHIND', 'BLOCKED-CHECKS', 'BLOCKED-THREADS',
976
+ 'REVIEW-PENDING', 'CLEAN-MERGEABLE',
977
+ ];
978
+
979
+ /** Prefix for the single `pr-verdict:*` label a PR carries at a time. */
980
+ const VERDICT_LABEL_PREFIX = 'pr-verdict:';
981
+
982
+ /**
983
+ * Map a canonical verdict to its lowercased `pr-verdict:*` label
984
+ * (e.g. `BLOCKED-CHECKS` -> `pr-verdict:blocked-checks`). Unknown/empty input
985
+ * fails closed to the `unknown` label.
986
+ *
987
+ * @param {string} verdict
988
+ * @returns {string}
989
+ */
990
+ function verdictLabel(verdict) {
991
+ const v = String(verdict || '').toUpperCase();
992
+ const known = MERGE_VERDICTS.includes(v) ? v : 'UNKNOWN';
993
+ return `${VERDICT_LABEL_PREFIX}${known.toLowerCase()}`;
994
+ }
995
+
996
+ /** The full label reconcile set — one label per canonical verdict. */
997
+ const VERDICT_LABELS = MERGE_VERDICTS.map((v) => `${VERDICT_LABEL_PREFIX}${v.toLowerCase()}`);
998
+
999
+ /**
1000
+ * The ONE place that maps the canonical 7-enum `verdict` onto the DEPRECATED
1001
+ * legacy `runShepherdPass` ladder (`pr-shepherd.js:34`). `verdict` is the single
1002
+ * consumer-facing vocabulary; the payload's `state` field is a back-compat
1003
+ * PROJECTION of the verdict through this map — never independently computed — so
1004
+ * the two can never disagree. Unknown/empty input fails closed to `UNKNOWN`.
1005
+ *
1006
+ * @param {string} verdict - a canonical MERGE_VERDICTS value.
1007
+ * @returns {string} the deprecated legacy state.
1008
+ */
1009
+ function verdictToLegacyState(verdict) {
1010
+ const v = String(verdict || '').toUpperCase();
1011
+ switch (v) {
1012
+ case 'CLEAN-MERGEABLE': return 'MERGE_READY';
1013
+ case 'REVIEW-PENDING': return 'NEEDS_REVIEW';
1014
+ case 'BLOCKED-THREADS': return 'NEEDS_REVIEW';
1015
+ case 'BLOCKED-CHECKS': return 'PENDING';
1016
+ // BEHIND maps to ESCALATE, not PENDING: the legacy runShepherdPass escalated a
1017
+ // behind head (handleBehindBase with --auto-rebase OFF, the default) so a human
1018
+ // rebases or opts into auto-rebase. Mapping it to PENDING would tell a legacy
1019
+ // `state` consumer to keep waiting on a PR that actually needs action.
1020
+ case 'BEHIND': return 'ESCALATE';
1021
+ case 'BLOCKED-CONFLICT': return 'ESCALATE';
1022
+ default: return 'UNKNOWN';
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * The back-compat legacy `state` for the `--pull` payload. A terminal GitHub
1028
+ * lifecycle (`MERGED`/`CLOSED`) wins over the merge verdict: the old dry-run
1029
+ * `runShepherdPass` hit `lifecycleOutcome` first (`pr-shepherd.js:403`) and
1030
+ * emitted `MERGED`/`CLOSED` so a legacy consumer stops polling / stops prompting a
1031
+ * human to merge an already-landed PR. `computeVerdict`'s 7-enum has no terminal
1032
+ * value, so we read the lifecycle (`adapter.readState().state`) here; only an open
1033
+ * PR falls through to the verdict projection.
1034
+ *
1035
+ * @param {string} verdict - a canonical MERGE_VERDICTS value.
1036
+ * @param {string} [prLifecycleState] - readState().state: OPEN | MERGED | CLOSED.
1037
+ * @returns {string} the deprecated legacy state.
1038
+ */
1039
+ function legacyStateFor(verdict, prLifecycleState) {
1040
+ const lifecycle = String(prLifecycleState || '').toUpperCase();
1041
+ if (lifecycle === 'MERGED' || lifecycle === 'CLOSED') return lifecycle;
1042
+ return verdictToLegacyState(verdict);
1043
+ }
1044
+
959
1045
  /**
960
1046
  * Run an optional read and SURFACE any failure instead of swallowing it: on
961
1047
  * throw, record `{ source, error }` into `degraded` (and optionally mark `source`
@@ -1030,7 +1116,6 @@ function gatherFailureExcerpts(runGh, checks, requiredSet, { maxFailures, maxExc
1030
1116
  * @param {string} [ctx.self] - shepherd's own login
1031
1117
  * @param {object} ctx.adapter - validated pr-state adapter
1032
1118
  * @param {(args: string[]) => string} ctx.runGh - injected `gh` runner (args → stdout)
1033
- * @param {Function} [ctx.runPass] - decision pass (default runShepherdPass), injectable for tests
1034
1119
  * @param {number} [ctx.maxFailures]
1035
1120
  * @param {number} [ctx.maxThreads]
1036
1121
  * @param {number} [ctx.maxExcerptLines]
@@ -1056,7 +1141,9 @@ async function gatherPrSnapshot(ctx) {
1056
1141
  const state = await adapter.readState(pr);
1057
1142
  const requiredSet = await safeRead(
1058
1143
  'requiredChecks',
1059
- () => adapter.readRequiredChecks({ owner, repo, base }),
1144
+ // `pr` lets the adapter fall back to the rollup `isRequired` set when branch
1145
+ // protection is unreadable (the Actions token can't read protection).
1146
+ () => adapter.readRequiredChecks({ owner, repo, base, pr }),
1060
1147
  { degraded, fallback: null },
1061
1148
  );
1062
1149
 
@@ -1158,6 +1245,11 @@ async function gatherPrSnapshot(ctx) {
1158
1245
  unreadable,
1159
1246
  });
1160
1247
 
1248
+ // Record which source answered the required-checks read (`protection` |
1249
+ // `rollup` | null) so the source is visible in the verdict evidence — the
1250
+ // rollup source is the CI path where branch protection is unreadable.
1251
+ evidence.requiredSource = adapter.lastRequiredSource || null;
1252
+
1161
1253
  return {
1162
1254
  state, requiredSet, threads, behind, conflicts, issueComments,
1163
1255
  botStatusBlockers, requiredChecks, pendingChecks, draft, reviewDecision,
@@ -1170,7 +1262,6 @@ async function gatherPrSnapshot(ctx) {
1170
1262
  async function gatherPullSignal(ctx) {
1171
1263
  const {
1172
1264
  pr, self, adapter, runGh,
1173
- runPass = runShepherdPass,
1174
1265
  maxFailures = DEFAULT_MAX_FAILURES,
1175
1266
  maxThreads = DEFAULT_MAX_THREADS,
1176
1267
  maxExcerptLines = DEFAULT_MAX_EXCERPT_LINES,
@@ -1193,12 +1284,12 @@ async function gatherPullSignal(ctx) {
1193
1284
  requiredChecks, pendingChecks, draft, reviewDecision, verdict, evidence, degraded,
1194
1285
  } = snap;
1195
1286
 
1196
- // Legacy decision pass (READ-ONLY dryRun) for back-compat `state`. Shares the
1197
- // snapshot's `degraded` so a pass-read failure is still surfaced in the payload.
1198
- const pass = await safeRead('pass', () => runPass({ ...ctx, adapter, dryRun: true }), {
1199
- degraded,
1200
- fallback: { state: 'UNKNOWN', reason: 'Decision pass could not complete a read failed; see verdict evidence.' },
1201
- });
1287
+ // `verdict` is the SINGLE consumer-facing vocabulary. The back-compat `state`
1288
+ // field is DERIVED from it (via legacyStateFor) the read-only `--pull` payload
1289
+ // no longer runs a second decision pass, so the two vocabularies can never
1290
+ // disagree. A terminal lifecycle (MERGED/CLOSED) still wins over the verdict so
1291
+ // legacy consumers see the landed outcome, matching the old dry-run pass.
1292
+ const legacyState = legacyStateFor(verdict, state.state);
1202
1293
 
1203
1294
  const failures = gatherFailureExcerpts(runGh, state.checks, requiredSet, { maxFailures, maxExcerptLines, degraded });
1204
1295
  const reviewThreads = buildReviewThreads(threads, self, { maxThreads });
@@ -1217,7 +1308,7 @@ async function gatherPullSignal(ctx) {
1217
1308
  });
1218
1309
 
1219
1310
  const summary = summarize({
1220
- state: pass.state,
1311
+ state: legacyState,
1221
1312
  failureCount: failures.length,
1222
1313
  threadCount: reviewThreads.length,
1223
1314
  blockers,
@@ -1225,11 +1316,10 @@ async function gatherPullSignal(ctx) {
1225
1316
 
1226
1317
  return buildPullPayload({
1227
1318
  pr,
1228
- state: pass.state,
1319
+ state: legacyState,
1229
1320
  verdict,
1230
1321
  evidence,
1231
1322
  degraded,
1232
- reason: pass.reason,
1233
1323
  summary,
1234
1324
  mergeable: state.mergeable || 'UNKNOWN',
1235
1325
  mergeStateStatus: state.mergeStateStatus || 'UNKNOWN',
@@ -1262,6 +1352,12 @@ module.exports = {
1262
1352
  renderPullSummary,
1263
1353
  buildPullPayload,
1264
1354
  computeVerdict,
1355
+ verdictToLegacyState,
1356
+ legacyStateFor,
1357
+ MERGE_VERDICTS,
1358
+ VERDICT_LABELS,
1359
+ VERDICT_LABEL_PREFIX,
1360
+ verdictLabel,
1265
1361
  isSkipped,
1266
1362
  isPending,
1267
1363
  buildBotStatusBlockers,
@@ -140,6 +140,13 @@ function searchRanked(projectRoot, query, limit, options = {}) {
140
140
  return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit);
141
141
  }
142
142
 
143
+ // Relevance-only BM25 recall that returns the raw bm25 `score` per entry, so a caller can
144
+ // apply a relevance floor. A no-match/empty query returns [] (no recency fallback). The
145
+ // per-turn memory-recall hook uses this to inject nothing unless a note clearly matches.
146
+ function searchRankedScored(projectRoot, query, limit, options = {}) {
147
+ return resolveStore(projectRoot, options).searchMemoriesRankedScored(query, limit);
148
+ }
149
+
143
150
  // Close and forget every cached default store. The CLI process is short-lived (the OS
144
151
  // closes the handle on exit), so this is mainly a lifecycle helper for long-lived hosts and
145
152
  // tests — it releases the SQLite/WAL handle before a temp dir is removed.
@@ -162,5 +169,6 @@ module.exports = {
162
169
  recent,
163
170
  count,
164
171
  searchRanked,
172
+ searchRankedScored,
165
173
  closeAll,
166
174
  };
@@ -1754,7 +1754,7 @@ function auditArtifactBlocker(projectRoot, audit) {
1754
1754
  return {
1755
1755
  id: 'd20-audit-artifact-current',
1756
1756
  title: 'D20 bd call-site kill-list artifact is not current',
1757
- detail: `Regenerate ${AUDIT_ARTIFACT}; current status: ${auditArtifact.reason}.`,
1757
+ detail: `Regenerate ${AUDIT_ARTIFACT} in one command: \`forge release regen-audit\` (then commit it). Current status: ${auditArtifact.reason}.`,
1758
1758
  evidence: auditArtifact.evidence,
1759
1759
  };
1760
1760
  }
@@ -2074,6 +2074,21 @@ function renderAuditGroup(audit, group) {
2074
2074
  ];
2075
2075
  }
2076
2076
 
2077
+ // Rewrite the tracked kill-list artifact from a live re-scan. This is the exact
2078
+ // one-liner (`forge release regen-audit`) the d20 staleness blocker points at:
2079
+ // every Beads-removal PR shifts the census, so rather than hand-editing the
2080
+ // artifact (and red-failing CI cross-platform until it matches byte-for-byte),
2081
+ // a developer regenerates it in one command and commits the diff. `options`
2082
+ // forwards to auditBdCallSites so the write uses the same scan roots the gate
2083
+ // compares against.
2084
+ function writeAuditArtifact(projectRoot, options = {}) {
2085
+ const audit = auditBdCallSites(projectRoot, options);
2086
+ const artifactPath = absolutePath(projectRoot, AUDIT_ARTIFACT);
2087
+ fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
2088
+ fs.writeFileSync(artifactPath, renderBdCallSiteAuditMarkdown(audit), 'utf8');
2089
+ return { path: AUDIT_ARTIFACT, audit };
2090
+ }
2091
+
2077
2092
  module.exports = {
2078
2093
  AUDIT_ARTIFACT,
2079
2094
  GROUPS,
@@ -2083,6 +2098,7 @@ module.exports = {
2083
2098
  canonicalizeAuditArtifact,
2084
2099
  renderBdCallSiteAuditMarkdown,
2085
2100
  renderReadinessReport,
2101
+ writeAuditArtifact,
2086
2102
  // Exposed for the premerge-de-stage certification tests.
2087
2103
  premergeEmbeddedGateStatus,
2088
2104
  premergeEmbeddedGateBlocker,
package/lib/rules-sync.js CHANGED
@@ -48,6 +48,10 @@ const CURSOR_RULE_FILES = {
48
48
  security: 'security-scanning.mdc',
49
49
  documentation: 'documentation.mdc',
50
50
  'kernel-tracking': 'kernel-tracking.mdc',
51
+ // The using-forge dispatch pointer as an always-apply Cursor rule — Cursor's native
52
+ // always-on carrier for the skill auto-trigger bootstrap (parallels the Claude SessionStart
53
+ // hook and the committed AGENTS.md for Codex). Thin pointer; full policy stays in the skill.
54
+ 'using-forge': 'using-forge.mdc',
51
55
  };
52
56
 
53
57
  const CANONICAL_RULE_NAMES = Object.keys(CURSOR_RULE_FILES);
@@ -172,17 +172,6 @@ function invokesForgeHook(line) {
172
172
  });
173
173
  }
174
174
 
175
- function invokesBeadsHook(line, hookName) {
176
- const tokens = commandTokens(line);
177
- return tokens.some((token, index) => (
178
- commandName(token) === 'bd'
179
- && toText(tokens[index + 1]).toLowerCase() === 'hooks'
180
- && toText(tokens[index + 2]).toLowerCase() === 'run'
181
- && toText(tokens[index + 3]).toLowerCase() === hookName
182
- && isExecutingTokenPosition(tokens, index)
183
- ));
184
- }
185
-
186
175
  function createDiagnostic(code, subject, message, repair, severity = 'hard-stop') {
187
176
  return {
188
177
  code,
@@ -216,7 +205,7 @@ function isUsableWindowsShellCandidate(candidate, options = {}) {
216
205
  function checkHookInstallation(projectRoot, options = {}) {
217
206
  const exec = options._exec || defaultExecFileSync;
218
207
  const platform = options.platform || process.platform;
219
- const expectedRelativeHooksPaths = ['.lefthook/hooks', '.beads/hooks'];
208
+ const expectedRelativeHooksPaths = ['.lefthook/hooks'];
220
209
  const requiredHooks = ['pre-commit', 'pre-push'];
221
210
 
222
211
  function resolveGitRoot() {
@@ -269,23 +258,10 @@ function checkHookInstallation(projectRoot, options = {}) {
269
258
  const content = fs.readFileSync(filePath, 'utf8');
270
259
  const runsLefthook = hasExecutableHookCommand(content, invokesLefthook);
271
260
  const runsForgeHook = hasExecutableHookCommand(content, invokesForgeHook);
272
- const runsBeadsHook = hasExecutableHookCommand(content, (line) => invokesBeadsHook(line, hookName));
273
261
 
274
262
  if (runsLefthook) return { active: true, provider: 'lefthook' };
275
263
  if (runsForgeHook) return { active: true, provider: 'forge' };
276
264
 
277
- if (runsBeadsHook) {
278
- const chainedHookPath = path.join(gitRoot, '.beads', 'hooks', hookName);
279
- if (path.resolve(chainedHookPath) !== normalizedFilePath && fs.existsSync(chainedHookPath)) {
280
- const chained = classifyHookFile(chainedHookPath, hookName, visited);
281
- if (chained.active) {
282
- return { active: true, provider: `beads->${chained.provider}` };
283
- }
284
- }
285
-
286
- return { active: false, provider: 'beads-unverified' };
287
- }
288
-
289
265
  return { active: false, provider: 'unknown' };
290
266
  } catch {
291
267
  return { active: false, provider: 'missing' };
@@ -333,7 +309,7 @@ function checkHookInstallation(projectRoot, options = {}) {
333
309
  state: 'inactive',
334
310
  verification: 'core.hooksPath',
335
311
  hooksPath,
336
- message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks" or ".beads/hooks".`
312
+ message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks".`
337
313
  };
338
314
  }
339
315
  } catch {
@@ -488,9 +464,11 @@ function normalizeProjectRoot(projectRoot) {
488
464
  // shared resolver (env > .forge/config.yaml > default 'kernel'). Never throws and
489
465
  // never warns from the health path.
490
466
  function resolveHealthIssueBackend(options, projectRoot) {
491
- if (options.issueBackend === 'kernel' || options.issueBackend === 'beads') {
467
+ if (options.issueBackend === 'kernel') {
492
468
  return options.issueBackend;
493
469
  }
470
+ // Anything else — including the retired `beads` value — falls through to the
471
+ // shared resolver, which answers 'kernel' for every unrecognized selector.
494
472
  return resolveIssueBackend({
495
473
  deps: options.backendDeps || {},
496
474
  env: options.env || process.env,
@@ -540,25 +518,16 @@ function checkRuntimeHealth(projectRoot, options = {}) {
540
518
  }
541
519
 
542
520
  if (!bd.available) {
543
- if (issueBackend === 'beads') {
544
- // Beads is the active issue backend, so bd is a genuine hard prerequisite.
545
- diagnostics.push(createDiagnostic(
546
- 'BD_MISSING',
547
- 'bd',
548
- 'bd is required for stage-entry workflow checks when the beads issue backend is selected.'
549
- ));
550
- } else {
551
- // Kernel is the default issue backend and needs no bd binary, so a missing bd
552
- // must NOT hard-stop stage entry (the "no Beads install required" contract).
553
- // Surface it as a non-blocking advisory instead.
554
- advisories.push(createDiagnostic(
555
- 'BD_MISSING',
556
- 'bd',
557
- 'bd is not installed. The kernel issue backend does not require it; install bd only if you switch to the beads backend.',
558
- undefined,
559
- 'advisory'
560
- ));
561
- }
521
+ // The kernel is the only issue backend and needs no bd binary, so bd is never a
522
+ // prerequisite and a missing bd must NOT hard-stop stage entry (the "no Beads
523
+ // install required" contract). It stays a non-blocking advisory.
524
+ advisories.push(createDiagnostic(
525
+ 'BD_MISSING',
526
+ 'bd',
527
+ 'bd is not installed. Forge does not require it — the kernel issue backend is the only backend.',
528
+ undefined,
529
+ 'advisory'
530
+ ));
562
531
  }
563
532
 
564
533
  if (!gh.available) {