@fnclaude/cli 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,238 @@
1
1
  # @fnclaude/cli
2
2
 
3
- The Claude Code command-line interface. Provides the `fnc` command for managing Claude Code sessions, projects, tasks, and remote triggers.
3
+ `claude`, with the rough edges filed off.
4
4
 
5
- ## Installation
5
+ ```sh
6
+ fnc opus max ~/src/myproject -- "refactor the auth module"
7
+ ```
8
+
9
+ No `--model claude-opus-4-5`, no `--effort max`, no `--print` gymnastics. `@fnclaude/cli` (the `fnc` binary) sits in front of `claude` and translates short, readable invocations into the full-form flags `claude` expects. Magic positional words for model and effort, capital-letter short flags for everything claude makes you spell out, and a config file for the auto-features you want on every launch. Bun-runtime; install via `bun add -g fnclaude` (or `npm i -g fnclaude`).
10
+
11
+ ## Platform support
12
+
13
+ Supported on **Linux** and **macOS**. The codebase has a Windows fallback path (`spawn + process.exit` instead of `process.execve`) but it has never been exercised — treat it as untested. If you run on Windows, expect breakage.
14
+
15
+ ## Install
6
16
 
7
- ```bash
8
- npm install @fnclaude/cli
9
- bun add @fnclaude/cli
17
+ ```sh
18
+ bun add -g fnclaude
19
+ # or
20
+ npm install -g fnclaude
10
21
  ```
11
22
 
23
+ Requires the **Bun runtime** for terminal session management. Node.js is supported for installation, but session execution runs via Bun.
24
+
12
25
  ## Requirements
13
26
 
14
- This package requires the **Bun runtime** for terminal session management. The underlying PTY layer uses node-pty with Bun's native adapter, which provides efficient cross-platform support for interactive shell sessions. Node.js as the JavaScript runtime is supported for package management and installation, but session execution runs via Bun.
27
+ - [Bun](https://bun.sh/) >= 1.0
28
+ - [claude](https://github.com/anthropics/claude-code) CLI installed and on your `$PATH`
29
+
30
+ ## Quick start
31
+
32
+ ```sh
33
+ # Launch in the current directory
34
+ fnc .
35
+
36
+ # Specific model in a specific project
37
+ fnc sonnet ~/src/myproject
38
+
39
+ # High-effort opus session
40
+ fnc opus high ~/src/myproject
41
+
42
+ # Attach a shared tools directory
43
+ fnc ~/src/myproject -A ~/src/shared-tools
44
+
45
+ # Pass a prompt inline
46
+ fnc . -- "refactor the auth module"
47
+
48
+ # Collapse multiple short flags
49
+ fnc -BVC .
50
+ ```
51
+
52
+ ## Features
53
+
54
+ ### Magic positional words
55
+
56
+ The first two positional slots can be a model alias and an effort level. `fnc` intercepts them before `claude` ever sees the args.
57
+
58
+ ```sh
59
+ fnc opus max ~/src/proj # --model claude-opus-4-5 --effort max
60
+ fnc sonnet ~/src/proj # --model claude-sonnet-4-5
61
+ fnc haiku low ~/src/proj # --model claude-haiku-4-5 --effort low
62
+ fnc ~/src/proj # no model flag — claude picks the default
63
+ ```
64
+
65
+ Supported model aliases: `opus`, `sonnet`, `haiku`.
66
+ Supported effort levels: `low`, `medium`, `high`, `xhigh`, `max`.
67
+
68
+ A directory that happens to be named `opus`? Prefix it: `fnc ./opus`.
69
+
70
+ ### Capital-letter short flags
71
+
72
+ `claude`'s long options are the right thing to pass and a chore to type. `fnc` maps each one to a capital-letter short flag that collapses with standard POSIX rules.
73
+
74
+ ```sh
75
+ fnc -BVC ~/src/proj # --brief --verbose --chrome
76
+ fnc -T ~/src/proj # --tmux
77
+ fnc -D ~/src/proj # --dangerously-skip-permissions
78
+ ```
79
+
80
+ Full mapping in the [flag reference](#short-translations-fnc--claude) below.
81
+
82
+ ### Prompt after `--`
15
83
 
16
- ## Quick Start
84
+ Pass a prompt inline without `--print` or redirection — just drop a `--` and write the prompt. When `--name`/`-n` isn't already set, `fnc` generates a 1–3-word session label from the prompt via Haiku (see [Auto-name from prompt](#auto-name-from-prompt) below).
17
85
 
18
- ```bash
19
- # Launch Claude Code with default settings
20
- fnc
86
+ ```sh
87
+ fnc sonnet src/ -- "add integration tests for the payments module"
88
+ ```
21
89
 
