examples: software-factory and stale-issues for the README use cases - #450
Conversation
…digest) Two README use-case flows authored against the real @relayflows/surface package and registered in examples/tsconfig.json. software-factory deploys today: `flows deploy … --on linear:team=ENG` launches implementer → deterministic tests → adversarial review agent whose verdict is a file → tests again → `gh pr create` (draft + findings when the review blocks, ending step_failed). stale-issues is the scheduled automation: deterministic issue fetch, one f.llm step with a JSON schema gate, one Slack digest. `flows schedule` is in flight; until it ships the README shows `flows run --cloud` on demand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reachedNext included review available in 16 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
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. Comment |
There was a problem hiding this comment.
Devin Review found 6 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
|
|
||
| await f.run(TEST, { timeout: "15m" }); | ||
|
|
||
| const verdict = await f.run("if [ -f review.passed ]; then echo PASSED; else echo BLOCKED; fi"); |
There was a problem hiding this comment.
🟡 Existing pass marker overrides review
When the checkout contains review.passed, verdict reports PASSED even if the adversary writes review.blocked. The flow opens a non-draft PR despite a blocked review.
Learn more
The verdict files live in the repository workspace, so either one can predate the adversary step. The current test checks only whether review.passed exists. A blocked verdict cannot override an existing pass marker, and simultaneous markers also resolve as passed.
Example: The target repository already tracks review.passed. The adversary writes review.blocked after finding a regression. The command still prints PASSED, and the flow opens a ready-for-review PR instead of the promised blocked draft.
Recommended fix: Remove both verdict markers immediately before the adversary runs, then accept PASSED only when review.passed exists and review.blocked does not. Treat both present or both absent as blocked or as a gate failure.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in a3fdef0: artifacts live in .relayflow/ which is wiped at the start of every run, and PASSED requires review.passed present AND review.blocked absent.
| `curl -sf -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/${input.repo}/issues?state=open&per_page=100" ` + | ||
| `| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const now=Date.now();console.log(JSON.stringify(JSON.parse(s).filter(i=>!i.pull_request).map(i=>({number:i.number,title:i.title,labels:i.labels.map(l=>l.name),updatedDaysAgo:Math.floor((now-Date.parse(i.updated_at))/864e5),comments:i.comments}))))})'`, |
There was a problem hiding this comment.
🟡 Issue discovery stops after 100
Repositories with over 100 open issues make issues fetch only the first page. The digest undercounts issues and omits later pages from triage.
Learn more
GitHub's REST issues endpoint paginates results, and per_page=100 only raises one page's limit. The command never follows response pagination links. Filtering pull requests after fetching does not expose omitted pages.
Example: A repository has 140 open issues and no open pull requests. The request returns 100, the digest reports 100 open issues, and issues 101–140 never reach the classifier.
Recommended fix: Fetch every page before filtering and mapping. Prefer gh api --paginate --slurp or implement Link-header pagination, then combine all pages into one JSON array.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in a3fdef0: the fetch now runs under node, follows pagination (sort=updated&direction=asc, up to 20 pages) and only stops on a short page.
| { output: { type: "object", required: ["stale", "attention"], properties: { | ||
| stale: { type: "array" }, attention: { type: "array" } } } }, |
There was a problem hiding this comment.
🟡 Malformed triage entries pass validation
When f.llm returns malformed entries, triage accepts them because both arrays omit items schemas. Null entries crash digest construction; incomplete objects post undefined values.
Learn more
The cast to Triage does not add runtime checks. JSON Schema arrays without an items declaration accept values of any type. The later formatter assumes every item is a non-null object with numeric number and string title and reason fields.
Example: A valid response under the current schema is { "stale": [null], "attention": [] }. Validation passes, but triage.stale.map(line) throws while reading i.number, so no digest is posted.
Recommended fix: Give both arrays an items object schema with required: ["number", "title", "reason"], matching property types, and additionalProperties: false. Keep the top-level required fields and consider disallowing extra top-level properties as well.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in a3fdef0: both arrays declare items (integer number ≥ 1, bounded title/reason, additionalProperties: false, maxItems: 50); findings are also filtered to issue numbers that were actually fetched.
| `curl -sf -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/${input.repo}/issues?state=open&per_page=100" ` + | ||
| `| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const now=Date.now();console.log(JSON.stringify(JSON.parse(s).filter(i=>!i.pull_request).map(i=>({number:i.number,title:i.title,labels:i.labels.map(l=>l.name),updatedDaysAgo:Math.floor((now-Date.parse(i.updated_at))/864e5),comments:i.comments}))))})'`, |
There was a problem hiding this comment.
Fixed in a3fdef0: input.repo is validated against owner/name before any step, and the fetch takes it as a node argv (node -e '…' 'owner/name') rather than interpolating it into the shell.
| await f.run(`gh pr create --title ${JSON.stringify(issue.title)} --body-file summary.md`); | ||
| return f.done("success"); | ||
| } | ||
| await f.run("{ cat summary.md; printf '\\n\\n## Adversarial review: BLOCKED\\n\\n'; cat review.md; } > pr-body.md"); | ||
| await f.run(`gh pr create --draft --title ${JSON.stringify(`[blocked] ${issue.title}`)} --body-file pr-body.md`); |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in a3fdef0: titles go through shellWord (single-quoted, the SURFACE.md idiom) and are capped at 200 chars; nothing from the ticket reaches /bin/sh unquoted any more.
| { output: { type: "object", required: ["stale", "attention"], properties: { | ||
| stale: { type: "array" }, attention: { type: "array" } } } }, |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in a3fdef0: model text is escaped for Slack mrkdwn (& < >), links are built only from the validated repo and the integer issue number, and findings not in the fetched set are dropped.
Review swarm: maintainabilityNo fresh transcript was produced for run |
Review swarm: historyNo fresh transcript was produced for run |
Review swarm: structureNo fresh transcript was produced for run |
Review swarm: FAILED
Cloud run: |
…tput, fix verdict and git add Review round on #450 (Devin, Cursor), all valid: - software-factory: ticket titles reached `gh pr create` through JSON.stringify, which leaves `$(…)` and backticks live under /bin/sh — a crafted Linear/GitHub title ran commands with the worker's token. Titles now pass through `shellWord` (single-quoted, the SURFACE.md idiom) and are capped. Flow artifacts moved to `.relayflow/`, excluded via `.git/info/exclude`, so `git add -A` needs no pathspec (the `:!`-only form errored) and a leftover `review.passed` from a previous run cannot override a fresh `review.blocked`; PASSED now requires the pass marker and the absence of the block marker. - stale-issues: `input.repo` is validated as owner/name before use and the fetch runs under node with the repo as an argv, never interpolated into the shell; GitHub pagination is followed (oldest-updated first) so repositories over 100 issues are triaged in full; the LLM schema now declares `items` (integer number, bounded strings, no extra keys); digest lines only reference issue numbers that were actually fetched and escape model text for Slack mrkdwn, so a response cannot forge links or mentions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a3fdef0. Configure here.
…node -e Cursor on #450: node -e may place an [eval] placeholder at argv[1], so argv.slice(1)[0] can be the wrong slot. Use argv.at(-1), which is the quoted repository on every Node version; probed the request URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Two example flows the README's "What can you do with this?" section points at, authored against the real surface package and registered in
examples/tsconfig.json(npm --prefix packages/surface run typecheck:examplespasses;flows checkpasses on both, the Slack one underRELAYFLOWS_SLACK_MOCK=1).examples/software-factory/— deployable today:flows deploy … --repo o/r --on linear:team=ENG --approver you. Implementer → deterministic tests → adversarial review (verdict is a file) → tests →gh pr create; a blocked review opens a draft PR with the findings and endsstep_failed.examples/stale-issues/— the scheduled digest: deterministic GitHub fetch, onef.llmwith a JSON-schema gate, onef.slack.post. Targetsflows schedule(in flight); documentsflows run --cloudon demand until then.The Review use case points at the existing
examples/pr-review-pipeline/, which #434's successor is making runnable.🤖 Generated with Claude Code
Note
Low Risk
Additive example and documentation only; no changes to core runtime, though the samples exercise shell, GitHub, and Slack when users deploy them.
Overview
Adds two README-linked example flows under
examples/, each with a short README and a@relayflows/surfaceflow file, and registers them inexamples/tsconfig.jsonfor example typechecking.software-factorywires ticket-driven automation: implementer agent → gatedsummary.md→ deterministicnpm test(exit-code gated) → adversarial reviewer that must writereview.passedorreview.blockedunder.relayflow→ tests again → commit/push andgh pr create(draft with findings when blocked,step_failed). Deploy/local run instructions coverflows deploywith--ontriggers and--local-agent.stale-issuesis a scheduled digest pattern: validated repo name, journaled Node fetch of all open issues (paginated, no PRs) → singlef.llmtriage behind JSON schema → Slack post with mrkdwn escaping and issue numbers filtered to the fetched set. Documentsflows scheduleand interimflows run --cloudplusRELAYFLOWS_SLACK_MOCK=1for local check.Both examples emphasize untrusted input: shell arguments via
shellWord/quoting, artifacts outside git’s tracked tree, and deterministic gates instead of agent-reported pass/fail.Reviewed by Cursor Bugbot for commit 7306df1. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds two runnable example flows for the README's "What can you do with this?" section, both registered in
examples/tsconfig.jsonand passingflows check, with a hardening pass so they're safe to deploy with untrusted input.examples/software-factorymaps a ticket to a PR: implementer agent → deterministic tests → adversarial review (verdict written to a file) → tests →gh pr create; a blocked review opens a draft PR with findings and endsstep_failed.examples/stale-issuesfetches open issues deterministically, classifies them with one LLM step behind a JSON-schema gate, and posts a single Slack digest;flows scheduleis in flight, so the README documentsflows run --clouduntil it ships.Security hardening
Written for commit 7306df1. Summary will update on new commits.