forge-orkes 0.73.1 → 0.75.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.
@@ -80,16 +80,45 @@ mkdir -p "$wt_root"
80
80
  git worktree prune
81
81
  git worktree add -b forge/${anchor} --lock --reason "forge session m-${milestone_id}" "$wt_root/${anchor}" main
82
82
  wt="$wt_root/${anchor}"
83
- # Submodules don't auto-populate in a new worktree — init them if present. Reuses the
83
+
84
+ # --- Hydrate the worktree ---
85
+ # A git worktree is a clean checkout of *tracked* files only. Everything required but
86
+ # untracked — submodule working trees, gitignored config/secrets — is absent until we
87
+ # put it there. Hydration is that ordered step; submodule init is one instance of it.
88
+
89
+ # (1) Submodules don't auto-populate in a new worktree — init them if present. Reuses the
84
90
  # superproject's shared .git/modules object store (no re-clone, just a working-tree
85
91
  # checkout); ccache absorbs the subsequent rebuild cost across worktrees.
86
92
  if [ -n "$(git -C "$wt" submodule status 2>/dev/null)" ]; then
87
93
  ( cd "$wt" && git submodule update --init --recursive )
88
94
  fi
95
+
96
+ # (2) Required-but-gitignored artifacts (e.g. .mcp.json — MCP config, often secret-bearing;
97
+ # .env.local; experimental hook configs) named in orchestration.worktree_hydrate. A clean
98
+ # checkout lacks them, and the failure is SILENT: MCP servers that never appear, an env-
99
+ # dependent command that misbehaves. Copy each from the superproject, guarded source-exists
100
+ # && dest-absent → never overwrites, and we NEVER `git add` a hydrated path: it stays
101
+ # gitignored/untracked in both trees, so a secret like .mcp.json never enters git history.
102
+ sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
103
+ | sed -n '/^[[:space:]]*worktree_hydrate:/,/^[[:space:]]*[a-z_]*:/p' \
104
+ | sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d "\"'" \
105
+ | while IFS= read -r rel; do # line-based: a path may contain spaces; robust across sh/bash/zsh
106
+ [ -n "$rel" ] || continue
107
+ [ -e "$repo_root/$rel" ] || continue # source-exists guard (no-op when nothing applies)
108
+ [ -e "$wt/$rel" ] && continue # dest-absent guard (idempotent — never clobbers)
109
+ mkdir -p "$(dirname "$wt/$rel")"
110
+ cp -p "$repo_root/$rel" "$wt/$rel"
111
+ done
112
+
113
+ # (3) Dependency install (node_modules etc.) is DEFERRED to the executing/verifying preflight
114
+ # on purpose — Forge core owns no package-manager concept, and the node_modules desire path is
115
+ # already handled there. That is where the false-green `tsc` class (a no-op tsc against a missing
116
+ # node_modules reporting green) is closed — not here.
117
+
89
118
  ( cd "$wt" && git hook run pre-commit || true )
