forge-orkes 0.62.0 → 0.64.0

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 (56) hide show
  1. package/bin/create-forge.js +250 -99
  2. package/experimental/conventions/README.md +87 -0
  3. package/experimental/conventions/install.sh +179 -0
  4. package/experimental/conventions/source/rules/code-quality.md +17 -0
  5. package/experimental/conventions/source/rules/laravel-http.md +21 -0
  6. package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
  7. package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
  8. package/experimental/conventions/source/rules/laravel-services.md +21 -0
  9. package/experimental/conventions/source/rules/python.md +14 -0
  10. package/experimental/conventions/source/rules/testing.md +15 -0
  11. package/experimental/conventions/source/rules/vue-inertia.md +24 -0
  12. package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
  13. package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
  14. package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
  15. package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
  16. package/experimental/conventions/uninstall.sh +97 -0
  17. package/experimental/m10/README.md +130 -0
  18. package/experimental/m10/install.sh +198 -0
  19. package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
  20. package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
  21. package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
  22. package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
  23. package/experimental/m10/source/mcp-server/README.md +74 -0
  24. package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
  25. package/experimental/m10/source/mcp-server/index.js +460 -0
  26. package/experimental/m10/source/mcp-server/package.json +17 -0
  27. package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
  28. package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
  29. package/experimental/m10/uninstall.sh +69 -0
  30. package/package.json +3 -2
  31. package/template/.claude/hooks/forge-release-fold.sh +9 -1
  32. package/template/.claude/settings.json +1 -7
  33. package/template/.claude/skills/jarvis/SKILL.md +204 -0
  34. package/template/.claude/skills/upgrading/SKILL.md +105 -176
  35. package/template/.forge/FORGE.md +2 -2
  36. package/template/.forge/checks/forge-board-render.sh +11 -5
  37. package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
  38. package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
  39. package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
  40. package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
  41. package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
  42. package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
  43. package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
  44. package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
  45. package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
  46. package/template/.forge/checks/forge-jarvis.sh +160 -0
  47. package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
  48. package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
  49. package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
  50. package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
  51. package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
  52. package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
  53. package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
  54. package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
  55. package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
  56. package/template/.forge/templates/project.yml +11 -0
