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.
- package/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- 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
|
|
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'
|
|
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
|
-
|
|
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
|
-
//
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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:
|
|
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:
|
|
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,
|
package/lib/project-memory.js
CHANGED
|
@@ -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
|
};
|
package/lib/release-readiness.js
CHANGED
|
@@ -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}
|
|
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);
|
package/lib/runtime-health.js
CHANGED
|
@@ -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'
|
|
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"
|
|
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'
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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) {
|