90
119
  ```
91
120
 
92
- Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty. On failure → cleanup partial state, refuse mode.
121
+ Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty and each configured `worktree_hydrate` artifact that exists in the superproject is present in the worktree. On failure → cleanup partial state, refuse mode.
93
122
 
94
123
  `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
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.73.1",
3
+ "version": "0.75.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"
@@ -249,10 +249,27 @@ compute_id() {
249
249
  # Hyphen is OPTIONAL in the token ([A-Z]+-?[0-9]+): refactor ids (R100) carry none
250
250
  # (ADR-023), so requiring a hyphen would hide them and drop the floor to 0. Split the
251
251
  # alpha prefix from the digits in awk so R does not match e.g. FR (anchor ^prefix$).
252
- main_max="$(git show main:.forge/reservations.yml 2>/dev/null \
253
- | grep -oE '[A-Z]+-?[0-9]+' \
254
- | awk -v p="$prefix" '{ a=$0; sub(/[-0-9].*$/,"",a); d=$0; gsub(/[^0-9]/,"",d);
255
- if(a==p){ n=d+0; if(n>m)m=n } } END{print m+0}')"
252
+ #
253
+ # READ BOTH main AND origin/main (#26): in the local-main-first workflow (ADR-010 —
254
+ # primary checkout parked on main, all work in worktrees reaching main via PRs), local
255
+ # `main` routinely lags origin/main by whatever merged since the last fast-forward, so a
256
+ # local-only floor hands out ids origin/main already claimed — #19's collision class,
257
+ # relocated to the one input that still isn't machine-shared. Best-effort-fetch origin
258
+ # first, then take the max across whichever of {main, origin/main} resolve. Every leg
259
+ # degrades to nothing offline / with no remote (the same posture the rest of the script
260
+ # takes) — a fetch failure or a missing ref is never fatal, it just contributes 0.
261
+ # FORGE_RESERVE_NO_FETCH=1 skips the fetch (fixtures with a local file:// origin want the
262
+ # deterministic ref state they set up, not a network round-trip).
263
+ ref_reservations_max() { # $1 = git ref → max reserved id for $prefix on that ref (0 if absent)
264
+ git show "$1:.forge/reservations.yml" 2>/dev/null \
265
+ | grep -oE '[A-Z]+-?[0-9]+' \
266
+ | awk -v p="$prefix" '{ a=$0; sub(/[-0-9].*$/,"",a); d=$0; gsub(/[^0-9]/,"",d);
267
+ if(a==p){ n=d+0; if(n>m)m=n } } END{print m+0}'
268
+ }
269
+ [ "${FORGE_RESERVE_NO_FETCH:-0}" = 1 ] || git fetch --quiet origin main 2>/dev/null || true
270
+ main_local_max="$(ref_reservations_max main)"; [ -n "$main_local_max" ] || main_local_max=0
271
+ main_origin_max="$(ref_reservations_max origin/main)"; [ -n "$main_origin_max" ] || main_origin_max=0
272
+ main_max="$(awk -v a="$main_local_max" -v b="$main_origin_max" 'BEGIN{print (a>b)?a:b}')"
256
273
  [ -n "$main_max" ] || main_max=0
257
274
 
258
275
  next="$(awk -v a="$ledger_max" -v b="$intree_max" -v c="$main_max" \
@@ -285,6 +285,98 @@ else
285
285
  fail "case10b --days 1 dropped the kind's max row"
286
286
  fi
287
287
 