@@ -0,0 +1,223 @@
1
+ ---
2
+ name: orchestrating
3
+ description: "[Experimental — M10] Optional backend for Forge stream isolation. Bootstrap worktrees, coordinate merge queue, and optionally use MCP file claims. Prefer Chief/Streams as the user-facing orchestration model."
4
+ ---
5
+
6
+ # Orchestrating
7
+
8
+ Multi-agent session lifecycle substrate. Worktree isolation + merge queue remain
9
+ useful backend mechanics for Forge Streams. MCP/SQLite file claims are optional
10
+ and experimental defense-in-depth, not the primary coordination model.
11
+
12
+ ## M16 Positioning
13
+
14
+ Chief/Streams is the preferred user-facing orchestration model. The Project
15
+ Chief decides when a stream needs M10 mechanics such as worktree isolation,
16
+ branch anchoring, merge queue discipline, or claim checks. Users should normally
17
+ ask to start, pause, delegate, conflict-check, merge, or close streams; they
18
+ should not have to invoke M10 directly.
19
+
20
+ M10 still matters when concurrent streams need hard workspace separation or a
21
+ controlled merge path. It is a backend/substrate, not the project manager.
22
+
23
+ ## When to use
24
+
25
+ - The Project Chief selects M10 for a stream that needs worktree isolation,
26
+ merge-queue support, or optional file-claim checks.
27
+ - User explicitly invokes multi-agent mode (`/forge` argument selects multi-agent, or direct skill invocation).
28
+ - Legacy installs may route Full tier work through this skill when
29
+ `orchestration.auto` remains enabled.
30
+
31
+ Skip on Quick tier, single-phase work, or when bootstrap checks fail.
32
+
33
+ ## Enforcement scope — what claims actually protect
34
+
35
+ Claim coordination (`forge_claim_files` + the PreToolUse gate) works for **one** mode only: a **single parent session** operating across a Forge-managed sibling worktree dir (default `../<repo>-worktrees/{anchor}`, configurable via `orchestration.worktree_root`), where main's hooks + `.session-id` apply. **It does NOT reach `Agent isolation:"worktree"` sub-agents** — those run in `.claude/worktrees/agent-*` with no hook config (`.claude` is gitignored), so they fire no SessionStart, claim under nothing, and their edits are **not gated**. For within-milestone fan-out, **worktree isolation + disjoint files behind a frozen contract is the safety — not claims.** Never assume a fan-out sub-agent is claim-protected; it is not. (See ADR-007.)
36
+
37
+ ## Step 1: Bootstrap
38
+
39
+ Run all checks in `bootstrap-checks.md`:
40
+
41
+ 1. Git version ≥ 2.48
42
+ 2. LFS version ≥ 3.6 (skip if not installed)
43
+ 3. No divergent submodules — clean pinned submodules pass (auto-inited per worktree in Step 2); refuse only on `-`/`+`/`U` entries (see `bootstrap-checks.md` "Submodule handling")
44
+ 4. `core.hooksPath` empty or resolves inside worktree
45
+ 5. `git hook run pre-commit` smoke test in fresh worktree
46
+ 6. **Enforcement canary (advisory)** — `bash .claude/hooks/forge-claim-check-doctor.sh` reports `gate enforces`. On `GATE IS INERT`, warn loudly that claims provide no protection this session (worktree isolation still applies); do **not** refuse on this one — it never blocks, only surfaces.
47
+
48
+ **Any check 1–5 fails** → log reason, write `lifecycle.worktree_mode: refused` + `lifecycle.refused_reason` into active milestone state, emit fallback message (see `bootstrap-checks.md`), return to caller. Caller continues single-agent. Check 6 is advisory — it warns, never refuses.
49
+
50
+ ## Step 2: Session ID + worktree
51
+
52
+ ```bash
53
+ session_id=$(uuidgen | cut -c1-8)
54
+ # Anchor the worktree name to the milestone id so the branch is self-describing:
55
+ # the `m-{id}` token lets `forge` resume re-derive the milestone from the branch
56
+ # name alone (its id-token fallback), not only from lifecycle.worktree_branch
57
+ # state — which a moved/redone worktree can leave stale. The uuid suffix keeps the
58
+ # name collision-resistant and unique per session; the `forge/` prefix keeps the GC
59
+ # sweep simple. Milestone ids are dash-free, so `m-{id}` tokenizes cleanly.
60
+ anchor="m-${milestone_id}-${session_id}"
61
+
62
+ # Worktree root — configurable, repo-scoped by default (ADR-010).
63
+ # Read `orchestration.worktree_root` from .forge/project.yml; if unset, default
64
+ # to a per-repo sibling `../<repo-basename>-worktrees`. Per-repo siblings keep
65
+ # each repo's worktrees in their own dir (no cross-repo mixing when multiple
66
+ # repos share a parent), and resolve deterministically against the repo root
67
+ # rather than the shell cwd. Relative roots resolve against repo root;
68
+ # absolute roots and a leading `~` are honored as-is.
69
+ repo_root=$(git rev-parse --show-toplevel)
70
+ wt_root=$(sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
71
+ | sed -n 's/^[[:space:]]*worktree_root:[[:space:]]*//p' | tr -d "\"'" | head -1)
72
+ wt_root="${wt_root:-../$(basename "$repo_root")-worktrees}"
73
+ case "$wt_root" in
74
+ /*) : ;; # absolute — use as-is
75
+ "~"*) wt_root="$HOME${wt_root#\~}" ;; # expand leading ~
76
+ *) wt_root="$repo_root/$wt_root" ;; # relative → resolve against repo root
77
+ esac
78
+ mkdir -p "$wt_root"
79
+
80
+ git worktree prune
81
+ git worktree add -b forge/${anchor} --lock --reason "forge session m-${milestone_id}" "$wt_root/${anchor}" main
82
+ wt="$wt_root/${anchor}"
83
+ # Submodules don't auto-populate in a new worktree — init them if present. Reuses the
84
+ # superproject's shared .git/modules object store (no re-clone, just a working-tree
85
+ # checkout); ccache absorbs the subsequent rebuild cost across worktrees.
86
+ if [ -n "$(git -C "$wt" submodule status 2>/dev/null)" ]; then
87
+ ( cd "$wt" && git submodule update --init --recursive )
88
+ fi
89
+ ( cd "$wt" && git hook run pre-commit || true )
90
+ ```
91
+
92
+ Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty. On failure → cleanup partial state, refuse mode.
93
+
94
+ `anchor` (= `m-{milestone_id}-{session_id}`) names the worktree dir **and** branch from here on; every reference below that used to be `{session_id}` is now `{anchor}`. `session_id` (the bare uuid) is retained only as the collision key + audit id in state.
95
+
96
+ The **absolute** path Forge resolved (`$wt_root/${anchor}`) is recorded in `lifecycle.worktree_path` (Step 3). Every later operation — Step 4 handback, Step 5 teardown, Step 6 crash-recovery re-attach — uses that recorded path verbatim, never re-deriving it. This decouples in-flight worktrees from the config: changing `orchestration.worktree_root` later (or moving a worktree on disk) does not invalidate them.
97
+
98
+ ## Step 3: State update
99
+
100
+ Write into `.forge/state/milestone-{id}.yml`:
101
+
102
+ ```yaml
103
+ lifecycle:
104
+ session_id: "{session_id}" # short uuid — collision key + audit id only (not a name, not a claim id)
105
+ worktree_anchor: "m-{id}-{session_id}" # milestone-anchored name of the worktree dir + branch
106
+ claim_session_id: "{claim_session_id}" # Claude session id — owner of all file claims (see Identity)
107
+ worktree_path: "{absolute path Forge resolved in Step 2 — $wt_root/m-{id}-{session_id}}"
108
+ worktree_branch: "forge/m-{id}-{session_id}"
109
+ worktree_mode: "active"
110
+ started_at: "{ISO8601}"
111
+ ```
112
+
113
+ Set `current.last_updated` in the milestone file. Do **not** write `state/index.yml` directly — it is a derived registry, regenerated by the `forge` rollup. (The orchestrator owns index.yml; worktree agents never touch it.)
114
+
115
+ ## Step 4: Hand back
116
+
117
+ Relocate the session into the worktree, then return control to caller (typically `executing`) — instead of only *assuming* the caller is already inside `lifecycle.worktree_path`. Apply the canonical **Native Worktree Entry** contract (identical to the forge ownership-gate option 1):
118
+
119
+ - **If the `EnterWorktree` tool is available**, call `EnterWorktree({path: {lifecycle.worktree_path}})` to relocate the session into the worktree before returning control, then log `Relocated into worktree {lifecycle.worktree_path} ({lifecycle.worktree_branch})` (independent of any prompt, so the cwd move is visible).
120
+ - **If `EnterWorktree` is unavailable** (older Claude Code CLI, or a non-Claude-Code adapter), fall back to instructing the caller to `cd {path}` (here `cd {lifecycle.worktree_path}`) before continuing — today's implicit assumption stated explicitly.
121
+ - Exit stays `ExitWorktree({action: "keep"})`; teardown remains Step 5 / `git worktree remove` — the native tool never removes the worktree.
122
+
123
+ All subsequent work runs inside `lifecycle.worktree_path`. Caller honors claim convention.
124
+
125
+ ### Claim convention (executing-skill contract)
126
+
127
+ State-file ownership: each session writes **only** its own `.forge/state/milestone-{own_id}.yml` and **appends** to `.forge/state/desire-paths/` (new files, never edited). It never writes `state/index.yml` — the registry is regenerated by rollup at teardown. Different milestones = different files, so these writes need no claim and never conflict across worktrees. Same-milestone parallelism is out of scope (one session per milestone — see the concurrent-invocation refusal in Edge Cases).
128
+
129
+ **Identity (critical — see ADR-007).** File claims are owned by the **Claude session id**, NOT the worktree uuid. The worktree uuid (`session_id` above) only feeds the worktree name (via `anchor`) and serves as the collision/audit key. Read the claim identity from the file the `forge-session-id.sh` SessionStart hook writes:
130
+
131
+ ```bash
132
+ claim_session_id=$(cat "${CLAUDE_PROJECT_DIR:-.}/.forge/.mcp-server/.session-id" 2>/dev/null)
133
+ ```
134
+
135
+ This is the same id the `forge-claim-check.sh` enforcement hook reads from PreToolUse stdin, so a session's own claims are recognized and other sessions' claims block. If the file is missing/empty the hook fail-opens (no enforcement) — surface that rather than proceeding silently in genuine multi-agent work.
136
+
137
+ Before any `Edit`, `Write`, `MultiEdit`, or `NotebookEdit` on a file outside `.forge/state/milestone-{own_id}.yml`:
138
+
139
+ 1. Call `forge_claim_files` with `{ session_id: claim_session_id, files: [...], ttl_seconds: 900 }`.
140
+ 2. On `granted` → proceed with edit.
141
+ 3. On `conflict: { holder_session, files: [...] }` → surface holder + files to user. Options:
142
+ - **wait** → poll `forge_claim_status` until released or TTL expiry.
143
+ - **skip** → drop the conflicted file from the task scope, continue with rest.
144
+ - **steal** → only if holder session is provably dead (PreToolUse hook validates). Otherwise refused.
145
+ 4. After edit batch → claim auto-extends on continued use; explicit `forge_release_claims` on plan-complete.
146
+
147
+ PreToolUse hook (installed by plan-03) enforces this — uncaught violations block at hook level, not skill level.
148
+
149
+ ## Step 5: Teardown
150
+
151
+ Triggered when caller signals work complete OR user requests teardown.
152
+
153
+ ### Step 5.0: Human Verification Gate (hard block — runs first)
154
+
155
+ Before any merge or worktree removal, read `current.human_verified` from `.forge/state/milestone-{id}.yml`.
156
+
157
+ - **Present** (`{at, method, ...}`, including a recorded `override`) → gate satisfied, proceed to the merge below.
158
+ - **Absent / null** → **REFUSE teardown.** Code-clean + FF-clean is **not** enough to close a milestone (FORGE.md → Human Verification Gate). Do **not** present teardown as an option, and never offer it as a peer alternative to the outstanding verification. Respond with what's still unverified and offer exactly two paths:
159
+ 1. **Verify now** → run the human verification (device session / visual / e2e walk). Have `verifying` (or this step) record `current.human_verified`, then re-invoke teardown.
160
+ 2. **Record override** → only on an explicit human instruction to close without verifying: write `current.human_verified: {at: "<ISO 8601>", method: "override", override: true, reason}` to the milestone file, surface it, then proceed.
161
+
162
+ Never merge + remove a worktree while `human_verified` is unset.
163
+
164
+ ```
165
+ forge_queue_commit(branch=forge/m-{id}-{session_id}, base_sha={merge_base})
166
+ ```
167
+
168
+ Branch on response status:
169
+
170
+ - **`merged`** (response has `new_main_sha`) → **reconcile the working tree first** — `forge_queue_commit` merges *ref-only* (`update-ref`), so the main checkout is now behind HEAD: `git checkout HEAD -- $(git diff --name-only {base_sha} {new_main_sha})` — surgical, merged files only; **never** `git checkout HEAD -- .` (would clobber unrelated WIP) → `forge_release_claims(claim_session_id)` (the Claude session id, not the worktree uuid) → `git worktree remove --force --force "$(yq '.lifecycle.worktree_path' .forge/state/milestone-{id}.yml)"` (double `--force`: Step 2 created the worktree `--lock`ed, and a single `--force` refuses to remove a locked worktree; read the path from state rather than re-deriving it so a moved/non-default worktree still removes cleanly) → `git branch -d forge/m-{id}-{session_id}` → clear `lifecycle.*` (set `worktree_mode: complete`, retain `session_id` for audit) → **State reconcile = Rollup:** regenerate `state/index.yml` from the now-merged `milestone-*.yml` files (`forge` Rollup 1.0) and commit it (`chore(forge): rollup state after teardown — {session_id}`). The merged milestone files are the source of truth; index.yml is never hand-merged.
171
+ - **`conflict`** → invoke `Skill(debugging)` with payload `{ conflicted_files, base_sha, messages, branch }`. Teardown blocks until debugging signals resolution (re-invoke teardown after fix).
172
+ - **`stale_base`** → caller rebases worktree branch onto `current_target_sha` from response, retries `forge_queue_commit`. Max 3 retries → escalate to conflict path.
173
+
174
+ ## Step 6: Crash recovery (next session start)
175
+
176
+ If no clean teardown happened previously:
177
+
178
+ 1. `git worktree prune` — drops stale admin dirs.
179
+ 2. `git branch --list 'forge/*'` — for each branch with no live worktree, prompt user. The branch suffix is the `anchor` (`m-{id}-{session_id}`); the milestone id is the `m-…` token, recoverable from the name alone even if state is stale:
180
+ - **resume** → re-attach at the path recorded in `lifecycle.worktree_path` (read from `state/milestone-{id}.yml` — the recorded absolute path survives a `worktree_root` config change after the fact). State missing the path → fall back to the current default (`<repo_root>/../<repo-basename>-worktrees/{anchor}`) and warn. `git worktree add <path> forge/{anchor}` and restore lifecycle state.
181
+ - **delete** → `git branch -D forge/{anchor}`.
182
+ 3. MCP server startup handles pidfile takeover + claim TTL expiry independently (see ADR-003).
183
+
184
+ ## Failure modes & operator notes
185
+
186
+ - **MCP server absent** — bootstrap check 5 (hook smoke) will not detect this. Skill detects on first `forge_claim_files` call: error `MCP_SERVER_UNAVAILABLE` → write `lifecycle.worktree_mode: degraded`, warn user, continue without claim coordination. Hard isolation (worktree) still active; coordination is downgraded to best-effort.
187
+ - **Disk full during worktree add** — `git worktree add` will error. Cleanup any partial `$wt_root/m-{id}-{session_id}/` dir, refuse mode, fall back.
188
+ - **Concurrent orchestrating invocations** — second invocation reads first's `lifecycle.session_id` in state. If present and `worktree_mode: active` → refuse (one orchestration per milestone). Use a separate milestone for parallel orchestrated work.
189
+ - **Worktree path collision** — UUIDv4 short (8 chars) collision negligible at <100 concurrent sessions. If `$wt_root/m-{id}-{session_id}/` already exists → regenerate session_id (the `anchor` recomputes with it), retry up to 3 times.
190
+ - **`worktree_root` changed mid-flight** — Step 3 records the resolved absolute `lifecycle.worktree_path`; teardown + crash-recovery read it from state, never re-derive. Editing `orchestration.worktree_root` in `project.yml` only affects newly created worktrees. Do not relocate live worktrees on disk by hand — `git worktree move` is the supported way; bare `mv` leaves the gitdir pointer stale.
191
+
192
+ ## Example: clean session
193
+
194
+ ```
195
+ user: /forge multi-agent
196
+ forge → orchestrating
197
+ orchestrating: bootstrap OK → session_id=a1b2c3d4 → anchor=m-9-a1b2c3d4 → worktree created at ../<repo>-worktrees/m-9-a1b2c3d4 → state written
198
+ orchestrating → executing (working dir: ../<repo>-worktrees/m-9-a1b2c3d4, branch: forge/m-9-a1b2c3d4)
199
+ executing: claim files → edit → commit (× N tasks)
200
+ executing → verifying (captures current.human_verified on human sign-off) → reviewing (all inside worktree)
201
+ reviewing → orchestrating (teardown)
202
+ orchestrating: Step 5.0 gate — current.human_verified present → forge_queue_commit → merged → release claims → remove worktree → delete branch
203
+ done.
204
+ ```
205
+
206
+ ## Example: conflict path
207
+
208
+ ```
209
+ orchestrating: forge_queue_commit → conflict { files: [src/auth.ts] }
210
+ orchestrating → debugging { conflicted_files, base_sha, branch }
211
+ debugging: user resolves → signal resolved
212
+ orchestrating: retry forge_queue_commit → merged → cleanup
213
+ ```
214
+
215
+ ## References
216
+
217
+ - ADR-001 — experimental track / opt-in carve-out
218
+ - ADR-002 — worktrees as isolation substrate
219
+ - ADR-003 — MCP server + per-repo SQLite
220
+ - ADR-004 — merge queue (forge_queue_commit status semantics)
221
+ - ADR-005 — session lifecycle (this skill realizes it)
222
+ - `.forge/research/milestone-10.md` — spike findings
223
+ - `bootstrap-checks.md` — bootstrap check matrix + fallback
@@ -0,0 +1,70 @@
1
+ # Bootstrap Checks
2
+
3
+ Run before worktree creation. Any failure → refuse worktree mode, fall back to single-agent.
4
+
5
+ | Check | Command | Pass criterion | Fail action | Reason |
6
+ |-------|---------|----------------|-------------|--------|
7
+ | Git version | `git --version` | major.minor ≥ 2.48 | refuse worktree mode | Known worktree bugs < 2.48 (admin-dir leaks, prune races) |
8
+ | LFS version | `git lfs version` (skip if not installed) | ≥ 3.6 OR not installed | refuse worktree mode | Known worktree-locking bugs < 3.6 |
9
+ | Submodule scan | `git submodule status` | empty OR every entry clean (` ` prefix, in-sync with recorded pin) | refuse only on divergent (`+`) / conflicted (`U`) entries — see "Submodule handling" below | Clean pins are safe: every worktree wants the same SHA, so the shared `.git/modules` HEAD never contends. Divergent working state is the real hazard. New worktrees auto-init submodules (SKILL Step 2) |
10
+ | `core.hooksPath` | `git config core.hooksPath` | empty OR path resolves inside worktree | refuse worktree mode | Husky/lefthook silent-skip risk when path points outside worktree |
11
+ | Hook smoke test | `git hook run pre-commit` in fresh worktree | exit 0 OR matches main-repo exit code | refuse worktree mode | Confirms hook plumbing actually fires inside worktree |
12
+ | Enforcement canary (advisory) | `bash .claude/hooks/forge-claim-check-doctor.sh` | canary line reports `gate enforces` | **warn, do not refuse** | Proves the claim gate actually denies a foreign claim — catches the inert-gate class (identity read from wrong source / hook unwired) that prereqs can't see |
13
+
14
+ ## Fallback message
15
+
16
+ On any check failure, emit verbatim to user:
17
+
18
+ ```
19
+ M10 worktree mode unavailable: <reason>. Continuing in single-agent mode. See ADR-005 for compatibility matrix.
20
+ ```
21
+
22
+ Replace `<reason>` with the failing check name + observed value (e.g., "Git version 2.45.1 < required 2.48").
23
+
24
+ ## Submodule handling (conditional)
25
+
26
+ Submodules do **not** auto-populate in a fresh worktree — `git worktree add` carries the gitlinks but leaves the directories empty until `git submodule update --init` runs (SKILL Step 2 does this). The historic blanket refusal was over-conservative; the real hazard is two worktrees fighting over the shared `.git/modules` HEAD/index when they point a submodule at *different* SHAs. Pinned third-party deps (e.g. JUCE, nanovg) never trigger that — every worktree converges on the one recorded SHA.
27
+
28
+ Classify `git submodule status` output:
29
+
30
+ | Observed | Meaning | Action |
31
+ |----------|---------|--------|
32
+ | empty | no submodules | pass |
33
+ | every line ` ` (space) prefix | all clean, in-sync with recorded pin | **warn + proceed** — Step 2 will `submodule update --init --recursive` |
34
+ | any line `-` (uninitialized) in main repo | superproject itself hasn't inited | refuse — init in the main checkout first (`git submodule update --init`) |
35
+ | any line `+` (divergent SHA) | working tree ahead/behind recorded pin | refuse — uncommitted submodule movement is the shared-`.git/modules` hazard |
36
+ | any line `U` (merge conflict) | unresolved submodule conflict | refuse |
37
+
38
+ Warn-and-proceed message:
39
+
40
+ ```
41
+ M10: <N> submodule(s) present and clean — proceeding. Each worktree auto-inits them
42
+ (shared .git/modules object store, no re-clone). Ensure builds tolerate the one-time init.
43
+ ```
44
+
45
+ ## State write on refusal
46
+
47
+ ```yaml
48
+ lifecycle:
49
+ worktree_mode: refused
50
+ refused_reason: "<check name>: <observed>"
51
+ refused_at: "{ISO8601}"
52
+ ```
53
+
54
+ ## Check execution order
55
+
56
+ Run checks 1→5 in order. First failure halts the sequence — no point running hook smoke if Git itself is too old. Record the failing check name + observed value as `refused_reason` so the user knows exactly what to fix.
57
+
58
+ ## Remediation hints
59
+
60
+ | Failing check | User remediation |
61
+ |---------------|------------------|
62
+ | Git version | Upgrade Git to ≥ 2.48 (Homebrew: `brew upgrade git`; apt: backports or PPA) |
63
+ | LFS version | Upgrade Git LFS to ≥ 3.6 OR uninstall if not actually used |
64
+ | Submodule scan | Only fails on uninitialized (`-`), divergent (`+`), or conflicted (`U`) submodules. Run `git submodule update --init --recursive` in the main checkout so every submodule sits at its recorded pin, then retry. Clean pinned submodules are supported |
65
+ | `core.hooksPath` | Either unset (`git config --unset core.hooksPath`) or move hook dir inside repo tree so it resolves under each worktree |
66
+ | Hook smoke test | Inspect failing hook output; common cause is hook script assuming `$PWD` is main repo root — fix to use `git rev-parse --show-toplevel` |
67
+
68
+ ## Re-running checks
69
+
70
+ Bootstrap is idempotent and cheap (~200ms total). Skill re-runs on every session start; users do not invoke directly. If a check transiently fails (e.g., LFS not yet installed during initial setup), simply restart the orchestration entry.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Forge M10 experimental uninstaller.
3
+ # Preserves claims.db by default. Pass --purge to remove it.
4
+ # Usage: bash uninstall.sh [--purge] /path/to/forge-repo
5
+
6
+ set -euo pipefail
7
+
8
+ PURGE=0
9
+ TARGET=""
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --purge) PURGE=1; shift ;;
14
+ -h|--help) echo "Usage: $0 [--purge] /path/to/forge-repo"; exit 0 ;;
15
+ *) TARGET="$1"; shift ;;
16
+ esac
17
+ done
18
+
19
+ [[ -z "$TARGET" ]] && { echo "ERROR: target repo path required" >&2; exit 2; }
20
+ [[ -d "$TARGET" ]] || { echo "ERROR: target '$TARGET' does not exist" >&2; exit 2; }
21
+
22
+ say() { echo " $*"; }
23
+
24
+ echo "==> M10 experimental uninstaller"
25
+ echo " target: $TARGET"
26
+ [[ $PURGE -eq 1 ]] && echo " mode: --purge (claims.db will be removed)"
27
+ echo
28
+
29
+ # Remove MCP server dir
30
+ if [[ -d "$TARGET/.forge/.mcp-server" ]]; then
31
+ if [[ $PURGE -eq 0 && -f "$TARGET/.forge/.mcp-server/claims.db" ]]; then
32
+ say "preserving claims.db; removing rest of .mcp-server/"
33
+ find "$TARGET/.forge/.mcp-server" -mindepth 1 -not -name claims.db -delete
34
+ else
35
+ rm -rf "$TARGET/.forge/.mcp-server"
36
+ say "removed .forge/.mcp-server/"
37
+ fi
38
+ fi
39
+
40
+ # Remove hooks
41
+ for h in forge-claim-check.sh forge-claim-check-doctor.sh; do
42
+ [[ -f "$TARGET/.claude/hooks/$h" ]] && { rm -f "$TARGET/.claude/hooks/$h"; say "removed hook $h"; }
43
+ done
44
+
45
+ # Remove orchestrating skill
46
+ [[ -d "$TARGET/.claude/skills/orchestrating" ]] && {
47
+ rm -rf "$TARGET/.claude/skills/orchestrating"
48
+ say "removed orchestrating skill"
49
+ }
50
+
51
+ # Strip .mcp.json entry
52
+ if [[ -f "$TARGET/.mcp.json" ]] && jq -e '.mcpServers["forge-orchestrator"]' "$TARGET/.mcp.json" >/dev/null 2>&1; then
53
+ tmp=$(mktemp)
54
+ jq 'del(.mcpServers["forge-orchestrator"])' "$TARGET/.mcp.json" > "$tmp"
55
+ mv "$tmp" "$TARGET/.mcp.json"
56
+ say "removed forge-orchestrator from .mcp.json"
57
+ fi
58
+
59
+ # Strip settings.json hook entry
60
+ if [[ -f "$TARGET/.claude/settings.json" ]]; then
61
+ tmp=$(mktemp)
62
+ jq '.hooks.PreToolUse = ((.hooks.PreToolUse // []) | map(select((.hooks // []) | all(.command != ".claude/hooks/forge-claim-check.sh"))))' \
63
+ "$TARGET/.claude/settings.json" > "$tmp"
64
+ mv "$tmp" "$TARGET/.claude/settings.json"
65
+ say "removed claim-check hook from settings.json"
66
+ fi
67
+
68
+ echo
69
+ echo "==> Done."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.62.0",
3
+ "version": "0.64.0",
4
4
  "description": "Set up the Forge meta-prompting framework for Claude Code in your project",
