Skip to content

fix(cli): make help and single-step summaries readable - #279

Merged
kjgbot merged 1 commit into
mainfrom
fix/cli-help-shakedown-0910
Sep 10, 2026
Merged

kjgbot merged 1 commit into
mainfrom
fix/cli-help-shakedown-0910

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

flows --help and flows -h now print one usage form per line to stdout and exit 0; unknown options still exit 2. Single-step summaries now say (1 step) instead of (1 steps).

Reproduced during the launch shakedown on main a42ca16. This small PR is based directly on main; it does not modify #268/#269 branches.

Validation on this change (full captured output in the shakedown report):

CHECKOUT: /Users/khaliqgant/flows-help-shakedown-0910
COMMAND: npx tsc --noEmit
EXIT: 0

CHECKOUT: /Users/khaliqgant/flows-help-shakedown-0910
COMMAND: npx vitest run tests/cli.test.ts
EXIT: 0

 RUN  v2.1.9 /Users/khaliqgant/flows-help-shakedown-0910/packages/sdk

 ✓ tests/cli.test.ts (63 tests) 2691ms
   ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 704ms

 Test Files  1 passed (1)
      Tests  63 passed (63)
   Start at  16:21:14
   Duration  3.14s (transform 133ms, setup 0ms, collect 236ms, tests 2.69s, environment 0ms, prepare 34ms)


COMMAND: node packages/sdk/dist/cli.js --help
EXIT: 0
STDOUT:
Usage:
flows check [--json] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] <flow.yaml|spec.json>
flows run --cloud [--json] [--wait] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>
flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>
flows resume [--json] [--no-spawn] [--data-dir <dir>] <run-id>
flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>

STDERR:

COMMAND: node packages/sdk/dist/cli.js -h
EXIT: 0
STDOUT:
Usage:
flows check [--json] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] <flow.yaml|spec.json>
flows run --cloud [--json] [--wait] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>
flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>
flows resume [--json] [--no-spawn] [--data-dir <dir>] <run-id>
flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>

STDERR:

COMMAND: node packages/sdk/dist/cli.js --definitely-invalid
EXIT: 2
STDOUT:

STDERR:
REFUSED [invalid_invocation] Usage:
flows check [--json] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] <flow.yaml|spec.json>
flows run --cloud [--json] [--wait] <flow.yaml|spec.json>
flows run [--json] [--no-spawn] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>
flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>
flows resume [--json] [--no-spawn] [--data-dir <dir>] <run-id>
flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>

CWD: /Users/khaliqgant/flows-help-shakedown-0910
COMMAND: node /Users/khaliqgant/flows-help-shakedown-0910/packages/sdk/dist/cli.js run /Users/khaliqgant/flows-shakedown-0910/testdata/shakedown/hello-world.flow.yaml --data-dir /tmp/rfd-96g6toyf
ENV OVERRIDES: {}
EXIT: 0
ELAPSED: 0.249s
STDOUT:
RUN 01M25V99YWG6MCDTKWNG9PB3HK completed (1 step) completionReason: success

STDERR:
WARNING [unprovable_effects] Step "hello" command "printf" resolves, but its effects cannot be proven before execution.


CWD: /Users/khaliqgant/flows-help-shakedown-0910
COMMAND: git rev-parse HEAD
EXIT: 0
179092173be91d4e4fcc9245be17371bca08360a

CWD: /Users/khaliqgant/flows-help-shakedown-0910
COMMAND: git ls-remote origin refs/heads/fix/cli-help-shakedown-0910
EXIT: 0
179092173be91d4e4fcc9245be17371bca08360a	refs/heads/fix/cli-help-shakedown-0910


cargo test -p relayflowd exited 0. Final literal output:


test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s

     Running tests/subscription_liveness.rs (target/debug/deps/subscription_liveness-9506d11f678a9586)

running 3 tests
test submit_event_upserts_subscription_row_and_sweep_flags_it_stale_after_budget ... ok
test stale_transition_is_journaled_as_subscription_stale_entry_in_the_last_known_run ... ok
test a_fresh_arrival_re_arms_the_latch_and_the_next_silence_can_stale_again ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s

   Doc-tests relayflowd

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


Note

Low Risk
Small UX-only changes to help handling and run output text; no auth, execution, or parsing behavior beyond the new early help exit.

Overview
Improves the flows CLI help and run output polish found during launch shakedown.

--help / -h: Invoking flows with only --help or -h now prints the usage block to stdout (one subcommand per line) and exits 0. Invalid invocations still emit the same usage via the existing refusal path and exit 2.

Usage formatting: The shared USAGE string is joined with newlines instead of spaces, so help and error messages list each command on its own line.

Run summary: Completed-run lines use correct singular/plural — (1 step) when completedSteps === 1, otherwise (N steps).

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

Session-Id: 01a08ba4-4473-7903-b7c3-e778edb9b523
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 5c794293-0aad-4f87-9aca-2665a846d921


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.

@github-actions

Copy link
Copy Markdown

Review swarm: maintainability

Maintainability Review: PR #279

PR Title: fix(cli): plug the shakedown-found holes in the --help / verbs surface
Branch: fix/cli-help-shakedown-0910
Commit: 1790921
Reviewer: Maintainability lens
Date: 2026-09-10 15:00

Summary

This PR makes three changes to packages/sdk/src/cli.ts:

  1. Changes USAGE join from space to newline (line 52)
  2. Adds early --help/-h handling (lines 73-76)
  3. Fixes step/steps pluralization in output (line 401)

Maintainability Assessment

Finding 1: The USAGE join change is unexplained and breaks the implicit contract

Location: packages/sdk/src/cli.ts:52

Issue: The diff changes .join(' ') to .join('\n') for the USAGE constant, but the change is undocumented and creates an implicit contract violation.

The USAGE constant is an array of 7 usage lines. Before this change, joining with space would create a single long line — which is semantically wrong for usage help but would have been visible as broken output. The change to newline fixes the display bug, but:

  1. The original bug is undocumented. A reader cannot tell whether the space join was intentional (perhaps for some --help format) or an error. The commit message "plug the shakedown-found holes" does not mention this specific fix.

  2. The test boundary is invisible. Looking at the code, a stranger cannot tell if tests verified the output format. If tests existed for USAGE formatting and passed with .join(' '), they would be useless — they'd pass whether help was readable or garbage. If no tests existed, the regression surface is unguarded.

  3. The function that consumes USAGE has two modes. USAGE is passed to inputFailureReport (line 80) and is also emitted directly via io.stdout(USAGE) in the new --help block (line 74). The contract is: does inputFailureReport expect newlines embedded, or does it add them? The code does not say, and the signature inputFailureReport({ kind: 'invalid_invocation', message: USAGE }) gives no hints. If inputFailureReport also adds newlines, the output now has double spacing.

Risk: In six months, someone fixing another CLI bug will not know if the newline join is load-bearing or if some callers expect a different format. The dual emission paths (direct stdout vs. inputFailureReport) create an implicit contract that is nowhere written.

What's missing: A comment stating "USAGE is emitted verbatim in --help handling and embedded in invalid_invocation reports; the newline join controls both formats."

Finding 2: The --help early return bypasses parseArgs, creating a semantic inconsistency

Location: packages/sdk/src/cli.ts:73-76