288
+ # --- Case 11: origin/main floor beats a lagging local main (#26) -------------
289
+ # The local-main-first collision: origin/main carries a MERGED higher claim
290
+ # (FR-300) that local main has not fast-forwarded to (local main still at FR-200).
291
+ # A local-main-only floor would hand out FR-201 — an id origin/main already used.
292
+ # The fix reads across {main, origin/main}, so the floor is FR-300 → next FR-301.
293
+ origin_repo="$(new_repo case11-origin)"
294
+ ( cd "$origin_repo"
295
+ mkdir -p .forge
296
+ cat > .forge/reservations.yml <<'YML'
297
+ reservations:
298
+ - kind: fr
299
+ id: FR-300
300
+ milestone: m-sibling
301
+ reserved_at: "2026-02-01"
302
+ summary: "merged on origin/main, ahead of local main"
303
+ YML
304
+ git add -A && git commit -qm "origin has FR-300" )
305
+ # Clone it (clone's origin → the seed repo), then rewind the clone's LOCAL main to
306
+ # an earlier commit carrying only FR-200 — simulating a primary checkout parked on a
307
+ # stale main while sibling work merged FR-300 to origin.
308
+ clone="$ROOT/repo.case11-clone"
309
+ git clone -q "$origin_repo" "$clone"
310
+ cd "$clone"
311
+ git config user.email t@example.com; git config user.name tester
312
+ # Build a divergent local main: reset to an orphan-free earlier state with FR-200.
313
+ cat > .forge/reservations.yml <<'YML'
314
+ reservations:
315
+ - kind: fr
316
+ id: FR-200
317
+ milestone: m-x
318
+ reserved_at: "2026-01-01"
319
+ summary: "local main lags"
320
+ YML
321
+ git add -A && git commit -qm "local main at FR-200 (behind origin/main FR-300)"
322
+ # origin/main (remote-tracking) still points at FR-300; local main is now at FR-200.
323
+ # Allow the helper to fetch the local file:// origin (deterministic, no network).
324
+ got="$(FORGE_RESERVE_NO_FETCH=1 "$HELPER" fr --milestone m-x)"
325
+ check "case11 origin/main floor (FR-300) beats lagging local main (FR-200) -> FR-301" "$got" "FR-301"
326
+ # Negative control: with the origin arm blind (simulate no remote-tracking ref by
327
+ # reading a repo that has NO origin at all), the floor falls back to local main only.
328
+ d="$(new_repo case11-noorigin)"; cd "$d"
329
+ mkdir -p .forge
330
+ cat > .forge/reservations.yml <<'YML'
331
+ reservations:
332
+ - kind: fr
333
+ id: FR-200
334
+ milestone: m-x
335
+ reserved_at: "2026-01-01"
336
+ summary: "no origin — local main is the only floor"
337
+ YML
338
+ git add -A && git commit -qm init
339
+ rm .forge/reservations.yml
340
+ got="$(FORGE_RESERVE_NO_FETCH=1 "$HELPER" fr --milestone m-x)"
341
+ check "case11 no-origin repo still floors on local main (FR-200) -> FR-201" "$got" "FR-201"
342
+
343
+ # --- Case 12: the fetch refreshes a STALE origin/main tracking ref (#26) ------
344
+ # The end-to-end proof: a sibling merges a higher claim to origin AFTER this clone
345
+ # was made, so the clone's origin/main remote-tracking ref is stale. Without the
346
+ # best-effort fetch the floor would miss it; with the fetch (default, NOT skipped)
347
+ # the helper refreshes origin/main and floors correctly. Uses a local file:// origin
348
+ # so the "fetch" is a deterministic local operation, no network.
349
+ seed="$(new_repo case12-seed)"
350
+ ( cd "$seed"
351
+ mkdir -p .forge
352
+ cat > .forge/reservations.yml <<'YML'
353
+ reservations:
354
+ - kind: fr
355
+ id: FR-200
356
+ milestone: m-x
357
+ reserved_at: "2026-01-01"
358
+ summary: "seed FR-200"
359
+ YML
360
+ git add -A && git commit -qm "seed FR-200" )
361
+ clone2="$ROOT/repo.case12-clone"
362
+ git clone -q "$seed" "$clone2"
363
+ # Sibling advances origin to FR-400 AFTER the clone → clone's origin/main is now stale.
364
+ ( cd "$seed"
365
+ cat > .forge/reservations.yml <<'YML'
366
+ reservations:
367
+ - kind: fr
368
+ id: FR-400
369
+ milestone: m-sibling
370
+ reserved_at: "2026-03-01"
371
+ summary: "merged to origin after the clone"
372
+ YML
373
+ git add -A && git commit -qm "origin advances to FR-400" )
374
+ cd "$clone2"
375
+ git config user.email t@example.com; git config user.name tester
376
+ # Do NOT set FORGE_RESERVE_NO_FETCH — the fetch is exactly what we are testing.
377
+ got="$("$HELPER" fr --milestone m-x)"
378
+ check "case12 fetch refreshes stale origin/main (FR-400) -> FR-401" "$got" "FR-401"
379
+
288
380
  # --- Report -----------------------------------------------------------------
289
381
  printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
290
382
  [ "$FAILED" -eq 0 ]
@@ -36,6 +36,29 @@ fi
36
36
 
37
37
  Surface the init if it ran (*"populated N uninitialised submodule(s) before baseline"*). A `+`/`U` line (divergent/conflicted submodule) is **not** an init case — STOP under Rule 4; it signals real submodule movement. (Desire-path: canvaz m-TIDES01 — empty `external/JUCE` failed the first CMake configure mid-task.)
38
38
 
