Skip to content

observe-run: prescribe a backgrounded force-run, so a long run reads as a long run - #262

Merged
thedavidmeister merged 9 commits into
mainfrom
260-observe-run-background-invocation
Aug 10, 2026
Merged

thedavidmeister merged 9 commits into
mainfrom
260-observe-run-background-invocation

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #260

The instruction that could not be followed

/human-fsm:observe-run step 1 said:

Run it in a foreground Bash call. It blocks for the whole run, which is
the point; wrapping it in anything that returns immediately abandons the wait.

A foreground Bash call is moved to the background at 600 seconds. The producer
run measured on 2026-08-10 took 32m 48s and the vetter 22m 30s, so the call is
backgrounded before the run ends every time — and it hands back nothing of the
stream when it is. Ten minutes of blank screen is indistinguishable from a hang,
which is how vetter run 20260810T102325Z — fast-forwarded, both stops walked,
three auditors dispatched and working — was interrupted and killed.

What step 1 says now

Start it in a background Bash call and read that call's output file as the
run goes. Backgrounded from the start it returns in seconds naming the file, and
the fast-forward verdict, the log: reattach path and running: are readable
there immediately — the earliest point at which "this is working" is provable,
instead of the latest. Read the file when you want to know where the run is, one
read at a time; the run's exit arrives as a task notification.

Nothing may sit between the stream and the reader: force-run flushes every
line as it prints it, and tail/head both hold the whole stream until the
writer exits. That was the first thing tried on 2026-08-10 and it showed nothing
well before any timeout was reached, so the step now says it outright rather
than leaving it to be rediscovered.

And if that output file is gone — restarted session, or a run this command did
not start — the log: line names where the same bytes are still being appended
and step 2's watch-run reattaches there.

force-run itself is untouched. Its fast-forward, exit-3 refusal, policy-stop
walk, streaming and exit code are what the issue says they are.

The kill claim, re-established rather than carried over

Step 1 asserted that killing the call kills the runner and releases the flock.
It still does, and it was probed rather than assumed — against a stand-in of the
same shape as force-run, a parent that streams a child holding an flock on an
open descriptor:

invocation after stopping the task
backgrounded from the start neither process alive; flock -n succeeded at once
foreground call the 600s ceiling backgrounded neither process alive; flock -n succeeded at once

The whole process tree the task started is killed, the runner among it, and the
lock goes with the descriptor. The paragraph now states that mechanism and says
where the claim comes from.

Two other harness facts were probed the same way and are what the new text rests
on: a foreground call that exceeds its timeout is moved to the background
rather than killed, with its output going to a task file readable line-by-line as
it accumulates; and <stream> | tail -20 against a source emitting one line a
second produced zero bytes in 8 seconds, which is the buffering the step now
bans by name.

Scope

No force-run behaviour changed. The Rust diff is test-only — a gate over the
step's text — plus the plugin version bump both version-hygiene jobs require.
Nothing in the issue turned out to need a code change to make the instruction
honest, so nothing was widened.

One thing worth a human's eye, raised here rather than acted on: step 2 still
opens "Only needed for a run this command did not start, or one whose stream was
cut." That is now also the route when a backgrounded task's output file is gone,
and step 1 points at it, but step 2's own sentence was left as it was.

