Skip to content

Commit 55a729f

Browse files
committed
Merge remote-tracking branch 'origin/main' into permission-mode-auto-default
# Conflicts: # CHANGELOG.md
2 parents fba95a3 + baa145d commit 55a729f

18 files changed

Lines changed: 3769 additions & 48 deletions

CHANGELOG.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,89 @@ oh-my-graph is **alpha software**. The graph YAML schema, the CLI, and the
4242
**absent means `dontAsk`**, so a run started before this change resumes under
4343
the mode it really ran rather than silently adopting the new default. The
4444
field is additive and the snapshot schema stays at 3.
45+
- **`runs list`, `show` and `watch` now state how many runs are IN FLIGHT,
46+
including when the answer is zero.** Asked how many graphs were running, an
47+
operator could only run `oh-my-graph runs list | grep -c RUNNING` and read
48+
`0` — an inference from ABSENCE, indistinguishable from a table that failed
49+
to render, a mis-filter, or broken status logic. The summary line counted
50+
what was HIDDEN and never counted what was ALIVE.
51+
52+
Before, on a run home with one settled run and nothing running:
53+
54+
```
55+
1 of 1 run(s) shown; 0 skipped.
56+
```
57+
58+
After, the same corpus:
59+
60+
```
61+
1 of 1 run(s) shown; 0 in flight; 0 skipped.
62+
```
63+
64+
With two live runs, one abandoned one and one directory this build cannot
65+
read, before:
66+
67+
```
68+
3 of 4 run(s) shown; 1 skipped (1 written by an incompatible snapshot schema) — pass --show-skipped to name them.
69+
```
70+
71+
After:
72+
73+
```
74+
3 of 4 run(s) shown; 2 in flight, 1 abandoned; 1 skipped (1 written by an incompatible snapshot schema) — pass --show-skipped to name them.
75+
```
76+
77+
It is the SAME line, not a second one: one line is one read, while a second
78+
line can be scrolled past, cropped by a pager, or emitted only
79+
conditionally — the exact failure mode this change exists to kill. An
80+
abandoned run is named in the same sentence when there is one, and is never
81+
added into the in-flight number: a reader asking "is anything running" is
82+
answered wrongly by a count that folds in dead runs, and badly served by
83+
silence about a run whose process died holding a lock.
84+
85+
The two single-run surfaces say the same sentence, so an operator learns one
86+
wording rather than three. `show`, before and after:
87+
88+
```
89+
Run run-done — PASS, 1 node(s)
90+
Run run-done — PASS, 1 node(s); 0 in flight.
91+
```
92+
93+
`watch`, before and after:
94+
95+
```
96+
run run-done is PASS
97+
run run-done is PASS; 0 in flight.
98+
```
99+
100+
The count is `runstatus.InFlightCount` over statuses the surfaces already
101+
derived, so nothing re-decides what in-flight means (ADR 0015 §4's open leg
102+
AND held lock stays the one rule, in `runstatus.Derive`). The machine-readable
103+
surfaces are unchanged: `/api/cards`, `/api/graph` and `events.jsonl` keep
104+
their shapes and gain no prose — a consumer counts live runs itself from the
105+
state token, as the dashboard's own page already does.
106+
- **The spawn retry now waits long enough to be useful.** #214 gave the
107+
assessor a bounded retry for a CLI that never started, and #226 extended it to
108+
the planner. Both shipped with a **3-attempt, 300ms** bound — a 600ms window —
109+
and then two more lanes died on the same failure *after* the retry was in
110+
place:
111+
112+
```
113+
assessor run: claude run: spawn failed: exec: "claude": executable file not found in $PATH
114+
planner run: claude run: spawn failed: exec: "claude": executable file not found in $PATH
115+
```
116+
117+
Four occurrences in one day, two of them post-fix, say the bound was **correct
118+
in shape and useless in size**: 600ms is less than a package manager takes to
119+
relink a binary. Widened to **5 attempts, 2s apart** — eight seconds of
120+
patience, which buys the common case. A machine that genuinely has no CLI
121+
installed still fails, eight seconds later, saying exactly what it said before.
122+
123+
Nothing else changed: only a spawn that never happened is retried. A refused
124+
reply, a non-zero exit, a timeout and a cancelled context still stop on the
125+
first answer.
126+
127+
### Changed
45128