39
+ **Gitignored artifacts.** A fresh worktree is a clean checkout of *tracked* files only. Anything gitignored but required — `.mcp.json` (MCP server config, often secret-bearing so it MUST stay gitignored), `.env.local`, experimental hook configs — is simply **absent**, and the failure is silent: MCP servers that never appear, an env-dependent command that misbehaves — no clear "missing config" error. `orchestrating` hydrates these when *it* creates the worktree, but a worktree from any other origin (a manual `git worktree add`, a `chief-of-staff` stream, a fresh clone) does not. So assert it here, regardless of how the workspace was created. Advisory and guarded — a clean no-op when `orchestration.worktree_hydrate` is unset or nothing applies, so existing installs are unaffected:
40
+
41
+ ```bash
42
+ # Copy required-but-gitignored artifacts named in orchestration.worktree_hydrate from the
43
+ # superproject (the MAIN checkout, resolved from the shared git common dir — this assertion
44
+ # runs INSIDE the worktree), guarded source-exists && dest-absent. Never overwrites, never
45
+ # git-adds — the file stays gitignored/untracked in both trees (a secret like .mcp.json never
46
+ # enters git history). No-op when the key is absent or nothing applies.
47
+ repo_root=$(git rev-parse --show-toplevel)
48
+ super=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's#/\.git/modules/.*##;s#/\.git/worktrees/.*##;s#/\.git$##')
49
+ sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
50
+ | sed -n '/^[[:space:]]*worktree_hydrate:/,/^[[:space:]]*[a-z_]*:/p' \
51
+ | sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d "\"'" \
52
+ | while IFS= read -r rel; do # line-based: robust across sh/bash/zsh; a path may contain spaces
53
+ [ -n "$rel" ] && [ -n "$super" ] && [ "$super" != "$repo_root" ] || continue # no key / main checkout → no-op
54
+ [ -e "$super/$rel" ] && [ ! -e "$repo_root/$rel" ] || continue # source-exists && dest-absent
55
+ mkdir -p "$(dirname "$repo_root/$rel")"; cp -p "$super/$rel" "$repo_root/$rel"
56
+ echo "hydrated: $rel"
57
+ done
58
+ ```
59
+
60
+ The loop prints one `hydrated: {path}` line per artifact it copied — surface those before the baseline (*"hydrated N gitignored artifact(s) before baseline"*), mirroring the submodule surface line; no output means nothing needed hydrating. The `$super != $repo_root` guard makes this a natural no-op in a non-worktree (main) checkout — where superproject and checkout are the same tree and there is nothing to hydrate from.
61
+
39
62
  ## Baseline Snapshot
40
63
 
41
64
  Run **before the first task begins**. Makes failure causality mechanical — no self-assessment.
@@ -15,6 +15,7 @@ Read these before acting on any intake. Jarvis **relays and triggers — it neve
15
15
 
16
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
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
+ - **UAT is the human's act — Jarvis never stands in for it.** UAT = the **operator** tests the work locally, with their own eyes and hands. An agent's browser/Playwright run, an e2e suite, or green CI is *automated verification* — necessary, **never sufficient, and never called UAT**. Agents are not users of the system, so "the agent tested it" can never satisfy UAT. The invariant this door holds: **a PR exists only after the operator's explicit in-session UAT confirmation** — there is **no agent-attestation path to PR-open**. This is Forge's Human Verification Gate (`in_session_signoff`) applied at the **merge boundary** instead of milestone-close. The failure mode it refuses: a session self-attesting "Real browser UAT" in a PR body and auto-merging on green CI — the agent's own run is not the human's acceptance. If you catch yourself about to open a PR because *you* checked the work, STOP: that is the gate this door exists to hold.
18
19
  - **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
20
  - **No new notification infrastructure.** PushNotification + the Monitor `tail -F` + the runner's existing log. No queue, no broker, no third-party service.
20
21
  - **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.