QA

  • Discriminating tests: marketplace_tests::observe_run_step_1_prescribes_an_invocation_a_long_run_survives
    (the file), plus seven unit tests over the pure gate —
    a_step_prescribing_the_mode_a_long_run_survives_is_clean,
    a_step_that_prescribes_a_foreground_call_is_the_instruction_260_is_about,
    an_unemphasised_mention_of_the_other_mode_is_not_a_prescription,
    a_step_that_emphasises_no_mode_prescribes_nothing,
    a_buffering_reader_the_step_never_names_is_not_banned,
    a_backgrounded_run_with_no_route_back_to_its_stream_is_a_fault,
    the_rule_is_read_in_its_own_step_and_a_neighbour_neither_breaks_nor_satisfies_it.
    Each fails on base: the base observe-run.md step 1 is exactly the
    **foreground** prescription the file test rejects, with no tail/head ban
    and no reattach route in the step, so all four fault variants fire on it —
    verified by applying that base text as mutant md: prescribes a foreground call and by dropping each token, below.

  • Mutations applied — 15 mutants, 13 kill-expected and 13 killed, each probe
    reporting the number of tests it ran (32) so a zero-match filter cannot fake a
    survival:

    mutant verdict
    observe-run.md: **background****foreground** KILLED
    observe-run.md: `tail` unbackticked in step 1 KILLED
    observe-run.md: `head` unbackticked in step 1 KILLED
    observe-run.md: `watch-run` unbackticked in step 1 KILLED
    observe-run.md: `log:` unbackticked in step 1 KILLED
    observe-run.md: step 1 heading unnumbered (## Force the run …) KILLED (panics: no section)
    observe-run.md: step 1 renumbered ## 3. KILLED (panics: no section)
    BASH_CALL_MODES: "foreground" dropped from the vocabulary KILLED
    long_run_step_faults: PrescribesForeground never raised KILLED
    long_run_step_faults: NoModePrescribed never raised KILLED
    long_run_step_faults: buffering-reader check inverted (! dropped) KILLED
    long_run_step_faults: reattach-route check deleted KILLED
    command_step: sibling predicate → |_l| false (step runs to EOF) KILLED
    emphasised_call_modes: **{mode}**{mode} (every mention counts) KILLED
    CONTROL — non-rule prose reworded: "killed once" → "killed twice", the measured durations changed to 9m 01s/4m 12s, "came to be read as a hang and killed" → "was mistaken for a hang and interrupted" SURVIVED, as intended: the gate reads the rule, not the paragraph

    One further mutant is reported as a known blind spot rather than a gap:
    negating the ban in place (**Nothing may sit between the stream and you.**
    **Anything may sit…**) SURVIVES. No gate over prose can see a step that
    keeps every token and means the opposite; that limit is stated in the comment
    above the gate rather than left implied. A sixteenth mutant — re-titling step 1
    while keeping ## 1. — also survives, correctly: a re-titled step 1 is still
    step 1, which is precisely what the scope keys on.

  • Oracle: the issue's own constraints, not the code. The four faults are its four
    bullets — a producer run always outlives a foreground call; the stream must
    reach the reader unbuffered; killing must still work or be disclaimed; a
    backgrounded run must stay observable. The durations (32m 48s / 22m 30s), the
    600s ceiling and the killed run 20260810T102325Z come from the issue body.
    The harness facts the new text asserts — foreground-over-timeout is backgrounded
    not killed, the task output file streams live, stopping the task kills the tree
    and frees the flock, | tail yields nothing — were each measured directly, not
    reasoned from documentation.

  • Category check: the issue asks for four properties and all four are covered.
    Three are pinned by the gate (no foreground prescription; the buffering ban
    names its readers; the reattach route names the printed line and the
    subcommand). The fourth — the flock claim — is a fact about the process tree
    rather than about the text, so it is established by the probe table above and
    the step states how it was established; a test asserting the sentence exists
    would pin the wrong thing.

Gates run: nix develop .#rust -c cargo test green (1234 unit + 113 integration,
0 failed) and
nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#rust-shell -c pre-commit run --all-files
green, with what denofmt and rustfmt wrote committed.

No producer or vetter run was forced to test this — both kill switches are in
place and the runs that motivated the issue cost $5.95 and $12.51.

Summary by CodeRabbit

  • New Features

    • Updated the human-fsm plugin to version 0.19.0.
    • Improved long-running task guidance with background execution, immediate status visibility, live output access, and reattachment instructions.
    • Added clearer guidance for safely stopping active runs and releasing resources.
  • Bug Fixes

    • Reduced issues caused by buffered or unavailable output during extended command execution.
    • Improved monitoring guidance to help prevent mistaken hang detection and premature termination.

…d call it outlives

Step 1 told the caller to hold a foreground `Bash` call — "it blocks for the
whole run, which is the point". A foreground call in this harness is moved to
the background at 600 seconds and hands back nothing of the stream when it is,
while the runs measured on 2026-08-10 were 32m 48s (producer) and 22m 30s
(vetter). So the instruction could not be honoured for a producer run, and the
ten minutes of silence it did buy read as a hang: vetter run 20260810T102325Z
was interrupted and killed three auditors deep, working.