22
- # Specify a model and effort level
23
- fnc opus max ~/my-project
90
+ ### Multi-directory MCP injection
24
91
 
25
- # Open Claude in a specific worktree
26
- fnc ~/my-project my-feature-branch
92
+ Need claude to see a second project's MCP config and settings? Pass it as an extra positional or with `-A`/`--also`. `fnc` injects `--add-dir`, `--mcp-config`, and `--settings` for each extra dir automatically.
27
93
 
28
- # Resume your most recent session
29
- fnc continue
94
+ ```sh
95
+ fnc src/ -A tools/ -A shared/
96
+ # or equivalently:
97
+ fnc src/ tools/ shared/
30
98
  ```
31
99
 
32
- ## Usage
100
+ Each extra dir gets `--mcp-config <dir>/.mcp.json` (if the file exists) and `--settings <dir>/.claude/settings.json` (if it exists). The primary dir is launched in; extra dirs are attached.
101
+
102
+ ### Auto-features you configure once
103
+
104
+ Tired of typing `--tmux` every launch? Set it once in config and forget it.
33
105
 
34
- ```bash
35
- fnc --help
106
+ ```toml
107
+ # ~/.config/fnclaude/config.toml
108
+ [auto]
109
+ tmux = "worktree" # inject --tmux when you're creating a worktree via -w
36
110
  ```
37
111
 
38
- For more details on available commands and features, run `fnc help` or see the main fnclaude documentation.
112
+ Off by default. The per-invocation override `--no-tmux` lets you escape for a single run without touching config.
113
+
114
+ `auto.tmux = "worktree"` is the only valid non-default value: it injects `--tmux` only when you're explicitly creating a new worktree via `-w` (which claude requires for `--tmux` anyway). `fnc` never spawns worktrees on its own — that's always a user-initiated action.
115
+
116
+ > **What about `--dangerously-skip-permissions` and `--ide`?** Use claude's own settings: `permissions.defaultMode` and `skipDangerousModePermissionPrompt` in `~/.claude/settings.json`, plus `autoConnectIde` in `~/.claude.json`. The CLI flags themselves (`-D`, `--dangerously-skip-permissions`, `-I`, `--ide`) still pass through verbatim for per-invocation use.
117
+
118
+ ### Auto-name from prompt
119
+
120
+ When you pass a prompt after `--` (and aren't resuming an existing session), `fnc` generates a short hyphenated session label via Haiku and injects it as `--name`. The call has a 3-second timeout; on timeout, missing API key, or any error, it falls back to a heuristic that strips stop-words and takes the first three meaningful tokens.
121
+
122
+ ```sh
123
+ fnc . -- "refactor the auth module"
124
+ # → --name refactor-auth-module
125
+ ```
39
126
 
40
- ## Key Features
127
+ Skipped for `-p`/`--print`, `-r`/`--resume`, `-c`/`--continue`, and `--from-pr` — those don't create new named sessions. Requires `ANTHROPIC_API_KEY`; suppress the missing-key warning with `FNCLAUDE_QUIET_MISSING_API_KEY=1` (or the config equivalent) if you'd rather just rely on the heuristic.
41
128
 
42
- - **Model selection**: Pass `opus`, `sonnet`, or `haiku` as the first argument to pick a model alias
43
- - **Effort levels**: Set effort with `low`, `medium`, `high`, `xhigh`, or `max` as the second argument
44
- - **Worktree switching**: Automatically swap to a named worktree with `-w <name>` or as a positional argument
45
- - **Session resume**: Resume previous sessions with `fnc continue` or pick one interactively with `fnc resume`
46
- - **MCP integration**: Built-in Model Context Protocol server for seamless Claude integration
129
+ ### Cross-cwd `--resume`
130
+
131
+ `claude --resume` normally exits with a "this conversation is from a different directory" message when you pick a session from elsewhere via the picker. `fnc` scans the last 4 KB of claude's output, catches that message after exit, and transparently re-execs a fresh `fnc` in the destination directory. The picker just works across all your projects — no flicker, no manual `cd`.
132
+
133
+ Linux and macOS only; on Windows `fnc` falls back to a plain exec (no PTY, no detection).
134
+
135
+ ### Worktree intercept
136
+
137
+ `fnc -w <name>` looks up `<name>` against the existing git worktrees of the project repo. If it matches, `fnc` swaps its cwd to that worktree and drops the `-w` flag — no new worktree is created, no duplicate. If it doesn't match, the flag passes through and the name doubles as the session `--name`.
138
+
139
+ ```sh
140
+ fnc -w feature-branch # cds to the feature-branch worktree if it exists
141
+ fnc -w new-thing # passes -w new-thing through; sets --name new-thing
142
+ ```
143
+
144
+ ### Auto-handoff from noop sessions
145
+
146
+ When `fnc` is launched with no positional path, it drops into a "noop" session whose system prompt makes claude act as a router: classify the user's prompt into general-Q&A, read-shaped, or action-on-a-project. Action requests get written as a handoff file to a temp path and the relaunch command fires automatically (or with a confirmation, depending on `auto.handoff`).
147
+
148
+ `auto.handoff` controls how the noop router proposes the relaunch:
149
+
150
+ - `"never"` — paste-flow only. claude renders the relaunch command and copies it to the clipboard.
151
+ - `"ask"` (default) — claude asks "Want me to switch you over now?" before doing anything.
152
+ - `"<N>"` (seconds, e.g. `"5"`) — countdown auto-switch. `"0"` means instant.
153
+
154
+ ## Reference
155
+
156
+ ### Argument grammar
157
+
158
+ The first two positional arguments may be "magic" shorthands:
159
+
160
+ - **Position 1**: if the value is exactly `opus`, `sonnet`, or `haiku`, it is translated to `--model <alias>` and consumed. Otherwise treated as a path.
161
+ - **Position 2**: only checked when position 1 was a model alias. If the value is exactly `low`, `medium`, `high`, `xhigh`, or `max`, it is translated to `--effort <level>` and consumed. Otherwise treated as a path.
162
+ - **Position 3+**: never magic.
163
+
164
+ To pass a literal directory named `opus`, prefix with `./`: `fnc ./opus`
165
+
166
+ After magic slots resolve, the first remaining positional is the directory to launch `claude` in. Subsequent positionals are "extra dirs" that each receive `--add-dir`, `--mcp-config`, and `--settings` injection (files must exist for the latter two).
167
+
168
+ Use `--` to separate `fnc` args from the prompt string:
169
+
170
+ ```sh
171
+ fnc sonnet src/ -- "do the thing"
172
+ ```
173
+
174
+ ### Flag reference
175
+
176
+ #### fnc-owned flags
177
+
178
+ | Flag | Long | Description |
179
+ |---|---|---|
180
+ | | `--no-tmux` | Suppress auto-`--tmux` for this invocation |
181
+ | `-A <dir>` | `--also <dir>` | Add an extra dir (repeatable) |
182
+ | `-h` | `--help` | Print the flag reference and exit |
183
+ | `-v` | `--version` | Print `fnc`'s version and exit |
184
+
185
+ #### Short translations (fnc → claude)
186
+
187
+ | Short | Long | Value |
188
+ |---|---|---|
189
+ | `-A` | `--also` | required (fnc-owned) |
190
+ | `-B` | `--brief` | none |
191
+ | `-C` | `--chrome` | none |
192
+ | `-D` | `--dangerously-skip-permissions` | none |
193
+ | `-F` | `--fork-session` | none |
194
+ | `-G` | `--agent` | required |
195
+ | `-I` | `--ide` | none |
196
+ | `-M` | `--permission-mode` | required |
197
+ | `-P` | `--from-pr` | optional |
198
+ | `-R` | `--remote-control` | optional |
199
+ | `-T` | `--tmux` | optional |
200
+ | `-V` | `--verbose` | none |
201
+ | `-W` | `--allowedTools` | required |
202
+
203
+ Short flags follow standard POSIX collapsing: `-BVC` expands to `-B -V -C`. Only the last flag in a collapsed group may take a value. All other flags pass through to `claude` verbatim.
204
+
205
+ ### Config file
206
+
207
+ Location: `$XDG_CONFIG_HOME/fnclaude/config.toml` (fallback `~/.config/fnclaude/config.toml`). A missing file is not an error — all defaults apply.
208
+
209
+ Precedence: **CLI flag > env var > config file > built-in default**
210
+
211
+ ```toml
212
+ [name]
213
+ model = "claude-haiku-4-5" # model for auto-generated session names
214
+ timeout = "3s" # timeout for the name-generation API call
215
+ quiet_missing_api_key = false
216
+
217
+ [auto]
218
+ tmux = "never" # "never" | "worktree"
219
+ handoff = "ask" # "never" | "ask" | non-negative integer seconds
220
+ ```
221
+
222
+ #### Env var mapping
223
+
224
+ | Config key | Env var |
225
+ |---|---|
226
+ | `name.model` | `FNCLAUDE_NAME_MODEL` |
227
+ | `name.timeout` | `FNCLAUDE_NAME_TIMEOUT` |
228
+ | `name.quiet_missing_api_key` | `FNCLAUDE_QUIET_MISSING_API_KEY` |
229
+ | `auto.tmux` | `FNCLAUDE_TMUX` |
230
+ | `auto.handoff` | `FNCLAUDE_HANDOFF` |
231
+
232
+ `ANTHROPIC_API_KEY` is read (standard) for the auto-name LLM call.
47
233
 
48
234
  ## Status
49
235
 
50
- The CLI is actively maintained. Current release is published to npm under `@latest` and `@next` dist-tags. See the main fnclaude repository for release notes and version history.
236
+ The CLI is a recent TypeScript rewrite of the original Go binary. It is actively maintained and published to npm under `@latest`. See the [main fnclaude repository](https://github.com/fnrhombus/fnclaude) for release notes and version history.
237
+
238
+ File bugs and feature requests on [GitHub Issues](https://github.com/fnrhombus/fnclaude/issues).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnclaude/cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "fnclaude CLI implementation (TypeScript rewrite, in progress)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/argParser.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { join as pathJoin } from 'node:path';
19
- import type { Args } from './args.js';
19
+ import { brandParsed, type ParsedArgs } from './args.js';
20
20
 
21
21
  // ── Magic-word vocabularies ────────────────────────────────────────────────
22
22
 
@@ -163,11 +163,15 @@ export function parseShortFlag(arg: string, rest: readonly string[]): ParseShort
163
163
  * parseArgs is the canonical argv parser. `home` is the user's home dir
164
164
  * (typically `os.homedir()`); it's used to derive the noop fallback path.
165
165
  *
166
+ * Returns a `ParsedArgs` — the first stage of the immutable argv pipeline.
167
+ * Notably absent: `worktreeMatched`. That value only becomes meaningful
168
+ * after the intercept stage and is part of `InterceptedArgs` instead.
169
+ *
166
170
  * Throws Error on invalid input (too many positionals, missing values,
167
171
  * collapsed-group misuse, two subcommands, etc.). The Go original returns
168
172
  * `(Args, error)`; TS uses throw for the natural shape.
169
173
  */
170
- export function parseArgs(argv: readonly string[], home: string): Args {
174
+ export function parseArgs(argv: readonly string[], home: string): ParsedArgs {
171
175
  let firstPath = '';
172
176
  const extraDirs: string[] = [];
173
177
  const passthrough: string[] = [];
@@ -339,7 +343,7 @@ export function parseArgs(argv: readonly string[], home: string): Args {
339
343
  const cwd = firstPathSet ? firstPath : defaultNoopDir(home);
340
344
  const usedNoopFallback = !firstPathSet;
341
345
 
342
- return {
346
+ return brandParsed({
343
347
  cwd,
344
348
  extraDirs,
345
349
  passthrough: finalPassthrough,
@@ -347,34 +351,17 @@ export function parseArgs(argv: readonly string[], home: string): Args {
347
351
  worktreeSet,
348
352
  worktreeArg,
349
353
  usedNoopFallback,
350
- worktreeMatched: false,
351
- };
354
+ });
352
355
  }
353
356
 
354
- // ── Passthrough inspection helpers (used downstream by buildArgv etc.) ─────
355
-
356
- /**
357
- * True when any token is `--setting-sources` or starts with `--setting-sources=`.
358
- */
359
- export function settingSourcesInPassthrough(passthrough: readonly string[]): boolean {
360
- return passthrough.some(
361
- (t) => t === '--setting-sources' || t.startsWith('--setting-sources='),
362
- );
363
- }
364
-
365
- /**
366
- * True when the exact token appears, or any `token=<anything>` form.
367
- */
368
- export function tokenInPassthrough(passthrough: readonly string[], long: string): boolean {
369
- const prefix = `${long}=`;
370
- return passthrough.some((t) => t === long || t.startsWith(prefix));
371
- }
372
-
373
- /**
374
- * True when --name or -n (bare or =value) appears anywhere in passthrough.
375
- */
376
- export function nameInPassthrough(passthrough: readonly string[]): boolean {
377
- return passthrough.some(
378
- (t) => t === '--name' || t === '-n' || t.startsWith('--name=') || t.startsWith('-n='),
379
- );
380
- }
357
+ // ── Passthrough inspection helpers ─────────────────────────────────────────
358
+ //
359
+ // The canonical implementations live in passthrough.ts. Re-exported here
360
+ // for back-compat with callers that grew up importing them from argParser.
361
+ // New code should import from passthrough.ts directly.
362
+
363
+ export {
364
+ nameInPassthrough,
365
+ settingSourcesInPassthrough,
366
+ tokenInPassthrough,
367
+ } from './passthrough.js';
@@ -16,7 +16,7 @@
16
16
  * port mirrors every case 1:1.
17
17
  */
18
18
 
19
- import type { Request } from '../mcp/protocol.js';
19
+ import type { RequestOverrides } from '../mcp/protocol.js';
20
20
 
21
21
  // ── Magic-word vocabularies (must match argParser.ts) ──────────────────────
22
22
 
@@ -219,7 +219,7 @@ export const transferDenyBareOK: ReadonlySet<string> = new Set([
219
219
  */
220
220
  export function applyOverrides(
221
221
  preserved: readonly string[],
222
- req: Request,
222
+ req: RequestOverrides,
223
223
  ): string[] {
224
224
  let out = preserved.slice();
225
225
 
package/src/args.ts CHANGED
@@ -1,62 +1,239 @@
1
1
  /**
2
- * Args holds the result of parsing fnclaude's own argv.
2
+ * Stage-typed argv pipeline.
3
3
  *
4
- * Mirrors the Go `Args` struct in src/main.go from the upstream Go
5
- * implementation. Property docstrings keep that mapping explicit; the parser
6
- * in argParser.ts is the only producer of values here.
4
+ * fnclaude's argv flows through five stages parse, resolve, intercept,
5
+ * auto-name, sanitize before being handed to buildArgv. Earlier the
6
+ * intermediate state was a single mutable `Args` bag passed through every
7
+ * step, each free to rewrite any field. The pipeline's *ordering* was a
8
+ * runtime invariant only: nothing stopped `buildArgv` from being called
9
+ * before `applyWorktreeIntercept`, and `worktreeMatched` started life as a
10
+ * placeholder boolean on the parsed shape, even though it has no meaningful
11
+ * value until intercept has run.
12
+ *
13
+ * The new shape uses distinct types per stage. Each function takes its
14
+ * predecessor's output type as input and returns the next stage's type.
15
+ * The shapes are structurally readonly and brand-discriminated, so:
16
+ *
17
+ * - `parseArgs` returns `ParsedArgs`, which has NO `worktreeMatched`
18
+ * field at all — the invariant "the intercept hasn't run yet" is
19
+ * encoded in the type, not a sentinel value.
20
+ * - `applyWorktreeIntercept` returns `InterceptedArgs`, which has
21
+ * `worktreeMatched` materialized. Passing a `ParsedArgs` to `buildArgv`
22
+ * is a compile error because `InterceptedArgs` is what `buildArgv`
23
+ * accepts.
24
+ * - Stages are immutable; each function returns a new value. The
25
+ * pipeline composes by value, not by aliased reference.
26
+ *
27
+ * The brand fields (`__stage`) only exist in the type system — they're
28
+ * never assigned at runtime. The brand functions (`brandParsed` etc.) are
29
+ * named casts that document the boundary at which the stage transition
30
+ * happens.
31
+ */
32
+
33
+ // ── Base shape ─────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Fields every stage carries. All readonly: stage transitions produce new
37
+ * objects, never mutate.
7
38
  */
8
- export interface Args {
39
+ export interface BaseArgs {
9
40
  /**
10
41
  * CWD is the directory claude will be launched in (first positional, or
11
- * the noop fallback when no positionals are given).
42
+ * the noop fallback when no positionals are given). The interpretation
43
+ * narrows as stages progress — `ParsedArgs.cwd` is whatever the user
44
+ * typed; later stages have it resolved to an absolute path and/or
45
+ * swapped to a matched-worktree path.
12
46
  */
13
- cwd: string;
47
+ readonly cwd: string;
14
48
 
15
49
  /**
16
50
  * ExtraDirs collects all -A / --also values in order. Positional 2 is the
17
51
  * worktree slot; -A is the only way to supply extra dirs.
18
52
  */
19
- extraDirs: string[];
53
+ readonly extraDirs: readonly string[];
20
54
 
21
55
  /**
22
56
  * Passthrough is everything else, preserved in order, to be forwarded to
23
57
  * claude verbatim. Short flags are already translated to their long
24
- * forms.
58
+ * forms. Later stages may *extend* this slice (e.g. the intercept pushes
59
+ * `--worktree <name>`, auto-name prepends `--name <name>`).
25
60
  */
26
- passthrough: string[];
61
+ readonly passthrough: readonly string[];
27
62
 
28
63
  /**
29
64
  * NoTmux is true when the user passed --no-tmux (eaten by fnclaude; not
30
65
  * forwarded to claude).
31
66
  */
32
- noTmux: boolean;
67
+ readonly noTmux: boolean;
33
68
 
34
69
  /**
35
70
  * WorktreeSet is true when the user passed -w / --worktree, OR supplied
36
- * a 2nd positional after magic + subcommand consumption.
71
+ * a 2nd positional after magic + subcommand consumption, OR the Resolve
72
+ * step picked up a `+workspace` suffix from a repo reference.
37
73
  */
38
- worktreeSet: boolean;
74
+ readonly worktreeSet: boolean;
39
75
 
40
76
  /**
41
77
  * WorktreeArg is the name/value given with -w / --worktree (or the 2nd
42
- * positional), or "" if the flag was bare.
78
+ * positional / +workspace suffix), or "" if the flag was bare.
43
79
  */
44
- worktreeArg: string;
80
+ readonly worktreeArg: string;
45
81
 
46
82
  /**
47
83
  * UsedNoopFallback is true when CWD was filled by the noop fallback (no
48
84
  * positional path given). Caller uses this to gate seed-noop behavior —
49
85
  * explicit paths don't get auto-seeded.
50
86
  */
51
- usedNoopFallback: boolean;
87
+ readonly usedNoopFallback: boolean;
88
+ }
89
+
90
+ // ── Brand machinery ────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * `Branded<T, K>` tags `T` with a phantom string literal `K` so two
94
+ * otherwise-structurally-identical types become assignment-incompatible.
95
+ * The `__stage` property exists only in the type — runtime values never
96
+ * carry it.
97
+ */
98
+ type Branded<T, K extends string> = T & { readonly __stage: K };
99
+
100
+ // ── Stage 1: parsed ────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Output of `parseArgs`. The argv has been split into structural fields,
104
+ * but no I/O has run yet — `cwd` is still the user-typed string, the
105
+ * worktree intercept hasn't queried git, and no autoname has been generated.
106
+ *
107
+ * Has NO `worktreeMatched` field. That value only becomes meaningful after
108
+ * the intercept stage, and encoding its absence in the type means a stale
109
+ * `worktreeMatched: false` can't accidentally be read by a downstream step.
110
+ */
111
+ export type ParsedArgs = Branded<BaseArgs, 'parsed'>;
112
+
113
+ // ── Stage 2: resolved ──────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Output of the Resolve / tilde-expand step. `cwd` is an absolute path
117
+ * (when a path or repo ref was resolved) or the noop fallback. The Resolve
118
+ * step may also have promoted a `+workspace` suffix into `worktreeSet` +
119
+ * `worktreeArg`.
120
+ *
121
+ * Still no `worktreeMatched` — that's the next stage.
122
+ */
123
+ export type ResolvedArgs = Branded<BaseArgs, 'resolved'>;
124
+
125
+ // ── Stage 3: intercepted ───────────────────────────────────────────────────
52
126
 
127
+ /**
128
+ * Output of `applyWorktreeIntercept`. `worktreeMatched` is now meaningful:
129
+ * true iff an existing worktree of the project repo matched
130
+ * `worktreeArg` (and `cwd` was swapped to that worktree's path). Downstream
131
+ * consumers (`buildArgv`'s auto-tmux gate, primarily) treat matched=true
132
+ * as "no new worktree being created this run" and avoid injecting flags
133
+ * that only make sense when claude is about to spin up a fresh worktree.
134
+ *
135
+ * `passthrough` may have been extended with `--worktree`, `--worktree <name>`,
136
+ * or `--name <name>` depending on whether the intercept matched.
137
+ */
138
+ export interface InterceptedFields {
53
139
  /**
54
- * WorktreeMatched is set by applyWorktreeIntercept when -w / --worktree
55
- * was resolved against an existing worktree of the project repo (cwd was
56
- * swapped to that worktree). Downstream consumers (buildArgv's auto-tmux
57
- * gate, primarily) treat matched=true as "no new worktree being created
58
- * this run" and avoid injecting flags that only make sense when claude
59
- * is about to spin up a fresh worktree.
140
+ * True iff -w / --worktree was resolved against an existing worktree of
141
+ * the project repo (and cwd was swapped to that worktree).
60
142
  */
61
- worktreeMatched: boolean;
143
+ readonly worktreeMatched: boolean;
62
144
  }
145
+
146
+ export type InterceptedArgs = Branded<BaseArgs & InterceptedFields, 'intercepted'>;
147
+
148
+ // ── Brand constructors ─────────────────────────────────────────────────────
149
+ //
150
+ // Each brand function is a named cast — it doesn't validate anything,
151
+ // it just documents *where* the stage transition happens in the pipeline.
152
+ // The asserted shape carries all the invariants the stage promises.
153
+
154
+ /**
155
+ * Stamp a `BaseArgs`-shaped value as `ParsedArgs`. Only `parseArgs` should
156
+ * call this.
157
+ */
158
+ export function brandParsed(a: BaseArgs): ParsedArgs {
159
+ return a as ParsedArgs;
160
+ }
161
+
162
+ /**
163
+ * Stamp a `BaseArgs`-shaped value as `ResolvedArgs`. Called after the
164
+ * Resolve / tilde-expand step (or to short-circuit when the input is
165
+ * already absolute and doesn't need resolution).
166
+ */
167
+ export function brandResolved(a: BaseArgs): ResolvedArgs {
168
+ return a as ResolvedArgs;
169
+ }
170
+
171
+ /**
172
+ * Stamp a `BaseArgs & InterceptedFields`-shaped value as `InterceptedArgs`.
173
+ * Only `applyWorktreeIntercept` should call this.
174
+ */
175
+ export function brandIntercepted(a: BaseArgs & InterceptedFields): InterceptedArgs {
176
+ return a as InterceptedArgs;
177
+ }
178
+
179
+ // ── Stage transitions (replace one or more fields, restamp the brand) ──────
180
+
181
+ /**
182
+ * Return a new `ResolvedArgs` with the given field overrides. Used by the
183
+ * Resolve step to swap `cwd` (and possibly `worktreeSet` / `worktreeArg`)
184
+ * without mutating the parsed value.
185
+ */
186
+ export function withResolved(
187
+ a: ParsedArgs | ResolvedArgs,
188
+ overrides: Partial<BaseArgs>,
189
+ ): ResolvedArgs {
190
+ return brandResolved({ ...(a as BaseArgs), ...overrides });
191
+ }
192
+
193
+ /**
194
+ * Return a new `InterceptedArgs` from a `ResolvedArgs` plus the
195
+ * intercept's outputs. `InterceptedFields` (currently just `worktreeMatched`)
196
+ * is mandatory in the overrides so the brand can't be applied without it.
197
+ */
198
+ export function withIntercepted(
199
+ a: ResolvedArgs,
200
+ overrides: Partial<BaseArgs> & InterceptedFields,
201
+ ): InterceptedArgs {
202
+ return brandIntercepted({ ...(a as BaseArgs), ...overrides });
203
+ }
204
+
205
+ /**
206
+ * Return a new `InterceptedArgs` with `passthrough` (or other fields)
207
+ * replaced. Used by the auto-name and sanitize steps — both operate on the
208
+ * passthrough slice and produce a new slice; the rest of the args carries
209
+ * through unchanged.
210
+ *
211
+ * Returns `InterceptedArgs` (not a separate "named" / "sanitized" type)
212
+ * because no new invariants are established at those steps — only the
213
+ * passthrough slice changes shape, and that's already an in-stage edit
214
+ * the intercept itself does.
215
+ */
216
+ export function withPassthroughUpdate(
217
+ a: InterceptedArgs,
218
+ overrides: Partial<BaseArgs>,
219
+ ): InterceptedArgs {
220
+ // Carry worktreeMatched through; only overrides win.
221
+ return brandIntercepted({
222
+ ...(a as BaseArgs & InterceptedFields),
223
+ ...overrides,
224
+ });
225
+ }
226
+
227
+ // ── Back-compat re-export ──────────────────────────────────────────────────
228
+
229
+ /**
230
+ * `Args` was the single mutable bag the pipeline threaded through before
231
+ * the stage-typed refactor. Retained as an alias for `InterceptedArgs` so
232
+ * external consumers (the published `index.ts` surface, test helpers in
233
+ * downstream tooling) keep working while the refactor lands. New code
234
+ * should use the stage-specific types directly.
235
+ *
236
+ * @deprecated Use `ParsedArgs` / `ResolvedArgs` / `InterceptedArgs` per
237
+ * the position in the pipeline.
238
+ */
239
+ export type Args = InterceptedArgs;