Skip to content

feat(cli): make defaultOnResult the default for buildCliCommands [OS-605]#812

Merged
galligan merged 2 commits into
mainfrom
os-605-surface-layer-output-handling-make-buildclicommands-handle
Mar 26, 2026
Merged

feat(cli): make defaultOnResult the default for buildCliCommands [OS-605]#812
galligan merged 2 commits into
mainfrom
os-605-surface-layer-output-handling-make-buildclicommands-handle

Conversation

@galligan

@galligan galligan commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Summary

buildCliCommands now uses defaultOnResult by default, so handler results are automatically output based on CLI flags (--output, --json, --jsonl). Previously, success values were silently discarded unless consumers explicitly passed onResult: defaultOnResult — a pattern that was non-obvious enough that consumers kept writing their own withOutput() wrappers instead.

Fixes https://linear.app/outfitter/issue/OS-605/surface-layer-output-handling-make-buildclicommands-handle-output-by

What changed

packages/cli/src/actions.ts:

  • buildCliCommands defaults onResult to defaultOnResult instead of undefined
  • onResult: null explicitly opts out (silently discards success values, only throws errors)
  • Updated TSDoc to document the new default and opt-out

apps/outfitter/src/cli.ts:

  • Added onResult: null since outfitter's actions handle their own output internally

The principle

Handlers return data. Surfaces present it. This was already the intended design — defaultOnResult existed and worked. This change makes it the default rather than an opt-in that nobody discovers.

Follow-up needed

Scaffold templates in plugins/outfitter/shared/templates/ still show handlers calling output() directly. These should be updated to reflect the new pattern (handlers return Result, surface handles output). Filed as a follow-up — not blocking since onResult: null provides the escape hatch for existing consumers.

Test plan

  • All 910 CLI tests pass
  • Typecheck passes (@outfitter/cli, outfitter)
  • Outfitter app opts out with onResult: null — no double-output

🤘🏻 In-collaboration-with: Claude Code

@linear

linear Bot commented Mar 25, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot added release:minor Contains new features (minor version bump) docs Documentation improvement package/cli @outfitter/cli and removed release:minor Contains new features (minor version bump) labels Mar 25, 2026

galligan commented Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

@galligan galligan marked this pull request as ready for review March 25, 2026 18:45
@greptile-apps

greptile-apps Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes defaultOnResult the automatic default for buildCliCommands, so handler results are output based on CLI flags without any extra wiring. It also introduces onResult: null as the explicit opt-out. The apps/outfitter CLI is updated to opt out since its actions still handle output directly.

Key changes:

  • buildCliCommands resolves onResult: null → undefined and onResult: undefined → defaultOnResult before delegating to internal helpers — clean and correct
  • Changeset is correctly tagged as major since any consumer of buildCliCommands that previously relied on success values being silently discarded (no onResult) will now get unexpected output
  • The defaultOnResult TSDoc was not updated and still describes the old explicit-pass pattern, which now conflicts with the updated buildCliCommands docs
  • The updated BuildCliCommandsOptions.onResult TSDoc dropped the important warning that custom callbacks are solely responsible for error handling — a behavioral contract that hasn't changed in runAction

Confidence Score: 4/5

Safe to merge — the runtime logic is correct and the breaking change is properly versioned; only documentation gaps remain.

The nullundefined mapping is correct, all three call-sites in buildCliCommands use the resolved onResult, and the opt-out in apps/outfitter/src/cli.ts is applied correctly. The only concerns are documentation: the defaultOnResult TSDoc is stale and the interface TSDoc omits the critical note that custom callbacks must handle errors themselves.

packages/cli/src/actions.ts — specifically the defaultOnResult TSDoc (lines 514–525) and the BuildCliCommandsOptions.onResult field doc (lines 48–57).

Important Files Changed