Step 1 now starts the run in a background call, routes the progress read to that
call's output file and the reattach to the printed `log:` path and `watch-run`,
and says outright that no `tail`, `head` or filter may sit between the stream
and the reader — the first attempt on 2026-08-10 piped through `tail` and saw
nothing before any timeout was reached.

The kill claim is re-established rather than carried over: stopping the
background task kills the whole process tree, the runner among it, and the flock
goes with the descriptor. Probed against a stand-in of the same shape (a parent
streaming a child holding an `flock` on an open descriptor) for a call
backgrounded from the start and for one the ceiling backgrounded; neither
process survived and the lock re-locked immediately.

The rule is pinned by `long_run_step_faults`, over step 1 alone: the emphasised
call mode must be `background`, both buffering readers must be named as
commands, and the reattach route must name the printed line and the subcommand.

Closes #260

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 975003b6-6e08-4668-9306-cc91f99d5a1c

📥 Commits

Reviewing files that changed from the base of the PR and between b7cc8f2 and c91d605.

📒 Files selected for processing (1)
  • plugins/human-fsm/commands/observe-run.md

Walkthrough

The PR updates the human-fsm plugin to version 0.19.0, changes observe-run to use background force-run execution with direct log reading and reattachment, records the related journal entry, and adds citation validation.

Changes

observe-run execution workflow

