forge-orkes 0.72.2 → 0.74.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.
- package/bin/create-forge.js +6 -4
- package/experimental/m10/source/mcp-server/README.md +2 -2
- package/experimental/m10/source/mcp-server/index.js +2 -2
- package/experimental/m10/source/skills/orchestrating/SKILL.md +7 -3
- package/package.json +1 -1
- package/template/.claude/hooks/forge-reserve.sh +367 -166
- package/template/.claude/hooks/tests/forge-reserve.test.sh +169 -0
- package/template/.claude/skills/chief-of-staff/SKILL.md +1 -0
- package/template/.claude/skills/forge/SKILL.md +8 -6
- package/template/.claude/skills/jarvis/SKILL.md +39 -4
- package/template/.forge/FORGE.md +4 -4
- package/template/.forge/checks/forge-check-lint.sh +12 -3
- package/template/.forge/checks/forge-hold-check.sh +2 -0
- package/template/.forge/checks/forge-jarvis-awareness.sh +62 -11
- package/template/.forge/checks/forge-jarvis-dispatch.sh +25 -1
- package/template/.forge/checks/forge-jarvis-prwatch.sh +8 -5
- package/template/.forge/checks/forge-jarvis-relay.sh +15 -6
- package/template/.forge/checks/tests/forge-jarvis-awareness.test.sh +53 -0
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +50 -0
- package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +30 -0
- package/template/.forge/templates/project.yml +6 -0
- package/template/.forge/templates/state/desire-path.yml +15 -45
|
@@ -116,6 +116,175 @@ check "case6 $N concurrent runs -> $N distinct ids" "$distinct" "$N"
|
|
|
116
116
|
ledger_lines="$(grep -c '^fr ' .git/forge/id-reservations)"
|
|
117
117
|
check "case6 $N distinct ledger fr lines" "$ledger_lines" "$N"
|
|
118
118
|
|
|
119
|
+
# --- Case 7: stale-lock break — identity unchanged -> break + clean re-acquire
|
|
120
|
+
# (R103). A foreign owner stamp sits in a lock dir backdated past STALE with
|
|
121
|
+
# nothing racing to change it: the breaker's re-read-before-rmdir sees the same
|
|
122
|
+
# stamp it saw when it judged the lock stale, so it proceeds to break it and
|
|
123
|
+
# allocate normally.
|
|
124
|
+
d="$(new_repo case7)"; cd "$d"
|
|
125
|
+
git commit --allow-empty -qm init
|
|
126
|
+
lockdir=".git/forge/id-reservations.lock"
|
|
127
|
+
mkdir -p "$lockdir"
|
|
128
|
+
printf '999999 1700000000\n' > "$lockdir/owner"
|
|
129
|
+
touch -t 202001010000 "$lockdir" 2>/dev/null || touch -d '2020-01-01' "$lockdir" 2>/dev/null || true
|
|
130
|
+
out="$(FORGE_RESERVE_STALE=1 FORGE_RESERVE_WAIT=3 "$HELPER" adr --milestone m-x 2>case7-err.log)"; rc=$?
|
|
131
|
+
check "case7 stale lock (unchanged owner) is broken and allocation succeeds" "$out" "ADR-001"
|
|
132
|
+
check "case7 stale-break exits 0" "$rc" "0"
|
|
133
|
+
if grep -qi 'stale' case7-err.log; then
|
|
134
|
+
pass "case7 stderr reports the stale-lock break"
|
|
135
|
+
else
|
|
136
|
+
fail "case7 stderr missing stale-lock break message"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# --- Case 8: stale-lock break — identity refreshed mid-wait -> no break ------
|
|
140
|
+
# (R103). A lock dir is backdated past STALE (so every poll judges it stale by
|
|
141
|
+
# mtime), but a background racer continuously rewrites the owner stamp inside
|
|
142
|
+
# it — simulating the original holder renewing the lock. The breaker's
|
|
143
|
+
# re-read-before-rmdir must never see a stable (seen == now) pair, so it never
|
|
144
|
+
# tears the lock down; it falls through to the bounded wait and exits 3.
|
|
145
|
+
# Racy-but-reliable, same style as case6's real concurrency: the writer refreshes
|
|
146
|
+
# far faster than the breaker's poll interval, so cumulative miss probability
|
|
147
|
+
# across the whole wait window is negligible.
|
|
148
|
+
d="$(new_repo case8)"; cd "$d"
|
|
149
|
+
git commit --allow-empty -qm init
|
|
150
|
+
lockdir=".git/forge/id-reservations.lock"
|
|
151
|
+
mkdir -p "$lockdir"
|
|
152
|
+
printf 'racer-0\n' > "$lockdir/owner"
|
|
153
|
+
touch -t 202001010000 "$lockdir" 2>/dev/null || touch -d '2020-01-01' "$lockdir" 2>/dev/null || true
|
|
154
|
+
( i=0
|
|
155
|
+
while [ -d "$lockdir" ] && [ "$i" -lt 200000 ]; do
|
|
156
|
+
printf 'racer-%s\n' "$i" > "$lockdir/owner" 2>/dev/null
|
|
157
|
+
i=$((i + 1))
|
|
158
|
+
done
|
|
159
|
+
) &
|
|
160
|
+
writer_pid=$!
|
|
161
|
+
out="$(FORGE_RESERVE_STALE=1 FORGE_RESERVE_WAIT=1 "$HELPER" adr --milestone m-x 2>case8-err.log)"; rc=$?
|
|
162
|
+
kill "$writer_pid" 2>/dev/null
|
|
163
|
+
wait "$writer_pid" 2>/dev/null
|
|
164
|
+
check "case8 continuously-refreshed lock is never broken (no allocation)" "$out" ""
|
|
165
|
+
check "case8 exits with lock-timeout (3)" "$rc" "3"
|
|
166
|
+
[ -d "$lockdir" ] && pass "case8 lock dir still stands (never rmdir'd)" || fail "case8 lock dir was removed"
|
|
167
|
+
|
|
168
|
+
# --- Case 9: compact — floor preserved, archive verbatim, idempotent (R104) --
|
|
169
|
+
# One kind (fr) with three rows: an OLD low id (should archive), an OLD row
|
|
170
|
+
# that happens to be the kind's MAX id (kept regardless of age — this is the
|
|
171
|
+
# row the floor needs), and a RECENT low id (kept via the retention window,
|
|
172
|
+
# not via max). Commit-then-remove-from-tree (same trick as case 3) proves the
|
|
173
|
+
# post-compaction file still yields the correct arm (c) floor: a subsequent
|
|
174
|
+
# reserve allocates immediately above the pre-compaction max, never reusing an
|
|
175
|
+
# archived id. A second compact run must archive nothing further.
|
|
176
|
+
d="$(new_repo case9)"; cd "$d"
|
|
177
|
+
mkdir -p .forge
|
|
178
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
179
|
+
cat > .forge/reservations.yml <<YML
|
|
180
|
+
reservations:
|
|
181
|
+
- kind: fr
|
|
182
|
+
id: FR-050
|
|
183
|
+
milestone: m-x
|
|
184
|
+
reserved_at: "2020-01-01T00:00:00Z"
|
|
185
|
+
summary: "old, not max -> archive"
|
|
186
|
+
- kind: fr
|
|
187
|
+
id: FR-300
|
|
188
|
+
milestone: m-x
|
|
189
|
+
reserved_at: "2020-06-01T00:00:00Z"
|
|
190
|
+
summary: "old but MAX -> keep"
|
|
191
|
+
- kind: fr
|
|
192
|
+
id: FR-105
|
|
193
|
+
milestone: m-x
|
|
194
|
+
reserved_at: "$now"
|
|
195
|
+
summary: "recent, not max -> keep"
|
|
196
|
+
YML
|
|
197
|
+
git add -A && git commit -qm init
|
|
198
|
+
out="$("$HELPER" compact 2>compact-err.log)"; rc=$?
|
|
199
|
+
check "case9 compact exits 0" "$rc" "0"
|
|
200
|
+
if grep -q 'id: FR-300' .forge/reservations.yml; then
|
|
201
|
+
pass "case9 compact kept the old MAX row (FR-300)"
|
|
202
|
+
else
|
|
203
|
+
fail "case9 compact dropped the kind's max row"
|
|
204
|
+
fi
|
|
205
|
+
if grep -q 'id: FR-105' .forge/reservations.yml; then
|
|
206
|
+
pass "case9 compact kept the recent non-max row (FR-105)"
|
|
207
|
+
else
|
|
208
|
+
fail "case9 compact dropped a within-window row"
|
|
209
|
+
fi
|
|
210
|
+
if grep -q 'id: FR-050' .forge/reservations.yml; then
|
|
211
|
+
fail "case9 compact left the old non-max row (FR-050) in the working file"
|
|
212
|
+
else
|
|
213
|
+
pass "case9 compact removed the old non-max row (FR-050) from the working file"
|
|
214
|
+
fi
|
|
215
|
+
if grep -q 'id: FR-050' .forge/reservations-archive.yml && grep -q 'summary: "old, not max -> archive"' .forge/reservations-archive.yml; then
|
|
216
|
+
pass "case9 compact archived FR-050 byte-verbatim (id + summary intact)"
|
|
217
|
+
else
|
|
218
|
+
fail "case9 compact archive missing or corrupted the dropped FR-050 row"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
git add -A && git commit -qm "compact"
|
|
222
|
+
rm .forge/reservations.yml
|
|
223
|
+
got="$("$HELPER" fr --milestone m-x)"
|
|
224
|
+
check "case9 compact preserves the floor: next fr id is FR-301, not a reuse" "$got" "FR-301"
|
|
225
|
+
|
|
226
|
+
# Restore + idempotency: a second compact on the same (already-compacted)
|
|
227
|
+
# registry must archive nothing new.
|
|
228
|
+
git checkout -q .forge/reservations.yml
|
|
229
|
+
arch_lines_before="$(wc -l < .forge/reservations-archive.yml | tr -d ' ')"
|
|
230
|
+
"$HELPER" compact >/dev/null 2>compact-err2.log
|
|
231
|
+
arch_lines_after="$(wc -l < .forge/reservations-archive.yml | tr -d ' ')"
|
|
232
|
+
check "case9 second compact is a no-op (archive line count unchanged)" "$arch_lines_after" "$arch_lines_before"
|
|
233
|
+
if grep -qi 'kept 2, archived 0' compact-err2.log; then
|
|
234
|
+
pass "case9 second compact summary reports archived 0"
|
|
235
|
+
else
|
|
236
|
+
fail "case9 second compact summary did not report archived 0"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# --- Case 10: compact --days override tightens the retention window ---------
|
|
240
|
+
d="$(new_repo case10)"; cd "$d"
|
|
241
|
+
mkdir -p .forge
|
|
242
|
+
five_days_ago="$(date -u -v-5d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '-5 days' +%Y-%m-%dT%H:%M:%SZ)"
|
|
243
|
+
cat > .forge/reservations.yml <<YML
|
|
244
|
+
reservations:
|
|
245
|
+
- kind: nfr
|
|
246
|
+
id: NFR-001
|
|
247
|
+
milestone: m-x
|
|
248
|
+
reserved_at: "$five_days_ago"
|
|
249
|
+
summary: "5 days old, not max"
|
|
250
|
+
- kind: nfr
|
|
251
|
+
id: NFR-002
|
|
252
|
+
milestone: m-x
|
|
253
|
+
reserved_at: "$five_days_ago"
|
|
254
|
+
summary: "5 days old, MAX"
|
|
255
|
+
YML
|
|
256
|
+
git add -A && git commit -qm init
|
|
257
|
+
"$HELPER" compact --days 14 >/dev/null 2>&1
|
|
258
|
+
check "case10 --days 14 keeps a 5-day-old non-max row" \
|
|
259
|
+
"$(grep -c 'id: NFR-001' .forge/reservations.yml)" "1"
|
|
260
|
+
d="$(new_repo case10b)"; cd "$d"
|
|
261
|
+
mkdir -p .forge
|
|
262
|
+
cat > .forge/reservations.yml <<YML
|
|
263
|
+
reservations:
|
|
264
|
+
- kind: nfr
|
|
265
|
+
id: NFR-001
|
|
266
|
+
milestone: m-x
|
|
267
|
+
reserved_at: "$five_days_ago"
|
|
268
|
+
summary: "5 days old, not max"
|
|
269
|
+
- kind: nfr
|
|
270
|
+
id: NFR-002
|
|
271
|
+
milestone: m-x
|
|
272
|
+
reserved_at: "$five_days_ago"
|
|
273
|
+
summary: "5 days old, MAX"
|
|
274
|
+
YML
|
|
275
|
+
git add -A && git commit -qm init
|
|
276
|
+
"$HELPER" compact --days 1 >/dev/null 2>&1
|
|
277
|
+
if grep -q 'id: NFR-001' .forge/reservations.yml; then
|
|
278
|
+
fail "case10b --days 1 kept a 5-day-old non-max row it should have archived"
|
|
279
|
+
else
|
|
280
|
+
pass "case10b --days 1 archives a 5-day-old non-max row"
|
|
281
|
+
fi
|
|
282
|
+
if grep -q 'id: NFR-002' .forge/reservations.yml; then
|
|
283
|
+
pass "case10b --days 1 still keeps the kind's max row"
|
|
284
|
+
else
|
|
285
|
+
fail "case10b --days 1 dropped the kind's max row"
|
|
286
|
+
fi
|
|
287
|
+
|
|
119
288
|
# --- Report -----------------------------------------------------------------
|
|
120
289
|
printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
|
|
121
290
|
[ "$FAILED" -eq 0 ]
|
|
@@ -227,6 +227,7 @@ Resume:
|
|
|
227
227
|
4. **Relocate into the stream worktree (Native Worktree Entry).** When the resumed stream's file records a `runtime.worktree`, apply the canonical Native Worktree Entry contract (identical to the forge ownership-gate option 1):
|
|
228
228
|
- **If the `EnterWorktree` tool is available**, call `EnterWorktree({path: {runtime.worktree}})` to relocate this session into the stream worktree **in place**, then log `Relocated into worktree {runtime.worktree} ({runtime.branch})` (independent of any prompt, so the cwd move is visible) and continue the resume from inside the worktree.
|
|
229
229
|
- **If `EnterWorktree` is unavailable** (older Claude Code CLI, or a non-Claude-Code adapter), fall back to printing the `cd {path}` instruction (here `cd {runtime.worktree}`) — today's implicit behavior made explicit.
|
|
230
|
+
- **On an EnterWorktree error/rejection** (not just tool absence) — e.g. a worktree→worktree relocation rejects the call under the tool's path constraint (ADR-010 roots live outside `.claude/worktrees/`) — fall back to the same printed `cd` instruction above, exactly as if the tool were unavailable.
|
|
230
231
|
- Streams with no `runtime.worktree` resume unchanged — no relocation attempted. Exit is always `ExitWorktree({action: "keep"})`; the Chief never lets the native tool remove a worktree (teardown/merge stays the Chief's existing flow).
|
|
231
232
|
5. Run conflict detection before continuing.
|
|
232
233
|
|
|
@@ -157,6 +157,7 @@ Present the four resolutions and wait for an explicit choice:
|
|
|
157
157
|
1. **Enter the worktree** *(recommended)* — relocate this session into the worktree **in place** (the operator no longer has to `cd` + re-run). This is the canonical **Native Worktree Entry** contract, reused verbatim at the `chief-of-staff` resume and `orchestrating` hand-back touch points:
|
|
158
158
|
- **If the `EnterWorktree` tool is available**, call `EnterWorktree({path: {worktree_path}})` — always `{path}` into the recorded `lifecycle.worktree_path`, never `{name}` (Forge owns worktree creation; `{name}` would spawn a new one on the wrong branch convention). Then log one line — `Relocated into worktree {worktree_path} ({branch})` — **independent of any confirmation prompt**, so the cwd move is always visible in the transcript. Continue routing to the requested skill from inside the worktree: writes now land in the owner, and the ownership gate is satisfied because this session **is** the owner.
|
|
159
159
|
- **If `EnterWorktree` is unavailable** (older Claude Code CLI, or a non-Claude-Code adapter), fall back to today's behavior: print the exact command (`cd {worktree_path}` then `/forge`); re-running there anchors to this milestone and writes land in the owner. This session does nothing further to the milestone.
|
|
160
|
+
- **On an EnterWorktree error/rejection** (not just tool absence) — e.g. a worktree→worktree relocation rejects the call under the tool's path constraint (ADR-010 roots live outside `.claude/worktrees/`) — fall back to the same printed `cd` instruction above, exactly as if the tool were unavailable.
|
|
160
161
|
- Exit is always `ExitWorktree({action: "keep"})` — Forge never lets the native tool **remove** a worktree; teardown stays option 3 / `git worktree remove`.
|
|
161
162
|
2. **Sync, then continue in the worktree** — bring main's commits in (`git -C {worktree_path} rebase main`, or merge), then work from the worktree. Use when main has changes the worktree needs first.
|
|
162
163
|
3. **Retire the worktree** — only if it is dead/superseded. Close the stream, `git worktree remove {worktree_path}`, and clear `lifecycle.worktree_*` on the milestone — which ends its ownership so main may write. **Confirm before removing; never auto-remove a worktree.**
|
|
@@ -178,17 +179,18 @@ Check `.forge/refactor-backlog.yml`:
|
|
|
178
179
|
**Promotion procedure (effort: standard only):**
|
|
179
180
|
|
|
180
181
|
1. Read backlog item `id` (e.g. `R61-002`). Synthesize milestone id `m-{R-id}` (e.g. `m-R61-002`).
|
|
181
|
-
2. **
|
|
182
|
-
3.
|
|
182
|
+
2. **Re-check status before writing anything.** Re-read the item's current `status`/`promoted_to` from `.forge/refactor-backlog.yml` (the copy read for the "{N} refactors" prompt may be stale). If `status: in_progress` or `promoted_to` is already set, short-circuit: *"{R-id} already in_progress (promoted to {promoted_to})."* — no declaration, no milestone file, no roadmap append.
|
|
183
|
+
3. **Enter the worktree first (R10 — this runs in Step 1, *before* Step 3's routing gate fires, so do not assume a worktree already exists):** if this session is still on the main checkout (`git rev-parse --git-dir` == `--git-common-dir`), `EnterWorktree` now; if R10 already entered one, this is simply the first write in it. Then write the declaration (`.forge/templates/streams/stream.yml` → `.forge/streams/{stream}.yml` — `stream.goal`, `stream.milestone: m-{R-id}`, `stream.status: draft`, `stream.tier: standard`, `runtime.branch`/`runtime.worktree` from the worktree just entered, best-known `ownership.owned_surfaces`/`shared_surfaces`) as the worktree's first commit and fast-forward-push it, per FORGE.md's Stream Integration Checkpoints intake-declaration trigger.
|
|
184
|
+
4. Copy `.forge/templates/state/milestone.yml` → `.forge/state/milestone-{m-R-id}.yml`. Populate:
|
|
183
185
|
- `milestone.id: m-{R-id}`
|
|
184
186
|
- `milestone.name: "Promoted from {R-id}: {backlog item title}"`
|
|
185
187
|
- `milestone.origin: {R-id}`
|
|
186
188
|
- `current.tier: standard`, `current.status: researching`
|
|
187
189
|
- `current.last_updated: {ISO date}`
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
5. Run **Rollup (1.0)** — regenerates `index.yml` from the new milestone file (do not hand-append the registry entry). New milestone id is `m-{R-id}` from step 1; for numeric ids, allocate via `id=$(.claude/hooks/forge-reserve.sh milestone --milestone m-new)` (ADR-023) — never scan `milestone-*.yml` for max+1 inline. The three milestone-id call sites (here, `discussing`, `prototyping`) share this ledger-backed allocator so they cannot disagree, and it scans live **+ archived** milestones so a burned id is never reused.
|
|
191
|
+
6. Append to `.forge/roadmap.yml` — one milestone entry + one phase (`refactor-{R-id}`, `requirements_source: refactor-backlog.yml#{R-id}`, `dependencies: []`).
|
|
192
|
+
7. Update `.forge/refactor-backlog.yml` item: `status: in_progress`, add `promoted_to: m-{R-id}`.
|
|
193
|
+
8. Confirm: *"Promoted {R-id} → milestone m-{R-id}. State + roadmap created. Routing to researching."*
|
|
192
194
|
|
|
193
195
|
```text
|
|
194
196
|
backlog pickup → effort: standard
|
|
@@ -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
|
|
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 wins — and 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."**
|
|
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.
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -127,12 +127,12 @@ Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
|
|
|
127
127
|
| `streams/{stream}.yml` | 15 KB | Stream state stays resumable without becoming history |
|
|
128
128
|
| `streams/{stream}/brief.md` | 8 KB | Context packet must fit into active session context |
|
|
129
129
|
| `streams/{stream}/packages/{id}.yml` | 10 KB | Delegated work contract stays bounded |
|
|
130
|
-
| `reservations.yml` | 50 KB |
|
|
130
|
+
| `reservations.yml` | 50 KB | Compacts to `reservations-archive.yml` via `forge-reserve compact` — per-kind max + 14-day tail retained |
|
|
131
131
|
| `prototypes/{slug}.md` | 8 KB | Current question + terse iteration log; terminal items archive |
|
|
132
132
|
|
|
133
133
|
### Accumulating Artifacts
|
|
134
134
|
|
|
135
|
-
Every new accumulating artifact declares: owner, size gate, archive/compaction rule, `derived` vs source-of-truth status, and migration behavior. Defaults: `roadmap.yml`, `requirements/*.yml`, `releases.yml`, `reservations.yml` are source-of-truth — load scoped slices, not whole history. `phases/`, `audits/`, `research/`, `archive/`, `context-archive.md` are historical — load only by milestone/stream/package/version. `prototypes/{slug}.md` is single-owner mutable while `exploring|parked`; terminal statuses archive to `prototypes/archive/`. Runtime/generated files (`.mcp-server/*.db*`, logs, spike DBs, `.git/forge/*`) are excluded from framework context unless debugging them. The board (ADR-025): `.forge/notion/map.yml` is derived, git-ignored, rebuildable; board config is the `board:` block in `project.yml` (absent ⇒ inert). Template copies sync mechanically — not separate concepts.
|
|
135
|
+
Every new accumulating artifact declares: owner, size gate, archive/compaction rule, `derived` vs source-of-truth status, and migration behavior. Defaults: `roadmap.yml`, `requirements/*.yml`, `releases.yml`, `reservations.yml` are source-of-truth — load scoped slices, not whole history. `phases/`, `audits/`, `research/`, `archive/`, `context-archive.md`, `reservations-archive.yml` are historical — load only by milestone/stream/package/version (R104: `reservations-archive.yml` holds every row `forge-reserve compact` has dropped from `reservations.yml`, verbatim; the working file alone still carries every kind's floor). `prototypes/{slug}.md` is single-owner mutable while `exploring|parked`; terminal statuses archive to `prototypes/archive/`. Runtime/generated files (`.mcp-server/*.db*`, logs, spike DBs, `.git/forge/*`) are excluded from framework context unless debugging them. The board (ADR-025): `.forge/notion/map.yml` is derived, git-ignored, rebuildable; board config is the `board:` block in `project.yml` (absent ⇒ inert). Template copies sync mechanically — not separate concepts.
|
|
136
136
|
|
|
137
137
|
### Fresh Agent Pattern
|
|
138
138
|
Task touches 20+ files or complex subsystem → spawn fresh executor with isolated context. Prevents context rot.
|
|
@@ -188,7 +188,7 @@ Quick tier skips init.
|
|
|
188
188
|
|
|
189
189
|
State lives in `.forge/`. One line per artifact; protocol detail lives in the named ADRs and skills:
|
|
190
190
|
|
|
191
|
-
- `project.yml` — vision, stack, design system, verification, constraints (<5 KB)
|
|
191
|
+
- `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
192
|
- `constitution.md` — active architectural gates
|
|
193
193
|
- `design-system.md` — component mapping table
|
|
194
194
|
- `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`.
|
|
@@ -204,7 +204,7 @@ State lives in `.forge/`. One line per artifact; protocol detail lives in the na
|
|
|
204
204
|
- `phases/milestone-{id}/{phase}-{name}/plan-{NN}.md` — task plans with must_haves frontmatter (`{id}`=milestone, `{phase}`=phase# verbatim, `{name}`=kebab, `{NN}`=seq)
|
|
205
205
|
- `refactor-backlog.yml` — refactoring catalog worked via quick-tasking; status vocab `pending | in_progress | done | dismissed | deferred`; terminal items auto-archive to `refactor-backlog-archive.yml` on the next reviewing/quick-tasking write
|
|
206
206
|
- `releases.yml` — append-only version-reservation registry (Version Reservation Protocol)
|
|
207
|
-
- `reservations.yml` — append-only id-reservation audit trail + cross-machine floor (ID Reservation Protocol)
|
|
207
|
+
- `reservations.yml` — append-only id-reservation audit trail + cross-machine floor (ID Reservation Protocol); `forge-reserve compact` (R104) moves dropped rows verbatim to `reservations-archive.yml`, retaining each kind's max + a 14-day tail
|
|
208
208
|
- `archive/milestone-{id}/` — archived milestone artifacts (archive-delete)
|
|
209
209
|
- `prototypes/{slug}.md` — resumable disposable-mockup state (`exploring | parked | graduated | discarded`); terminal → `prototypes/archive/`. Single-owner, one driver. See `Skill(prototyping)`.
|
|
210
210
|
|
|
@@ -104,6 +104,8 @@ JUSTIFY="$(git log --format=%B "$MB..$HEAD_REF" 2>/dev/null \
|
|
|
104
104
|
| sed -n 's/^Lint-Justify:[[:space:]]*//p' | head -1 | tr '"' "'")"
|
|
105
105
|
|
|
106
106
|
# Base-side gating_tests globs (same additive config the hold check reads).
|
|
107
|
+
# Twin parser: keep in sync with forge-hold-check.sh:conf_list() —
|
|
108
|
+
# duplicated deliberately (R075, self-contained-hooks convention).
|
|
107
109
|
GATING="$(git show "$MB:.forge/hold-config.yml" 2>/dev/null \
|
|
108
110
|
| awk '
|
|
109
111
|
/^[A-Za-z_]+:/ { insec = (index($0, "gating_tests:") == 1) ? 1 : 0; next }
|
|
@@ -314,6 +316,13 @@ add_finding() { # rule, file, line("" ok)
|
|
|
314
316
|
CHANGED="$(git diff --name-only "$MB" "$HEAD_REF" 2>/dev/null)" \
|
|
315
317
|
|| { printf 'forge-check-lint: git diff failed for %s\n' "$RANGE" >&2; exit 2; }
|
|
316
318
|
|
|
319
|
+
# Scratch file for classify_units | while-read (escapes the pipe subshell so
|
|
320
|
+
# add_finding's FINDINGS/HAS_A accumulation survives past the loop). No
|
|
321
|
+
# predictable path (R077): allocated on first use, trap-cleaned.
|
|
322
|
+
FCL_SCRATCH=""
|
|
323
|
+
cleanup_fcl_scratch() { [ -n "$FCL_SCRATCH" ] && rm -f "$FCL_SCRATCH"; }
|
|
324
|
+
trap cleanup_fcl_scratch EXIT INT TERM
|
|
325
|
+
|
|
317
326
|
while IFS= read -r path; do
|
|
318
327
|
[ -n "$path" ] || continue
|
|
319
328
|
base_name="$(basename "$path")"
|
|
@@ -323,6 +332,7 @@ while IFS= read -r path; do
|
|
|
323
332
|
.github/workflows/*.yml|.github/workflows/*.yaml|.gitlab-ci.yml)
|
|
324
333
|
ADDED="$(added_lines "$path")"
|
|
325
334
|
[ -n "$ADDED" ] || continue
|
|
335
|
+
[ -n "$FCL_SCRATCH" ] || FCL_SCRATCH="$(mktemp)" || { printf 'forge-check-lint: mktemp failed\n' >&2; exit 2; }
|
|
326
336
|
classify_units "$path" | while IFS=' ' read -r cls ustart uend; do
|
|
327
337
|
[ "$cls" != "clean" ] || continue
|
|
328
338
|
hit=""
|
|
@@ -333,11 +343,10 @@ while IFS= read -r path; do
|
|
|
333
343
|
$ADDED
|
|
334
344
|
EOF2
|
|
335
345
|
[ -n "$hit" ] && printf '%s %s %s\n' "$cls" "$path" "$ustart"
|
|
336
|
-
done > "$
|
|
346
|
+
done > "$FCL_SCRATCH" || true
|
|
337
347
|
while IFS=' ' read -r cls p ln; do
|
|
338
348
|
[ -n "$cls" ] && add_finding "$cls" "$p" "$ln"
|
|
339
|
-
done < "$
|
|
340
|
-
rm -f "${TMPDIR:-/tmp}/fcl_units.$$"
|
|
349
|
+
done < "$FCL_SCRATCH"
|
|
341
350
|
continue
|
|
342
351
|
;;
|
|
343
352
|
esac
|
|
@@ -132,6 +132,8 @@ fi
|
|
|
132
132
|
# --- base-side config (additive; the PR cannot rewrite the rules it is judged by)
|
|
133
133
|
CONF="$(git show "$MB:.forge/hold-config.yml" 2>/dev/null || true)"
|
|
134
134
|
|
|
135
|
+
# Twin parser: keep in sync with forge-check-lint.sh:GATING awk block —
|
|
136
|
+
# duplicated deliberately (R075, self-contained-hooks convention).
|
|
135
137
|
conf_list() { # key → one value per line from the base-side config
|
|
136
138
|
[ -n "$CONF" ] || return 0
|
|
137
139
|
printf '%s\n' "$CONF" | awk -v k="$1" '
|
|
@@ -23,20 +23,28 @@
|
|
|
23
23
|
# NO OWNED STATE: nothing here is persisted; the next ask recomputes from disk + git. The only
|
|
24
24
|
# durable Jarvis state anywhere is B4's relay markers (a different helper).
|
|
25
25
|
#
|
|
26
|
+
# BARE BRANCH LOOSENING (issue #25): the disk glob also matches a bare `m-<id>-slug` or
|
|
27
|
+
# `m<digits>-slug` branch (no `forge/` prefix) — a worktree created by hand or by another tool,
|
|
28
|
+
# not just the Worktree Convention's `forge/{anchor}`. Its milestone id is derived from the
|
|
29
|
+
# LONGEST-matching `.forge/state/milestone-*.yml` filename in that worktree, else the id's first
|
|
30
|
+
# hyphen-segment. A bare NUMBER alone (no `m` token) never matches — see derive_milestone_id.
|
|
31
|
+
#
|
|
26
32
|
# forge-jarvis-awareness.sh [--dry-run] [--worktree-root <dir>]
|
|
27
33
|
# --dry-run resolve + print structure without shelling to the fold/board (fast
|
|
28
34
|
# plumbing check); tests and quick sanity checks use it.
|
|
29
35
|
# --worktree-root override the worktree scan root (tests); default: real `git worktree list`.
|
|
30
36
|
#
|
|
31
37
|
# Host-truth commands are env-overridable so tests inject stubs and degradation is explicit:
|
|
32
|
-
# FORGE_JARVIS_FOLD
|
|
33
|
-
# FORGE_JARVIS_BOARD
|
|
38
|
+
# FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh
|
|
39
|
+
# FORGE_JARVIS_BOARD default .forge/checks/forge-board-render.sh
|
|
40
|
+
# FORGE_JARVIS_REPO_ROOT default the repo root derived from this script's own path (test seam:
|
|
41
|
+
# points the git-worktree glob at a throwaway fixture repo)
|
|
34
42
|
# BEST-EFFORT: a missing fold/board degrades to a stated 'unknown' / '(unavailable)', never a hard
|
|
35
43
|
# fail — the awareness answer always renders.
|
|
36
44
|
set -u
|
|
37
45
|
|
|
38
46
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
39
|
-
REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)"
|
|
47
|
+
REPO_ROOT="${FORGE_JARVIS_REPO_ROOT:-$(cd "$SELF_DIR/../.." && pwd)}" # .forge/checks → repo root
|
|
40
48
|
FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
|
|
41
49
|
BOARD="${FORGE_JARVIS_BOARD:-$REPO_ROOT/.forge/checks/forge-board-render.sh}"
|
|
42
50
|
TAB="$(printf '\t')"
|
|
@@ -61,9 +69,50 @@ fold_state() { # fold_state <milestone-id>
|
|
|
61
69
|
[ -n "$_rs" ] && printf '%s' "$_rs" || printf 'unknown'
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
# ──
|
|
65
|
-
#
|
|
66
|
-
#
|
|
72
|
+
# ── milestone id derivation for a matched branch (issue #25, bare-branch loosening). A
|
|
73
|
+
# forge/m-* branch keeps its original textual derivation unchanged (strip forge/, strip the
|
|
74
|
+
# 8-hex session suffix). A bare branch (m-<id>-slug | m<digits>-slug) derives the id from the
|
|
75
|
+
# LONGEST-matching .forge/state/milestone-*.yml file name in that worktree, else falls back to
|
|
76
|
+
# the id's first hyphen-segment. Same rule text as forge-jarvis-dispatch.sh's branch-derived
|
|
77
|
+
# milestone-dir pick (there against .forge/phases/milestone-*/ dirs instead of state files). ──
|
|
78
|
+
derive_milestone_id() { # derive_milestone_id <branch> <worktree-path>
|
|
79
|
+
_b="$1"; _p="$2"
|
|
80
|
+
case "$_b" in
|
|
81
|
+
forge/*)
|
|
82
|
+
_m="${_b#forge/}" # m-37[-<session>]
|
|
83
|
+
case "$_m" in
|
|
84
|
+
*-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
|
|
85
|
+
_m="${_m%-*}" ;; # → m-37
|
|
86
|
+
esac
|
|
87
|
+
printf '%s' "$_m"
|
|
88
|
+
return 0 ;;
|
|
89
|
+
esac
|
|
90
|
+
_base="$_b"
|
|
91
|
+
case "$_base" in
|
|
92
|
+
*-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
|
|
93
|
+
_base="${_base%-*}" ;; # strip an 8-hex session suffix
|
|
94
|
+
esac
|
|
95
|
+
_best=""
|
|
96
|
+
for _f in "$_p"/.forge/state/milestone-*.yml; do
|
|
97
|
+
[ -f "$_f" ] || continue
|
|
98
|
+
_id="$(basename "$_f" .yml)"; _id="${_id#milestone-}"
|
|
99
|
+
case "$_base" in
|
|
100
|
+
"m-$_id"|"m-$_id"-*|"m$_id"|"m$_id"-*)
|
|
101
|
+
[ "${#_id}" -gt "${#_best}" ] && _best="$_id" ;;
|
|
102
|
+
esac
|
|
103
|
+
done
|
|
104
|
+
if [ -n "$_best" ]; then
|
|
105
|
+
printf 'm-%s' "$_best"
|
|
106
|
+
else
|
|
107
|
+
_seg="${_base#m}"; _seg="${_seg#-}"; _seg="${_seg%%-*}" # first hyphen-segment fallback
|
|
108
|
+
printf 'm-%s' "$_seg"
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ── 1. live/parked slices — a DISK GLOB of worktrees on forge/m-* branches OR a bare m-<id>
|
|
113
|
+
# branch (issue #25). Same on-disk registry the relay's --list-logs arms tail -F on;
|
|
114
|
+
# recomputed here, never stored. Emits TSV: <milestone-id>\t<branch>\t<path>.
|
|
115
|
+
# --worktree-root globs a given dir instead (tests). ─────────────────────────────────────
|
|
67
116
|
list_slices() {
|
|
68
117
|
if [ -n "$WT_ROOT" ]; then
|
|
69
118
|
for _wt in "$WT_ROOT"/*/; do
|
|
@@ -75,12 +124,14 @@ list_slices() {
|
|
|
75
124
|
git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk -v tab="$TAB" '
|
|
76
125
|
/^worktree / { p = substr($0, 10) } # full remainder — paths may contain spaces
|
|
77
126
|
/^branch / {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
print m tab b tab p
|
|
127
|
+
b = $2; sub(/refs\/heads\//, "", b)
|
|
128
|
+
if (b ~ /^forge\/m-/ || b ~ /^m-[0-9A-Za-z]/ || b ~ /^m[0-9]/) {
|
|
129
|
+
print b tab p
|
|
82
130
|
}
|
|
83
|
-
}'
|
|
131
|
+
}' | while IFS="$TAB" read -r _b _p; do
|
|
132
|
+
_m="$(derive_milestone_id "$_b" "$_p")"
|
|
133
|
+
printf '%s%s%s%s%s\n' "$_m" "$TAB" "$_b" "$TAB" "$_p"
|
|
134
|
+
done
|
|
84
135
|
fi
|
|
85
136
|
}
|
|
86
137
|
|
|
@@ -233,6 +233,14 @@ materialize_work_order() {
|
|
|
233
233
|
# unconditionally missed milestone-m-EVT01 and fell through to a 38-dir refusal. Try the
|
|
234
234
|
# anchor verbatim before the m--stripped form, then both again minus one trailing
|
|
235
235
|
# -{session} token; first existing dir wins (exact/longer forms first).
|
|
236
|
+
#
|
|
237
|
+
# BARE BRANCH LOOSENING (issue #25): a worktree may also sit on a bare `m-<id>-slug` or
|
|
238
|
+
# `m<digits>-slug` branch (no `forge/` prefix) — same match family as the awareness/relay
|
|
239
|
+
# glob. Unlike the forge/ convention (id + at most one session token), a bare branch's slug
|
|
240
|
+
# can carry several descriptive words (m-176-marcy-mcp), so the id is found by repeatedly
|
|
241
|
+
# truncating the LAST hyphen-segment and testing for an existing milestone dir at each
|
|
242
|
+
# length — the longest-matching truncation wins (mirrors awareness's state-file prefix
|
|
243
|
+
# match, here against .forge/phases/milestone-*/ dirs instead).
|
|
236
244
|
_mdir=""
|
|
237
245
|
_branch="$(git -C "$WORKTREE" branch --show-current 2>/dev/null)"
|
|
238
246
|
case "$_branch" in
|
|
@@ -246,6 +254,22 @@ materialize_work_order() {
|
|
|
246
254
|
break
|
|
247
255
|
fi
|
|
248
256
|
done ;;
|
|
257
|
+
m-*|m[0-9]*)
|
|
258
|
+
_cand="$_branch"
|
|
259
|
+
while [ -n "$_cand" ]; do
|
|
260
|
+
for _try in "$_cand" "${_cand#m-}" "${_cand#m}"; do
|
|
261
|
+
[ -n "$_try" ] || continue
|
|
262
|
+
if [ -d "$WORKTREE/.forge/phases/milestone-$_try" ]; then
|
|
263
|
+
_mdir="$WORKTREE/.forge/phases/milestone-$_try"
|
|
264
|
+
break
|
|
265
|
+
fi
|
|
266
|
+
done
|
|
267
|
+
[ -n "$_mdir" ] && break
|
|
268
|
+
case "$_cand" in
|
|
269
|
+
*-*) _cand="${_cand%-*}" ;; # drop the last hyphen-segment, retry shorter
|
|
270
|
+
*) break ;;
|
|
271
|
+
esac
|
|
272
|
+
done ;;
|
|
249
273
|
esac
|
|
250
274
|
# Fallback (non-git fixture dirs / unconventional branches): a SINGLE milestone dir is
|
|
251
275
|
# unambiguous; several without a branch-derived pick is a refusal, never a guess.
|
|
@@ -260,7 +284,7 @@ materialize_work_order() {
|
|
|
260
284
|
return 2
|
|
261
285
|
fi
|
|
262
286
|
if [ "$_mdirs" -gt 1 ]; then
|
|
263
|
-
printf 'forge-jarvis-dispatch: REFUSED — %s milestone plan dirs in %s and no forge/m-* branch to pick by; ambiguous work order. Write slice.yml explicitly. RELAY this refusal to the operator.\n' "$_mdirs" "$WORKTREE" >&2
|
|
287
|
+
printf 'forge-jarvis-dispatch: REFUSED — %s milestone plan dirs in %s and no forge/m-* or bare m-<id> branch to pick by; ambiguous work order. Write slice.yml explicitly. RELAY this refusal to the operator.\n' "$_mdirs" "$WORKTREE" >&2
|
|
264
288
|
return 2
|
|
265
289
|
fi
|
|
266
290
|
fi
|
|
@@ -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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
@@ -31,16 +31,22 @@
|
|
|
31
31
|
# the payload rather than fold/gh/board (acceptance #3).
|
|
32
32
|
#
|
|
33
33
|
# HOST-TRUTH COMMANDS (env-overridable so tests inject stubs, and so degradation is explicit):
|
|
34
|
-
# FORGE_JARVIS_FOLD
|
|
34
|
+
# FORGE_JARVIS_FOLD default .claude/hooks/forge-release-fold.sh (release_state recompute)
|
|
35
|
+
# FORGE_JARVIS_REPO_ROOT default the repo root derived from this script's own path (test seam:
|
|
36
|
+
# points --list-logs's git-worktree glob at a throwaway fixture repo)
|
|
35
37
|
# (gh-based facts live in forge-jarvis-prwatch.sh, which documents its own FORGE_JARVIS_GH;
|
|
36
38
|
# the board recompute lives in awareness/promote — this helper consults the fold only.)
|
|
37
39
|
#
|
|
40
|
+
# BARE BRANCH LOOSENING (issue #25): --list-logs also matches a bare `m-<id>-slug` or
|
|
41
|
+
# `m<digits>-slug` worktree branch (no `forge/` prefix), same match rule as the awareness glob —
|
|
42
|
+
# it only needs the notify-log PATH, not the milestone id, so no id derivation lives here.
|
|
43
|
+
#
|
|
38
44
|
# BEST-EFFORT: a fold/gh/board that is absent or errors degrades to release_state=unknown and
|
|
39
45
|
# says so — never a silent success, never a hard fail of the relay path.
|
|
40
46
|
set -u
|
|
41
47
|
|
|
42
48
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
43
|
-
REPO_ROOT="$(cd "$SELF_DIR/../.." && pwd)"
|
|
49
|
+
REPO_ROOT="${FORGE_JARVIS_REPO_ROOT:-$(cd "$SELF_DIR/../.." && pwd)}" # .forge/checks → repo root
|
|
44
50
|
FOLD="${FORGE_JARVIS_FOLD:-$REPO_ROOT/.claude/hooks/forge-release-fold.sh}"
|
|
45
51
|
|
|
46
52
|
# ── host truth: the slice's release_state, recomputed from the fold (never the payload) ───────
|
|
@@ -148,12 +154,15 @@ mode_list_logs() {
|
|
|
148
154
|
done
|
|
149
155
|
else
|
|
150
156
|
# live path: `git worktree list` is the on-disk registry of worktrees; glob it to the
|
|
151
|
-
# forge/m-* slice branches
|
|
152
|
-
#
|
|
153
|
-
# would silently arm ZERO watches).
|
|
157
|
+
# forge/m-* slice branches (OR a bare m-<id>-slug / m<digits>-slug branch, issue #25) and
|
|
158
|
+
# emit each slice's notify-log path. Pinned with -C so the watch list never depends on the
|
|
159
|
+
# caller's cwd (the cwd-reset footgun: a snapped cwd would silently arm ZERO watches).
|
|
154
160
|
git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | awk '
|
|
155
161
|
/^worktree / { p=substr($0, 10) } # full remainder — paths may contain spaces
|
|
156
|
-
/^branch / {
|
|
162
|
+
/^branch / {
|
|
163
|
+
b=$2; sub(/refs\/heads\//, "", b)
|
|
164
|
+
if (b ~ /^forge\/m-/ || b ~ /^m-[0-9A-Za-z]/ || b ~ /^m[0-9]/) print p "/.forge/runner-work/notify.log"
|
|
165
|
+
}
|
|
157
166
|
'
|
|
158
167
|
fi
|
|
159
168
|
exit 0
|