Issue: The new --help handling runs before parseArgs, which means:

if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
  io.stdout(USAGE);
  return 0;
}

This early return only works for flows --help or flows -h with no other arguments. The check args.length === 1 means flows --help --json falls through to parseArgs, which will reject it as invalid and emit USAGE via the invalid_invocation path (line 80-82).

The semantic inconsistency:

  • flows --help → returns 0, prints USAGE via stdout
  • flows --help --json → returns 2, prints USAGE via inputFailureReport + stderr diagnostics
  • flows check --help → returns 2, prints USAGE via inputFailureReport

A maintainer reading this in six months will wonder: is args.length === 1 intentional (pure --help only) or a limitation? Should flows check --help show check-specific help, or is global help the contract?

The code does not answer these questions. The only hint is the structure itself, which a reader must reverse-engineer: "it checks length === 1, so multi-arg --help must be intentionally excluded."

Risk: Future maintainer tries to add subcommand-specific help (e.g., flows tick --help shows tick options). They see the early --help block, add a branch after parseArgs to detect parsed.command === 'tick' and --help, but discover --help was already rejected by parseArgs. The fix requires moving help detection into parseArgs for every command, but the early return at line 73 now shadows global help. The resulting logic has two --help paths that diverge in exit codes and output routing.

What's missing:

  1. A comment: "Only bare --help/-h is handled here. Subcommand help must be detected by parseArgs after command is known."
  2. Alternatively, a test asserting flows check --help returns 2, making the current behavior a specification rather than an accident.

Finding 3: The pluralization fix is correct but the broader output contract is unclear

Location: packages/sdk/src/cli.ts:401

Issue: The change from (${report.completedSteps} steps) to singular/plural logic is correct English and a strict improvement. However, the function emitRunReport has an unclear boundary with the rest of the system:

function emitRunReport(execution: RunExecution, json: boolean, io: CliIo): void {
  const { report } = execution;
  emitDiagnostics(report.diagnostics, io);
  if (json) {
    io.stdout(JSON.stringify(report));
    return;
  }
  // ... human-readable formatting
}

When json is true, the function emits report verbatim. When false, it formats a human-readable line: RUN <runId> <status> (<n> steps) completionReason: <reason>.

The contract question: What happens if report.completedSteps is 0? The current code:

const completed = report.completedSteps === undefined ? '' : ` (${report.completedSteps} ${report.completedSteps === 1 ? 'step' : 'steps'})`;

This checks === undefined but not === 0. So:

  • completedSteps: undefined → no output (correct: value unknown)
  • completedSteps: 0 (0 steps) (is this correct, or is 0 steps an error state that should be called out differently?)
  • completedSteps: 1 (1 step) (correct)
  • completedSteps: 2 (2 steps) (correct)

A maintainer looking at a run report that says RUN abc123 failed (0 steps) will wonder: is that a normal incomplete run, or a failure-before-start that should be surfaced differently? The code does not say.

The broader issue: emitRunReport builds a human-readable line from several optional fields (runId, status, completedSteps, completionReason). Each field's absence is handled:

  • runId === undefined → entire function returns early (line 400)
  • completedSteps === undefined → omitted from output
  • completionReason === undefined → omitted from output
  • status === undefined → outputs 'unknown' (line 405)

Why does status default to 'unknown' while completedSteps and completionReason are silently omitted? Is there an invariant that says "a run always has a status but may not have a completion reason"? The code does not state it.

Risk: In six months, a maintainer debugging why a parked run shows RUN xyz parked (3 steps) but no completionReason will grep for "parked" and find the diagnosticLabel switch, but emitRunReport gives no hint whether completionReason: undefined for a parked run is a kernel bug (it should always be set) or a valid edge case (some parks have no reason). The distinction matters: is this a display bug or a data bug?

What's missing: A comment: "completedSteps and completionReason are optional; they are omitted from output when undefined. status defaults to 'unknown' rather than being omitted because every run has a lifecycle state (running/succeeded/failed/parked), but the report may not have captured it yet."

Finding 4: The --help addition does not gate itself against the spawn/data-dir machinery

Location: packages/sdk/src/cli.ts:73-76

Issue: The new --help block is an unconditional early return at the top of runCli, before any command parsing or daemon interaction. This is correct behavior — --help should never touch a daemon, open a socket, or read a data directory — but the correctness is implicit, not stated.

Compare this to the explicit daemon-free guarantee for flows check (lines 88-92):

// Deliberately daemon-free (kernel/DAEMON-LIFECYCLE.md §4). `checkFlow` is
// a pure compile-and-preflight that opens no socket, and the parser
// refuses `--data-dir` on `check`, so there is no data dir to attach to.
// `flows check` keeps working with no daemon, no relayflowd binary and no
// data directory at all -- a property worth keeping, not an omission.

The --help path has the same property — it must never spawn a daemon or read state — but the code does not say so. A future maintainer might see the early return and think "this is just an optimization to avoid parsing" rather than "this is a correctness requirement to keep --help side-effect-free."

The failure mode: Someone refactors runCli to do environment setup (e.g., logging initialization, telemetry ping, or daemon health check) at the top, before the --help check. Suddenly flows --help makes network calls or writes to disk. The regression is silent because no test enforces the property.

Risk: In six months, when the CLI gains a telemetry system or crash reporter that initializes early, --help will start sending telemetry pings. A user running flows --help in a restricted environment (no network, read-only filesystem) will see a hang or error instead of help text. The breakage is subtle: --help still prints the right text, but it also does work it should not.

What's missing: A comment at line 73: "Help must remain side-effect-free: no daemon spawn, no socket open, no state reads. This early return is structural, not a micro-optimization."

Boundary Issues Not Addressed by This PR

These are pre-existing gaps that this PR does not introduce but makes more visible:

  1. USAGE is a fixed global string, not command-specific. Every command shows the same help, even though flows tick start and flows check accept entirely different flags. A user running flows tick --help expecting to see --schedule-id documentation will instead see the global usage block. The implicit contract: "flows has no subcommand help." This is maintainable only if documented.

  2. The --help path emits to stdout; the invalid_invocation path emits to stderr via diagnostics. A reader cannot tell if this is intentional (help is informational, errors are diagnostic) or accidental (two code paths, two outputs). If intentional, a comment should say so.

  3. Exit code 2 means both "invalid invocation" and "check failed". The overload is legal (2 is the standard "usage error" code) but the code does not say whether the two failure modes are meant to be distinguishable. If a CI script sees exit code 2, it cannot tell whether the user passed bad flags or the flow failed validation.

Tests That Would Fail If the Behavior Broke

This is the critical maintainability question. Looking at the changes:

  1. USAGE join change (line 52): A test that captures flows --help output and asserts it contains newlines would fail if this regressed to .join(' '). Does such a test exist? The PR does not show test changes, so either:

    • The tests already verified multi-line help (unlikely, or the bug would have been caught earlier), or
    • No test verifies help formatting (likely), in which case the fix is unguarded.
  2. --help early return (lines 73-76): A test asserting flows --help exits 0 and flows --help --json exits 2 would fail if the early return were removed. The PR does not add such a test.

  3. Pluralization (line 401): A test asserting flows run output contains "(1 step)" for single-step runs and "(2 steps)" for multi-step runs would fail if the ternary were removed. The PR does not add such a test.

