forge-workflow 0.1.0-beta.3 → 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 (122) hide show
  1. package/AGENTS.md +1 -1
  2. package/bin/forge-cmd.js +1 -1
  3. package/bin/forge.js +5 -0
  4. package/docs/reference/COMMANDS.md +3 -1
  5. package/docs/reference/shepherd.md +72 -1
  6. package/lib/adapters/greptile-review-adapter.js +1 -1
  7. package/lib/adapters/pr-state-adapter.js +103 -8
  8. package/lib/agents-config.js +5 -0
  9. package/lib/commands/_issue.js +31 -46
  10. package/lib/commands/_manifest.js +1 -1
  11. package/lib/commands/_resolve-command-opts.js +36 -29
  12. package/lib/commands/claim.js +2 -4
  13. package/lib/commands/hooks.js +155 -8
  14. package/lib/commands/plan.js +23 -115
  15. package/lib/commands/prime.js +8 -1
  16. package/lib/commands/release.js +1 -2
  17. package/lib/commands/serve.js +5 -2
  18. package/lib/commands/setup.js +0 -1
  19. package/lib/commands/shepherd.js +36 -3
  20. package/lib/commands/skill.js +275 -0
  21. package/lib/commands/status.js +37 -32
  22. package/lib/commands/test.js +32 -0
  23. package/lib/commands/worktree.js +27 -1
  24. package/lib/core/runtime-graph.js +88 -14
  25. package/lib/forge-issues.js +13 -464
  26. package/lib/harness-capability-matrix.js +2 -2
  27. package/lib/hook-renderer.js +54 -12
  28. package/lib/issue-backend.js +42 -3
  29. package/lib/kernel/broker.js +28 -0
  30. package/lib/kernel/migrations.js +30 -2
  31. package/lib/kernel/schema.js +35 -0
  32. package/lib/kernel/sqlite-driver.js +150 -0
  33. package/lib/memory-recall.js +151 -0
  34. package/lib/orientation.js +303 -6
  35. package/lib/pr-bundle.js +6 -2
  36. package/lib/pr-monitor/reconcile-executor.js +659 -0
  37. package/lib/pr-monitor/reconcile-tick.js +138 -0
  38. package/lib/pr-monitor/reconcile.js +0 -0
  39. package/lib/pr-monitor/render-sticky.js +14 -0
  40. package/lib/pr-monitor/shepherd-lease.js +243 -0
  41. package/lib/pr-monitor/upsert-sticky.js +1 -1
  42. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  43. package/lib/pr-pull.js +70 -15
  44. package/lib/project-memory.js +8 -0
  45. package/lib/rules-sync.js +4 -0
  46. package/lib/runtime-health.js +15 -46
  47. package/lib/skill-eval.js +750 -0
  48. package/lib/status/identity.js +46 -0
  49. package/lib/status/presenter.js +0 -35
  50. package/lib/status/snapshot.js +11 -16
  51. package/lib/upgrade-safety.js +8 -9
  52. package/lib/using-forge.js +315 -0
  53. package/lib/workflow/enforce-stage.js +5 -5
  54. package/lib/workflow/state-manager.js +23 -23
  55. package/package.json +1 -1
  56. package/rules/using-forge.md +24 -0
  57. package/scripts/forge-team/index.sh +0 -5
  58. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  59. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  60. package/scripts/test.js +8 -1
  61. package/skills/claim-safety/SKILL.md +4 -0
  62. package/skills/claim-safety/evals/scorecard.json +41 -0
  63. package/skills/coverage.json +83 -0
  64. package/skills/dev/SKILL.md +4 -0
  65. package/skills/dev/evals/scorecard.json +41 -0
  66. package/skills/gates/SKILL.md +80 -0
  67. package/skills/gates/evals/evals.json +38 -0
  68. package/skills/gates/evals/scorecard.json +41 -0
  69. package/skills/hermes-forge/SKILL.md +1 -0
  70. package/skills/hermes-forge/evals/scorecard.json +41 -0
  71. package/skills/issue-basics/SKILL.md +1 -0
  72. package/skills/issue-basics/evals/scorecard.json +41 -0
  73. package/skills/kernel/SKILL.md +38 -0
  74. package/skills/kernel/evals/scorecard.json +41 -0
  75. package/skills/memory/SKILL.md +16 -1
  76. package/skills/memory/evals/scorecard.json +41 -0
  77. package/skills/parallel-deep-research/SKILL.md +1 -0
  78. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  79. package/skills/plan/SKILL.md +6 -0
  80. package/skills/plan/evals/scorecard.json +41 -0
  81. package/skills/portability/SKILL.md +47 -0
  82. package/skills/portability/evals/evals.json +34 -0
  83. package/skills/portability/evals/scorecard.json +41 -0
  84. package/skills/research/SKILL.md +1 -0
  85. package/skills/research/evals/scorecard.json +41 -0
  86. package/skills/review/SKILL.md +6 -0
  87. package/skills/review/evals/scorecard.json +41 -0
  88. package/skills/rollback/SKILL.md +1 -0
  89. package/skills/rollback/evals/scorecard.json +41 -0
  90. package/skills/setup/SKILL.md +91 -0
  91. package/skills/setup/evals/evals.json +42 -0
  92. package/skills/setup/evals/scorecard.json +41 -0
  93. package/skills/shepherd/SKILL.md +76 -36
  94. package/skills/shepherd/evals/evals.json +21 -9
  95. package/skills/shepherd/evals/scorecard.json +41 -0
  96. package/skills/ship/SKILL.md +6 -0
  97. package/skills/ship/evals/scorecard.json +41 -0
  98. package/skills/smith/SKILL.md +8 -0
  99. package/skills/smith/evals/scorecard.json +41 -0
  100. package/skills/sonarcloud/SKILL.md +1 -0
  101. package/skills/sonarcloud/evals/scorecard.json +41 -0
  102. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  103. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  104. package/skills/status/SKILL.md +3 -0
  105. package/skills/status/evals/scorecard.json +41 -0
  106. package/skills/triage-ready/SKILL.md +2 -0
  107. package/skills/triage-ready/evals/scorecard.json +41 -0
  108. package/skills/using-forge/SKILL.md +104 -0
  109. package/skills/using-forge/evals/scorecard.json +41 -0
  110. package/skills/validate/SKILL.md +4 -0
  111. package/skills/validate/evals/scorecard.json +41 -0
  112. package/skills/verify/SKILL.md +4 -0
  113. package/skills/verify/evals/scorecard.json +41 -0
  114. package/skills/worktree/SKILL.md +87 -0
  115. package/skills/worktree/evals/evals.json +38 -0
  116. package/skills/worktree/evals/scorecard.json +41 -0
  117. package/lib/adapters/beads-issue-adapter.js +0 -127
  118. package/lib/beads-nudge.js +0 -91
  119. package/lib/commands/board.js +0 -64
  120. package/lib/status/beads-snapshot.js +0 -145
  121. package/scripts/forge-team/lib/dashboard.sh +0 -316
  122. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