Layer / File(s) Summary
Update background run guidance
.claude-plugin/marketplace.json, plugins/human-fsm/.claude-plugin/plugin.json, plugins/human-fsm/commands/observe-run.md, mistake-journal.jsonl
The plugin version changes to 0.19.0. observe-run now starts force-run in the background, reads its output file directly, avoids buffering pipes, supports watch-run reattachment, and documents process-tree termination and flock release. Journal entry LJ-0004 records the related failure and rule change.
Validate observe-run citations
pr-review-report-rs/src/main.rs
A Rust test parses citations in observe-run.md, requires at least one citation, and verifies that each cited journal ID exists. It supports unavailable repository files.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: prescribing a backgrounded force-run for long runs.
Linked Issues check ✅ Passed The changes satisfy issue #260 by enabling observable background runs, preventing buffering, preserving termination behavior, and documenting log reattachment.
Out of Scope Changes check ✅ Passed The version bump, journal entry, and citation test support the documented objective and do not introduce unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 260-observe-run-background-invocation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/human-fsm/commands/observe-run.md`:
- Around line 61-66: Update the explanation in the observe-run documentation:
state that tail without -f waits for EOF, while head emits the requested lines
and may close the pipe early, causing force-run to receive a broken-pipe error.
Preserve the guidance to avoid piping the stream and read the file directly.

In `@pr-review-report-rs/src/main.rs`:
- Around line 63691-63699: Update the validation logic around the
BUFFERING_READERS and REATTACH_ROUTE checks to reject contradictory
instructions, not merely accept token mentions: require explicit prohibition of
each buffering reader and affirmative routing for each reattach token. Add
negative fixtures covering prescribed buffering and denied reattachment, while
preserving faults for missing required instructions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e2c637f-abc8-46ed-abd5-e69b025c2482

📥 Commits

Reviewing files that changed from the base of the PR and between f5f685b and ad44137.

📒 Files selected for processing (4)
  • .claude-plugin/marketplace.json
  • plugins/human-fsm/.claude-plugin/plugin.json
  • plugins/human-fsm/commands/observe-run.md
  • pr-review-report-rs/src/main.rs

Comment thread plugins/human-fsm/commands/observe-run.md Outdated
Comment thread pr-review-report-rs/src/main.rs Outdated
thedavidmeister and others added 5 commits August 10, 2026 12:19
… it from the step

Step 1 of `/observe-run` carried the measurement as narrative: the two run
durations, the killed vetter run and what it had already done, and the
post-mortem on the `tail` pipe. That is the account of an incident, and the
account belongs in `mistake-journal.jsonl` where authoring pays for it once,
not in a command a model reads to find out what to do.

LJ-0004 is that incident, verified against the artifacts rather than carried
over from the narrative: the foreground instruction as it read, the two piped
invocations and the empty task output file ten minutes in, the interrupt at
7m 07s, the truncated trace at 52 tool calls with three auditors dispatched and
no `final` metrics row, and the backgrounded producer call that returned in
seconds as the counter-example. Its rule is this step, so it is an addressed
kind and not a rule-less backlog member.

Step 1 keeps the rule — background call, read its output file, nothing
buffering between the stream and the reader, `log:` plus `watch-run` as the
route back — and cites `[LJ-0004]` where the narrative was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s one

`tail` and `head` were described as failing the same way — "both hold the whole
stream until the writer exits". Only `tail` does. `head -n` takes its lines and
closes the pipe; `force-run` then stops echoing but keeps waiting on the run, so
the call still blocks to the end and hands back a stale head of the stream
instead of the stream. Both are banned, for their own reasons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reworked at 12132de03734a647b64ffff27eed673001c01089. Two things, and the second is the one that shrinks the prompt: the incident is now LJ-0004 in mistake-journal.jsonl, and step 1 keeps the rule and cites it.

Every figure was re-derived from the artifacts, and three did not survive

The narrative this PR carried was written from recollection. Before it went into the journal — where a fabricated measurement is the worst defect the artifact can have, which is why LJ-0002 exists — each figure was checked against metrics/runs.jsonl, review-runs/, review.log, the retained task output files and the session transcript. What held:

claim verdict
producer 20260810T083614Z = 32m 48s HOLDS — "durationMs": 1967592
vetter 20260810T091521Z = 22m 30s HOLDS — "durationMs": 1350441
vetter 20260810T103008Z = 7m 06s HOLDS — "durationMs": 426315
20260810T102325Z killed, trace survives at ~52 tool calls HOLDS — token-profile reads tool calls 52; boot + one usage row and no final row
it had fast-forwarded, walked both stops, dispatched three auditors HOLDS — review.log has both FORCED past … lines and review run START, no END; the trace has three Agent dispatches with a .lens row each

And what did not:

  1. The pipe was tail -40, then tail -50 — never tail -20. The | tail -20/zero-bytes-in-8-seconds figure is real but it is this PR's own synthetic probe, run at 10:52 against the killtest stand-in, not the incident. It is not in the entry as incident evidence.
  2. The interrupt landed at 7m 07s, not "nearly 10 mins". The call went out at 10:21:23.892Z and the rejection came back at 10:28:31.307Z. The human's words were "hanging nearly 10 mins" — quoted as words — but the run never reached the 600s ceiling before it was killed. The ceiling is what makes the instruction unfollowable; the blank screen is what made this particular call look dead.
  3. There was no second "hanging". One kill, one complaint. hanging nearly 10 mins at 10:28:49Z and rerun without the tail pipe at 10:29:58Z are the only two, and the "read as a hang a second time" line in the issue body is not in the record. The entry says one kill.

Two things the artifacts gave that the recollection did not, and both are in the entry:

  • The buffering is measured, not inferred. The | tail -40 call was backgrounded at 600s, and a Read of its task output file five seconds later returned the file exists but the contents are empty. That file reached its final 6,648 bytes at 09:37 — the instant the run exited. Ten minutes of run, zero bytes.
  • The counter-example was already in the same session. The producer at 08:36:08Z was started with run_in_background: true — i.e. by not following step 1 — and its output file was readable six seconds later with the fast-forward verdict, the log: path and the runner invocation in it. It then ran 32m 48s to a clean exit, unwatched and unkilled.

LJ-0004

interactive population, because the mistake is the instruction's and the caller's — the vetter run is the casualty, not the culprit, and no cron run count measures whether a human following a plugin command got it right. Kind foreground-call-prescribed-for-work-that-outlives-it, deliberately distinct from LJ-0001's backgrounded-gate-strands-the-run: those are opposite failures, and collapsing them would read this entry as a recurrence of #249's rule, which it is not.

Its rule is this step, landing in this change, so the verdict reads it as addressed rather than as backlog — the loop the journal exists for, closed once.

Step 1

Keeps the rule: the background invocation, reading that call's output file, no tail/head/filter between the stream and the reader, log: + watch-run as the route back, and what stopping the task does to the runner and the flock. Loses the durations, the killed-run story and the tail post-mortem, which are now [LJ-0004].

force-run is untouched.

Byte count, honestly, because the cut is not the whole delta:

step 1
before this PR (f5f685b) 1,852
this PR at ad44137 3,770
exposition cut −194
head correctness fix (below) +271
now 3,847

The exposition cut is real but small, because ad44137's step 1 was already mostly rule. The net is +77, and it buys a correct statement of a mechanism that was wrong.

CodeRabbit

Swept via GraphQL rather than the check state. Two unresolved threads, both answered, both resolved.

head was described wrongly, and that is fixed. The step said tail and head "both hold the whole stream until the writer exits". Only tail does. Tracing head through force_run_mode gives a different symptom from the broken-pipe abort CodeRabbit proposed: stream_run_output returns the moment writeln! errors, and the next statement is child.wait() — so the runner keeps going, force-run keeps blocking, the pipeline does not return early, and the caller gets the first few lines delivered at the end. Both readers are now banned, each for its own mechanism.

The gate accepts a step that keeps every token and means the opposite. True, and it is the blind spot the comment above the gate already states: GOOD_STEP's step 2 is deliberately wrong so the scoping test can prove a rule satisfied next door is not satisfied here. Not tightened, because requiring a literal prohibition phrase turns the gate into a checksum of two sentences — exactly what an_unemphasised_mention_of_the_other_mode_is_not_a_prescription exists to prevent — and any phrase it demanded could be negated in the following clause anyway. Raised for a human rather than silently changed.

Gates

  • main merged in, twice: d854b15 (Journal LLM mistakes separately from the prompt, so rules can be deleted #258, which is what makes appending to the journal safe — it turned the seeded-journal tests from equality to presence/containment) and f8cfe7d (Make CLAUDE.md a router: 47,797 → 20,983 bytes, with a pointer per moved file #263, the CLAUDE.md router). Both clean; the full suite was re-run on the merge commit, not trusted to the absence of markers.

  • nix develop .#rust -c cargo test1,388 passed, 0 failed (1,267 unit + 121 integration).

  • pre-commit run --all-files under the pinned rainix rust-shell — green, with the one rustfmt rewrite committed and the re-run clean.

  • Mutation pass, 7 mutants over the two things that carry the new behaviour — the journal line and the step's citation. 6 kill-expected, 6 killed; 1 control survived as intended. Every anchor was asserted to match exactly once before the mutant was applied, every run routed its output through a file, and every run reported its own test count (1,267) so a zero-match filter could not fake a survival. Baseline and restored both green.

    mutant verdict
    LJ-0004's rule → absent KILLED
    LJ-0004's population interactivevetter KILLED
    evidence loses the tool calls 52 quote KILLED
    evidence loses the empty-task-file quote KILLED
    [LJ-0004][LJ-0009] in the step (citation resolves to nothing) KILLED
    citation moved out of step 1 into step 2 KILLED
    CONTROL — the entry's narrative reworded (7m 07s → "a long silence") SURVIVED, as intended: the gate reads the entry's structure and its quoted evidence, not its prose

One thing found and left alone

every_journal_citation_in_a_prompt_resolves_to_an_entry scans RUNNER_CONTEXT_FILEScampaign-prompt.txt, campaign-worker-prompt.txt, review-prompt.txt, CLAUDE.md. A plugin command is not in that set, so the [LJ-0004] this step now carries is outside the repo's general citation gate. That is arguably right — a plugin command is read when a human invokes it, not shipped into every cron turn, which is the cost the set is about — but it means nothing generic would catch the citation going stale.

Pinned specifically instead, in journal_tests::the_journal_records_the_incident_the_observe_run_rule_came_from: the id must resolve to an entry, and it must appear in step 1, not merely somewhere in the file. Both are mutation-verified above. Whether the general scan should widen to cover plugins/human-fsm/commands/ is a design call, left for a human.

Also noted, not touched: cargo clippy -D warnings locally flags collapsible_match at main.rs:4233. It is byte-identical to origin/main and dates to 944a214 (2026-08-03), so it is a local-toolchain-vs-pinned-CI difference and not this branch's.

The gate guarded the wrong direction. It watched for someone editing
`**background**` back to `**foreground**` — a regression nobody has made —
while the mistake that actually happened was a human following a wrong
instruction. It could not see the failure that would matter, a step saying
`**background**` in a sentence meaning the opposite, and its three constants
were a second source of truth for what one paragraph must contain.

Removes the whole `#260` block: `BASH_CALL_MODES`, `BUFFERING_READERS`,
`REATTACH_ROUTE`, `LongRunStepFault`, `emphasised_call_modes`,
`long_run_step_faults`, `command_step`, `GOOD_STEP` and all eight tests.
Nothing outside the block referenced any of it.