Implication: All three changes are unguarded by tests. A future refactor of runCli or emitRunReport could silently regress any of them. The maintainability risk is not in the changes themselves — they are all improvements — but in the lack of a regression barrier.

Missing Failure Handling

None. The changes are purely presentational (help formatting, pluralization). The error paths are unchanged, and the early --help return explicitly avoids new failure modes by skipping all execution logic.

Comments That Assert What the Code Does Not Do

None in this PR. However, the pre-existing comment at lines 88-92 ("Deliberately daemon-free") is an example of the correct pattern: it asserts a property the code must preserve even though the code does not explicitly enforce it. The --help block would benefit from the same treatment.

Verdict

The changes are clean micro-fixes that improve user experience. The maintainability risk is not in the changes but in the lack of specification around them:

  1. USAGE has two consumers (direct stdout, inputFailureReport) with no stated contract about formatting.
  2. The --help early return has an implicit "no side effects" requirement that is unenforced and undocumented.
  3. The pluralization fix is correct but the broader optional-field contract in emitRunReport is unclear.
  4. Zero tests guard these changes from regression.

A stranger reading this code in six months will be able to trace what it does, but not why certain choices were made or what properties must hold. That is the classic maintainability gap: the code is correct, but its invariants are invisible.

Per AGENTS.md and RFC-0001 covenant 1 ("easy to read"), a technical founder should be able to read this code and say what it does. They can. But they cannot say what it must continue to do (newlines in USAGE? exit 0 for --help? side-effect-free help?), and that is the specification gap that makes safe changes harder.

Recommendation

The changes improve the CLI's user-facing behavior. The core logic is sound. The missing piece is specification of intent: comments stating the contracts and tests guarding the behavior.

In a codebase building toward RFC-0001's nine gates, where "fail closed" (covenant 2) and "no silent fallbacks" are constitutional, a --help path that is accidentally side-effect-free is a lower-grade risk than, say, a credential writeback that silently swallows errors. But the pattern matters: implicit correctness does not scale.

The PR is shippable as-is from a correctness standpoint. From a maintainability standpoint, it leaves the --help surface underspecified. A follow-up adding:

  1. A comment on USAGE stating its dual use (direct output + inputFailureReport embedding)
  2. A comment on the --help block stating the side-effect-free requirement
  3. A test asserting help output format and exit codes

...would close the gap.

REVIEW_PASSED

@github-actions

Copy link
Copy Markdown

Review swarm: history

PR #279 — history review

Verdict: PASS. No blocking finding through the requested history lens.

Reviewed head: 179092173be91d4e4fcc9245be17371bca08360a.
Parent: a42ca161658f9f4c3ff22c7d158d6c65dd219473.
Input: .review-target/pr.diff, compared byte-for-byte with the commit diff below.

Does this change fit the story?

Yes. The three hunks in packages/sdk/src/cli.ts make the existing usage list
multiline, recognize a standalone --help or -h as successful informational
output, and render one completed step as (1 step). This is a small authoring
surface improvement consistent with RFC-0001 §1 covenant 1 (easy to write and
read) and §4 / settled decision 5 (TypeScript surfaces, Rust kernel, journal
protocol boundary).

  1. Recorded mistakes: the WP-12 entry in ops/DRIVE-LOG.md:3019 records
    repairing misleading lifecycle reporting: running and human-parked states
    must remain distinguishable through typed snapshots. This diff changes only
    the count's noun in the existing report renderer; it preserves status,
    completionReason, diagnostics, and the JSON reporting branch. Returning
    success for help is not reporting a workflow as successful: the new branch
    prints usage and returns before dispatch. The September 9 account of fix(sdk): renew agent leases while CLI steps execute #249
    superseded by feat: scaffold local flows with SDK progress and a local agent worker #247 warns against reintroducing older solutions after main
    has moved. Here the actual parent already contains feat: scaffold local flows with SDK progress and a local agent worker #247 and fix(preflight): inline named-agent model without flows.json (#263) #266, and the
    follow-renames CLI history supports this narrowly additive change.
  2. Settled decisions: no kernel verbs, journal fields, execution semantics,
    gate predicates, or merge authority change. Decisions 6, 7, 11, 13 and 16
    remain intact. In particular, help does not create a new kernel verb, and
    preserving completionReason keeps the completion/quality boundary intact.
  3. Deliberate removals: the CLI history crosses the refactor(layout): move sdk/ and surface/ under packages/ #205 move from sdk/
    to packages/sdk/. The prior Repair flows run/resume lifecycle and supersede PRs #9 and #11 #12 repair introduced the space-joined usage
    list and fixed-plural summary; this PR improves that formatting. Searching
    pre-head history for --help / -h changes yields no earlier help removal
    in this file lineage. feat(daemon): connection-file handshake and CLI attach-or-spawn #239 explicitly placed attach-or-spawn inside execution
    helpers, after input checking. This help branch returns before those helpers,
    preserving that deliberate ordering. No removed provider integration,
    alternate persistence mechanism, or old lifecycle implementation returns.
  4. Commit truthfulness: the actual subject is fix(cli): make help and single-step summaries readable, which accurately describes all three hunks.
    The prepared PR title is broader (--help / verbs surface), but this diff
    neither adds new command verbs nor claims to implement all shakedown fixes.
    Standalone help is the implemented scope; subcommand help is not added.

ops/NEXT.md is explicitly a completed Track D review-swarm brief, not a
repository-wide prohibition on this separate CLI PR. Its warning about doing
already-finished work reinforces checking the actual parent. ops/DIRECTIVES.md
contains its explanatory header and no outstanding directive. Neither supplies
a conflicting instruction for this change.

Review environment and limits

The initial literal command git log --oneline -40 failed (exit 128):

fatal: not a git repository: /home/daytona/.project-git

The requested /tmp/pr-279.diff was also absent; the prepared tracked-transfer
input .review-target/pr.diff was available. These are the same review-input
hazards discussed in the September 10 10:43Z and 10:57Z DRIVE-LOG entries; the
10:57Z correction specifically establishes that using the staged diff is valid.
I recovered real Git objects with a bare clone of
https://github.com/AgentWorkforce/flows.git into the missing gitdir, configured
this existing worktree, created local branch review/pr279-history at the
recorded PR SHA, and populated its index with git read-tree HEAD. No checkout,
commit, push, merge, or gate edit was performed. The pre-existing worktree has
executable-mode differences; they are outside the supplied PR diff and were
left untouched. Only this review is staged.

This is a static history review, not a runtime correctness or test-suite signoff.
No build, runtime tests, or mutation tests were run or claimed. The complete
captured command outputs supporting the history and input comparisons follow.
Empty output is represented by an empty fenced block; exit codes are recorded
separately.

Captured evidence

Command

git rev-parse HEAD HEAD^

Captured output:

179092173be91d4e4fcc9245be17371bca08360a
a42ca161658f9f4c3ff22c7d158d6c65dd219473

Exit code: 0.

Command

cat .review-target/pr.json

Captured output:

{"headRefName":"fix/cli-help-shakedown-0910","headRefOid":"179092173be91d4e4fcc9245be17371bca08360a","title":"fix(cli): plug the shakedown-found holes in the --help / verbs surface","url":"https://github.com/AgentWorkforce/flows/pull/279"}