package/AGENTS.md CHANGED
@@ -17,7 +17,7 @@ This project ships a **default TDD-first workflow template** with 6 workflow sta
17
17
 
18
18
  **Utility**: `/status` — Context check before starting work (not a numbered stage)
19
19
 
20
- **Utility**: `/shepherd <pr>` — Monitor-driven PR shepherd: one bounded pass that reads CI and check state, re-runs a flaky required check (Tier-A), or escalates, then hands off. It is a utility command, **not** a workflow stage, and does not sit between `/review` and the handoff. It **never merges** (the human merges in the GitHub UI) and **never resolves review threads** (that stays with `/review`). `--auto-rebase` is opt-in and default OFF. See [docs/reference/shepherd.md](docs/reference/shepherd.md).
20
+ **Utility**: `/shepherd <pr>` — Autonomous PR ownership. `forge shepherd daemon` is a singleton reconcile daemon that owns **all** open PRs for the repo: it converges CI check state into kernel verdicts, re-runs flaky required checks (Tier-A), reaps orphan watchers, and self-retires when no PRs remain so agents read verdicts (`forge shepherd <pr> --pull --json`, `forge shepherd events`) instead of hand-polling. `forge shepherd <pr>` is the one-shot bounded pass for a single PR. It is a utility, **not** a workflow stage. It **never merges** (the human merges in the GitHub UI) and **never resolves review threads** (that stays with `/review`). `--auto-rebase` is opt-in and default OFF; kill-switches: `FORGE_SHEPHERD_DISABLE`, `forge gate disable rail.auto_shepherd`. **Startup/containment**: the daemon is a repo singleton (O_EXCL lease — a duplicate start is a clean no-op) started in the harness background shell so it is session-reaped; agents never launch it as a detached spawn (automatic per-command launch is a planned follow-up, W-S4c). See [docs/reference/shepherd.md](docs/reference/shepherd.md).
21
21
 
22
22
  ## Automatic Change Classification
23
23
 