The entry-shape assertions over LJ-0004 go with it, for the same reason: the
parser already validates the entry on every read, so asserting its fields again
was a second source of truth. What is left is the one property nothing else
guards — `every_journal_citation_in_a_prompt_resolves_to_an_entry` scans the
runner context files, and a plugin command is not one, so a `[LJ-0004]` that
resolved to nothing would go unnoticed.

Step 1's buffering ban is cut to the operative instruction and one clause on
why, naming `tail` because that is the one a caller reaches for; the `head`
mechanism was speculative and the citation is what the detail is for. The kill
paragraph keeps its corrected mechanism and its probe, without the stand-in's
anatomy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Cut the conformance gate, at b7cc8f26fb917dd40581f1c50d9f82cee8f6656d. The ask was that the task not block and run in the background; the gate was answering a question nobody asked.

Removed

The whole #260 block in main.rsBASH_CALL_MODES, BUFFERING_READERS, REATTACH_ROUTE, LongRunStepFault, emphasised_call_modes, long_run_step_faults, command_step, GOOD_STEP and all eight tests. Checked before deleting: nothing outside the block referenced any of it, so there is no dangling reference.

It guarded the wrong direction. What it watched for was someone editing **background** back to **foreground** — a regression nobody has made — while the mistake that actually happened was a human following a wrong instruction. It could not see the failure that would matter (a step saying **background** in a sentence meaning the opposite passes it clean), and its three constants were a second source of truth for what one paragraph must contain.