46129
- **An unresolvable `{{ inputs.x }}` or `{{ artifacts.id }}` now says that a
47130
merely-quoted placeholder is resolved too, and how to quote one.** The two
@@ -175,6 +258,61 @@ oh-my-graph is **alpha software**. The graph YAML schema, the CLI, and the
175258

176259
### Documented
177260

261+
- **Every rule in the batch-lane idiom now ends in a disposition: the check
262+
that enforces it, named, or one sentence saying why it is judgement**
263+
([`graphs/backlog-batch.yaml`](graphs/backlog-batch.yaml)). The header stated
264+
seven hard-won rules and left a reader to guess which of them anything
265+
enforced — a rule with a test behind it read exactly like one no code has
266+
ever looked at.
267+
268+
Three rules are mechanically checked, all three as tests over `graphs/` in
269+
`internal/graph/shipped_graphs_test.go`, so a violation fails THIS repo's
270+
build rather than warning at a user's own graph:
271+
`TestIndependentLanesFailIndependently` (rule 5 — two or more weakly
272+
connected components in the resolved graph must come with
273+
`on_fail: continue`), `TestASessionGateCitesTheColdSafeFragment` (rule 3 — a
274+
node that resumes a session and retries must have spliced `e2e-verify`, where
275+
the cold-safe wording exists once), and
276+
`TestAGatingReviewCarriesItsRecoveryArc`, extended to rule 6's second half:
277+
it already refused a narrowed review check with no `feedback:` arc, and now
278+
equally refuses an arc on a node whose check still accepts `FINDINGS:` — an
279+
arc the failing verdict can never reach. Rule 3's risky shape keeps its
280+
existing advisory warning from `handoff.LintSessions`; the test guards only
281+
what the warning cannot, that the wording has not been copied out by hand.
282+
283+
Rules 2, 4 and 7 stay prose, each saying why in the header: rule 2 would have
284+
to read prompt prose for an instruction, the predicate family this repo
285+
measured and rejected at 110 noise in 114 hits
286+
([`docs/measurements/0213-tool-grant-predicate.md`](docs/measurements/0213-tool-grant-predicate.md));
287+
rule 4's subject is a lane's diff, which does not exist when a graph is
288+
loaded; and rule 7 asks whether a repeat is a shape or a difference in a key,
289+
which is the judgement it exists to provoke.
290+
291+
Rule 1 — "lanes must not share files" — stays prose too, and was measured
292+
before it was written off, in
293+
[`docs/measurements/0034-lane-file-ownership-predicate.md`](docs/measurements/0034-lane-file-ownership-predicate.md).
294+
Over 45 graphs and 216 resolved nodes, only one graph can fire the predicate
295+
at all (a planned graph may not declare a `worktree:`), and the lexical form
296+
produced **1 hit, of which the hand-check made 1 noise** — both lanes cite
297+
`CONTRIBUTING.md` as the address of the commit-trailer convention and neither
298+
edits it. Verdict: DO-NOT-SHIP. The command is the address for those numbers:
299+
300+
```sh
301+
go run docs/measurements/0034-lane-file-ownership-predicate.go
302+
```
303+
304+
What survives of rule 1 reads no paths at all and is a test:
305+
`TestAWorktreeGraphLeavesNoNodeOutsideALane` refuses a graph that declares
306+
lanes and leaves some node without one, since that node runs in the user's
307+
own tree and so shares files with every lane at once.
308+
309+
No new `lint` sweep and no new load error — a new warning owes a measured
310+
noise rate, and the corpus that could measure one is n=1 across everything
311+
this repo ships. The header also settles the fragment question so it is not
312+
reopened: the lane shape is already a fragment,
313+
[`graphs/fragments/gated-lane.yaml`](graphs/fragments/gated-lane.yaml), and
314+
no second lane fragment is coming. Nothing in the graph body changed, and
315+
both runtimes' `lint` output for the eight shipped graphs is unchanged.
178316
- **ADR 0033 — the run, not the node, is the unit of engine-run evidence, and
179317
ADR 0030 is deliberately not extended one level down**
180318
([`docs/adr/0033-the-run-is-the-unit-of-evidence-not-the-node.md`](docs/adr/0033-the-run-is-the-unit-of-evidence-not-the-node.md),
@@ -210,6 +348,37 @@ oh-my-graph is **alpha software**. The graph YAML schema, the CLI, and the
210348
while `cmd/oh-my-graph/version.go:9` reads `0.11.0`. The clause that survived
211349
the deletion is the true half: a run that types nothing is byte-for-byte the
212350
run that shipped in v0.10.0.
351+
- **`graphs/backlog-batch.yaml`'s header diagrammed four nodes the graph no
352+
longer has, and its rule 1 advised an edit that will not load.** ADR 0029
353+
folded lane A into a single `use: gated-lane` node, and commit `d232ce4`
354+
deleted `dev-a`/`e2e-a`/`review-a`/`pr-a` — but that commit's diff on this
355+
file begins at `@@ -45,15 +45,24 @@`, so it never reached the header. The
356+
runtime ids are `lane-a/dev`, `lane-a/e2e`, `lane-a/review`, `lane-a/pr`, as
357+
the file's own body comment already said (`graphs/backlog-batch.yaml:88`) and
358+
as loading it prints:
359+
360+
```sh
361+
go run ./cmd/oh-my-graph run graphs/backlog-batch.yaml --dry-run \
362+
--input repo=/tmp --input checks_command="make local" \
363+
--input task_a=x --input task_b=y
364+
# Graph "backlog-batch" (8 nodes): lane-a/dev, lane-a/e2e, lane-a/review,
365+
# lane-a/pr, dev-b, e2e-b, review-b, pr-b
366+
```
367+
368+
The second line was worse than stale. Rule 1 offered "make dev-b depend on
369+
`pr-a`" as the way to serialize two overlapping tasks, and `depends_on:
370+
[pr-a]` is a load error — `depends_on unknown node`,
371+
`internal/graph/validate.go:454` — so a reader who took the advice got a
372+
graph that would not run. It now names `lane-a/pr`
373+
(`graphs/backlog-batch.yaml:16`). `init` emits this file and users copy it,
374+
which is why a stale comment here is a stale instruction to a stranger.
375+
376+
Comments only: no node, prompt, or key changed, the graph still validates at
377+
8 nodes, and no verdict clause moved, so the qualifier sweep's counts are
378+
untouched. The remaining thirteen shipped graphs and fragments were audited
379+
against v0.11.0 in the same pass and left alone — none of them names `auto`,
380+
so neither `--accept-loaded-user-config` nor anything else new in v0.11.0
381+
reaches them.
213382

214383
## [v0.11.0] - 2026-08-21
215384

DESIGN.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,11 +1808,21 @@ oh-my-graph resume <run-id> (--approve <gate-id> | --reject <gate-id> | --retry-
18081808
`VERDICT`, which would have kept the very conflation the enumeration
18091809
removes) and, under its total, one line saying how much of the run home the
18101810
table covers: how many runs are shown out of how many directories were found,
1811-
how many were skipped and under which reason. It is printed whether or not
1811+
**how many of them are in flight**, and how many were skipped and under which
1812+
reason (`3 of 4 run(s) shown; 2 in flight, 1 abandoned; 1 skipped (…)`). It is
1813+
printed whether or not
18121814
anything was skipped, because a reader must be able to tell "nothing was
18131815
hidden from me" apart from "64 of 325 runs are shown" — a summary that
18141816
appeared only when there was something to report renders those two cases
1815-
identically. `--show-skipped` names each skipped directory and quotes the
1817+
identically. The in-flight clause is there for the same reason and is stated
1818+
AT ZERO: "no run is running" and "the status column is broken" are what an
1819+
empty `RUNNING` column renders identically, and only the first is the
1820+
conclusion an operator draws from it. `show` and `watch` append the same
1821+
clause to their own status lines (`run run-x is PASS; 0 in flight.`) so one
1822+
wording answers the question on every surface. The count is
1823+
`runstatus.InFlightCount` over statuses those surfaces already derived —
1824+
`Derive` remains the only place the open-leg-AND-held-lock rule lives, and no
1825+
surface re-decides it. `--show-skipped` names each skipped directory and quotes the
18161826
reader's own reason, one line each on stderr; that per-run detail used to be
18171827
unconditional and was four fifths of the command's output on a long-lived run
18181828
home. That per-run sentence is `runstatus.Unreadable` and is the ONE wording

0 commit comments

Comments
 (0)