package/bin/forge-cmd.js CHANGED
@@ -72,7 +72,7 @@ function findPlanDocForBranch(branch) {
72
72
 
73
73
  const COMMAND_DESCRIPTIONS = {
74
74
  status: 'Detect current workflow stage (1-7)',
75
- plan: 'Create branch + Beads + design doc',
75
+ plan: 'Create branch + issue + design doc',
76
76
  dev: 'Implement with TDD (RED-GREEN-REFACTOR)',
77
77
  validate: 'Run type check, lint, security, tests',
78
78
  check: 'Alias for validate (deprecated — use validate)',
package/bin/forge.js CHANGED
@@ -4159,6 +4159,11 @@ async function main() {
4159
4159
  console.error(`Error running '${command}':`, err.message);
4160
4160
  process.exit(1);
4161
4161
  }
4162
+ // NOTE: the autonomous-shepherd per-command trigger is intentionally NOT wired here.
4163
+ // The reconcile engine + daemon land in this PR; the auto-fire wiring is deferred to a
4164
+ // follow-up (W-S4c) because a naive dispatch-finally trigger spawns a session-outliving
4165
+ // daemon on EVERY command, which breaks test isolation (Windows rmSync EBUSY) and needs
4166
+ // its own firing-policy + containment design. Start the daemon via `forge shepherd daemon`.
4162
4167
  return;
4163
4168
  }
4164
4169
 
@@ -59,6 +59,7 @@ forge orient [--budget N] [--json]
59
59
  forge recap <issue> [--budget N] [--json]
60
60
  forge recall [query] [--limit N] [--all] [--json]
61
61
  forge remember <note> [--tag <label>]... [--json]
62
+ forge skill for "<situation>" [--json]
62
63
  ```
63
64
 
64
65
  - `forge plan` creates the implementation plan, kernel issue, and feature branch/worktree from a researched feature description; it prints the created issue id, branch name, and the suggested next command.
@@ -66,7 +67,8 @@ forge remember <note> [--tag <label>]... [--json]
66
67
  - `forge validate` runs the validation orchestration pipeline (conflict markers, type check, lint, security, tests) with no arguments — see also `bun run check` under Validation And Packaging.
67
68
  - `forge ship` creates the pull request from validated feature work (wraps `gh pr create`); `--dry-run` previews without creating a PR.
68
69
  - `forge status` is the one-glance orientation command: where you are, what to run next, and your active work; `--full` also shows blocked/stale/recently-completed issues.
69
- - `forge prime` emits the bounded session-entry orientation envelope agents read at the start of a session.
70
+ - `forge prime` emits the bounded session-entry orientation envelope agents read at the start of a session; it now leads with a bounded (<=20-line) **live-state** block — current stage, claimed issue(s), ready-work count, enabled gates/rails, and one progressive-adoption nudge — collected best-effort (never blocks, honest fallbacks) before the deterministic orientation.
71
+ - `forge skill for "<situation>"` is the deterministic intent-to-skill router: it reads the canonical skill catalog and prints the best-fit Forge skill(s) plus why, as the reasoning fallback for harnesses without a SessionStart hook that auto-injects the `using-forge` dispatch skill. `--json` emits the machine-readable routing result. The `forge skill` noun is structured so later waves can add `forge skill eval` / `forge skill scores`.
70
72
  - `forge orient` emits bounded project orientation from deterministic source files (broader than `prime`, still token-budgeted via `--budget`).
71
73
  - `forge recap <issue>` is the issue-scoped counterpart to `forge orient`/`forge prime` — it summarizes a single issue from the same deterministic file assembly instead of the whole project. Requires an issue id; running it with no id (or `--help`) prints usage only.
72
74
  - `forge recall` retrieves project-memory notes from the kernel-backed memory store; omit `query` to list recent notes.
@@ -6,13 +6,48 @@ polling / rerun / escalation loop a human otherwise runs by hand after
6
6
  the pre-merge gate (the embedded documentation-and-handoff gate in `/ship` and `/review`).
7
7
 
8
8
  ```bash
9
- forge shepherd <pr-number>
9
+ forge shepherd daemon # singleton reconcile daemon: owns ALL open PRs
10
+ forge shepherd <pr-number> # one bounded pass over a single PR
10
11
  forge shepherd <pr-number> --auto-rebase # opt-in, default OFF
11
12
  forge shepherd <pr-number> --pull # read-only: WHY it is blocked + what to fix
12
13
  forge shepherd <pr-number> --pull --json # same payload as machine-readable JSON
13
14
  forge shepherd <pr-number> --bundle --json # read-only: the COMPLETE PR-state bundle
15
+ forge shepherd watch <pr-number> # single-PR constant monitor loop
16
+ forge shepherd events <pr-number> --since <seq> # read new journal events back
14
17
  ```
15
18
 
19
+ ## Daemon vs bounded pass (two modes, one verb)
20
+
21
+ There are two ways to run the shepherd, both under the single `forge shepherd`
22
+ verb:
23
+
24
+ - **`forge shepherd daemon` — the singleton reconcile daemon (W-S4b), the default
25
+ ownership model.** It acquires the machine-wide shepherd lease for this repo
26
+ (exiting immediately as a clean no-op if a live daemon already owns it),
27
+ heartbeats, and converges the *entire* PR world every ~60s: self-registering
28
+ hand-opened PRs, restarting killed watchers, reaping verified orphan watchers,
29
+ converging CI check state into kernel verdicts, and retiring merged/closed PRs.
30
+ It **self-retires** — releases the lease, kills its verified children, exits —
31
+ once no PRs remain open. Today it is started explicitly — in a harness
32
+ background shell at session start, or `forge shepherd daemon` — not run by hand
33
+ as a detached process; automatic per-command launch (W-S4c) is a planned
34
+ follow-up, not yet wired. Once running, an agent does not poll: the daemon owns
35
+ the convergence loop.
36
+ - **`forge shepherd <pr>` — one bounded pass.** Reads one PR's state, takes at
37
+ most one Tier-A action, exits. The point-in-time surface for a single PR (see
38
+ *Bounded-pass model* below).
39
+ - **`forge shepherd watch <pr>` / `watch --adopt`** — foreground streaming watch of
40
+ one PR, or (`--adopt`) adopt every currently-open PR into the watcher set.
41
+ - **`forge shepherd events <pr> --since <seq>`** — the event deltas for a PR since a
42
+ cursor; **`forge shepherd <pr> --pull --json` / `--bundle`** read the kernel verdict
43
+ + rollup without taking any action.
44
+
45
+ Session start: if the repo has open PRs, start `forge shepherd daemon` in the
46
+ harness background shell so it is reaped with the session. No liveness check is
47
+ needed first — the O_EXCL singleton lease makes a duplicate start a clean no-op.
48
+ (A `forge prime` open-PR + daemon-liveness line is a planned follow-up — W-S5 —
49
+ not yet wired.)
50
+
16
51
  ## `--pull`: the actionable blocker payload
17
52
 
18
53
  `--pull` is a **strictly read-only** signal-gather. It computes the decision
@@ -139,6 +174,42 @@ same verb (or `forge shepherd events`) on its own cadence.
139
174
  - **Auth taxonomy.** 401 (expiry) pauses and surfaces; 403 insufficient-scope is
140
175
  a hard-stop; 403 with `Retry-After` honors the delay and resumes next pass.
141
176
 
177
+ ## GitHub Actions backstop — auto-updated heads re-trigger CI (`FORGE_PR_TOKEN`)
178
+
179
+ The `pr-monitor.yml` Actions workflow can auto-update an otherwise-clean-but-behind
180
+ PR branch (merge base into the head). That push must **re-trigger CI on the new
181
+ head**, or the head sits with no required checks running and can never merge.
182
+
183
+ GitHub deliberately does **not** start new workflow runs for events created with
184
+ the default `GITHUB_TOKEN` — a `pull_request: synchronize` it produces lands in an
185
+ *approval-required* state instead of running. So an auto-update authored by
186
+ `GITHUB_TOKEN` leaves a **CI-dead head**. (Official rule:
187
+ <https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow>.)
188
+
189
+ **Fix (maintainer action required):** create a repository secret named
190
+ **`FORGE_PR_TOKEN`** holding a fine-grained **PAT** (or GitHub-App installation
191
+ token). The PAT's OWN permissions — not the workflow's `permissions:` block, which
192
+ only governs the built-in `GITHUB_TOKEN` — must cover every call the step makes:
193
+
194
+ | Permission | Why |
195
+ | --- | --- |
196
+ | **Contents: write** | push the update-branch merge to the head |
197
+ | **Pull requests: write** | `PUT /pulls/{n}/update-branch` (the update-branch API) |
198
+ | **Checks: write** | create the `forge/auto-update` marker check run |
199
+ | **Workflows** | only if the base branch may change `.github/workflows/**` (the merge would carry it) |
200
+
201
+ The auto-update-branch step uses the token and falls back to `GITHUB_TOKEN` when
202
+ the secret is absent:
203
+
204
+ ```yaml
205
+ GH_TOKEN: ${{ secrets.FORGE_PR_TOKEN || github.token }}
206
+ ```
207
+
208
+ With the secret set, auto-updated heads re-run CI automatically. Without it, the
209
+ workflow behaves exactly as before (no regression) — it just cannot auto-run CI on
210
+ the updated head. Forge only wires the code path and reads the secret; **creating
211
+ the secret is the maintainer's responsibility** — Forge never fabricates a token.
212
+
142
213
  ## Per-harness behavior
143
214
 
144
215
  - **Claude Code / Codex:** invoke `forge shepherd <pr>` directly; an external
@@ -86,7 +86,7 @@ function matchThreadsToCommitsWithGit(threads, projectRoot, opts = {}) {
86
86
  }
87
87
 
88
88
  const exec = opts._exec || ((cmd, args) => {
89
- return execFileSync(cmd, args, { encoding: 'utf8' });
89
+ return execFileSync(cmd, args, { encoding: 'utf8', windowsHide: true });
90
90
  });
91
91
 
92
92
  let sinceCommit = opts.sinceCommit;
@@ -78,9 +78,12 @@ class PrStateAdapter {
78
78
  this.id = options.id || 'pr-state-adapter';
79
79
  this.kind = 'pr-state';
80
80
  this.name = options.name || this.id;
81
+ // windowsHide keeps the shepherd's per-poll gh/git calls from flashing a
82
+ // console window on Windows when the watcher runs detached (issue 931e7924).
81
83
  const defaultRunner = (cmd, args, opts = {}) => execFileSync(cmd, args, {
82
84
  encoding: 'utf8',
83
85
  timeout: options.timeout || 30000,
86
+ windowsHide: true,
84
87
  ...(opts.cwd ? { cwd: opts.cwd } : {}),
85
88
  });
86
89
  this._gh = options.gh || defaultRunner;
@@ -127,17 +130,59 @@ class PrStateAdapter {
127
130
  }
128
131
 
129
132
  /**
130
- * Read the branch-protection required-checks set.
133
+ * Read the required-checks set for a PR, with a two-source strategy so the
134
+ * verdict is not permanently UNKNOWN in CI.
131
135
  *
132
- * Returns `null` when the protection endpoint is unreadable (e.g. 403
133
- * insufficient scope, the branch is not protected, or the payload shape is
134
- * unexpected) so the caller can escalate rather than guess. Re-throws non-auth
135
- * errors.
136
+ * 1. **Branch protection** (`.../protection/required_status_checks`) the
137
+ * authoritative set, but this REST endpoint needs repo `Administration:read`,
138
+ * which GitHub Actions' `GITHUB_TOKEN` can NEVER hold (administration is not a
139
+ * grantable `permissions:` scope). So in CI this ALWAYS 403/404s and the set
140
+ * was permanently null → verdict UNKNOWN on every PR.
141
+ * 2. **statusCheckRollup `isRequired`** (GraphQL, on the PR head commit) — the
142
+ * fallback. It is readable with the plain PR-read scope the Actions token DOES
143
+ * hold (it is what `gh pr checks --required` uses) and it covers BOTH classic
144
+ * branch protection AND repository rulesets.
136
145
  *
137
- * @param {{ owner: string, repo: string, base: string }} ctx
146
+ * Known limitation of the fallback: the rollup only lists contexts that
147
+ * PRODUCED a run, so a required context that never ran at all is invisible on
148
+ * this path — missing-required detection is best-effort when the source is the
149
+ * rollup. That is strictly better than a permanent UNKNOWN.
150
+ *
151
+ * Returns `null` only when BOTH sources are unreadable (existing fail-closed
152
+ * behaviour). `lastRequiredSource` records which source answered
153
+ * (`'protection'` | `'rollup'` | `null`) so callers can surface it as evidence.
154
+ * Re-throws non-auth protection errors (unchanged).
155
+ *
156
+ * @param {{ owner: string, repo: string, base: string, pr?: string|number }} ctx
138
157
  * @returns {Promise<string[] | null>}
139
158
  */
140
- async readRequiredChecks({ owner, repo, base }) {
159
+ async readRequiredChecks({ owner, repo, base, pr }) {
160
+ this.lastRequiredSource = null;
161
+ const fromProtection = this._readProtectionRequired({ owner, repo, base });
162
+ if (Array.isArray(fromProtection)) {
163
+ this.lastRequiredSource = 'protection';
164
+ return fromProtection;
165
+ }
166
+ // Protection unreadable (auth/scope/not-protected/unexpected shape) — fall back
167
+ // to the rollup `isRequired` set the Actions token CAN read.
168
+ const fromRollup = this._readRollupRequired({ owner, repo, pr });
169
+ if (Array.isArray(fromRollup)) {
170
+ this.lastRequiredSource = 'rollup';
171
+ return fromRollup;
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Branch-protection required set, or `null` when unreadable (auth/scope/
178
+ * not-protected/unexpected shape). Re-throws non-auth errors so a genuine
179
+ * outage is not silently masked. Split out so `readRequiredChecks` can fall
180
+ * back cleanly.
181
+ *
182
+ * @param {{ owner: string, repo: string, base: string }} ctx
183
+ * @returns {string[] | null}
184
+ */
185
+ _readProtectionRequired({ owner, repo, base }) {
141
186
  const apiPath = `repos/${owner}/${repo}/branches/${encodeURIComponent(base)}/protection/required_status_checks`;
142
187
  try {
143
188
  const raw = this._gh('gh', ['api', apiPath]);
@@ -150,13 +195,63 @@ class PrStateAdapter {
150
195
  } catch (error) {
151
196
  const auth = classifyAuthError(error);
152
197
  if (auth) {
153
- // Unreadable protection (auth/scope/not-protected) — caller escalates.
198
+ // Unreadable protection (auth/scope/not-protected) — fall back to rollup.
154
199
  return null;
155
200
  }
156
201
  throw error;
157
202
  }
158
203
  }
159
204
 
205
+ /**
206
+ * Fallback required set from the PR head commit's `statusCheckRollup`, reading
207
+ * per-context `isRequired(pullRequestNumber:)` via GraphQL. Readable with plain
208
+ * PR-read scope (unlike branch protection). Returns the deduped names of every
209
+ * required CheckRun/StatusContext, `[]` when the rollup is readable but nothing
210
+ * is required, or `null` when the rollup itself is unreadable (fail-closed).
211
+ *
212
+ * @param {{ owner: string, repo: string, pr?: string|number }} ctx
213
+ * @returns {string[] | null}
214
+ */
215
+ _readRollupRequired({ owner, repo, pr }) {
216
+ const prNum = Number.parseInt(String(pr), 10);
217
+ if (!Number.isInteger(prNum) || prNum <= 0) return null;
218
+ // pr is inlined as a validated integer (no injection); owner/repo are GitHub
219
+ // name-charset identifiers. Shape verified against the live GraphQL API.
220
+ const query = `query { repository(owner: "${owner}", name: "${repo}") { `
221
+ + `pullRequest(number: ${prNum}) { headRef { target { ... on Commit { `
222
+ + `statusCheckRollup { contexts(first: 100) { nodes { __typename `
223
+ + `... on CheckRun { name isRequired(pullRequestNumber: ${prNum}) } `
224
+ + `... on StatusContext { context isRequired(pullRequestNumber: ${prNum}) } `
225
+ + `} } } } } } } } }`;
226
+ try {
227
+ const raw = this._gh('gh', ['api', 'graphql', '-f', `query=${query}`]);
228
+ const data = JSON.parse(raw || '{}');
229
+ const nodes = data
230
+ && data.data
231
+ && data.data.repository
232
+ && data.data.repository.pullRequest
233
+ && data.data.repository.pullRequest.headRef
234
+ && data.data.repository.pullRequest.headRef.target
235
+ && data.data.repository.pullRequest.headRef.target.statusCheckRollup
236
+ && data.data.repository.pullRequest.headRef.target.statusCheckRollup.contexts
237
+ && data.data.repository.pullRequest.headRef.target.statusCheckRollup.contexts.nodes;
238
+ // No rollup at all (e.g. statusCheckRollup null) → cannot determine the set.
239
+ if (!Array.isArray(nodes)) return null;
240
+ const required = [];
241
+ for (const node of nodes) {
242
+ if (node && node.isRequired === true) {
243
+ const name = node.name || node.context;
244
+ if (name) required.push(name);
245
+ }
246
+ }
247
+ // Dedupe matrix duplicates (same context reported by multiple jobs).
248
+ return [...new Set(required)];
249
+ } catch {
250
+ // GraphQL unreadable (auth/network/etc.) — fail closed to null.
251
+ return null;
252
+ }
253
+ }
254
+
160
255
  /**
161
256
  * Read ahead/behind divergence against the base ref.
162
257
  *
@@ -93,6 +93,10 @@ Forge currently supports Claude Code, Codex, and Cursor. Hermes support is plann
93
93
  - **Codex** - OpenAI's CLI agent, skills-based workflow
94
94
  - **Cursor** - IDE-integrated, native Plan/Ask/Debug modes
95
95
 
96
+ ## Skill Dispatch (auto-trigger)
97
+
98
+ Before ANY response — including clarifying questions or exploring the codebase — if there is even a 1% chance a Forge skill applies, invoke it, then announce \`Using [skill] to [purpose]\`. Invoke the \`using-forge\` dispatch skill (auto-discovered from your agent's own skills — Forge setup installs it into each harness's skills dir; it carries the 1%-rule and routing table), or run \`forge skill for "<situation>"\` for the deterministic best-fit skill. This is agent-agnostic — never branch on harness identity.
99
+
96
100
  ## Quick Start
97
101
 
98
102
  \`\`\`bash
@@ -962,6 +966,7 @@ Choose based on server documentation.
962
966
  module.exports = {
963
967
  detectProjectMetadata,
964
968
  generateAgentsMd,
969
+ generateAgentsMdContent,
965
970
  generateCursorConfig,
966
971
  generateArchitectureDoc,
967
972
  generateConfigurationDoc,
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
4
4
  const { resolveIssueBackend, hasExplicitBackendSignal, shouldUseKernelBroker } = require('../issue-backend');
5
- const { maybeWarnUnmigratedBeads } = require('../beads-nudge');
6
5
  const {
7
6
  ISSUE_COMMAND_SCHEMA_VERSION,
8
7
  ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
@@ -17,11 +16,10 @@ const { checkReadFirst } = require('../grounding/read-first');
17
16
  const { recordContextLoaded } = require('../grounding/context-events');
18
17
 
19
18
  // The Forge issue command surface. Each subcommand routes through the shared
20
- // runIssueOperation, which selects the active backend (Kernel by --kernel /
21
- // --issue-backend kernel / FORGE_ISSUE_BACKEND=kernel; Beads otherwise) and performs
22
- // any backend-specific argument translation. This module therefore carries NO direct
23
- // issue-tracker invocation or argv translation — those live in the backend
24
- // abstraction (lib/forge-issues.js + the issue adapters).
19
+ // runIssueOperation, which dispatches to the Forge Kernel the only issue backend.
20
+ // This module therefore carries NO direct issue-tracker invocation or argv
21
+ // translation those live in the backend abstraction (lib/forge-issues.js + the
22
+ // kernel issue adapter).
25
23
  const SUBCOMMANDS = {
26
24
  create: {
27
25
  description: 'Create an issue via Forge',
@@ -67,10 +65,8 @@ const SUBCOMMANDS = {
67
65
  description: 'Show issue statistics via Forge',
68
66
  usage: 'forge issue stats [flags]',
69
67
  },
70
- // KAP-7: derived read queries. The backend abstraction maps each to its tracker
71
- // equivalent (the Kernel passes the operation name through unchanged; the Beads
72
- // backend maps each to its passthrough subcommand). They are READS, so they are
73
- // intentionally NOT in WRITE_SUBCOMMANDS.
68
+ // KAP-7: derived read queries. The Kernel passes the operation name through
69
+ // unchanged. They are READS, so they are intentionally NOT in WRITE_SUBCOMMANDS.
74
70
  blocked: {
75
71
  description: 'Show blocked issues via Forge',
76
72
  usage: 'forge issue blocked [flags]',
@@ -106,9 +102,7 @@ const SUBCOMMANDS = {
106
102
  usage: 'forge issue owns <id> [--json]',
107
103
  },
108
104
  // Active-lease listing (kernel issue 7dc229d4). A bare passthrough to the Kernel
109
- // lease table (kernel_claims); Beads has no lease table to enumerate, so
110
- // forge-issues.js rejects the Beads path explicitly. A READ, so intentionally NOT
111
- // in WRITE_SUBCOMMANDS.
105
+ // lease table (kernel_claims). A READ, so intentionally NOT in WRITE_SUBCOMMANDS.
112
106
  claims: {
113
107
  description: 'Show active issue leases (claims) via Forge',
114
108
  usage: 'forge issue claims [--json]',
@@ -389,8 +383,7 @@ function formatIssueHelp() {
389
383
  }
390
384
 
391
385
  // Map a CLI subcommand to the backend operation name. `dep` fans out to
392
- // `dep.<action>`; every other subcommand uses its own name (the backend performs
393
- // any tracker-specific translation, e.g. Beads claim -> `update <id> --claim`).
386
+ // `dep.<action>`; every other subcommand uses its own name.
394
387
  function resolveIssueOperation(subcommand, args) {
395
388
  if (subcommand === 'dep') {
396
389
  return `dep.${normalizeArgs(args)[0]}`;
@@ -400,10 +393,8 @@ function resolveIssueOperation(subcommand, args) {
400
393
 
401
394
  // The Kernel create payload (buildCreatePayload) reads only the --title flag, so a
402
395
  // bare leading positional (`forge create "title"`) would be ignored and the title
403
- // would default to the minted UUID. For parity on the KERNEL PATH ONLY, translate a
404
- // single leading bare positional into `--title <value>` when no explicit
405
- // --title/--title= is present. The Beads backend keeps its native positional
406
- // handling (this never runs for the Beads path).
396
+ // would default to the minted UUID. Translate a single leading bare positional into
397
+ // `--title <value>` when no explicit --title/--title= is present.
407
398
  function withKernelCreateTitle(args) {
408
399
  const hasTitle = args.some(
409
400
  arg => arg === '--title' || (typeof arg === 'string' && arg.startsWith('--title=')),
@@ -454,10 +445,12 @@ function validateDepArgs(args) {
454
445
  return null;
455
446
  }
456
447
 
457
- // Resolve the active issue backend (kernel|beads) and thread it into opts so the
458
- // shared runIssueOperation deps see it. OPT-IN ONLY: opts is left byte-identical
459
- // when no explicit signal is present (env/config/explicit), preserving the Beads
460
- // default path. A copy is returned — the caller's opts object is never mutated.
448
+ // Normalize any explicit issue-backend signal and thread the resolved value into
449
+ // opts so the shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
450
+ // byte-identical when no explicit signal is present (env/config/explicit), so the
451
+ // no-signal path stays untouched. A copy is returned — the caller's opts object is
452
+ // never mutated. A retired value (`beads`) warns here with the migrate pointer and
453
+ // resolves to the kernel.
461
454
  function withResolvedIssueBackend(projectRoot, opts = {}) {
462
455
  const env = opts.env || process.env;
463
456
  const signalContext = { deps: opts, env, projectRoot };
@@ -479,13 +472,13 @@ function withResolvedIssueBackend(projectRoot, opts = {}) {
479
472
 
480
473
  // The Kernel broker returns the issue-command contract shape
481
474
  // ({ ok, schema_version, command, data, next_commands } or { ok:false, error })
482
- // rather than the Beads-style { success, output }. The bin/forge.js result printer
475
+ // rather than the legacy { success, output }. The bin/forge.js result printer
483
476
  // keys on `success`/`output`, so a raw kernel contract would render as
484
477
  // "Command failed". Normalize ONLY the contract shape (ok defined, success
485
478
  // undefined) into { success, output } here, at the command boundary — the kernel
486
479
  // contract itself stays untouched. Every other result passes through byte-identical.
487
480
  //
488
- // Response-contract parity (the Beads behavior the Kernel replaced):
481
+ // Response-contract guarantees:
489
482
  // * SUCCESS → the printed envelope carries `ok:true` (consumers gate on it).
490
483
  // * FAILURE → the contract `exit_code` is surfaced as `result.exitCode` so the bin
491
484
  // printer exits with the error class's code (not always 1); and on
@@ -609,8 +602,7 @@ function splitLeadingIds(args = []) {
609
602
  // which broke envelope parity for multi-id close. `ok` is true only when every id
610
603
  // closed; per-id outcomes live in `data.results` and the contract `exit_code` of the
611
604
  // first failure is surfaced as `exitCode` so the bin printer exits with the error
612
- // class's code. KERNEL PATH ONLY: the Beads passthrough keeps its single
613
- // `close id1 id2 ...` invocation.
605
+ // class's code.
614
606
  async function runKernelBatchClose(runner, operation, ids, flags, projectRoot, opts, verifyEnabled = false) {
615
607
  const results = [];
616
608
  let allSucceeded = true;
@@ -700,13 +692,11 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
700
692
  return { success: false, error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
701
693
  }
702
694
 
703
- // Backend-agnostic --help short-circuit: print the subcommand's usage and return
704
- // BEFORE resolving the backend or dispatching any operation. Without this, a help
705
- // request was forwarded to the active backend as an operation arg absorbed
706
- // harmlessly by Beads (which swallowed --help), but broken under the Kernel default:
707
- // the plural path failed with a bare "Command failed" and the singular path
708
- // SILENTLY minted a junk issue (and could queue a GitHub projection). Help must
709
- // never touch a backend.
695
+ // --help short-circuit: print the subcommand's usage and return BEFORE resolving
696
+ // the backend or dispatching any operation. Without this, a help request is
697
+ // forwarded to the Kernel as an operation arg: the plural path fails with a bare
698
+ // "Command failed" and the singular path SILENTLY mints a junk issue (and could
699
+ // queue a GitHub projection). Help must never touch a backend.
710
700
  if (normalizeArgs(args).some(arg => arg === '--help' || arg === '-h')) {
711
701
  return { success: true, output: `${spec.usage}\n\n${spec.description}` };
712
702
  }
@@ -749,17 +739,16 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
749
739
  }
750
740
  }
751
741
 
752
- // Both backends are reached through the same runIssueOperation seam. Naming the
753
- // injected local `runIssueOperation` keeps the dispatch a literal call to a binding
754
- // named `runIssueOperation` (the kernel-evidence gate is syntactic) while still
755
- // honoring an injected runner; the `kernelBroker: opts.kernelBroker` passthrough is
756
- // a runtime no-op (undefined under Beads) that documents the Kernel-capable surface.
742
+ // The Kernel is reached through the runIssueOperation seam. Naming the injected
743
+ // local `runIssueOperation` keeps the dispatch a literal call to a binding named
744
+ // `runIssueOperation` (the kernel-evidence gate is syntactic) while still honoring
745
+ // an injected runner.
757
746
  const runIssueOperation = opts.runIssueOperation || defaultRunIssueOperation;
758
747
  const operation = resolveIssueOperation(subcommand, args);
759
748
  const operationArgs = resolveOperationArgs(subcommand, args, opts);
760
749
 
761
- // Check-after-write (gate.issue_verify): resolved ONCE per invocation, kernel
762
- // path only. Reads and the Beads path never trigger a read-back.
750
+ // Check-after-write (gate.issue_verify): resolved ONCE per invocation. Reads never
751
+ // trigger a read-back.
763
752
  const verifyEnabled = VERIFIED_SUBCOMMANDS.has(subcommand)
764
753
  && shouldUseKernelBroker(opts)
765
754
  && isIssueVerifyEnabled(projectRoot, opts);
@@ -779,7 +768,7 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
779
768
  projectRoot,
780
769
  { ...opts, kernelBroker: opts.kernelBroker },
781
770
  );
782
- // Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a Beads
771
+ // Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a legacy
783
772
  // {success,output} shape). Warn-only: attaches verified/mismatches, never
784
773
  // changes the result's success or exit code.
785
774
  if (verifyEnabled && result && typeof result === 'object' && result.ok === true && result.success === undefined) {
@@ -787,10 +776,6 @@ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
787
776
  }
788
777
  // Best-effort, non-blocking: mirror a stage-transition comment into stage_runs.
789
778
  recordStageTransitionFromComment(subcommand, operationArgs, result, opts);
790
- // Best-effort, non-blocking: nudge a returning 0.0.10 user whose empty Kernel
791
- // read hides an unmigrated legacy issue store (kernel issue a5399f3d). The hint
792
- // text lives in lib/beads-nudge.js so this hot path stays token-free.
793
- maybeWarnUnmigratedBeads(subcommand, result, projectRoot, rawOpts);
794
779
  // Grounding (gate.read_first): a successful `forge show <id>` counts as reading
795
780
  // the issue, so append a `context.loaded` event. Best-effort and awaited (a
796
781
  // fire-and-forget append could lose the event when the CLI process exits); a
@@ -26,7 +26,6 @@ const commands = [
26
26
  { file: "add.js", module: require("./add") },
27
27
  { file: "audit.js", module: require("./audit") },
28
28
  { file: "blocked.js", module: require("./blocked") },
29
- { file: "board.js", module: require("./board") },
30
29
  { file: "claim.js", module: require("./claim") },
31
30
  { file: "claims.js", module: require("./claims") },
32
31
  { file: "clean.js", module: require("./clean") },
@@ -73,6 +72,7 @@ const commands = [
73
72
  { file: "shepherd.js", module: require("./shepherd") },
74
73
  { file: "ship.js", module: require("./ship") },
75
74
  { file: "show.js", module: require("./show") },
75
+ { file: "skill.js", module: require("./skill") },
76
76
  { file: "stage.js", module: require("./stage") },
77
77
  { file: "stale.js", module: require("./stale") },
78
78
  { file: "status.js", module: require("./status") },