Filename Overview
packages/cli/src/actions.ts Core change: buildCliCommands now resolves onResult to defaultOnResult when not provided, converting null to undefined for internal runAction calls. Logic is correct, but the updated TSDoc omits the critical note that custom onResult callbacks are fully responsible for error handling.
apps/outfitter/src/cli.ts Correctly opts out of the new default by passing onResult: null, with a clear inline comment. No issues.
.changeset/cli-default-on-result.md Correctly marks the change as major for @outfitter/cli since it alters default runtime behavior for all existing consumers of buildCliCommands.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["buildCliCommands(source, options)"] --> B{options.onResult}
    B -- "=== null" --> C["onResult = undefined\nopt-out: silently discard success"]
    B -- "=== undefined not set" --> D["onResult = defaultOnResult\nNEW DEFAULT"]
    B -- "custom function" --> E["onResult = custom fn\ncaller responsible for errors"]

    C --> F["runAction with onResult"]
    D --> F
    E --> F

    F --> G{onResult truthy}
    G -- "Yes" --> H["await onResult and return"]
    G -- "No undefined" --> I{result.isErr}
    I -- "Yes" --> J["throw result.error"]
    I -- "No" --> K["return success discarded"]

    H --> L["defaultOnResult:\nthrow if err else output value"]
Loading

Comments Outside Diff (2)

  1. packages/cli/src/actions.ts, line 48-57 (link)

    Custom onResult error-handling responsibility no longer documented

    The previous TSDoc explicitly warned: "When provided, handler errors are not auto-thrown — the callback is responsible for error handling." This is still the actual runtime behavior (runAction lines 304–311 return immediately after calling onResult, bypassing the auto-throw), but the new docs only describe the default and the null opt-out — the custom-callback case is completely silent.

    A consumer who provides a custom onResult that doesn't call throw ctx.result.error for error results will silently swallow errors. Since defaultOnResult does throw, users extending it may not realize they need to replicate that behavior. Consider adding a note like:

  2. packages/cli/src/actions.ts, line 514-525 (link)

    defaultOnResult TSDoc is now stale

    The function's description says "Pass this to buildCliCommands({ onResult: defaultOnResult }) to get automatic output…" and the @example block shows exactly that pattern. However, the whole point of this PR is that you no longer need to pass it explicitly — it's the new default. This directly contradicts the updated buildCliCommands example, which removed the explicit onResult: defaultOnResult call.

    The TSDoc and example should be updated to reflect the new role of defaultOnResult:

    typescript

    • // Extend the default with extra logging
    • const commands = buildCliCommands(registry, {
    • onResult: async (ctx) => {
    • logger.info("action completed", { id: ctx.action.id });
      
    • await defaultOnResult(ctx);
      
    • },
    • });

    */

    
    
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/cli/src/actions.ts
Line: 48-57

Comment:
**Custom `onResult` error-handling responsibility no longer documented**

The previous TSDoc explicitly warned: *"When provided, handler errors are not auto-thrown — the callback is responsible for error handling."* This is still the actual runtime behavior (`runAction` lines 304–311 return immediately after calling `onResult`, bypassing the auto-throw), but the new docs only describe the default and the `null` opt-out — the custom-callback case is completely silent.

A consumer who provides a custom `onResult` that doesn't call `throw ctx.result.error` for error results will silently swallow errors. Since `defaultOnResult` does throw, users extending it may not realize they need to replicate that behavior. Consider adding a note like:

```suggestion
  /**
   * Called after each handler returns with `Result.ok` or `Result.err`.
   *
   * Defaults to {@link defaultOnResult}, which outputs success values
   * based on CLI flags (`--output`, `--json`, `--jsonl`) and throws errors.
   * Pass `null` to disable and silently discard success values.
   *
   * When a custom function is provided, it is **fully responsible for error
   * handling** — errors are not auto-thrown. The callback receives the raw
   * `Result` and must throw (or otherwise surface) errors itself.
   */
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/cli/src/actions.ts
Line: 514-525

Comment:
**`defaultOnResult` TSDoc is now stale**

The function's description says "Pass this to `buildCliCommands({ onResult: defaultOnResult })` to get automatic output…" and the `@example` block shows exactly that pattern. However, the whole point of this PR is that you no longer need to pass it explicitly — it's the new default. This directly contradicts the updated `buildCliCommands` example, which removed the explicit `onResult: defaultOnResult` call.

The TSDoc and example should be updated to reflect the new role of `defaultOnResult`:

```suggestion
/**
 * Default `onResult` callback that outputs success values and throws errors.
 *
 * This is automatically used by {@link buildCliCommands} unless overridden.
 * Import and reference it directly when composing a custom callback that
 * partially delegates to the default behavior.
 *
 * @example
 * ```typescript
 * // Extend the default with extra logging
 * const commands = buildCliCommands(registry, {
 *   onResult: async (ctx) => {
 *     logger.info("action completed", { id: ctx.action.id });
 *     await defaultOnResult(ctx);
 *   },
 * });
 * ```
 */
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (10): Last reviewed commit: "chore: add changeset" | Re-trigger Greptile