@@ -89,24 +90,56 @@ When a watched log grows, pipe it back through the relay core — do **not** buz
89
90
  `--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:
90
91
 
91
92
  - an **ambiguity** reaches the phone as the question verbatim (it is a question, not a claim);
92
- - 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);
93
+ - a **`done`** means **"ready for the operator's local UAT"** — the slice's work is built in its worktree and **no PR exists yet**. It is a *cue to UAT*, not a merge claim; relay it as such and hand off to the UAT gate (below). A payload that claims a merge or a PR at `done` contradicts the flow and is relayed as a **loud discrepancy** (host truth winsand under this flow the host shows no PR at `done`);
94
+ - a **halt** is phrased from the host's `release_state`, named as a halt;
93
95
  - **fork / irreversible / budget** relay verbatim, named.
94
96
 
95
97
  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.
96
98
 
99
+ ### The UAT gate (post-`done`) — no PR until the operator accepts
100
+
101
+ A `done` ping is the cue that the work is ready for the operator's **local UAT**, not that it shipped. There is no PR yet, and Jarvis opens one **only** after the operator's explicit in-session confirmation (the hard boundary in "What Jarvis is NOT"). On a relayed `done`, run this sequence **in order** — never skip ahead to a PR:
102
+
103
+ 1. **Spin up the slice's local dev server** so the operator can actually try it. Read the project's dev-lane command from `project.yml`:
104
+
105
+ ```sh
106
+ # jarvis.dev_lane_cmd — the stack-specific command that starts THIS slice's local dev server.
107
+ # Run it in the slice worktree (the work under test lives there), detached like the runner.
108
+ ```
109
+
110
+ If `jarvis.dev_lane_cmd` is **set**, run it in the slice worktree and tell the operator where it's up. If it is **absent**, do not fail — tell the operator to start their dev lane manually for this worktree (Forge owns no dev-server concept; the "how" is the project's, per FR-248). Either way, name the worktree so the operator knows what they're testing.
111
+ 2. **Relay "ready for your UAT."** Buzz the operator: the work is built and the local server is up (or ask them to start it) — *"ready for your UAT on the local server."* This is a cue to the human, phrased as such.
112
+ 3. **Wait for the operator's explicit confirmation.** Do nothing irreversible until the operator says, in session, that they've tested it and it's good (a plain "looks good" / "ship it" / "approved"). **Your own browser run, a passing e2e suite, or green CI never substitutes** — see the UAT boundary. No confirmation → the slice sits; you do not open a PR.
113
+ 4. **On explicit confirmation only — open the PR and arm the merge.** This is a **trigger on the operator's word**, not an approval Jarvis grants (the "relays and triggers, never decides" boundary):
114
+
115
+ ```sh
116
+ gh pr create --fill --head <slice-branch> # open the PR now that the human has accepted
117
+ gh pr merge <num> --auto --squash # arm auto-merge; CI + branch protection gate the actual merge
118
+ ```
119
+
120
+ Then, and only then, arm the come-try watch on that PR number (below). The order is load-bearing: dev lane → ready-for-UAT → **explicit confirmation** → PR-open + arm merge → come-try. A PR that exists before step 3 is the failure this gate refuses.
121
+
122
+ **On UAT failure — findings become an answer, and the slice re-dispatches.** If the operator says UAT failed, their findings are the fix instructions: land them as the runner's answer for the slice's `done` phase and re-dispatch, exactly like a parked-ambiguity answer (below) — no PR is opened, no new machinery:
123
+
124
+ ```sh
125
+ .forge/checks/forge-jarvis-answer.sh --work-dir <dir> --phase <N> --slice <worktree> --answer "<operator's UAT findings>"
126
+ ```
127
+
128
+ The runner resumes the slice with the findings in hand and fixes them; the next `done` re-enters this same UAT gate. (`<N>` is the slice's final/`done` phase — the one the operator just tested.)
129
+
97
130
  ### The come-try tap and the answer round-trip
98
131
 
99
132
  Two moments need a second source beyond the log line:
100
133
 
101
- - **"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:
134
+ - **"Merged, on staging — come try."** After the operator has accepted the work and Jarvis has opened + armed the PR (the UAT gate, above), the step-2 auto-merge and the staging deploy land minutes later — that moment is never in the log. So **once the PR is open (post-UAT), never at `done`**, arm the bounded watch on that PR number:
102
135
 
103
136
  ```sh
104
137
  .forge/checks/forge-jarvis-prwatch.sh --pr <num> [--staging-env <name>]
105
138
  ```
106
139
 
107
- 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.
140
+ 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. The come-try moment therefore lands *after* the operator's own acceptance, so nothing reaches staging unverified.
108
141
 
109
- - **Park → answer → resume.** When the operator answers a parked ambiguity (typed or from the phone), close the loop through the runner's own convention:
142
+ - **Park → answer → resume.** When the operator answers a parked ambiguity (typed or from the phone) — or supplies UAT-failure findings (the UAT gate, above) — close the loop through the runner's own convention:
110
143
 
111
144
  ```sh
112
145
  .forge/checks/forge-jarvis-answer.sh --work-dir <dir> --phase <N> --slice <worktree> --answer "<reply>"
@@ -205,3 +238,5 @@ B3 delivered intake → spec → detached dispatch; B4 added the callback relay;
205
238
  - **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).
206
239
 
207
240
  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.
241
+
242
+ **m-48 — UAT-gated dispatch** (later refinement, 0.74.0): the callback relay's back-half was re-sequenced so a `done` ping means "ready for the operator's local UAT" (no PR yet), Jarvis spins up the slice's local dev server (`jarvis.dev_lane_cmd`), waits for the operator's **explicit in-session UAT confirmation**, and only then triggers PR-open + arm merge — with the come-try PR watch re-armed *after* that operator-confirmed PR-open, and UAT failures routed back through the answer-file re-dispatch. The hard boundary — UAT is the human's act, no agent-attestation path to PR-open — lives in "What Jarvis is NOT". Nothing reaches staging unverified.
@@ -59,6 +59,8 @@ Where worktrees live and what they're named — single source of truth, base con
59
59
 
60
60
  **Recorded path is authoritative once set** — `lifecycle.worktree_path` (milestone) / `runtime.worktree` (stream) is read verbatim, never re-derived; changing `worktree_root` affects only new worktrees; move a live worktree only with `git worktree move` + update the record. No recorded path → resolve from the convention. `forge` boot and `chief-of-staff` adoption **validate advisorily**: recorded path outside the resolved root → warn (never block, never move); an explicit `worktree_root` is honored.
61
61
 