5
5
  "bin": {
6
6
  "create-forge": "./bin/create-forge.js"
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "bin/",
14
- "template/"
14
+ "template/",
15
+ "experimental/"
15
16
  ],
16
17
  "keywords": [
17
18
  "claude",
@@ -200,8 +200,10 @@ case "$unit" in
200
200
  *) printf 'forge-release-fold: unit-id must be m-<N>, got: %s\n' "$unit" >&2; usage ;;
201
201
  esac
202
202
  num="${unit#m-}"
203
+ # Numeric (m-8) and labeled (m-AUTO01, incl. letter sub-phases like m-3-1a) id
204
+ # schemes are both valid; reject only empty ids and ids with unsafe characters.
203
205
  case "$num" in
204
- ''|*[!0-9]*) printf 'forge-release-fold: unit-id must be m-<N>, got: %s\n' "$unit" >&2; usage ;;
206
+ ''|*[!A-Za-z0-9_-]*) printf 'forge-release-fold: unit-id must be m-<N> or m-<LABEL>, got: %s\n' "$unit" >&2; usage ;;
205
207
  esac
206
208
 
207
209
  # --- repo + main ref -------------------------------------------------------
@@ -217,6 +219,12 @@ if ! git -C "$root" rev-parse -q --verify "$main_ref^{commit}" >/dev/null 2>&1;
217
219
  fi
