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/docs/guides/SETUP.md
CHANGED
|
@@ -12,9 +12,12 @@ This guide covers supported Forge adoption paths. Use [Quickstart](../../QUICKST
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add -D forge-workflow
|
|
15
|
+
bun add -D forge-workflow@beta
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
> The current release is a prerelease under the `beta` dist-tag — install with
|
|
19
|
+
> `@beta`. A bare `forge-workflow` resolves to the older stable `latest`.
|
|
20
|
+
|
|
18
21
|
The package exposes `forge`, `forge-workflow`, and `forge-preflight`.
|
|
19
22
|
|
|
20
23
|
`install.sh` is a thin bootstrapper. It installs or invokes `forge-workflow` and delegates setup to the package; it is not a separate implementation of setup behavior.
|
package/docs/guides/SUPPORT.md
CHANGED
|
@@ -108,6 +108,11 @@ Create isolated work:
|
|
|
108
108
|
forge worktree create <slug> --branch <branch-name>
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
The new branch is forked from the repository's default branch (`origin/<default>`
|
|
112
|
+
when present, else the local default), not the current checkout — so a worktree
|
|
113
|
+
made from a WIP branch does not inherit unrelated commits. Override with
|
|
114
|
+
`--base <ref>` to fork from a specific ref; `create` prints the base it used.
|
|
115
|
+
|
|
111
116
|
Remove it:
|
|
112
117
|
|
|
113
118
|
```bash
|
|
@@ -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.
|
|
@@ -105,12 +107,21 @@ forge sync
|
|
|
105
107
|
|
|
106
108
|
```bash
|
|
107
109
|
forge worktree create <slug> --branch <branch-name>
|
|
110
|
+
forge worktree create <slug> --base <ref>
|
|
108
111
|
forge worktree remove <slug>
|
|
109
112
|
forge clean --dry-run
|
|
110
113
|
```
|
|
111
114
|
|
|
112
115
|
Slugs must not contain `..`, `/`, or `\`.
|
|
113
116
|
|
|
117
|
+
A new worktree's branch is forked from the repository's **default branch**
|
|
118
|
+
(`origin/<default>` when the remote ref exists, else the local default) — **not**
|
|
119
|
+
the checkout's current branch/HEAD — so a worktree created from a WIP branch never
|
|
120
|
+
silently inherits unrelated commits. Pass `--base <ref>` to fork from a specific
|
|
121
|
+
ref instead; an invalid `--base` errors and creates nothing. `create` prints the
|
|
122
|
+
base it used (e.g. `Created worktree <path> on <branch> (based on origin/main).`)
|
|
123
|
+
so the fork point is never silent.
|
|
124
|
+
|
|
114
125
|
## Adapters
|
|
115
126
|
|
|
116
127
|
```bash
|
|
@@ -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
|
|
@@ -67,6 +102,43 @@ up from there.
|
|
|
67
102
|
a `/loop`) that re-invokes the bounded pass with a debounce of at least 60
|
|
68
103
|
seconds and cancel-in-progress. The shepherd itself never waits in-process.
|
|
69
104
|
|
|
105
|
+
## Auto-start on ship (`rail.auto_shepherd`)
|
|
106
|
+
|
|
107
|
+
`forge shepherd watch <pr>` is the constant, self-stopping local monitor loop
|
|
108
|
+
(≈60 s jittered cadence; appends events to the per-PR NDJSON journal under
|
|
109
|
+
`.forge/pr-monitor/<repo>-<pr>/`; self-stops on `PR_MERGED`/`PR_CLOSED`). On a
|
|
110
|
+
successful `forge ship`, the new PR's watcher is **auto-started detached** so a
|
|
111
|
+
shipped PR is tended without a manual trigger. The spawn is best-effort and
|
|
112
|
+
**never fails ship** (a spawn or config-read error degrades to "not started"),
|
|
113
|
+
and it is idempotent — the watch-lifecycle PID/journal lock prevents a second
|
|
114
|
+
watcher for the same PR.
|
|
115
|
+
|
|
116
|
+
This auto-start is governed by the default-ON, unlocked **`rail.auto_shepherd`**
|
|
117
|
+
rail. Opt out with `forge gate disable rail.auto_shepherd` (re-enable with
|
|
118
|
+
`forge gate enable rail.auto_shepherd`); when disabled, `forge ship` skips the
|
|
119
|
+
auto-start. This keeps the behavior honestly toggleable through the same config
|
|
120
|
+
surface as every other rail.
|
|
121
|
+
|
|
122
|
+
## Surfacing events back to the agent (`forge hooks shepherd-events`)
|
|
123
|
+
|
|
124
|
+
The constant watch loop is the PRODUCER: it writes per-PR NDJSON journals under
|
|
125
|
+
`.forge/pr-monitor/<repo>-<pr>/`, while the `forge shepherd events <pr> --since
|
|
126
|
+
<seq>` pull surface reads existing records back from them. But a journal only
|
|
127
|
+
helps if the working agent sees it. `forge hooks shepherd-events` is the thin,
|
|
128
|
+
agent-agnostic CONSUMER: it reads the NEW budget events across all open-PR journals
|
|
129
|
+
since a persisted per-PR **consumer cursor** (kept in `consumer.cursor`, distinct
|
|
130
|
+
from the watcher's snapshot), renders a **compact, capped** summary of the
|
|
131
|
+
actionable transitions only — verdict changes, failed checks, new review threads,
|
|
132
|
+
merged/closed — then advances the cursor so nothing re-surfaces.
|
|
133
|
+
|
|
134
|
+
For Claude Code this is wired as a **UserPromptSubmit** context hook (the honest
|
|
135
|
+
capability matrix: only Claude exposes that additionalContext surface; Cursor /
|
|
136
|
+
Codex / Hermes carry an explicit skip reason). It is **additive and FAIL-OPEN** —
|
|
137
|
+
a missing/empty digest, a corrupt journal, or no `.forge/pr-monitor` at all never
|
|
138
|
+
blocks a prompt — and it reads the user's own local journal only: it never
|
|
139
|
+
injects into stdin and never drives the agent. Any other harness can call the
|
|
140
|
+
same verb (or `forge shepherd events`) on its own cadence.
|
|
141
|
+
|
|
70
142
|
## Terminal states
|
|
71
143
|
|
|
72
144
|
| State | Meaning |
|
|
@@ -102,6 +174,42 @@ seconds and cancel-in-progress. The shepherd itself never waits in-process.
|
|
|
102
174
|
- **Auth taxonomy.** 401 (expiry) pauses and surfaces; 403 insufficient-scope is
|
|
103
175
|
a hard-stop; 403 with `Retry-After` honors the delay and resumes next pass.
|
|
104
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
|
+
|
|
105
213
|
## Per-harness behavior
|
|
106
214
|
|
|
107
215
|
- **Claude Code / Codex:** invoke `forge shepherd <pr>` directly; an external
|
|
@@ -111,5 +219,8 @@ seconds and cancel-in-progress. The shepherd itself never waits in-process.
|
|
|
111
219
|
|
|
112
220
|
## State
|
|
113
221
|
|
|
114
|
-
Progress is durable in GitHub PR comments and labels plus `git`.
|
|
115
|
-
|
|
222
|
+
Progress is durable in GitHub PR comments and labels plus `git`. The one local
|
|
223
|
+
store is the constant monitor's per-PR journal under
|
|
224
|
+
`.forge/pr-monitor/<repo>-<pr>/` (the append-only `events.ndjson` + snapshot and
|
|
225
|
+
consumer cursors) — the delivery/replay surface for `forge shepherd watch` and
|
|
226
|
+
`events --since`. The bounded shepherd pass itself keeps no separate local state.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy `.forge/` home creation (activation foundation).
|
|
5
|
+
*
|
|
6
|
+
* Discovery ≠ initialization. The global Forge plugin (SessionStart hook +
|
|
7
|
+
* `activation` skill) must create NOTHING in a user's repo. The FIRST verb that
|
|
8
|
+
* performs a real MUTATION (e.g. `forge claim`, `forge create`, `forge
|
|
9
|
+
* remember`) is what lazily materializes the bare-minimum `.forge/` skeleton.
|
|
10
|
+
* Read-only verbs (`ready`, `show`, `status`, `recap`, …) never call this, so a
|
|
11
|
+
* bare repo stays untouched until the user actually changes state.
|
|
12
|
+
*
|
|
13
|
+
* "Bare-minimum" here is strictly LESS than `forge init --minimal`: this writes
|
|
14
|
+
* ONLY `.forge/config.yaml` with every gate disabled (the `minimal` adoption
|
|
15
|
+
* profile). It installs NO git hooks, NO lefthook.yml, NO protected-paths
|
|
16
|
+
* manifest, NO `.mcp.json`, and NO scripts tree — those remain the opt-in
|
|
17
|
+
* payload of `forge setup`, never forced. Progressive growth: heavier slices
|
|
18
|
+
* initialize themselves on first use.
|
|
19
|
+
*
|
|
20
|
+
* @module activation/ensure-forge-home
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const { renderAdoptionConfigYaml } = require('../adoption-profiles');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Core verbs that MUTATE project state and therefore need `.forge/` to exist.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately excludes `init`/`setup` — they own `.forge/` creation with their
|
|
32
|
+
* own (richer) logic, and pre-creating a minimal config would trip their
|
|
33
|
+
* no-clobber guard. Also excludes every read-only verb, so those write nothing.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately EXCLUDES `gate` and `stage`: each has read-only subcommands
|
|
36
|
+
* (`gate status`/`gate check`, `stage --list`/`--current`) that must not create
|
|
37
|
+
* `.forge/` — a verb-level trigger would violate the foundation's own
|
|
38
|
+
* "read-only writes nothing" invariant. Their genuinely-mutating forms
|
|
39
|
+
* self-manage without ensureForgeHome: `gate enable|disable` and `role` write
|
|
40
|
+
* via the config writer (which creates `.forge/config.yaml` if absent), while
|
|
41
|
+
* `gate approve|reject` and `stage --start|--complete` write kernel events/runs
|
|
42
|
+
* through the broker (which lazily creates its own store). Subcommand-level
|
|
43
|
+
* granularity is deferred; excluding the whole verb is the correct, safe default
|
|
44
|
+
* because the read-only forms are the common case. `role` is retained: it has NO
|
|
45
|
+
* read-only form (every valid invocation writes config), so it violates nothing.
|
|
46
|
+
*
|
|
47
|
+
* A command module may override membership by exporting `mutating: true|false`;
|
|
48
|
+
* this Set is the default classification for the foundation.
|
|
49
|
+
*/
|
|
50
|
+
const MUTATING_VERBS = new Set([
|
|
51
|
+
'claim',
|
|
52
|
+
'close',
|
|
53
|
+
'create',
|
|
54
|
+
'comment',
|
|
55
|
+
'update',
|
|
56
|
+
'add',
|
|
57
|
+
'new',
|
|
58
|
+
'remember',
|
|
59
|
+
'role',
|
|
60
|
+
'patch',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether a verb should lazily ensure the `.forge/` home.
|
|
65
|
+
*
|
|
66
|
+
* A command module's explicit `mutating` boolean wins; otherwise fall back to
|
|
67
|
+
* the default {@link MUTATING_VERBS} classification.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} name - Command/verb name being dispatched.
|
|
70
|
+
* @param {object} [command] - The resolved command module (may declare `mutating`).
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function isMutatingVerb(name, command) {
|
|
74
|
+
if (command && typeof command === 'object' && typeof command.mutating === 'boolean') {
|
|
75
|
+
return command.mutating;
|
|
76
|
+
}
|
|
77
|
+
return MUTATING_VERBS.has(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the bare-minimum, gates-disabled config body.
|
|
82
|
+
*
|
|
83
|
+
* Reuses the canonical `minimal` adoption profile so the produced YAML is
|
|
84
|
+
* schema-valid and identical in spirit to `forge init --minimal` (minus the
|
|
85
|
+
* hooks/protected-paths side effects). Overridable via `deps.renderConfig` for
|
|
86
|
+
* tests that don't want to depend on the profile renderer.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} [deps]
|
|
89
|
+
* @returns {string} config.yaml contents
|
|
90
|
+
*/
|
|
91
|
+
function renderMinimalConfig(deps = {}) {
|
|
92
|
+
const render = deps.renderConfig || (() => renderAdoptionConfigYaml('minimal'));
|
|
93
|
+
return render();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Idempotently create the bare-minimum `.forge/` skeleton for a mutating verb.
|
|
98
|
+
*
|
|
99
|
+
* No-clobber by construction: if `.forge/` already exists (inited repo, or a
|
|
100
|
+
* partially-created home), this is a NO-OP and never touches the user's files.
|
|
101
|
+
* The kernel/issue store (`.forge/kernel/…`) is created lazily by the broker on
|
|
102
|
+
* the mutating verb itself — this function only guarantees the config skeleton.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} [projectRoot=process.cwd()] - Repo root to initialize.
|
|
105
|
+
* @param {object} [deps] - Injectable seams: `fs`, `renderConfig`.
|
|
106
|
+
* @returns {{ created: boolean, reason?: string, configPath: string }}
|
|
107
|
+
*/
|
|
108
|
+
function ensureForgeHome(projectRoot = process.cwd(), deps = {}) {
|
|
109
|
+
const fsImpl = deps.fs || fs;
|
|
110
|
+
const forgeDir = path.join(projectRoot, '.forge');
|
|
111
|
+
const configPath = path.join(forgeDir, 'config.yaml');
|
|
112
|
+
|
|
113
|
+
// Idempotent + self-healing. The presence of `config.yaml` — NOT the `.forge/`
|
|
114
|
+
// directory alone — is what marks the home as initialized. Keying the
|
|
115
|
+
// no-clobber check on the config FILE means a half-init (dir created but config
|
|
116
|
+
// never written: disk full, permission error, or the process killed between
|
|
117
|
+
// mkdir and write) is COMPLETED on the next call instead of being permanently
|
|
118
|
+
// stuck behind a dir-exists early-return. We only ever ADD a missing
|
|
119
|
+
// config.yaml and never overwrite an existing one, so a real inited repo is
|
|
120
|
+
// still never clobbered. `mkdirSync({recursive})` is a no-op if the dir exists.
|
|
121
|
+
if (fsImpl.existsSync(configPath)) {
|
|
122
|
+
return { created: false, reason: 'config-exists', configPath };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fsImpl.mkdirSync(forgeDir, { recursive: true });
|
|
126
|
+
fsImpl.writeFileSync(configPath, renderMinimalConfig(deps), 'utf8');
|
|
127
|
+
return { created: true, configPath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
ensureForgeHome,
|
|
132
|
+
isMutatingVerb,
|
|
133
|
+
renderMinimalConfig,
|
|
134
|
+
MUTATING_VERBS,
|
|
135
|
+
};
|
|
@@ -95,11 +95,60 @@ function readBeadsSidecar(beadsDir, file) {
|
|
|
95
95
|
return found ? parseJsonl(fs.readFileSync(found, 'utf8'), path.basename(found)) : [];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// The *.jsonl sidecars this migrator knows how to map into the Kernel. Any other *.jsonl
|
|
99
|
+
// present in the store is unmapped data; the loader records it so the import can report it
|
|
100
|
+
// as an honest gap rather than dropping it silently.
|
|
101
|
+
const HANDLED_SIDECARS = new Set([
|
|
102
|
+
'issues.jsonl',
|
|
103
|
+
'comments.jsonl',
|
|
104
|
+
'dependencies.jsonl',
|
|
105
|
+
'events.jsonl',
|
|
106
|
+
'interactions.jsonl',
|
|
107
|
+
'labels.jsonl',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// Scan the same locations readBeadsSidecar reads (the dir itself, a `backup/` subdir, and the
|
|
111
|
+
// parent) for *.jsonl files this migrator does NOT map. Returns their sorted, de-duplicated
|
|
112
|
+
// basenames (`unmigrated`) so importBeadsSnapshot can surface each as a `sidecar.<name>`
|
|
113
|
+
// field-gap — closing the "an unknown/config sidecar was dropped without a word" data-loss
|
|
114
|
+
// class — plus any `scanErrors`: a candidate dir ABSENT from disk (ENOENT) is expected and
|
|
115
|
+
// swallowed, but a REAL scan failure (EACCES/ENOTDIR/…) could hide sidecars silently, so it is
|
|
116
|
+
// reported too rather than masquerading as "no sidecars".
|
|
117
|
+
function detectUnmigratedSidecars(beadsDir) {
|
|
118
|
+
const dirs = [beadsDir, path.join(beadsDir, 'backup'), path.dirname(beadsDir)];
|
|
119
|
+
const unmigrated = new Set();
|
|
120
|
+
const scanErrors = [];
|
|
121
|
+
for (const dir of dirs) {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(dir);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// A missing candidate dir is the normal case (not every layout has a backup/ subdir);
|
|
127
|
+
// only ENOENT is safe to swallow. Any other error is a real scan failure that may hide
|
|
128
|
+
// sidecars — record it so importBeadsSnapshot can surface an honest gap.
|
|
129
|
+
if (err && err.code !== 'ENOENT') {
|
|
130
|
+
scanErrors.push({ dir, code: err.code || 'EUNKNOWN' });
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry.endsWith('.jsonl') && !HANDLED_SIDECARS.has(entry)) {
|
|
136
|
+
unmigrated.add(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
unmigrated: [...unmigrated].sort((a, b) => a.localeCompare(b)),
|
|
142
|
+
scanErrors,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
98
146
|
function loadBeadsSnapshotFromDirectory(beadsDir) {
|
|
99
147
|
if (!beadsDir || typeof beadsDir !== 'string') {
|
|
100
148
|
throw new TypeError('beadsDir must be a directory path');
|
|
101
149
|
}
|
|
102
150
|
|
|
151
|
+
const { unmigrated, scanErrors } = detectUnmigratedSidecars(beadsDir);
|
|
103
152
|
return {
|
|
104
153
|
beadsDir,
|
|
105
154
|
issues: readBeadsSidecar(beadsDir, 'issues.jsonl'),
|
|
@@ -108,6 +157,8 @@ function loadBeadsSnapshotFromDirectory(beadsDir) {
|
|
|
108
157
|
events: readBeadsSidecar(beadsDir, 'events.jsonl'),
|
|
109
158
|
interactions: readBeadsSidecar(beadsDir, 'interactions.jsonl'),
|
|
110
159
|
labels: readBeadsSidecar(beadsDir, 'labels.jsonl'),
|
|
160
|
+
unmigratedSidecars: unmigrated,
|
|
161
|
+
sidecarScanErrors: scanErrors,
|
|
111
162
|
};
|
|
112
163
|
}
|
|
113
164
|
|
|
@@ -725,6 +776,22 @@ function importBeadsSnapshot(snapshot = {}, options = {}) {
|
|
|
725
776
|
...(Array.isArray(snapshot.events) ? snapshot.events : []).map(event => mapBeadsActivityEventToKernel(event, importedAt)),
|
|
726
777
|
...(Array.isArray(snapshot.interactions) ? snapshot.interactions : []).map(interaction => mapBeadsInteractionToKernel(interaction, importedAt)),
|
|
727
778
|
];
|
|
779
|
+
// Honest field-gap report: any *.jsonl sidecar present in the store that this migrator does
|
|
780
|
+
// not map (e.g. config.jsonl, or a future/unknown sidecar) is surfaced here rather than
|
|
781
|
+
// silently dropped. The loader records these on `unmigratedSidecars`.
|
|
782
|
+
for (const sidecar of Array.isArray(snapshot.unmigratedSidecars) ? snapshot.unmigratedSidecars : []) {
|
|
783
|
+
addGap(gaps, seenGaps, `sidecar.${sidecar}`, 'present in the Beads store but has no Kernel target — not migrated');
|
|
784
|
+
}
|
|
785
|
+
// A directory the sidecar scan could not read (EACCES/ENOTDIR/…, but NOT a merely-absent dir)
|
|
786
|
+
// might hide sidecars; surface it so the loss is never silent.
|
|
787
|
+
for (const scanError of Array.isArray(snapshot.sidecarScanErrors) ? snapshot.sidecarScanErrors : []) {
|
|
788
|
+
addGap(
|
|
789
|
+
gaps,
|
|
790
|
+
seenGaps,
|
|
791
|
+
`sidecar-scan.${path.basename(scanError.dir)}`,
|
|
792
|
+
`could not scan ${scanError.dir} for unmapped sidecars (${scanError.code}) — any sidecars there were not migrated`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
728
795
|
const kernel = {
|
|
729
796
|
issues: sourceIssues.map(issue => mapBeadsIssueToKernel(issue, importedAt, gaps, seenGaps)),
|
|
730
797
|
dependencies: collectDependencies(normalizedSnapshot)
|
|
@@ -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/adoption-profiles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const YAML = require('yaml');
|
|
2
2
|
|
|
3
|
-
const ADOPTION_VERSION = '0.0.
|
|
3
|
+
const ADOPTION_VERSION = '0.0.16';
|
|
4
4
|
const RUNTIME_ANCESTRY = 'forge.runtimeGraph.currentCommandFlow@0.0.17';
|
|
5
5
|
const WORKFLOW_PHASES = Object.freeze(['plan', 'dev', 'validate', 'ship']);
|
|
6
6
|
const WORKFLOW_GATES = Object.freeze(['gate.plan-exit', 'gate.dev-exit', 'gate.validate-exit', 'gate.ship-entry']);
|
|
@@ -32,12 +32,18 @@ function harnessAdapter(targets) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function adoptionConfig({ gatesEnabled, issueEnabled, harnessTargets, protectedPaths, rails }) {
|
|
35
|
+
function adoptionConfig({ gatesEnabled, issueEnabled, harnessTargets, protectedPaths, rails, disabledRails = [] }) {
|
|
36
|
+
// Disabled rails are written through the same `workflow.gates.<rail-id>.enabled`
|
|
37
|
+
// toggle surface `forge gate disable` uses, so the resolver + the installed hooks
|
|
38
|
+
// read one consistent signal. Only unlocked rails (e.g. rail.tdd_intent) may appear.
|
|
39
|
+
const railGateOverrides = Object.fromEntries(
|
|
40
|
+
disabledRails.map(railId => [railId, { enabled: false }]),
|
|
41
|
+
);
|
|
36
42
|
return {
|
|
37
43
|
...(rails ? { rails } : {}),
|
|
38
44
|
workflow: {
|
|
39
45
|
phases: enabledEntries(WORKFLOW_PHASES),
|
|
40
|
-
gates: enabledEntries(WORKFLOW_GATES, gatesEnabled),
|
|
46
|
+
gates: { ...enabledEntries(WORKFLOW_GATES, gatesEnabled), ...railGateOverrides },
|
|
41
47
|
},
|
|
42
48
|
adapters: {
|
|
43
49
|
issue: issueAdapter(issueEnabled),
|
|
@@ -54,7 +60,14 @@ const PROFILE_CONFIGS = Object.freeze({
|
|
|
54
60
|
gatesEnabled: false,
|
|
55
61
|
issueEnabled: false,
|
|
56
62
|
harnessTargets: [],
|
|
57
|
-
|
|
63
|
+
// Minimal = zero active enforcement, so there is nothing left to guard: with the
|
|
64
|
+
// gates disabled and the TDD rail off, the protected-path guard has no enforcement
|
|
65
|
+
// surface to protect. Leaving it empty keeps `init --minimal` fully inert instead of
|
|
66
|
+
// contradicting itself by still guarding config.yaml.
|
|
67
|
+
protectedPaths: [],
|
|
68
|
+
// Minimal = no active enforcement: the TDD pre-commit rail is off (it is a
|
|
69
|
+
// strong default, not a hard floor), so `init --minimal` installs inert hooks.
|
|
70
|
+
disabledRails: ['rail.tdd_intent'],
|
|
58
71
|
}),
|
|
59
72
|
},
|
|
60
73
|
standard: {
|
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,
|