The entry-shape assertions over LJ-0004 went with it, for the same reason: journal_entry already validates every entry on every read, so re-asserting its fields was the same duplication one level down.

Kept

mistake-journal.jsonl LJ-0004 unchanged — the entry is the artifact, and the evidence belongs there rather than in a prompt. The two version bumps. Step 1's substance: background Bash call, Read its output file, nothing piped, log: + watch-run as the route back. The kill paragraph keeps its corrected mechanism and the fact that it was probed — the old text ("kill this call and the runner dies") was wrong for a backgrounded call.

The one Rust thing left is 28 lines, and it is not a conformance gate over the prose:

fn the_observe_run_citation_resolves()

every_journal_citation_in_a_prompt_resolves_to_an_entry scans RUNNER_CONTEXT_FILES, and a plugin command is deliberately not one — it is read when a human invokes it, not shipped into a cron run's context on every turn. So a [LJ-0004] that resolved to nothing would go unnoticed, and a citation resolving to nothing has deleted the reasoning rather than moved it. That is the only property it asserts.

Compressed

The buffering ban was two mechanisms at length. tail is what actually bit on 2026-08-10; the head mechanism was reasoned from the source, not observed. Cut to the instruction and one clause:

Pipe it into nothing at all — and read the file instead. force-run flushes every line as it prints it, and a pipe throws that away: tail without -f reads to EOF before printing anything, so the output file stays empty for the whole run [LJ-0004].

Step 1 byte count — it does not get under, and here is exactly where

step 1
before this PR (f5f685b) 1,852
this PR at ad44137 3,770
now (b7cc8f2) 2,948

−822 from the last head, but +1,096 over the 1,852 target. I did not trim substance to reach it. Per paragraph, against what was there before:

paragraph before now note
heading + code block 102 102 untouched
fast-forward 497 497 untouched, pre-existing
streams through the watch filter 285 285 untouched, pre-existing
the invocation 156 680 was two lines of **foreground**; now the background call, the Read, why a foreground call cannot be held, and the citation
the pipe ban 258 new
the route back when the file is gone 195 new
--force policy stops 588 588 untouched, pre-existing
killing mid-run 210 325 corrected mechanism plus the probe