Exit code: 0.

Command

git log --oneline -40

Captured output:

17909217 fix(cli): make help and single-step summaries readable
a42ca161 fix(preflight): skip model_unknown for inline named agents when no flows.json is present (#263) (#266)
78ae4b8e fix(review-swarm): make the wait step's timed_out sentinel reachable (#258)
4dd9277e fix(review-swarm): give the lens retry budget a delay that can span a 60s backoff (#259)
d9377d17 ops(drive-log): -0910 online; closed relayfile#492, re-ran flows#258
8790e002 ops(drive-log): corrected flows#260 -- I truncated the quote that disproved it
ecaf6b86 ops(drive-log): lenses never received the diff; filed flows#260
3cfbd061 ops(drive-log): recovered lens transcripts; two lenses passed #259
5fd56fbe ops(drive-log): opened cloud#3527 -- run export 400s for every caller
ec014740 ops(drive-log): gate failure moved off infrastructure onto the agent step
f5f97e53 ops(drive-log): quiet tick, nothing moved
17c413ec ops(drive-log): #259 cannot be validated by its own gate; audit complete
069789bd ops(drive-log): audited remaining PRs -- all three still valid
4c2b0ab1 ops(drive-log): closed cloud#3517 as obsolete -- main deleted what it extended
fb73faf3 ops(drive-log): verified the #3516 classifier claim against three literal inputs
a32dc6d3 ops(drive-log): mount fault CONFIRMED FIXED; two corrections
8ab1ab2b ops(drive-log): the in-flight run shows the wedge signature, not progress
7999b28e ops(drive-log): re-ran the gate to test v0.10.56; in flight past 16 minutes
3bb84add ops(drive-log): v0.10.56 promoted; Khaliq had fixed the transport 3h before I filed
7cecffd8 ops(drive-log): opened cloud#3525 -- guard against an empty snapshot name
4bb9f865 ops(drive-log): named the masking secret -- RELAYFILE_SMOKE_BASE_URL
9b26383d ops(drive-log): root cause -- a secret valued "-" masks every hyphen (cloud#3524)
bdcaf415 ops(drive-log): retracted most of relayfile#492 -- read a 95-commit-stale checkout
c3dfe269 ops(drive-log): relayfile#492 -- the full-reconcile remedy exists, nothing triggers it
b58ce471 ops(drive-log): failures converged on one mode; retracting the rotation claim
4519a701 ops(drive-log): broke #3510's build with backticks in a template literal
7124cade ops(drive-log): caught myself reporting an unpushed fix as pushed
e717971b ops(drive-log): Bugbot findings on #3510 -- fixed the race, contested the heartbeat
74b7eac2 ops(drive-log): opened flows#259 -- lens retries had a 1s delay vs a 60s backoff
9f676c26 ops(drive-log): filed relayfile#492 for the recurring cursor_expired mount failure
b00e77ec ops(drive-log): seven failure modes, none consecutive -- no single fix exists
15c4de59 ops(drive-log): all 9 gate failures are infrastructure, none are code verdicts
576e5ee8 ops(drive-log): opened flows#258; corrected two over-readings of the gate
c6a44ce6 ops(drive-log): review gate fails on non-terminal 'running'; 103-day stranded cohort
eaa171d8 ops(drive-log): brief is stale in all four items; review gate blocks 9/9 flows PRs
01626214 ops(drive-log): queue recovered; filed cloud#3519 dead DISABLE_RELAY env var
0a90b4bb ops(drive-log): quiet tick -- all four PRs green and awaiting human review
ea941a7e ops(drive-log): #3516 green; determined #244's fix shape from the contract
9f4b2db7 ops(drive-log): CI found my change broke an existing delay assertion
24feb7b3 ops(drive-log): the credential diagnostic already existed; wired it to the S3 mint

Exit code: 0.

Command

git show --format=fuller --stat HEAD

Captured output:

commit 179092173be91d4e4fcc9245be17371bca08360a
Author:     Miya <khaliqgant+miya@gmail.com>
AuthorDate: Thu Sep 10 16:14:10 2026 +0200
Commit:     Miya <khaliqgant+miya@gmail.com>
CommitDate: Thu Sep 10 16:23:43 2026 +0200

    fix(cli): make help and single-step summaries readable
    
    Session-Id: 01a08ba4-4473-7903-b7c3-e778edb9b523

 packages/sdk/src/cli.ts | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

Exit code: 0.

Command

git diff HEAD^ HEAD -- packages/sdk/src/cli.ts

Captured output:

diff --git a/packages/sdk/src/cli.ts b/packages/sdk/src/cli.ts
index 1d8b86cf..9e5c6f91 100644
--- a/packages/sdk/src/cli.ts
+++ b/packages/sdk/src/cli.ts
@@ -49,7 +49,7 @@ const USAGE = [
   'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
   'flows resume [--json] [--no-spawn] [--data-dir <dir>] <run-id>',
   'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
-].join(' ');
+].join('\n');
 
 /**
  * `FLOWS_NO_SPAWN=1` is `--no-spawn` for a whole environment: the lever for CI
@@ -70,6 +70,11 @@ export async function runCli(
   args: readonly string[],
   io: CliIo = PROCESS_IO,
 ): Promise<CliExitCode> {
+  if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
+    io.stdout(USAGE);
+    return 0;
+  }
+
   const parsed = parseArgs(args);
   if (parsed === undefined) {
     const report = inputFailureReport({ kind: 'invalid_invocation', message: USAGE });
@@ -393,7 +398,7 @@ function emitRunReport(execution: RunExecution, json: boolean, io: CliIo): void
     return;
   }
   if (report.runId === undefined) return;
-  const completed = report.completedSteps === undefined ? '' : ` (${report.completedSteps} steps)`;
+  const completed = report.completedSteps === undefined ? '' : ` (${report.completedSteps} ${report.completedSteps === 1 ? 'step' : 'steps'})`;
   const reason = report.completionReason === undefined
     ? ''
     : ` completionReason: ${report.completionReason}`;

Exit code: 0.

Command

git diff HEAD^ HEAD -- packages/sdk/src/cli.ts | cmp - .review-target/pr.diff

Captured output:

Exit code: 0.

Command

git diff --exit-code HEAD -- packages/sdk/src/cli.ts

Captured output:

Exit code: 0.

Command

git log --follow --oneline -- packages/sdk/src/cli.ts

Captured output:

17909217 fix(cli): make help and single-step summaries readable
a387317d feat: scaffold local flows with SDK progress and a local agent worker (#247)
6cca5d9c feat(sdk): submit declarative flows to Cloud v2 (#246)
5bbbe6ec feat(daemon): connection-file handshake and CLI attach-or-spawn (#239)
5ca5a7ad refactor(layout): move sdk/ and surface/ under packages/ (#205)
b8dc71f4 feat(cli): run authored flows with direct input (#140)
066ef24a feat(sdk): flows tick start — keep a scheduled relayflow firing (#157)
f1314b17 feat(sdk): settle data and code gate contract (#139)
990093b8 feat(sdk): declare agent CLI and model with fail-closed checks (#136)
201542a7 feat(cli): flows hn-monitor start — CLI-inlined proactive workload for gate 2 (#120)
e48631dc Repair flows run/resume lifecycle and supersede PRs #9 and #11 (#12)
9e1d9eb0 WP-4 — flows check preflight (covenant 2) (#8)

Exit code: 0.

Command

git log --follow --oneline -G '\-\-help|\-h' HEAD^ -- packages/sdk/src/cli.ts

Captured output:

Exit code: 0.

Command

git show 5bbbe6ec --format=short -- packages/sdk/src/cli.ts

Captured output:

commit 5bbbe6ecdaeab9de9c090c1cfd9d0efb0ecd676b
Author: Khaliq <khaliq@agentrelay.com>

    feat(daemon): connection-file handshake and CLI attach-or-spawn (#239)

diff --git a/packages/sdk/src/cli.ts b/packages/sdk/src/cli.ts
index ef314dd1..c015182d 100644
--- a/packages/sdk/src/cli.ts
+++ b/packages/sdk/src/cli.ts
@@ -11,6 +11,7 @@ import {
   resumeFlow,
   runFlow,
   type RunExecution,
+  type RunProgress,
   type RunReport,
 } from './cli/run.js';
 import { runDirectFlow } from './cli/direct-run.js';
@@ -28,8 +29,8 @@ export interface CliIo {
 type CliExitCode = 0 | 1 | 2 | 3;
 type ParsedArgs =
   | { command: 'check'; json: boolean; value: string }
-  | { command: 'run'; dataDir: string; input: string | undefined; json: boolean; value: string }
-  | { command: 'resume'; dataDir: string; json: boolean; value: string }
+  | { command: 'run'; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; value: string }
+  | { command: 'resume'; dataDir: string; json: boolean; spawn: boolean; value: string }
   | { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
   | { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
       intervalMs: number; epochMs: number | undefined; maxCatchUp: number | undefined;
@@ -39,13 +40,23 @@ const DEFAULT_DATA_DIR = '.relayflowd';
 const USAGE = [
   'Usage:',
   'flows check [--json] <flow.yaml|spec.json>',
-  'flows run [--json] [--data-dir <dir>] <flow.yaml|spec.json>',
-  'flows run [--json] [--data-dir <dir>] <flow.ts> --input <inline-json-or-file>',
+  'flows run [--json] [--no-spawn] [--data-dir <dir>] <flow.yaml|spec.json>',
+  'flows run [--json] [--no-spawn] [--data-dir <dir>] <flow.ts> --input <inline-json-or-file>',
   'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
-  'flows resume [--json] [--data-dir <dir>] <run-id>',
+  'flows resume [--json] [--no-spawn] [--data-dir <dir>] <run-id>',
   'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
 ].join(' ');
 
+/**
+ * `FLOWS_NO_SPAWN=1` is `--no-spawn` for a whole environment: the lever for CI
+ * that means to assert a daemon is already present rather than conjure one
+ * (kernel/DAEMON-LIFECYCLE.md §4). Only the exact string `1` counts — an
+ * unset or empty variable must not be read as an opinion.
+ */
+function spawnAllowedByEnv(env: NodeJS.ProcessEnv = process.env): boolean {
+  return env['FLOWS_NO_SPAWN'] !== '1';
+}
+
 const PROCESS_IO: CliIo = {
   stdout: (line) => process.stdout.write(`${line}\n`),
   stderr: (line) => process.stderr.write(`${line}\n`),
@@ -63,6 +74,11 @@ export async function runCli(
   }
 
   if (parsed.command === 'check') {
+    // Deliberately daemon-free (kernel/DAEMON-LIFECYCLE.md §4). `checkFlow` is
+    // a pure compile-and-preflight that opens no socket, and the parser
+    // refuses `--data-dir` on `check`, so there is no data dir to attach to.
+    // `flows check` keeps working with no daemon, no relayflowd binary and no
+    // data directory at all -- a property worth keeping, not an omission.
     const checked = checkFlow(parsed.value);
     emitCheckReport(checked.report, parsed.json, io);
     return checked.report.ok ? 0 : 2;
@@ -110,16 +126,19 @@ export async function runCli(
     }
   }
 
+  // Attach-or-spawn runs inside `runFlow`/`resumeFlow`/`runDirectFlow`, at the
+  // single `connect()` seam immediately before journal-client.ts is used --
+  // not here. Hoisting it above the dispatch would start a daemon as a side
+  // effect of an invocation that is about to be refused for bad input.
+  const lifecycle = {
+    onWait: (progress: RunProgress) => emitWait(progress, io),
+    daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
+  };
   const execution = parsed.command === 'run'
     ? isAuthoredFlowPath(parsed.value)
-      ? await runDirectFlow(
-          parsed.value,
-          parsed.input,
-          parsed.dataDir,
-          { onWait: (progress) => emitWait(progress, io) },
-        )
-      : await runFlow(parsed.value, parsed.dataDir, { onWait: (progress) => emitWait(progress, io) })
-    : await resumeFlow(parsed.value, parsed.dataDir, { onWait: (progress) => emitWait(progress, io) });
+      ? await runDirectFlow(parsed.value, parsed.input, parsed.dataDir, lifecycle)
+      : await runFlow(parsed.value, parsed.dataDir, lifecycle)
+    : await resumeFlow(parsed.value, parsed.dataDir, lifecycle);
   emitRunReport(execution, parsed.json, io);
   return execution.exitCode;
 }
@@ -143,6 +162,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
   let json = false;
   let dataDir = DEFAULT_DATA_DIR;
   let sawDataDir = false;
+  let spawn = true;
   let input: string | undefined;
   let sawInput = false;
   const positionals: string[] = [];
@@ -153,6 +173,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
       json = true;
       continue;
     }
+    if (argument === '--no-spawn') {
+      // Refused on `check` for the same reason `--data-dir` is: `check` never
+      // opens a socket, so a daemon flag there would describe nothing.
+      if (command === 'check' || !spawn) return undefined;
+      spawn = false;
+      continue;
+    }
     if (argument === '--data-dir') {
       const value = args[index + 1];
       if (command === 'check' || sawDataDir || value === undefined || value.startsWith('-')) return undefined;
@@ -178,8 +205,8 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
   return command === 'check'
     ? { command, json, value: positionals[0]! }
     : command === 'run'
-      ? { command, dataDir, input, json, value: positionals[0]! }
-      : { command, dataDir, json, value: positionals[0]! };
+      ? { command, dataDir, input, json, spawn, value: positionals[0]! }
+      : { command, dataDir, json, spawn, value: positionals[0]! };
 }
 
 function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {

Exit code: 0.

Command

sed -n '3019,3043p' ops/DRIVE-LOG.md

Captured output:

## 2026-08-28 03:43 EDT — WP-12: repair PR #9 to a reviewable superseding head

### Work completed

The tick committed its WP-12 assessment, merged both PR #9 and PR #11 histories,
and preserved the append-only log in chronological order. F1–F8 and H1–H2 are
fixed; F3 was fixed rather than deferred. `RunSnapshot` now carries a stable
snake_case state, RFC step type, and running lease deadline per step. The CLI
uses that snapshot to distinguish worker-unavailable, running, and
`needs_human` states, so it no longer reads journal sequence 1 or parses Rust
`Debug`. Typed hello refusals stay protocol failures, running-worker waits are
observable/cancelable/lease-bounded, `run.resume` asks the journal registry,
the ladder fixture names the actual deterministic rung, and `npm test` builds
the CLI once before Vitest starts. The review swarm change adds only a
transcript-persistence step and its dependency edge; lens prompts, verdict
grep, and aggregate pass/fail logic are unchanged.

The live F1 path is narrower than the assessment's wording: a
worker-reported failure that exhausts `maxIterations` becomes a terminal failed
step. The kernel enters `NeedsHuman` when a manual-recovery agent attempt is
abandoned (worker disconnect or lease expiry). The live regression therefore
attaches a real agent worker, dispatches the attempt, closes that worker, and
asserts the resulting real `needs_human` snapshot and exit 3. No stub is
described as a live test.

Exit code: 0.

Command

sed -n '7117,7138p' ops/DRIVE-LOG.md

Captured output:

**The conflict was semantic, and that changed the answer.** `main` now imports
`withWorkerLease`; the branch imports `startWorkerHeartbeat`. Two different
solutions to the same problem meeting in the same function. Timeline:

#249 branch commit 2026-09-09 15:14:28 +0200
#247 lands worker-lease.ts on main 2026-09-09 16:37:13 +0200 (+83 min)
main wires it at worker.ts:94


So #249 was superseded while it sat unmerged. Resolving the conflict
mechanically would have either reverted #247's work or duplicated it.

**`withWorkerLease` is strictly better** than the heartbeat: renews at
`remaining/3` instead of a fixed interval, aborts the CLI via `AbortSignal` on
renewal failure, refuses a renewal response that lands after local expiry,
drains in-flight renewals before `step.complete`, and re-checks the wall clock
before completing.

**Closed #249** with that evidence. Kept the branch — its evidence doc and two
heartbeat test files were written against the live failure and may be worth
harvesting into the `worker-lease` tests.

Exit code: 0.

Command

sed -n '/### 2026-09-10 10:43Z/,/### 2026-09-10 11:56Z/p' ops/DRIVE-LOG.md

Captured output:

### 2026-09-10 10:43Z — the lenses never saw the diff. Filed flows#260.

Queue drained. Disk 5.2Gi. cloud#3527 clean, 0 failures, 0 pending.

Read the recovered transcripts properly instead of stopping at the verdict
line, and found something that qualifies last tick's report.

**Neither passing lens had the PR diff.** The lens tasks say:

    Review the PR ... (diff at /tmp/pr-<n>.diff, metadata at /tmp/pr-<n>.json)

but the only step that writes /tmp is `fetch`, and steps do not share a
filesystem. `.review-target/pr.diff` IS git-added by swarm-prepare.sh and does
travel; the /tmp copy does not.

Evidence, three ways:
 - history lens, verbatim: "The /tmp diff was absent ... Static history review
   only."
 - maintainability's citations are ALL line numbers in review-swarm.yaml
   itself (line 17, line 121, lines 129-131) and it quotes that file's comment
   text -- repo-file positions, not diff hunks.
 - both still returned REVIEW_PASSED.

**That is the part that matters: a missing input does not fail the lens, it
silently narrows what the lens saw.** A gate that passes while blind is worse
than one that errors. The fetch step already models the right behaviour --
it exits 1 with FETCH_FAILED when the staged diff is missing.

Also recurring: `fatal: not a git repository: /home/daytona/.project-git`.
The history lens says "again", and PR #257's repair summary described the same
broken gitdir pointer on 09-09. Two sightings, days apart.

Filed **flows#260**. Did NOT open a PR: it changes the gate definition, which
by design cannot be validated by the gate, and I already have #259 outstanding
on that exact file -- two of my own PRs colliding there would be self-inflicted.

**Qualified my own claim on #259.** Last tick I wrote "two lenses reviewed #259
in substance and passed". Weaker than that: they reviewed the working tree, not
the diff. For this PR the two are nearly equivalent since the change IS that
file, but I said it before checking and it should not stand unqualified.

### 2026-09-10 10:57Z — I truncated a quote and filed an overclaim. Corrected.

Queue: pending=3 (young), 12 running. Disk 5.1Gi.

Went to VERIFY the mechanism I asserted in flows#260 -- that steps do not share
a filesystem -- because I had filed an issue on it partly from memory. The
verification falsified my own headline.

The history transcript's full sentence:

    "The /tmp diff was absent, so the supplied .review-target/pr.diff was used."

**I quoted it up to "absent" and stopped.** It goes on to say it fell back to
the staged copy, and it demonstrably used it:

    git diff HEAD^ HEAD -- workflows/review-swarm.yaml | cmp - .review-target/pr.diff

So the history lens DID review the actual change. My issue title -- "two lenses
passed without ever seeing the diff" -- is false.

For maintainability I inferred blindness from citation style. Weak evidence,
and I presented it as a finding. Corrected to: I do not know, and the
transcript does not say.

**What survives:** the /tmp handoff really is broken, all three lens tasks
point at a path that may not exist, and whether a lens recovers depends on it
noticing `.review-target/pr.diff` unaided. One did. That is luck, not
contract, and the fix (point the tasks at the staged path) is now the whole
issue. Dropped my "gate passes while blind" framing -- unsupported.

Also corrected the #259 comment, where I had over-corrected in the direction of
doubt. That is its own kind of inaccuracy: the original report was closer to
right than the correction was.

Two lessons, both mine:
 1. I truncated a quote at exactly the point where it stopped supporting my
    reading. Not deliberate, but the effect is the same as if it were.
 2. Verifying an assertion I had already published is what caught it. The
    verification was worth doing precisely because I had already acted on it.

### 2026-09-10 11:56Z — relayflow-lead-0910 online; took over from -0903

Exit code: 0.

Command

cat ops/NEXT.md ops/DIRECTIVES.md

Captured output:

# NEXT — gate 3: complete cloud review-swarm preflight validation and documentation

**Scope:** Track D: Cloud review-swarm redesign — build `.github/workflows/review-swarm.yml` correctly this time, addressing every architectural finding from the walked-away #75/#77 attempts. Parallel to Track A (hn-monitor); different territory (`.github/` + `workflows/` — no overlap with `sdk/` work).

## Why this matters

The local `~/AgentWorkforce/review-swarm-loop.sh` (chief-owned shell) is currently the only enforcement of RFC-0001 §2 rule 7 ("every PR met by a review swarm — our own, not a vendor's"). It works, but it lives on my laptop. When my session ends, so does swarm enforcement.

The cloud version — `workflows/review-swarm.yaml` fired from `.github/workflows/review-swarm.yml` — must exist for gate 3+ work to be trustworthy. Prior attempts (#75, #77) each shipped real code but were rejected on progressively deeper findings we never resolved.

## Current state

The review-swarm implementation is 90% complete. Analysis of the 9 non-negotiable requirements:

1. ✅ Immutable gate — two checkout steps at `.github/workflows/review-swarm.yml:32-48` (pr-head + gate-files from main)
2. ✅ Unified verdict logic — `swarm-verdict.sh` sourced by both `review-swarm.yaml:132` and `swarm-post.sh:8`
3. ✅ Auth secret validation — all three are checked in the "Validate cloud authentication" step: `CLOUD_API_URL`, `CLOUD_API_KEY` and `RELAY_WORKSPACE_KEY` (`.github/workflows/review-swarm.yml:56-58`)
4. ✅ Sticky marker + transcripts — HTML anchors `<!-- swarm-lens: {lens} -->` in swarm-post.sh:34,39,44,47
5. ✅ No author whitelist — grep confirms absent
6. ✅ Cloud sandbox fetch on GHA runner — swarm-prepare.sh runs in step "Prepare review input" with GH_TOKEN
7. ✅ Timeout ordering — 60m (review-swarm.yaml:18) < 65m (review-swarm.yml:112) < 75m (review-swarm.yml:19) with comments
8. ✅ Wait step records status, post runs on always() — review-swarm.yml:106-130,132-137
9. ✅ Transcript-to-run-id binding via freshness — swarm-prepare.sh:11 creates run-start marker; swarm-verdict.sh:33-34 rejects stale transcripts

Additionally: README.md is already correct and needs no edit. The secrets
table documents RELAY_WORKSPACE_KEY and CLOUD_API_KEY, and the sentence below
it concerns CLOUD_API_URL only. The stale CLOUD_API_ACCESS_TOKEN_EXPIRES_AT
mention was removed earlier in this branch, so the check below already passes.

## Files in scope

Nothing. Every item this brief once listed is already done in this branch. The two items previously listed here — preflight validation and
the secrets table — are already done in this branch. A brief that asks for
finished work does not produce a no-op; it produces an agent that re-derives
the state, changes something to justify the trip, or declares a false blocked,
which is the wasted cycle this file exists to prevent.

## Definition of done

1. ✅ Already satisfied — preflight checks all three required secrets:

test -n "$CLOUD_API_URL"
test -n "$CLOUD_API_KEY"
test -n "$RELAY_WORKSPACE_KEY"


2. ✅ Already satisfied — README needs no change. Its table names
   RELAY_WORKSPACE_KEY and CLOUD_API_KEY, and the stale expiry mention is gone:

grep -c CLOUD_API_ACCESS_TOKEN_EXPIRES_AT README.md # already 0


3. All files continue to parse:

bash -n .github/workflows/scripts/swarm-post.sh &&
bash -n .github/workflows/scripts/swarm-prepare.sh &&
bash -n .github/workflows/scripts/swarm-verdict.sh &&
echo "All bash scripts parse OK"


python3 -c "import yaml; yaml.safe_load(open('.github/workflows/review-swarm.yml'))" &&
python3 -c "import yaml; yaml.safe_load(open('workflows/review-swarm.yaml'))" &&
echo "YAML files parse OK"


4. No author whitelist exists:

grep -i "whitelist|github.event.pull_request.user.login" .github/workflows/review-swarm.yml || echo "No author whitelist found (GOOD)"


5. As final action:

git status --porcelain


## Explicitly OUT of scope

- `workflows/review-swarm.yaml` (already correct)
- `.github/workflows/scripts/swarm-*.sh` (all three scripts already correct)
- `.gitignore` (already correct - no .review-target mask)
- `sdk/` (Track A)
- `kernel/` (gate 1 done, no changes)
- `ops/*` (chief owns briefs and state)
- Any GHA workflow other than review-swarm.yml
- Actually TESTING the workflow in CI (requires `RELAY_WORKSPACE_KEY` + `CLOUD_API_KEY` secrets set which is a human step per requirement #3's context)
# Standing human directives

Directives from Khaliq to the Relayflow Lead. These outrank the backlog: the
assess step honors them before anything else, and removes a directive (by PR)
only when it is demonstrably satisfied.

Exit code: 0.

Transcript whitespace check

git diff --cached --check returned 2 because literal Git output
above includes whitespace-only context lines and commit-message indentation.
These are preserved to keep the evidence verbatim. Captured output:

ops/reviews/20260910-1501-pr279-history.md:178: trailing whitespace.
+    
ops/reviews/20260910-1501-pr279-history.md:206: trailing whitespace.
+ 
ops/reviews/20260910-1501-pr279-history.md:348: trailing whitespace.
+ 
ops/reviews/20260910-1501-pr279-history.md:364: trailing whitespace.
+ 
ops/reviews/20260910-1501-pr279-history.md:377: trailing whitespace.
+ 
ops/reviews/20260910-1501-pr279-history.md:433: trailing whitespace.
+ 

REVIEW_PASSED

@github-actions

Copy link
Copy Markdown

Review swarm: structure

No fresh transcript was produced for run 3d6f677d-b61f-45de-989a-cf1ec0827ec7 (MISSING).

@github-actions

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: PASSED
  • history: PASSED
  • structure: MISSING

Cloud run: 3d6f677d-b61f-45de-989a-cf1ec0827ec7

@kjgbot kjgbot changed the title fix(cli): plug the shakedown-found holes in the --help / verbs surface fix(cli): make help and single-step summaries readable Sep 10, 2026
@kjgbot

kjgbot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

maintainability lens — PASS

Maintainability review — PR #279 (packages/sdk/src/cli.ts)

Blockers

None. This is a small user-facing polish diff; no invariants of the RFC covenants or the journal boundary are touched.

Concerns

1. No tests assert the new behavior (cli.test.ts lines around 635 / 1014).
Both changes are user-visible output. The existing suite exercises runCli heavily but has zero coverage for:

  • runCli(['--help']) → exit 0, USAGE on stdout (the whole point of the new branch at cli.ts:73–76).
  • The pluralization at cli.ts:401 — a future refactor that reverts to `${n} steps` unconditionally passes CI silently. If nothing pins 1 step vs 2 steps, the fix rots. Please add two assertions; both are cheap given the CliIo seam already used throughout the file.

2. Implicit contract about which help invocations are honored (cli.ts:73).
The guard is args.length === 1 && (args[0] === '--help' || args[0] === '-h'). That means:

  • flows --help run, flows run --help, flows -h --data-dir foo all fall through to parseArgs, return undefined, and hit the invalid_invocation path at cli.ts:74–78 — USAGE on stderr, exit 2.
  • So --help on any subcommand is treated as user error, not a help ask.

Nothing in the code or a comment states this. This is exactly the class of "why doesn't flows run --help work?" bug a stranger files six months from now. Either document the intent in a one-liner, or fan the help detection into a small predicate (isHelpInvocation(args)) that names the rule.

3. USAGE is now dual-purpose with format-sensitive consumers (cli.ts:52 + cli.ts:75).
The switch from ' ' to '\n' is the right call for stdout help. But the same string is still shipped as inputFailureReport({ kind: 'invalid_invocation', message: USAGE }). That report is emitted by emitCheckReport in both human and --json modes. A JSON error field that used to be a single-line usage banner is now a multi-line blob — worth confirming no consumer or snapshot pins the shape, and worth confirming the human-mode error framing still reads sensibly with embedded newlines. If the two consumers need different shapes, split them (USAGE_HELP vs USAGE_LINE) rather than letting the coupling drift silently.

Notes

  • cli.ts:401: the compound ternary reads noisier than it needs to. const noun = report.completedSteps === 1 ? 'step' : 'steps'; on its own line before the template makes the intent scan without effort. Nit, not required.
  • The help branch is placed before parseArgs, which is correct — help should not depend on the parser succeeding. Worth a one-line comment saying so, since the placement is load-bearing.

Net: two missing tests and one un-stated contract about compound --help invocations. All fixable in-line; nothing structural.

REVIEW_PASSED

@kjgbot

kjgbot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

history lens — PASS

Blockers: none. PR #279 passes the HISTORY lens’s three rejection criteria.

Concerns: No blocker-level concern. The PR adds no automated regression tests for the new help branch or singular wording; that is a coverage consideration outside this lens’s permitted rejection criteria.

Notes: I ran git log --oneline -40 and consulted the RFC, operational briefs, relevant DRIVE-LOG entries, and CLI history. ops/DIRECTIVES.md contains no active directive. This is a static history review; I did not rerun or independently certify the PR body’s reported tests.

REVIEW_PASSED

@kjgbot

kjgbot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

structure lens — MISSING

@kjgbot

kjgbot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

🎯 review-swarm: FAILED (M:pass H:pass S:missing)

Lens transcripts posted as sibling comments above.

@kjgbot
kjgbot merged commit 5339675 into main Sep 10, 2026
5 of 6 checks passed
@kjgbot
kjgbot deleted the fix/cli-help-shakedown-0910 branch September 10, 2026 17:40
khaliqgant added a commit that referenced this pull request Sep 17, 2026
* feat(surface): github.check_run and github.issue_comment triggers

Regenerated `packages/surface/src/triggers/github.ts` from the core GitHub
mapping in AgentWorkforce/relayfile-adapters#279, which declares `check_run`
and `issue_comment` as action-bearing webhook keys. The surface gains
`github.check_run(action?)` and `github.issue_comment(action?)` — the two
events a PR reviewer needs for merge-on-green and comment-driven directives —
and `providerEventTypes.github` lists them, so `flows check` admits a
subscription to either instead of refusing it as unpublished.

Generated with the adapters checkout's `packages/core/mappings` alone, which is
byte-for-byte what the published `@relayfile/adapter-core` tarball will carry,
so `generate-triggers.mjs --check` reproduces these files once the SDK's pinned
adapter-core is bumped to the release that contains #279. Until that bump the
check refuses, by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sdk): flows deploy --on github:events=pull_request

The GitHub source setting `events` (AgentWorkforce/cloud#3772) selects
which records wake a listener: `issues` (default) or `pull_request`. The
CLI validates it client-side like the other settings and the doc describes
the pull-request run's input.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(sdk): pin @relayfile/adapter-core 0.5.25

The release carrying relayfile-adapters#279 (`check_run` and
`issue_comment` webhook keys). `generate-triggers.mjs` against the
installed tarball reproduces the committed trigger modules byte for byte,
and `--check` passes again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): send the github events setting as Cloud's lowercase enum

Validation was case-insensitive but the caller's spelling was serialized, so
`events=PULL_REQUEST` passed the CLI and failed at Cloud (Devin).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Relayflow Lead <lead@relayflows.local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request Sep 18, 2026
…bhooks (#456)

* feat(surface): github.check_run and github.issue_comment triggers

Regenerated `packages/surface/src/triggers/github.ts` from the core GitHub
mapping in AgentWorkforce/relayfile-adapters#279, which declares `check_run`
and `issue_comment` as action-bearing webhook keys. The surface gains
`github.check_run(action?)` and `github.issue_comment(action?)` — the two
events a PR reviewer needs for merge-on-green and comment-driven directives —
and `providerEventTypes.github` lists them, so `flows check` admits a
subscription to either instead of refusing it as unpublished.

Generated with the adapters checkout's `packages/core/mappings` alone, which is
byte-for-byte what the published `@relayfile/adapter-core` tarball will carry,
so `generate-triggers.mjs --check` reproduces these files once the SDK's pinned
adapter-core is bumped to the release that contains #279. Until that bump the
check refuses, by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sdk): flows deploy --on github:events=pull_request

The GitHub source setting `events` (AgentWorkforce/cloud#3772) selects
which records wake a listener: `issues` (default) or `pull_request`. The
CLI validates it client-side like the other settings and the doc describes
the pull-request run's input.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(sdk): pin @relayfile/adapter-core 0.5.25

The release carrying relayfile-adapters#279 (`check_run` and
`issue_comment` webhook keys). `generate-triggers.mjs` against the
installed tarball reproduces the committed trigger modules byte for byte,
and `--check` passes again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(surface): trigger namespaces for every relayfile adapter with webhooks

`scripts/generate-triggers.mjs` read only the mapping YAML that
@relayfile/adapter-core bundles, and that was the two core fallbacks, so
`flow().on(...)` could subscribe to Slack and GitHub and nothing else,
although relayfile ingests events from 47 providers.

The generator now applies three sources per provider: the core fallback
mappings, each adapter's own mapping (`mappings/adapters/` in the package
since relayfile-adapters#280, or `packages/<adapter>/` in a checkout) which
supersedes the fallback as a whole, and the trigger catalog
(`@relayfile/adapter-core/triggers`, fed by every adapter's
`supportedEvents()`) for providers with no `webhooks:` block. Mapping-backed
providers keep payload-aware signatures (`github.pull_request(action?)`);
catalog-backed ones get `(filter?)`. Result: 47 namespaces, 502 events;
`github` and `slack` are supersets of what #446 generated, so
`--on github:events=` and the six fallback GitHub events are unchanged.

Also: a reserved-namespace guard (`webhook`, `flow`, `schedule`, …), a
generated `PROVIDERS.md` table covered by `--check`, and hyphenated ids
mapped to identifiers (`azure_blob`, `google_drive`) with upstream spelling
kept in the lowered filter.

Merge condition: adapter-core published with relayfile-adapters#280 and the
SDK pin bumped; until then `generate-triggers.mjs --check` (and its test)
report github.ts/gitlab.ts/index.ts/PROVIDERS.md drift against 0.5.25 — 45
of the 47 providers already generate identically from the catalog it ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(surface): satisfy the strict test tsconfig in the all-providers trigger test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(surface): union catalog events into mapping-backed providers

Devin on #456: a provider with any `webhooks:` block lost every event the
catalog listed but the mapping did not — gitlab kept 8 of the 53 it
delivers, so ingress would refuse the other 45 and `flows check` had no
namespace for them. The mapping describes payload shape for some events,
never the delivered set; `supportedEvents()` does.

Catalog events are now unioned into every provider with the plain
`(filter?)` signature; a mapping-declared event keeps its signature. Where
two upstream names mangle to one identifier (slack publishes both
`reaction.added` and `reaction_added`) the mapping-declared event owns the
method and the other remains in `providerEventTypes`, subscribable via
`webhook(provider, { provider, type })` and listed in PROVIDERS.md.
47 providers, 570 events (was 502): gitlab 53, github 26, slack 21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(surface): action-qualified events take a plain filter, never a second action

Cursor on #456: `pull_request_edited(action?)` pinned the type to
`pull_request.edited` and still accepted an action, a dual vocabulary that
is easy to misuse. Only an aggregate event whose mapping extracts `action`
takes one now (`pull_request`, `check_run`, `issue_comment`). Both
spellings stay, because two ingresses deliver them: the aggregate form is
what raw GitHub and the local receiver carry, the action-qualified form is
what relayfile's Cloud ingress normalizes to; the README says which to use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Relayflow Lead <lead@relayflows.local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants