feat(flows): Software Garden v3 — stop when the implementer changed nothing, relay its question

Merged
KH
9d ago
+1126-20

Incident

Run 91c1a5cd (wepost-no/wepost-saga, 2026-10-09, 1h budget): the implementer couldn't identify which file the Linear ticket meant. It asked about a "gamification README" / "NATIVE 3.0" that doesn't exist and made no commits. The flow still ran the whole check pipeline anyway:

  • the first check failed in 2s
  • check-repair ran for 7 min / $1.63, ending with "I'll just wait for the original background test command to finish naturally"
  • a 10-minute re-check followed

Only after all that did publish see no commits and stop with done("needs_human"). That's about 17 wasted minutes and no PR, and the implementer's question never reached the customer. Separately, every run pays for check-discovery again (~1.5 min, $0.66), because .relayflow/check.sh is never committed.

Changes

All of these are in the generator (web/lib/flow-workflows.ts, web/lib/flow-onboarding.ts) and published as Software Garden v3 (web/public/flows/software-garden/v3.flow.ts, sha256 a54a282e0ac607bf4481aee5c3353c7a5c63178da44080ba72ff872b24fcba73, 95632 bytes).

  1. Fail fast when nothing changed. After the implementer finishes, and only if it did not time out, FLOW_CHANGE_CHECK_COMMAND compares the branch with the base commit and prints unchanged, changed or unknown.
    • Only a definite unchanged stops the run: the tracked files hold what the base holds. It runs git diff --no-renames --name-only $base -- (base vs working tree), so committed, staged and unstaged edits count, an undone edit doesn't, and a rename counts its source's deletion.
    • Not counted: working files the base doesn't have, whether committed, staged or edited again (the drop step's rule); anything under .relayflow/, even when the repository commits it (the flow writes the run's checkCommand there itself before the implementer); and untracked files, which nothing in the flow publishes. Up to five untracked files are named on stderr.
    • When the run stops here, checks, repair, base comparison, push and PR are all skipped. It ends with done("needs_human") and a customer-facing message that includes the relayed question.
    • changed, unknown and anything unrecognised carry on exactly as before.
  2. The implementer states the ambiguity. Its task now includes FLOW_NEEDS_INPUT_HINT: if the ticket is ambiguous or the target can't be identified, don't guess and don't commit. Write .relayflow/needs-input.md with the specific questions and the candidates found (e.g. "Did you mean docs/native-gamification-plan.md?").
    • It's appended to the implementer's task as a fixed hint rather than put in its editable default prompt, so it also applies to saved custom prompts.
    • .relayflow/ is a working file, so the question is never committed.
    • FLOW_NEEDS_INPUT_COMMAND relays the question in the contract below. If there's no file, it falls back to the end of the implementer's own last message (AgentResult.summary). The flow passes a line-aligned window of its end plus the message's full UTF-8 size, and the relay keeps the end in bytes (below).
  3. input.checkCommand. Input gains checkCommand?: string. When it's a non-empty string it replaces check discovery. Otherwise the author-editable const checkCommand = "" and then discovery apply, unchanged.
  4. Help future runs skip setup. When checks pass on a check script that was discovered, the ecosystem default, or changed by check-repair (tracked by cksum before the first repair), the PR body says so. It suggests committing the script as .relayflow/check.sh or setting the flow's test command in Cloud.
  5. Phase lines. relayflow phase: implementing|checking|repairing|reviewing|publishing are printed on stderr (console.error). They only log and don't change control flow.

Everything else is identical.

needs-input output contract

When the run stops because nothing changed, FLOW_NEEDS_INPUT_COMMAND prints this on its step's stdout. The flow's stop message (console.error) repeats it after its first line:

relayflow needs-input-source: needs-input.md | implementer-summary | none
relayflow needs-input: <one line of the question>            (zero or more, in order)
relayflow needs-input-truncated: <kept> of <total> bytes     (only when cut)
  • The source record comes first and appears exactly once.
  • The text is sanitized before printing:
    • ANSI colour sequences and control characters (CR included) are removed
    • tabs become spaces
    • each line is trimmed and blank lines are dropped
  • It's then bounded to 4000 bytes, cut at a line boundary (or a UTF-8 character boundary for a single line), and to 60 lines. A needs-input.md keeps its start. The implementer's summary keeps its end, where the question is. For a summary the flow had to cut, <total> is the whole message's size.
  • Every printed line carries one of the three prefixes, so the text can't forge a record. A line that already starts with relayflow needs-input-source: comes out as relayflow needs-input: relayflow needs-input-source: ….
  • Exit status is 0 on every path.

Example (what 91c1a5cd would have produced):

Stopped: the implementer made no changes, so there was nothing to check or publish. No checks ran, no branch was pushed and no pull request was opened. Its question or reason follows, when it gave one: answer it on the ticket and run again.
relayflow needs-input-source: needs-input.md
relayflow needs-input: Which file is the "gamification README"?
relayflow needs-input: Did you mean docs/native-gamification-plan.md?

Version decision

Published versions are immutable:

  • publish-software-garden.mts writes v<N+1> and never rewrites an earlier version.
  • software-garden-artifact.test.ts fails if a published file's sha256 changes.

So this is a new v3 cut by the publish script, with v1 and v2 untouched.

The catalog still pins v1 (catalogVersion 5, ref e3f5442, sha256 b89a36e2…). v2 (the 180-minute plan, #178) isn't served yet either. This PR doesn't move the pin, because source.ref has to be a commit on main that contains the file, and that only exists after merge.

Follow-ups

In this repo, after merge: a small PR that moves software-factory's catalog source to v3.flow.ts:

  • release: software-garden-v3, ref = the merge commit, sha256 a54a282e…
  • advance catalogVersion 5 → 6
  • add row 6 to SOURCE_BY_CATALOG_VERSION in software-garden-artifact.test.ts

Cloud must change:

  • Activation upgrade path. upgradeRecommendedFlowActivation (flow-activations.ts) refuses a changed source at the same catalogVersion ("The catalog changed an artifact without advancing its version"). So existing Software Garden activations only move to v3 once the catalog bump above lands. They then need an upgrade, either the owner-triggered upgrade or a batch upgrade. New activations get v3 from the catalog directly.
  • Fallback regeneration. recommended-flow-fallback-source.ts is still generated from the old flows example (AgentWorkforce/flows@8b33ebab…/examples/software-factory), not Garden. If the fallback should serve Garden, point update-recommended-flow-fallback-source at web/public/flows/software-garden/v3.flow.ts at the pinned ref, and re-run npm run recommended-flow-fallback-source:update so recommended-flow-fallback.test.ts pins the new sha256.
  • Relay needs-input to Linear. When a Garden run ends needs_human, parse the relayflow needs-input* records from its stop message or step output and post them on the ticket.
    • The flow has already sanitized and bounded the text. Cloud should still escape it as untrusted Markdown before posting.
    • needs-input-source: none means there's nothing to ask: post a generic "made no changes" note, or skip.
  • Pass checkCommand. Add the flow's test command as a setting and include it in the run input ({ approver, issue, checkCommand }). Empty or absent keeps discovery.

Test plan

Commands run locally (latest run: review round 1, commit 6bf465a). CI's test.yml runs the same, plus the prpm reinstall:

CommandResult
npm ciok
cd web && npx vitest run46 files, 575 tests passed
cd router && npx vitest run9 files, 97 tests passed
cd web && npx tsc --noEmit0 errors
cd router && npx tsc --noEmit0 errors
npm run verify:recommended-flowscatalog gates ok; software-factory verified software-garden-v1
npm run verify:setup-skillssetup skills match prpm.lock
npx flows check v3.flow.ts (@relayflows/sdk 2.0.42, scratch dir)CHECK PASSED
tsc --strict on v3 against @relayflows/surface 2.0.420 errors
sha256sum web/public/flows/software-garden/v3.flow.tsa54a282e… = manifest; v1 and v2 byte-identical

The repo has no lint script or ESLint config, so typecheck stands in for lint.

Tests in web/lib/test/flow-needs-input.test.ts (27). The 9 added in the review round fail against the previous generator and pass now:

  • FLOW_CHANGE_CHECK_COMMAND on real git repos:
    • unchanged with only working files, including a committed summary.md that was edited again (staged or not); a committed or edited .relayflow/check.sh the base already has; only untracked files (named on stderr); an edit that was undone or reverted.
    • changed for a commit, an unstaged edit, a staged new file, a deletion, a rename onto summary.md/plan.md (committed or staged), or an edit to the repository's own summary.md.
    • unknown (never unchanged) for an empty or missing base, or outside a repo.
  • FLOW_NEEDS_INPUT_COMMAND: the record contract, with sanitizing (ANSI, CR, BEL, tabs, blank lines), forged prefixes, fallback to the implementer's summary, none, and UTF-8-safe truncation at 4000 bytes and 60 lines. A file keeps its start. A summary keeps its end: the last whole lines, or a character-aligned tail of one line. agent_summary_bytes sets <total>.
  • Check report: the setup note appears for discovered and default scripts and for repaired scripts. It doesn't appear for input, an unrepaired repository script, failed checks, or a caller that sets nothing (the old output).
  • Generated-flow simulations:
    • No-commit fail-fast: no check, repair, base check, push or PR, needs_human, and the question in the stop message.
    • Summary fallback: the flow's own relay call runs for real on a multibyte 2000-line message, and the question at the end survives.
    • Carries on as before: for changed, unknown, '' and garbled output, and for a timed-out implementer.
    • input.checkCommand: used, trimmed, takes precedence over the author's constant, and is ignored when empty, blank, absent or not a string.
    • Second resolve: checkSetup becomes default when it writes the default script.
    • Ticket metadata: a missing or non-string source, identifier or url doesn't throw.
    • Other: the implementer hint is present in all three workflows, the digest is taken before repair, and the phase-line order is checked.

Two existing assertions changed:

  • flow-budget.test.ts "fits a typical run … into 30-45 minutes" was already at 44.98 simulated minutes. The change check is one more step, which the simulation charges 5 s like every step, so the bound is now 45 + 5/60 with a comment.
  • flow-onboarding.test.ts looks for the new second-resolve line.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Changes core Software Garden control flow (early exit, check command precedence, and customer-facing relay contract); mistakes could skip legitimate work or drop questions, though extensive new tests cover the shell commands and generated flow paths.

Overview
Software Garden v3 updates the flow generator and ships a new published artifact (v3.flow.ts). The main behavioral change is fail-fast when the implementer leaves the repo unchanged: after implementation (and only if the implementer did not time out), a git-based change check ends the run with needs_human instead of running checks, repair, push, or opening a PR.

When that happens, the run relays the implementer’s question to the ticket via structured relayflow needs-input* stdout records—from .relayflow/needs-input.md when written, otherwise from the tail of the implementer’s last message. The implementer task now instructs agents to write that file instead of guessing when a ticket is ambiguous.

Check setup gains optional input.checkCommand (Cloud “test command”), which overrides in-flow discovery; passing runs can note discovered/default/repaired check scripts in the PR so repos can commit .relayflow/check.sh or set a command to skip rediscovery. Ticket metadata is read through a safe string helper so bad local input does not throw. Phase progress lines go to stderr during implement/check/repair/publish/review.

Tests add flow-needs-input.test.ts and a small budget-test slack for the extra change-check step; the catalog manifest bumps to version 3 (catalog pin to v3 is explicitly out of scope for this PR).

Reviewed by Cursor Bugbot for commit 6bf465a51224355c46d2b20c3fd5db2cbe688c49. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Software Garden v3 stops the run as soon as the implementer made no changes, and relays its question to the ticket, so a run like 91c1a5cd no longer checks, repairs, and re-checks an empty branch for ~17 minutes and $1.63 before giving up.

  • After the implementer finishes, a change check against the base commit ends the run with done("needs_human") on a definite unchanged, skipping checks, repair, base comparison, push, and PR. changed, unknown, and garbled output carry on exactly as before, and a timed-out implementer is never stopped here.
  • The change check counts committed, staged, and unstaged edits; working files the base lacks, .relayflow/ contents, and untracked files don't count (a few untracked files are named on stderr), and a rename still counts its source's deletion.
  • The implementer's task now says: if the ticket is ambiguous, write .relayflow/needs-input.md with the specific questions and candidates instead of guessing or committing. The stop message relays it, with the end of the implementer's own last message as fallback, as sanitized and bounded relayflow needs-input* records.
  • Input gains optional checkCommand, which replaces check discovery when non-empty; the author's constant and discovery apply otherwise.
  • When checks pass on a discovered, defaulted, or repaired script, the PR body suggests committing it as .relayflow/check.sh or setting a test command in Cloud so future runs skip setup. checkSetup is marked default when the re-resolve writes the default script. Phase lines are printed on stderr for progress visibility, and ticket metadata that is missing or not a string reads as blank instead of throwing.

Follow-ups

  • The catalog still pins v1; a follow-up PR advances the pin to v3 with the merge commit's sha256.
  • Cloud must parse the needs-input records and post them to the Linear ticket, and pass the flow's test command as checkCommand; existing activations need an upgrade to move to v3.

Agent Relay sessions

  • claude session f4d3db03 · opened via gh pr create · last active 2026-10-09
  • claude session 5d87b716 · contributor · ran gh pr commands · last active 2026-10-10