Everything above 1,852 is on the keep list: the invocation paragraph, the pipe ban, the route back, and the corrected kill mechanism. Getting under the old number means deleting one of those. The old number bought its size by being wrong — 156 bytes that could not be followed.

The two pre-existing untouched paragraphs are 1,085 bytes of the 2,948, and the --force policy paragraph alone is 588. If step 1 should be smaller than it was, that is where the room is, and it is not this PR's text.

Gates

  • nix develop .#rust -c cargo test1,380 passed, 0 failed (1,259 unit + 121 integration). Unit is −8 from the last head, which is exactly the eight tests removed.
  • pre-commit run --all-files under the pinned rainix rust-shell — green, with the one denofmt rewrap committed and the re-run clean.
  • No mutation pass: there is no Rust behaviour in this PR. The diff is the journal line, the command markdown, two version strings, and one test that resolves a citation.
  • CI checked once at this head; jobs still running, nothing red.

Diffstat, whole PR against main

 .claude-plugin/marketplace.json              |  2 +-
 mistake-journal.jsonl                        |  1 +
 plugins/human-fsm/.claude-plugin/plugin.json |  2 +-
 plugins/human-fsm/commands/observe-run.md    | 26 ++++++++++++++++++++++----
 pr-review-report-rs/src/main.rs              | 28 ++++++++++++++++++++++++++++
 5 files changed, 53 insertions(+), 6 deletions(-)

…t of step 1 alone

The replacement paragraph argued its case at length — the 600-second ceiling,
the blank screen, "read as a hang and killed", "returns in seconds", the EOF
mechanism, the empty output file. All of it is incident narrative, and LJ-0004
is where that lives now; the step gets the instruction and the citation.

Four paragraphs become one: background call, read the file it names, no pipe of
any kind, one read then move on, and the reattach route when the file is gone.
The kill correction keeps its second sentence and nothing else.

The pre-existing paragraphs are restored byte-identical to main. This PR's diff
is two hunks in text this PR owns; the fast-forward, watch-filter and --force
paragraphs are not this PR's to edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Cut to the instruction, at fc9d046e0602307a2f94e67bc7ef670a9a82cb88. The diff is now two hunks in text this PR owns, and nothing else in observe-run.md is touched.

The whole change to the command

-Run it in a **foreground** `Bash` call. It blocks for the whole run, which is
-the point; wrapping it in anything that returns immediately abandons the wait.
+Start it in a **background** `Bash` call and read the output file it names.
+Don't pipe it — not `tail`, not `head`, nothing: anything between the stream and
+you holds it. One read when you want to know where the run is, then get on with
+something else; the exit arrives as a task notification. If that file is gone,
+the `log:` line names where the same bytes are still going and step 2's
+`watch-run` reattaches there. [LJ-0004]

 **Killing mid-run is sometimes right** — the 2026-08-09 run was killed once.
-Kill this call and the runner dies with it, which releases the flock, because
-the lock is an open descriptor on the runner process.
+Stopping the background task kills the runner and releases the flock [LJ-0004].
hunk before after
the invocation 156 429 +273
the kill correction 131 79 −52
total 287 508 +221

At the previous head the same region ran to 2,055 bytes across four paragraphs. It is now one paragraph and one sentence.

Everything cut — the 600-second ceiling, the blank screen, the killed run, "returns in seconds", the EOF mechanism, the empty output file, the probe anatomy — is in LJ-0004, and [LJ-0004] is the pointer to all of it. None of it was moved there to make room; the entry was already written and is unchanged.

Restored byte-identical to main: the fast-forward paragraph (497), the watch-filter description (285), and the --force policy-stops paragraph (588). Earlier heads of this branch had rewritten all three. They are not this PR's to edit and the diff no longer contains them.

Operative instructions, all present: background Bash call; read the output file it names; no pipe of any kind, both tail and head named; one read then get on with something else; exit arrives as a task notification; log: + step 2's watch-run as the route back when the file is gone; stopping the task kills the runner and releases the flock.

One thing to flag on the kill citation

