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.
- package/AGENTS.md +1 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +5 -0
- package/docs/reference/COMMANDS.md +3 -1
- package/docs/reference/shepherd.md +72 -1
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/agents-config.js +5 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/hooks.js +155 -8
- package/lib/commands/plan.js +23 -115
- package/lib/commands/prime.js +8 -1
- package/lib/commands/release.js +1 -2
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +0 -1
- package/lib/commands/shepherd.js +36 -3
- 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 +27 -1
- package/lib/core/runtime-graph.js +88 -14
- package/lib/forge-issues.js +13 -464
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +54 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +150 -0
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- 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 +14 -0
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +1 -1
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +70 -15
- package/lib/project-memory.js +8 -0
- 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 +8 -9
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +1 -1
- 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/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/beads-nudge.js +0 -91
- 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/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>` —
|
|
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 +
|
|
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
|
|
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
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
*
|
|
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) —
|
|
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
|
*
|
package/lib/agents-config.js
CHANGED
|
@@ -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,
|
package/lib/commands/_issue.js
CHANGED
|
@@ -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
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// issue
|
|
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
|
|
71
|
-
//
|
|
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)
|
|
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
|
|
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.
|
|
404
|
-
//
|
|
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
|
-
//
|
|
458
|
-
// shared runIssueOperation deps see it. OPT-IN ONLY: opts is left
|
|
459
|
-
// when no explicit signal is present (env/config/explicit),
|
|
460
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
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
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
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
|
|
762
|
-
//
|
|
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
|
|
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") },
|