forge-orkes 0.63.0 → 0.64.2

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 (25) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/hooks/forge-release-fold.sh +9 -1
  3. package/template/.claude/skills/jarvis/SKILL.md +204 -0
  4. package/template/.forge/checks/forge-board-render.sh +11 -5
  5. package/template/.forge/checks/forge-jarvis-answer.sh +85 -0
  6. package/template/.forge/checks/forge-jarvis-awareness.sh +133 -0
  7. package/template/.forge/checks/forge-jarvis-bridge-clean.sh +133 -0
  8. package/template/.forge/checks/forge-jarvis-dispatch.sh +198 -0
  9. package/template/.forge/checks/forge-jarvis-instruments.sh +149 -0
  10. package/template/.forge/checks/forge-jarvis-notify-logonly.sh +46 -0
  11. package/template/.forge/checks/forge-jarvis-promote.sh +116 -0
  12. package/template/.forge/checks/forge-jarvis-prwatch.sh +109 -0
  13. package/template/.forge/checks/forge-jarvis-relay.sh +242 -0
  14. package/template/.forge/checks/forge-jarvis.sh +161 -0
  15. package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
  16. package/template/.forge/checks/tests/forge-jarvis-awareness.test.sh +118 -0
  17. package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
  18. package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
  19. package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
  20. package/template/.forge/checks/tests/forge-jarvis-notify-logonly.test.sh +63 -0
  21. package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
  22. package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
  23. package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
  24. package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
  25. package/template/.forge/templates/project.yml +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.63.0",
3
+ "version": "0.64.2",
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"
@@ -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.
@@ -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 decided in this order (FR-207 gap-closure): dirty = keep; a git lock naming a **live** server pid = keep; a lock with a **dead** pid = orphan even when freshly modified (unlock-then-remove — RC locks every spawned bridge under the server's pid, so a fresh-looking bridge whose server died is still reapable); mtime freshness applies only to **unlocked** bridges. `git worktree remove` refusing a dirty worktree stays the 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 (whose rows carry the fold-derived `release_state`), 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.
@@ -47,6 +47,12 @@ json_escape() {
47
47
  printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | tr '\n' ' '
48
48
  }
49
49
 
50
+ # Milestone-file id → unit id. Numeric files (milestone-8.yml) need the m-
51
+ # prefix added; labeled files (milestone-m-AUTO01.yml) already carry it.
52
+ unit_id() {
53
+ case "$1" in m-*) printf '%s' "$1" ;; *) printf 'm-%s' "$1" ;; esac
54
+ }
55
+
50
56
  # ---------------------------------------------------------------------------
51
57
  # Config: read a scalar under the top-level `board:` block of project.yml.
52
58
  # ---------------------------------------------------------------------------