[LJ-0004] on the kill sentence resolves, and the incident does evidence the kill half — rejecting the call killed run 20260810T102325Z, which is why review.log has no END line for it and metrics/runs.jsonl no final row. The flock-release half rests on the stand-in probe, which is recorded in this PR's earlier description rather than in the entry. Worth knowing if the entry is ever read as the sole source for that sentence.

Gates

  • nix develop .#rust -c cargo test1,380 passed, 0 failed (1,259 unit + 121 integration).
  • pre-commit run --all-files under the pinned rainix rust-shell — green, nothing rewritten.
  • No mutation pass: no Rust behaviour in this PR.
  • CI checked once at this head — nothing red, remaining jobs still running.

Diffstat

 .claude-plugin/marketplace.json              |  2 +-
 mistake-journal.jsonl                        |  1 +
 plugins/human-fsm/.claude-plugin/plugin.json |  2 +-
 plugins/human-fsm/commands/observe-run.md    | 11 +++++++----
 pr-review-report-rs/src/main.rs              | 28 ++++++++++++++++++++++++++++
 5 files changed, 38 insertions(+), 6 deletions(-)

The journal entry, the two version bumps and the 28-line the_observe_run_citation_resolves test are unchanged.

`[LJ-0004]` pointed at an entry that evidences only half the claim. The incident
does show that stopping the task kills the runner — the killed run wrote no
`END` line and no `final` metrics row — but the flock release rests on a probe
the entry does not carry, and it should not: a successful mechanism check is not
a mistake, and padding a mistake journal with one to justify a citation is the
same defect in a different file.

The sentence stands as a bare instruction. The invocation paragraph keeps its
citation, which is fully backed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed c91d605: approve
Rulings-conformance: checked against the CLAUDE.md Invariants section and every ruling stated for this work, including the four corrections that reshaped it. (1) "wouldnt it be better to have a journal of llm mistakes separate from the campaign prompt that we can analyse the prompt against separately without shipping it to context" — LJ-0004 is one line in mistake-journal.jsonl; nothing is added to campaign-prompt.txt or review-prompt.txt. (2) "262 is full of exposition that should go in the mistake journal" — the measured narrative (600s ceiling, the 32m 48s / 22m 30s durations, the killed run 20260810T102325Z, the tail post-mortem, the flock probe) is out of the command file and in the entry, cited once as [LJ-0004]. (3) "this seems hugely complex, all i wanted was the task to not block and to run in the bg" — the 228-line conformance gate over step 1 is deleted in full, along with the entry-shape assertions that duplicated journal_entry's own validation. What remains is 28 lines asserting the citation resolves, kept because delete_next makes entry deletion a designed eventuality, so a dangling id is a real failure rather than a hypothetical one. (4) "still too much exposition in the prompt" and "literally the diff you just showed me is still too much cruft" — this PR's own text is now one paragraph plus one sentence: 2,055 bytes at b7cc8f2, 508 now. (5) "i don't need you to change existing things, just the diff" — my scope widening reverted: the fast-forward, watch-filter and --force paragraphs are byte-identical to main, guaranteed by rebuilding the file from origin/main and re-applying only this PR's two hunks rather than hand-checking. observe-run.md is +7/-4 lines. (6) "comments: no process" — step 1 describes what to do now and does not narrate its own history. (7) The [LJ-0004] on the kill sentence was removed: the entry evidences the kill but the flock release rests on a stand-in probe that is a successful mechanism check, so it has no place in a mistake journal and the sentence stands as a bare instruction. The one remaining citation is fully backed. (8) "producer resolves merge conflicts — merge base in, never rebase" — main merged in twice (d854b15 #258, f8cfe7d #263), full suite run on the merge commit each time. Gates: 1,380 tests pass, pre-commit run --all-files green at pinned rainix 53e96a7d with nothing rewritten, 0 unresolved CodeRabbit threads. No mutation pass: no Rust behaviour remains in this PR, only a citation-resolution assertion. Merging with pending (not red) checks — 7 pass, 1 skipped, 12 still starting from the last push, 0 failures.

@thedavidmeister
thedavidmeister merged commit beea871 into main Aug 10, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

observe-run step 1 tells the caller to hold a foreground call that outlives the 600s ceiling — runs read as hangs

1 participant