62
+ **`orchestration.worktree_hydrate`** (optional, `project.yml`; forge#18) lists superproject-relative paths to required-but-gitignored artifacts (e.g. `.mcp.json`) a fresh worktree's tracked-only checkout lacks. Copy is guarded `source-exists && dest-absent` — never overwrites, never `git add`s, so the secret stays untracked in both trees. Applied by `orchestrating` at creation and re-asserted idempotently in `executing`'s Workspace Prerequisites, covering worktrees of any origin. Omit ⇒ no propagation. The per-worktree runtime counterpart of the install-time carve-out propagation below (State Ownership § Stable shared).
63
+
62
64
  ## Workflow Tiers
63
65
 
64
66
  Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
@@ -188,7 +190,7 @@ Quick tier skips init.
188
190
 
189
191
  State lives in `.forge/`. One line per artifact; protocol detail lives in the named ADRs and skills:
190
192
 
191
- - `project.yml` — vision, stack, design system, verification, constraints (<5 KB)
193
+ - `project.yml` — vision, stack, design system, verification, constraints (<5 KB). Optional `jarvis.dev_lane_cmd` (m-48) is the stack-agnostic seam Jarvis runs to spin up a slice's local dev server for operator UAT before a PR opens; absent ⇒ manual lane start. See `docs/jarvis.md`.
192
194
  - `constitution.md` — active architectural gates
193
195
  - `design-system.md` — component mapping table
194
196
  - `requirements/m{N}.yml` — per-milestone structured requirements with `[NEEDS CLARIFICATION]` markers. **FR/DEF/NFR ids are globally unique across all milestone files** — reserve via the ID Reservation Protocol before writing. M9 e2e gate fields (`e2e`, `observable_outcome`, `validated`, hash) lazy-migrate; absent ⇒ `false`.
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env sh
2
2
  # forge-jarvis-prwatch.sh — the bounded "merged, on staging — come try" watch
3
- # (m-37, Brief-R2 step 4 B4(c); contract handoff-step4-jarvis.md REV2).
3
+ # (m-37, Brief-R2 step 4 B4(c); contract handoff-step4-jarvis.md REV2; re-sequenced m-48).
4
4
  #
5
- # The runner's `done` ping fires at PR-open; step-2 auto-merge + the staging deploy land minutes
6
- # LATER, so that flagship "come try it" moment never appears in the notify log. This watch is the
7
- # SECOND bounded source for it: on a `done`, poll `gh` for the PR's merge + staging-deploy state
8
- # with a HARD bound and, on merge+deploy, emit the come-try text once.
5
+ # Under UAT-gated dispatch (m-48) a PR is opened by Jarvis ONLY after the operator's explicit
6
+ # in-session UAT confirmation never at the runner's `done` (which now means "ready for the
7
+ # operator's local UAT", no PR yet). This watch is armed on THAT post-UAT PR: step-2 auto-merge +
8
+ # the staging deploy land minutes LATER, so the flagship "come try it" moment never appears in the
9
+ # notify log. On the opened PR, poll `gh` for its merge + staging-deploy state with a HARD bound
10
+ # and, on merge+deploy, emit the come-try text once. Because it arms only after the operator has
11
+ # accepted the work, nothing reaches staging unverified.
9
12
  #
10
13
  # STRICTLY BOUNDED + self-terminating (NFR-036 / Fork 3): it stops on merge+deploy, on a closed
11
14
  # PR, or when the iteration/wall bound is hit — it NEVER becomes a standing daemon and owns no
@@ -99,6 +99,20 @@ verification:
99
99
  # # only — the private key lives host-side, in the deploy Environment).
100
100
  # # Empty ⇒ git config gpg.ssh.allowedSignersFile.
101
101
 
102
+ # orchestration: # OPTIONAL — worktree behavior (M10 / streams). Absent → defaults.
103
+ # worktree_root: "" # Where Forge worktrees live. Default ../<repo-basename>-worktrees.
104
+ # # Relative → resolves against repo root; absolute / ~ honored verbatim.
105
+ # worktree_hydrate: [] # OPTIONAL list of superproject-relative paths — required-but-gitignored
106
+ # # artifacts copied into a fresh worktree so it can build and reach its
107
+ # # tools (a clean worktree checks out TRACKED files only). Guarded
108
+ # # source-exists && dest-absent: never overwrites, never git-adds, so a
109
+ # # secret-bearing file stays gitignored/untracked in both trees and never
110
+ # # enters git history (.mcp.json the canonical case). Applied by
111
+ # # orchestrating at creation AND asserted in executing's Workspace
112
+ # # Prerequisites → worktrees of ANY origin covered. Omit → no propagation.
113
+ # # e.g. [".mcp.json", ".env.local"]. Dependency install stays with the
114
+ # # executing/verifying preflight, not this key.
115
+
102
116
  success_criteria: # How do we know we're done?
103
117
  - "" # e.g., "User can create and edit posts"
104
118
  - "" # e.g., "All tests pass with >80% coverage"
@@ -154,6 +168,12 @@ models:
154
168
  # # no flag (platform default). TRUST WARNING: bypassPermissions means
155
169
  # # phone-spawned sessions act without prompts — opt in only where the
156
170
  # # phone lane carries the same trust as an attended terminal.
171
+ # dev_lane_cmd: "" # OPTIONAL. Shell command Jarvis runs (in the slice worktree) to spin up
172
+ # # the LOCAL dev server for the operator's UAT after a `done` ping — the
173
+ # # UAT-gated dispatch seam (m-48). Stack-specific: Forge owns no dev-server
174
+ # # concept, so the project supplies the "how" (e.g. its `dev:all` lane).
175
+ # # Absent → Jarvis tells the operator to start the lane manually (no failure).
176
+ # # The PR opens only AFTER the operator's explicit in-session UAT — never here.
157
177
 
158
178
  # attribution: # OPT-IN Driven-by attribution (hold-waiver patch) — default OFF.
159
179
  # driven_by: true # When true, the merge-floor check-quality lint