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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -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.
@@ -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 <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
@@ -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`. There is no
115
- separate local state store.
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 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
  *
@@ -1,6 +1,6 @@
1
1
  const YAML = require('yaml');
2
2
 
3
- const ADOPTION_VERSION = '0.0.15';
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
- protectedPaths: ['.forge/config.yaml'],
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: {
@@ -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,