@@ -135,8 +141,8 @@ emit_deferred() {
135
141
  base=$(basename "$f" .yml); id="${base#milestone-}"
136
142
  nm=$(yget "$f" milestone name); rs=$(yget "$f" lifecycle deferred_reason)
137
143
  [ "$first" = 1 ] && first=0 || printf ',\n'
138
- printf ' {"surface": "milestone", "id": "m-%s", "name": "%s", "deferred_at": "%s", "reason": "%s"}' \
139
- "$id" "$(json_escape "$nm")" "$(json_escape "$da")" "$(json_escape "$rs")"
144
+ printf ' {"surface": "milestone", "id": "%s", "name": "%s", "deferred_at": "%s", "reason": "%s"}' \
145
+ "$(unit_id "$id")" "$(json_escape "$nm")" "$(json_escape "$da")" "$(json_escape "$rs")"
140
146
  done
141
147
  # Surface 2: deferred REFACTOR-backlog items (status: deferred). Block-scan: accumulate
142
148
  # each `- id:` item's fields, emit when its status is deferred.
@@ -174,8 +180,8 @@ emit_deferred() {
174
180
  [ "$hit" = "1" ] || continue
175
181
  base=$(basename "$f" .yml); id="${base#milestone-}"
176
182
  [ "$first" = 1 ] && first=0 || printf ',\n'
177
- printf ' {"surface": "active-milestone", "id": "m-%s", "name": "%s", "deferred_at": "", "reason": "phase/task deferred inside active milestone"}' \
178
- "$id" "$(json_escape "$(yget "$f" milestone name)")"
183
+ printf ' {"surface": "active-milestone", "id": "%s", "name": "%s", "deferred_at": "", "reason": "phase/task deferred inside active milestone"}' \
184
+ "$(unit_id "$id")" "$(json_escape "$(yget "$f" milestone name)")"
179
185
  done
180
186
  }
181
187
 
@@ -200,7 +206,7 @@ emit_projection() {
200
206
  ufirst=1
201
207
  for f in "$FORGE"/state/milestone-*.yml; do
202
208
  [ -f "$f" ] || continue
203
- base=$(basename "$f" .yml); id="${base#milestone-}"; unit="m-$id"
209
+ base=$(basename "$f" .yml); id="${base#milestone-}"; unit=$(unit_id "$id")
204
210
  nm=$(yget "$f" milestone name)
205
211
  status=$(yget "$f" current status)
206
212
  tier=$(yget "$f" current tier)
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis-answer.sh — the park → answer → resume round-trip
3
+ # (m-37, Brief-R2 step 4 B4(d); contract handoff-step4-jarvis.md REV2).
4
+ #
5
+ # When a slice PARKS on an ambiguity, the relay (B4) buzzes the operator the question. The
6
+ # operator's reply comes back to Jarvis (typed or from the phone). This helper closes the loop:
7
+ # it lands that reply as the runner's CONVENTIONED answer file, then re-dispatches the slice so
8
+ # it resumes PAST the park — not a cold replay.
9
+ #
10
+ # THE CONVENTION IS FIXED (m-35, landed 0.61.0 — the named Prerequisite): the answer file is
11
+ # <work-dir>/phase-<N>/answer.md
12
+ # beside that phase's phase-report.yml. Do NOT invent a new location. On its next invocation the
13
+ # runner re-assembles that phase's prompt WITH the parked state + this answer and continues; a
14
+ # fresh park after a new answer fires a new ping. All of that is m-35 runner behavior — consumed,
15
+ # never changed (NFR-037: no runner edit).
16
+ #
17
+ # Re-dispatch goes through forge-jarvis-dispatch.sh (the detached-runner seam from B3), so the
18
+ # resume runs headless in the slice worktree exactly like the original dispatch.
19
+ #
20
+ # --work-dir <dir> the runner's --work-dir for this slice (holds phase-<N>/) [required]
21
+ # --phase <N> the parked phase index (→ phase-<N>/answer.md) [required]
22
+ # --slice <worktree> the slice worktree to re-dispatch [required]
23
+ # --answer <text> the operator's reply; or --answer-file <path>; or via stdin
24
+ # --dry-run write nothing, dispatch nothing — print the resolved plan and exit
25
+ # Extra args after `--` are forwarded to forge-jarvis-dispatch.sh verbatim.
26
+ set -u
27
+
28
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
29
+ DISPATCH="${FORGE_JARVIS_DISPATCH:-$SELF_DIR/forge-jarvis-dispatch.sh}"
30
+
31
+ WORK_DIR=""; PHASE=""; SLICE=""; ANSWER=""; ANSWER_FILE=""; DRY=0
32
+ while [ $# -gt 0 ]; do
33
+ case "$1" in
34
+ --work-dir) WORK_DIR="${2:-}"; shift 2 ;;
35
+ --phase) PHASE="${2:-}"; shift 2 ;;
36
+ --slice) SLICE="${2:-}"; shift 2 ;;
37
+ --answer) ANSWER="${2:-}"; shift 2 ;;
38
+ --answer-file) ANSWER_FILE="${2:-}"; shift 2 ;;
39
+ --dry-run) DRY=1; shift ;;
40
+ --) shift; break ;;
41
+ *) printf 'forge-jarvis-answer: unknown argument: %s\n' "$1" >&2; exit 2 ;;
42
+ esac
43
+ done
44
+ # After `--`, the remaining "$@" is forwarded to dispatch verbatim — argument
45
+ # boundaries preserved (never flattened to a string; args with spaces survive).
46
+
47
+ [ -n "$WORK_DIR" ] && [ -n "$PHASE" ] && [ -n "$SLICE" ] || {
48
+ printf 'forge-jarvis-answer: --work-dir, --phase and --slice are all required\n' >&2; exit 2; }
49
+
50
+ # The phase index comes from a park ping (semi-trusted runner log) and is built
51
+ # into a write path — a non-numeric value could traverse out of the work dir.
52
+ case "$PHASE" in
53
+ *[!0-9]*|'') printf 'forge-jarvis-answer: --phase must be a numeric phase index, got: %s\n' "$PHASE" >&2; exit 2 ;;
54
+ esac
55
+
56
+ # Resolve the answer text: --answer, else --answer-file, else stdin.
57
+ if [ -z "$ANSWER" ]; then
58
+ if [ -n "$ANSWER_FILE" ] && [ -f "$ANSWER_FILE" ]; then
59
+ ANSWER="$(cat "$ANSWER_FILE")"
60
+ elif [ ! -t 0 ]; then
61
+ ANSWER="$(cat)"
62
+ fi
63
+ fi
64
+
65
+ # NEG-CONTROL: no answer content → write nothing, re-dispatch nothing (the slice stays parked).
66
+ if [ -z "$ANSWER" ]; then
67
+ printf 'forge-jarvis-answer: no answer content — nothing written, slice stays parked\n' >&2
68
+ exit 3
69
+ fi
70
+
71
+ DEST="$WORK_DIR/phase-$PHASE/answer.md" # the EXACT m-35 convention
72
+
73
+ if [ "$DRY" = 1 ]; then
74
+ printf 'answer plan: write → %s ; then re-dispatch %s --slice %s --work-dir %s %s\n' \
75
+ "$DEST" "$DISPATCH" "$SLICE" "$WORK_DIR" "$*"
76
+ exit 0
77
+ fi
78
+
79
+ # 1) land the answer at the conventioned path
80
+ mkdir -p "$WORK_DIR/phase-$PHASE" 2>/dev/null || true
81
+ printf '%s\n' "$ANSWER" > "$DEST" || { printf 'forge-jarvis-answer: could not write %s\n' "$DEST" >&2; exit 1; }
82
+ printf 'forge-jarvis-answer: answer landed at %s\n' "$DEST" >&2
83
+
84
+ # 2) re-dispatch the slice so the runner resumes past the park (m-35 prompt re-assembly)
85
+ "$DISPATCH" --slice "$SLICE" --work-dir "$WORK_DIR" "$@"
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env sh
2
+ # forge-jarvis-awareness.sh — "what's in play?" answered LIVE, never remembered (m-37,
3
+ # Brief-R2 step 4 B5 awareness; contract handoff-step4-jarvis.md REV2; FR-203/204; NFR-036).
4
+ #
5
+ # The operator asks Jarvis "what's in play?" and every fact in the answer is COMPUTED from a
6
+ # live host source at ask-time — there is no Jarvis-owned status store (NFR-036 / pain 4: the
7
+ # drift class). Three live sources, three questions:
8
+ # 1. WHICH SLICES ARE RUNNING (this machine) → a DISK GLOB of git worktrees on forge/m-*
9
+ # branches (`git worktree list`), NOT the RC session listing (which is blind to detached
10
+ # `claude -p` workers — B1 probe ix). Parked/dispatched slices are worktrees on disk, so a
11
+ # glob sees them; the session listing does not.
12
+ # 2. WHERE EACH SLICE STANDS (cross-machine) → the fold (forge-release-fold.sh m-N) per slice:
13
+ # unmerged | merged | validated — git-derived, identical on any clone.
14
+ # 3. THE BOARD PROJECTION (cross-machine) → forge-board-render.sh --emit: the rollup's view
15
+ # of every unit, offline + deterministic.
16
+ #
17
+ # PER-MACHINE SCOPE (F11 — stated plainly in the output): the disk glob is THIS machine's live/
18
+ # parked slices only; forge is a two-machine repo, so the other machine's in-flight worktrees are
19
+ # invisible here. The git-synced board + fold ARE the cross-machine truth; the glob is the local
20
+ # "what's spinning right now" layer. The answer says so, so a quiet glob is never mistaken for
21
+ # "nothing in flight anywhere".
22
+ #
23
+ # NO OWNED STATE: nothing here is persisted; the next ask recomputes from disk + git. The only
24
+ # durable Jarvis state anywhere is B4's relay markers (a different helper).
25
+ #
26
+ # forge-jarvis-awareness.sh [--dry-run] [--worktree-root <dir>]
27
+ # --dry-run resolve + print structure without shelling to the fold/board (fast
28
+ # plumbing check); tests and quick sanity checks use it.
29
+ # --worktree-root override the worktree scan root (tests); default: real `git worktree list`.
30
+ #
31
+ # Host-truth commands are env-overridable so tests inject stubs and degradation is explicit:
32
+ # FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh
33
+ # FORGE_JARVIS_BOARD default .forge/checks/forge-board-render.sh
34
+ # BEST-EFFORT: a missing fold/board degrades to a stated 'unknown' / '(unavailable)', never a hard
35
+ # fail — the awareness answer always renders.
36
+ set -u
37
+
38
+ SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
39
+ REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)" # .forge/checks → repo root
40
+ FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
41
+ BOARD="${FORGE_JARVIS_BOARD:-$REPO_ROOT/.forge/checks/forge-board-render.sh}"
42
+ TAB="$(printf '\t')"
43
+
44
+ DRY_RUN=0
45
+ WT_ROOT=""
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --dry-run) DRY_RUN=1; shift ;;
49
+ --worktree-root) WT_ROOT="${2:-}"; shift 2 ;;
50
+ *) printf 'forge-jarvis-awareness: unknown argument: %s\n' "$1" >&2; exit 2 ;;
51
+ esac
52
+ done
53
+
54
+ # ── host truth: a slice's release_state from the fold (never remembered). Degrades to 'unknown'. ─
55
+ fold_state() { # fold_state <milestone-id>
56
+ _mid="${1:-}"
57
+ [ -n "$_mid" ] || { printf 'unknown'; return 0; }
58
+ [ -f "$FOLD" ] || { printf 'unknown'; return 0; }
59
+ _out="$(sh "$FOLD" "$_mid" 2>/dev/null)" || { printf 'unknown'; return 0; }
60
+ _rs="$(printf '%s\n' "$_out" | grep -m1 'release_state=' | sed 's/.*release_state=//; s/[[:space:]].*//')"
61
+ [ -n "$_rs" ] && printf '%s' "$_rs" || printf 'unknown'
62
+ }
63
+
64
+ # ── 1. live/parked slices — a DISK GLOB of worktrees on forge/m-* branches. Same on-disk registry
65
+ # the relay's --list-logs arms tail -F on; recomputed here, never stored. Emits TSV:
66
+ # <milestone-id>\t<branch>\t<path>. --worktree-root globs a given dir instead (tests). ──────
67
+ list_slices() {
68
+ if [ -n "$WT_ROOT" ]; then
69
+ for _wt in "$WT_ROOT"/*/; do
70
+ [ -d "$_wt" ] || continue
71
+ _b="$(basename "$_wt")"
72
+ printf '%s%s(root-scan)%s%s\n' "$_b" "$TAB" "$TAB" "${_wt%/}"
73
+ done
74
+ else
75
+ git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk -v tab="$TAB" '
76
+ /^worktree / { p = substr($0, 10) } # full remainder — paths may contain spaces
77
+ /^branch / {
78
+ if ($2 ~ /refs\/heads\/forge\/m-/) {
79
+ b = $2; sub(/refs\/heads\//, "", b) # forge/m-37[-<session>]
80
+ m = b; sub(/^forge\//, "", m); sub(/-[0-9a-f]{8}$/, "", m) # → m-37
81
+ print m tab b tab p
82
+ }
83
+ }'
84
+ fi
85
+ }
86
+
87
+ # ── render ──────────────────────────────────────────────────────────────────────────────────────
88
+ printf '── What'\''s in play? (host-verified, computed live) ──────────────────\n'
89
+
90
+ printf '\n▸ Live / parked slices on THIS machine (git worktree glob):\n'
91
+ _slices="$(list_slices)"
92
+ if [ -n "$_slices" ]; then
93
+ printf '%s\n' "$_slices" | while IFS="$TAB" read -r _m _br _pt; do
94
+ if [ "$DRY_RUN" -eq 1 ]; then _rs='(dry-run)'; else _rs="$(fold_state "$_m")"; fi
95
+ printf ' %-10s %-26s release_state=%-9s %s\n' "$_m" "$_br" "$_rs" "$_pt"
96
+ done
97
+ else
98
+ printf ' (none on this machine)\n'
99
+ fi
100
+
101
+ printf '\n▸ Board projection (git-synced — cross-machine truth):\n'
102
+ if [ "$DRY_RUN" -eq 1 ]; then
103
+ printf ' (--dry-run: forge-board-render.sh --emit not invoked)\n'
104
+ elif [ -f "$BOARD" ]; then
105
+ _emit="$(sh "$BOARD" --emit 2>/dev/null)"
106
+ if [ -n "$_emit" ]; then
107
+ # In-flight is what's "in play": one line per non-complete unit. Line-oriented JSON → grep/sed,
108
+ # no jq dependency (Article X native-first).
109
+ _inflight="$(printf '%s\n' "$_emit" | grep '"forge_id"' | grep -vE '"status":[[:space:]]*"complete"')"
110
+ if [ -n "$_inflight" ]; then
111
+ # Order-independent field extraction — one sed per key, so a key-order change
112
+ # in the board emit degrades a field to '?' instead of silently dropping the line.
113
+ printf '%s\n' "$_inflight" | head -40 | while IFS= read -r _ln; do
114
+ _id="$(printf '%s' "$_ln" | sed -n 's/.*"forge_id":[[:space:]]*"\([^"]*\)".*/\1/p')"
115
+ _st="$(printf '%s' "$_ln" | sed -n 's/.*"status":[[:space:]]*"\([^"]*\)".*/\1/p')"
116
+ _rs="$(printf '%s' "$_ln" | sed -n 's/.*"release_state":[[:space:]]*"\([^"]*\)".*/\1/p')"
117
+ printf ' %s status=%s release_state=%s\n' "${_id:-?}" "${_st:-?}" "${_rs:-?}"
118
+ done
119
+ else
120
+ printf ' (all board units complete — nothing in flight)\n'
121
+ fi
122
+ else
123
+ printf ' (board projection unavailable — degraded, not fatal)\n'
124
+ fi
125
+ else
126
+ printf ' (forge-board-render.sh not found — projection unavailable)\n'
127
+ fi
128
+
129
+ printf '\nScope: the slice list above is PER-MACHINE (this machine'\''s worktrees only).\n'
130
+ printf 'On this two-machine repo the git-synced board + fold are the cross-machine truth;\n'
131
+ printf 'a quiet slice list here does not mean nothing is in flight on the other machine.\n'
132
+
133
+ exit 0