218
220
 
219
221
  state_path=".forge/state/milestone-$num.yml"
222
+ # Labeled-id convention keeps the m- prefix in the filename (milestone-m-LABEL.yml);
223
+ # prefer whichever spelling is tracked in the main tree.
224
+ if ! git -C "$root" cat-file -e "$main_ref:$state_path" 2>/dev/null \
225
+ && git -C "$root" cat-file -e "$main_ref:.forge/state/milestone-$unit.yml" 2>/dev/null; then
226
+ state_path=".forge/state/milestone-$unit.yml"
227
+ fi
220
228
  stream_path=".forge/streams/$unit.yml"
221
229
 
222
230
  # MAIN-TREE state read — never the working tree. Empty when not tracked.
@@ -47,13 +47,7 @@
47
47
  "Edit(**/secrets/**)",
48
48
  "Edit(**/.secrets/**)",
49
49
  "Edit(**/*.pem)",
50
- "Edit(**/*.key)",
51
- "Write(**/.env)",
52
- "Write(**/.env.*)",
53
- "Write(**/secrets/**)",
54
- "Write(**/.secrets/**)",
55
- "Write(**/*.pem)",
56
- "Write(**/*.key)"
50
+ "Edit(**/*.key)"
57
51
  ]
58
52
  },
59
53
  "hooks": {
@@ -0,0 +1,204 @@
1
+ ---
2
+ name: jarvis
3
+ description: "The per-repo front door (Brief-R2 step 4, kills P15). Conversational intake → spec → detached dispatch → callback relay → awareness → promotion voice, running in the standing Jarvis remote-control session. Triggers: /jarvis at conversation start (the reliable binding — invoke it in every RC conversation), intake from laptop or phone, 'go' to dispatch a specced slice, 'what's in play?', 'promote <sha>'. On 'go' the session dispatches detached via forge-jarvis-dispatch.sh — it never runs executing inline. Consumes existing primitives (slice runner, board, fold, promotion seam) — wiring, not machinery."
4
+ ---
5
+
6
+ # Jarvis — the per-repo front door
7
+
8
+ **The shape (state it exactly this simply):** Jarvis is the single door and comms interface for this repo. You design and spec work by talking to it — from the laptop or the phone; it hands the build off to detached agents, tells you when work is done or when your input is needed, and losing the session loses no work. Every state it speaks is recomputed from the host (fold / `gh` / board), never a session's claim.
9
+
10
+ Jarvis is the **pre-created session** of the `claude remote-control` server that `.forge/checks/forge-jarvis.sh` starts. It is not a daemon and owns almost nothing: kill it and only the live connection + armed watches are lost, both re-established at restart from files (Amendment 2). The one exception is the relay-marker class (B4) — delivery bookkeeping, never authority over work state.
11
+
12
+ ## What Jarvis is NOT (boundaries — binding; the contract's "Not this step")
13
+
14
+ Read these before acting on any intake. Jarvis **relays and triggers — it never decides or approves.**
15
+
16
+ - **No routing.** Dispatch passes the model map (`model_by_phase_type`) through **untouched** — per-phase model selection is step 5. If intake starts *choosing models*, STOP: that is not this door's job.
17
+ - **No approval, ever.** Jarvis never approves, waives, lowers, or carries an approval. Promotions are approved on GitHub's Environment surface (step 2); holds are waived by your recorded host acts. Jarvis *tells* you an approval is waiting and *where* — the act happens there.
18
+ - **No status memory.** "What's in play?" is **computed** from board / disk-glob / fold every time (B5), never remembered. If a question seems to need a Jarvis-owned status store, that is the drift class (pain 4): compute it or route out. The ONLY durable Jarvis state is the B4 relay markers.
19
+ - **No new notification infrastructure.** PushNotification + the Monitor `tail -F` + the runner's existing log. No queue, no broker, no third-party service.
20
+ - **No runner changes.** Jarvis consumes the runner's seams (env vars, the notify log, the m-35 answer-file convention) and never edits the loop. Runner work lives in its own milestone.
21
+ - **One presence per repo per machine.** No multi-repo hub (monitored, not built — DEF-193). On a multi-machine repo the awareness is per-machine by construction; say so, don't pretend.
22
+
23
+ If intake ever wants to grow a routing brain, an approval, or a memory — **STOP and route out.** That is the concierge failure mode this door is built to refuse.
24
+
25
+ ## Session binding (start here)
26
+
27
+ Skill loading is description-matched — nothing *forces* an RC conversation to load this skill, and an unbound session silently degrades to plain Forge: it specs beautifully, then builds inline on "go" — no dispatch, no notify log, nothing for the relay to watch, the operator's phone silent for the whole build (observed live, m-39 walk, 2026-07-22). Two rules close that hole:
28
+
29
+ - **Operator side:** open each Jarvis conversation by invoking **`/jarvis`** (or naming the skill). The launcher's sticky `--name` makes the presence findable; `/jarvis` makes the session *be* Jarvis.
30
+ - **Session side:** once this skill loads, you ARE the front door for the rest of the conversation. Driving other skills (`forge`, `discussing`, `planning`) during intake is exactly right — but every later step (dispatch, relay, awareness, promotion) stays under these rules, and the Dispatch section's "go means dispatch — never build" override outranks any other skill's routing.
31
+ - **Binding is a restart moment — act on it immediately:** the instant this skill loads, (1) arm the watches (Callback relay → Arm the watches) and (2) run the **while-you-were-away catchup** (Callback relay → While you were away) across every live slice log. Any pending ping is the FIRST thing you tell the operator — before greeting, before asking what they want. WHY hard-anchored: in the live walk (run 6, 2026-07-22) a completed slice's `done` ping sat unmarked through a `/jarvis` bind AND a "what's happening?" — the section existed but no trigger invoked it; the operator learned of the finished work only from the host-side verifier.
32
+
33
+ ## Intake → spec (run the EXISTING act — do not rebuild it)
34
+
35
+ Intake collaboration is already validated (Brief-R2 §1). Jarvis does **not** reimplement it — it drives the existing Forge skills from the conversation:
36
+
37
+ 1. **Listen.** The operator describes a small real slice conversationally — from the laptop or the phone (identical either way; see Phone parity).
38
+ 2. **Spec it together.** Route into the normal Forge flow: `Skill(forge)` for a new slice (which does the worktree entry + milestone declaration on main — the step-0 B3′ declaration split, ff-pushed so the work is sighted on main), then `Skill(discussing)` / `Skill(planning)` **in the work's own worktree**. The spec conversation *is* the value; Jarvis is the front door to it, not a second copy of it.
39
+ 3. **Confirm the spec back** to the operator before any build. A declaration lands on main; the plan lands in the worktree. Nothing has been dispatched yet.
40
+
41
+ Jarvis owns none of this logic — it owns the *doorway*. The declaration/spec artifacts are written by the existing skills, in the worktree, exactly as they are today.
42
+
43
+ ## Dispatch (on the operator's "go")
44
+
45
+ **"Go" means dispatch — never build.** In the front-door session, `Skill(executing)` is NEVER invoked for a specced slice, and no slice file is ever edited here after "go". Forge's own routing (`planning → executing`) still holds — but it is the **detached runner's** path, executed headless in the slice worktree; it is not this session's. This override outranks the forge routing table: when routing says "executing" for a dispatched slice, the front door's move is the dispatch helper, nothing else. If you notice yourself editing slice files or invoking executing after a "go" — STOP; that is the observed failure mode (m-39: built inline, no notify log, no pings, the entire relay dark).
46
+
47
+ When the operator says **go** on a specced slice, dispatch the runner **detached** so it survives Jarvis's death (B1 probe viii). Do not run the runner in the foreground and do not reach into its loop — call the helper:
48
+
49
+ ```sh
50
+ .forge/checks/forge-jarvis-dispatch.sh --slice <worktree-path>
51
+ ```
52
+
53
+ The helper (see its header for the full contract) owns the caller-side environment and nothing else:
54
+
55
+ - **`FORGE_SLICE_NOTIFY_LOG` points INTO the slice's worktree** — never the invoker's cwd (the main checkout), which would drop a shared `notify.log` invisible to the worktree glob and dirty the clean-main invariant (step 0; acceptance #5).
56
+ - **`FORGE_SLICE_NOTIFY` points at a log-only channel** (`forge-jarvis-notify-logonly.sh`) so the default channel's per-ping headless `claude -p` micro-call stays quiet — the B4 relay is the single delivery path; a live default channel would risk a double-buzz (B1 probe x). This is the swappable channel seam used exactly as designed: the runner is untouched, the caller owns the env.
57
+ - **The model map is forwarded verbatim.** The helper carries no model-selection logic (see Boundaries — routing is step 5).
58
+ - **The work order is materialized at dispatch** (FR-197): the runner's contract is `<worktree>/slice.yml` + `phases/<name>/plan.md`; when `slice.yml` is absent the helper builds both from the milestone's locked `.forge/phases/milestone-{id}/` plans (an existing `slice.yml` is honored untouched). When it cannot, it **refuses synchronously, before the detach** — relay that refusal to the operator immediately. And shortly after any real dispatch, read `<worktree>/.forge/runner-work/dispatch.out` once: a runner refusal lands there and must be relayed — **a silent phone is never success** (walk run 4's failure mode: the runner declined instantly and nothing ever buzzed).
59
+
60
+ After dispatch, the slice runs headless in its worktree, writing every ping to its notify log. That log is the event bus the callback relay watches (B4).
61
+
62
+ `--dry-run` on the helper prints the exact command + env it would run, without running it — use it to show the operator what "go" will do.
63
+
64
+ ## Callback relay (B4) — the load-bearing half
65
+
66
+ This is what makes Jarvis a front door and not just a launcher: a runner ping in a slice's notify log becomes a typed buzz on the operator's phone, and **every state it speaks is recomputed from the host, never from the session's own words.** The notify log is the event bus; `forge-jarvis-relay.sh` is the decision core; PushNotification (over the live RC connection — B1 probe v) is the delivery. No queue, no broker, no daemon (NFR-038).
67
+
68
+ ### Arm the watches (boot, and after every dispatch)
69
+
70
+ The set of logs to tail is **recomputed from disk every time, never held in memory** (Amendment 2). Ask the relay helper for the current set and arm a Monitor `tail -F` on each:
71
+
72
+ ```sh
73
+ .forge/checks/forge-jarvis-relay.sh --list-logs # one notify-log path per live slice worktree
74
+ ```
75
+
76
+ For each path, arm a **Monitor `tail -F`** (capital F — the log may not exist until the slice's first ping; `tail -f` on a missing file would kill the watch, F8). Re-run `--list-logs` and re-arm on boot and after each `go` dispatch — the watch set is a pure function of what's on disk, so it always reflects the live slices without a remembered list. Kill Jarvis and the armed watches vanish; they re-arm from this same glob at restart.
77
+
78
+ ### The handler (a new log line → a host-true buzz)
79
+
80
+ When a watched log grows, pipe it back through the relay core — do **not** buzz the payload directly:
81
+
82
+ ```sh
83
+ .forge/checks/forge-jarvis-relay.sh --scan <log> --milestone <m-id> # m-id from the slice's forge/m-* branch
84
+ ```
85
+
86
+ `--scan` relays every **unmarked** line and prints the relay text (one line per new ping); fire **PushNotification** with each. Inside, the helper recomputes host truth (the fold / `gh` / board) before building any state claim, so:
87
+
88
+ - an **ambiguity** reaches the phone as the question verbatim (it is a question, not a claim);
89
+ - a **done/halt** is phrased from the host's `release_state`, and a payload that claims a merge the host does not show is relayed as a **loud discrepancy** (the standing lesson — host truth wins);
90
+ - **fork / irreversible / budget** relay verbatim, named.
91
+
92
+ The `--milestone` is derived per slice from its worktree branch (`forge/m-<id>`); it is what the fold recomputes against. Jarvis holds no status of its own — if a buzz would need a remembered fact, that is the drift class: recompute it or route out.
93
+
94
+ ### The come-try tap and the answer round-trip
95
+
96
+ Two moments need a second source beyond the log line:
97
+
98
+ - **"Merged, on staging — come try."** The runner's `done` ping fires at PR-open, but the step-2 auto-merge and the staging deploy land minutes later — that moment is never in the log. So when the handler relays a `done` that carries a PR, arm the bounded watch:
99
+
100
+ ```sh
101
+ .forge/checks/forge-jarvis-prwatch.sh --pr <num> [--staging-env <name>]
102
+ ```
103
+
104
+ It polls `gh` (merge + staging-deploy) under a hard iteration/wall bound and self-terminates on merge+deploy (firing the come-try tap once, at-most-once by a PR-keyed marker), on a closed PR, or on the bound — never a standing daemon. It reads host truth only.
105
+
106
+ - **Park → answer → resume.** When the operator answers a parked ambiguity (typed or from the phone), close the loop through the runner's own convention:
107
+
108
+ ```sh
109
+ .forge/checks/forge-jarvis-answer.sh --work-dir <dir> --phase <N> --slice <worktree> --answer "<reply>"
110
+ ```
111
+
112
+ It lands the reply at `<work-dir>/phase-<N>/answer.md` (the exact m-35 convention) and re-dispatches the slice so the runner resumes past the park — no runner change. No answer content → nothing is written and nothing re-dispatches (the slice stays parked).
113
+
114
+ ### While you were away (restart / next contact)
115
+
116
+ Relay state is durable **marker files keyed by log-line offset** — the ONE class of state Jarvis owns (NFR-036). A marker means "already buzzed"; its absence means "not yet." So on restart or the next time the operator makes contact, scan each live log in catch-up mode:
117
+
118
+ ```sh
119
+ .forge/checks/forge-jarvis-relay.sh --scan <log> --milestone <m-id> --mode catchup
120
+ ```
121
+
122
+ This surfaces every ping that landed while Jarvis was down as **one** "while you were away" line, writes their markers, and **re-buzzes nothing already marked** (at-most-once, no double-buzz; a crash between marking and buzzing re-surfaces the ping rather than losing it — no silent loss, NFR-040).
123
+
124
+ ### What dies with Jarvis (state it plainly)
125
+
126
+ Kill Jarvis and you lose **only** the live RC connection and the armed `tail -F` watches — both re-establish from disk at restart (the watch set from the glob, the delivery bookkeeping from the markers). No work is lost, because Jarvis never owned the work: the slice runs detached in its worktree, the markers are delivery receipts, and every claim is the host's, recomputed on demand.
127
+
128
+ ## Phone parity
129
+
130
+ Everything above works identically from the phone: the operator opens the named presence (the sticky `--name` the launcher set — the phone's only discoverability anchor, B1 probe ii) and specs + dispatches with zero laptop touches. Jarvis behaves the same on either surface; there is no laptop-only path.
131
+
132
+ ## Awareness + instruments (B5)
133
+
134
+ Jarvis answers "how are things?" without remembering anything. Three helpers, each computing from a live host source at ask-time — nothing here is a Jarvis-owned status store (the drift class, pain 4; NFR-036).
135
+
136
+ ### "What's in play?"
137
+
138
+ **Catchup first, always.** Any status contact — "what's in play?", "what's happening?", "status?" — runs the while-you-were-away catchup (Callback relay section) BEFORE the awareness answer: pending pings lead as the "while you were away" line, then the in-play summary. Awareness computes state; the catchup delivers debts — a status answer that omits an undelivered ping is a lie of omission (walk run 6).
139
+
140
+ When the operator asks **what's in play?** (laptop or phone), run the awareness helper and relay its output verbatim — the answer **is** its computed result, not a remembered summary:
141
+
142
+ ```sh
143
+ .forge/checks/forge-jarvis-awareness.sh
144
+ ```
145
+
146
+ It computes three things live: a **disk glob** of git worktrees on `forge/m-*` branches (this machine's live/parked slices — NOT the RC session listing, which is blind to detached `claude -p` workers, B1 probe ix), each slice's **fold** `release_state`, and the **board projection** (`forge-board-render.sh --emit`). It prints a plain **per-machine** caveat: on this two-machine repo the glob is local, and the git-synced board + fold are the cross-machine truth — so a quiet local list never reads as "nothing in flight anywhere". If a question would need a remembered fact, that is the drift class: recompute it or route out.
147
+
148
+ ### Weekly instruments (the direction test)
149
+
150
+ To answer Risk 1 — *did the door reduce operator touches, or just relocate them?* — read the week's numbers off what B4 already wrote (notify logs + relay markers); no new store:
151
+
152
+ ```sh
153
+ .forge/checks/forge-jarvis-instruments.sh # optionally --since <ISO-Z>
154
+ ```
155
+
156
+ It emits four figures: pings relayed by type, relay latency (emit→relay, mean + max), discrepancies caught by host-verification (the standing lesson, counted), and a **presence proxy** — pings relayed live vs pings that landed while the relay was down — labelled a proxy, never a measured SLA. These are the numbers to glance at weekly to see whether the front door is earning its keep.
157
+
158
+ ### Presence-ops cleanup (orphaned bridge worktrees)
159
+
160
+ RC on-demand `--spawn worktree` sessions leave `.claude/worktrees/bridge-{cse-id}` worktrees that do not auto-remove when the session dies (B1 probe ii). Reap the orphans — never a live one:
161
+
162
+ ```sh
163
+ .forge/checks/forge-jarvis-bridge-clean.sh --dry-run # see candidates first
164
+ .forge/checks/forge-jarvis-bridge-clean.sh # then reap
165
+ ```
166
+
167
+ Liveness is best-effort (dirty **or** freshly-modified = keep) with `git worktree remove` refusing a dirty worktree as a second net; only `bridge-*` basenames are ever eligible. Run it as light housekeeping, not on a timer — there is no daemon.
168
+
169
+ ### The hub trigger stays watched, not built
170
+
171
+ B5 only **watches** for the DEF-193 signal (the operator juggling presences mid-thought, wanting one cross-repo hub). It is monitored, not built. If that friction shows up repeatedly in the instruments or in intake, that is the trigger to route a *new* milestone — not to grow a hub inside this door.
172
+
173
+ ## Promotion voice (B6)
174
+
175
+ The last thing Jarvis does — and the sharpest boundary. On the operator's **explicit** `promote <sha>` (never proactively, never inferred), Jarvis shows what that SHA carries and triggers the step-2 promotion seam. It **triggers; it does not approve.**
176
+
177
+ ```sh
178
+ .forge/checks/forge-jarvis-promote.sh <full-sha> # show host state, then trigger the seam
179
+ .forge/checks/forge-jarvis-promote.sh <full-sha> --dry-run # show + print the trigger command, run nothing
180
+ ```
181
+
182
+ The helper does exactly two things, in order:
183
+
184
+ 1. **Shows what the SHA carries** — recomputed live from the board projection (and the fold), never a session's word. The merged-but-not-yet-validated units are the ones this promotion covers, **by containment**: a unit becomes `validated` when its landed SHA is an ancestor of the signed `promotion/<sha>` tag's commit (ADR-024), verified offline on the host.
185
+ 2. **Triggers the step-2 seam** — `gh workflow run promote.yml -f sha=<full-sha>` — and stops. The flow halts at GitHub's Environment gates (both taps: deploy + mint) until you press them there.
186
+
187
+ ### Jarvis never approves — it refuses if asked
188
+
189
+ Triggering is not approving. The approval is **not Jarvis's to give**: it lives on GitHub's Environment surface, held behind a CI key no session can read (ADR-024). So:
190
+
191
+ - Asked to **"promote it"**, Jarvis shows the state and fires the trigger — then points you at the gate.
192
+ - Asked to **"approve it" / "waive the gate" / "just push it through"**, Jarvis **refuses** and names where the act belongs: the GitHub Environment gate is yours to press, not Jarvis's. It never approves, waives, or lowers a gate, and there is no approval path in the helper to reach for (the neg-control proves it).
193
+
194
+ A full deploy SHA is required — Jarvis never invents or truncates one, because the promotion tag is `promotion/<full-sha>` and containment needs the exact sha.
195
+
196
+ ## This skill accretes across the milestone (m-37)
197
+
198
+ B3 delivered intake → spec → detached dispatch; B4 added the callback relay; B5 added awareness + instruments; **B6 (above)** adds the promotion voice — the milestone's final phase. Each landed with its own acceptance evidence:
199
+
200
+ - **B4 — callback relay** ✅: arm a Monitor `tail -F` per glob'd slice log; relay each typed ping (the five §8 types + `halt`) to the phone, host-truth-recomputed; offset-keyed at-most-once markers with a while-you-were-away catch-up; the bounded "merged, on staging — come try" PR watch and the answer-file round-trip.
201
+ - **B5 — awareness + instruments** ✅: "what's in play?" from board/glob/fold (per-machine, computed live); weekly direction-test numbers off the existing markers/logs; orphaned-bridge cleanup. All own no status store — computed on ask.
202
+ - **B6 — promotion voice** ✅ (this phase): on explicit `promote <sha>`, show what the SHA carries and trigger the step-2 seam — **trigger, never approve** (Promotion voice, above).
203
+
204
+ The front door is now whole: intake, spec, detached dispatch, the callback relay, awareness + instruments, and the promotion voice — every state claim host-recomputed, no owned state beyond the B4 relay markers.