@galligan galligan force-pushed the fix/cli/derive-array-flags branch from 869116c to b6f62cd Compare March 25, 2026 19:08
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 3defe22 to 64c34ec Compare March 25, 2026 19:08
@github-actions github-actions Bot added the release:minor Contains new features (minor version bump) label Mar 25, 2026
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 64c34ec to 9286642 Compare March 25, 2026 19:34
@github-actions github-actions Bot added the testing Test-related label Mar 25, 2026
@galligan

Copy link
Copy Markdown
Contributor Author

@greptileai review

@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 9286642 to 8cbabff Compare March 25, 2026 22:06
@galligan galligan force-pushed the fix/cli/derive-array-flags branch from b6f62cd to 9584d09 Compare March 25, 2026 22:06
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 8cbabff to 7754919 Compare March 26, 2026 00:08
@galligan galligan force-pushed the fix/cli/derive-array-flags branch 2 times, most recently from ebf3344 to bd832f2 Compare March 26, 2026 02:35
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch 2 times, most recently from 13e0b46 to 70904d5 Compare March 26, 2026 15:41
@galligan galligan force-pushed the fix/cli/derive-array-flags branch from bd832f2 to 58b1d54 Compare March 26, 2026 15:41
@github-actions github-actions Bot added release:major Contains breaking changes (major version bump) and removed release:minor Contains new features (minor version bump) labels Mar 26, 2026
@galligan galligan force-pushed the fix/cli/derive-array-flags branch from 58b1d54 to 1395a41 Compare March 26, 2026 16:20
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 70904d5 to 6859c0f Compare March 26, 2026 16:20

galligan commented Mar 26, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Mar 26, 11:48 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Mar 26, 11:49 PM UTC: Graphite rebased this pull request as part of a merge.
  • Mar 26, 11:52 PM UTC: Graphite rebased this pull request as part of a merge.
  • Mar 26, 11:53 PM UTC: @galligan merged this pull request with Graphite.

@galligan galligan changed the base branch from fix/cli/derive-array-flags to graphite-base/812 March 26, 2026 23:48
@galligan galligan changed the base branch from graphite-base/812 to main March 26, 2026 23:48
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from 6859c0f to e3f83a8 Compare March 26, 2026 23:49
@galligan galligan force-pushed the os-605-surface-layer-output-handling-make-buildclicommands-handle branch from e3f83a8 to 1f90a74 Compare March 26, 2026 23:52
@galligan galligan merged commit 8d1bf72 into main Mar 26, 2026
11 checks passed
@galligan galligan deleted the os-605-surface-layer-output-handling-make-buildclicommands-handle branch March 26, 2026 23:53
galligan added a commit that referenced this pull request Mar 26, 2026
…6] (#813)

## Summary

CI Summary job fails intermittently even when all individual jobs pass. Observed 3-4 times in a single session across PRs #806, #810, #811, #812.

**Root cause:** When `gt submit` force-pushes a branch, GitHub Actions cancels the in-flight run. The `ci-summary` job checks `contains(needs.*.result, 'cancelled')`, which catches cancelled jobs from the superseded run — not actual failures.

Fixes https://linear.app/outfitter/issue/OS-616/ci-summary-job-fails-spuriously-after-force-push-due-to-cancelled-job

## What changed

`.github/workflows/ci.yml`:

1. **Dropped `cancelled` from the failure check** — only `failure` triggers the exit. Cancelled jobs from superseded runs aren't real failures.

2. **Added `concurrency` group** — `ci-${{ github.ref }}` with `cancel-in-progress: true` ensures only one CI run per branch exists at a time, preventing the race structurally.

## Test plan

- [x] Workflow YAML is syntactically valid
- [x] The concurrency group scopes to the branch ref, so main and PR branches don't cancel each other
- [x] `cancel-in-progress: true` only cancels runs on the same ref (safe for stacked PRs on different branches)

🤘🏻 In-collaboration-with: [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation improvement package/cli @outfitter/cli release:major Contains breaking changes (major version bump) testing Test-related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant