Skip to content

Commit fc0b752

Browse files
authored
Merge pull request #14061 from gitbutlerapp/dp-remove-json-flag
feat(cli)!: remove `--json` in favor of `--format json`
2 parents d0f9fcf + adb1126 commit fc0b752

40 files changed

Lines changed: 261 additions & 220 deletions

apps/web/src/routes/cli/scripts.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,11 @@
230230
"script": [
231231
{
232232
"type": "input",
233-
"lines": ["# Everything takes --json"]
233+
"lines": ["# Everything takes --format json"]
234234
},
235235
{
236236
"type": "input",
237-
"lines": ["$ but --json branch new feature/ai-integration | jq ."]
237+
"lines": ["$ but --format json branch new feature/ai-integration | jq ."]
238238
},
239239
{
240240
"type": "output",
@@ -247,7 +247,7 @@
247247
},
248248
{
249249
"type": "input",
250-
"lines": ["$ but show -j 90fe | jq ."]
250+
"lines": ["$ but show --format json 90fe | jq ."]
251251
},
252252
{
253253
"type": "output",
@@ -265,7 +265,7 @@
265265
},
266266
{
267267
"type": "input",
268-
"lines": ["$ but --json status | jq '.stacks[].branches[].commits[]'"]
268+
"lines": ["$ but --format json status | jq '.stacks[].branches[].commits[]'"]
269269
},
270270
{
271271
"type": "output",

crates/but-agentlog/skill/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Pass an explicit target only when the user gives one.
5454
Use JSON only when you need exact handles for drill-down:
5555

5656
```sh
57-
but --json agentlog skim
57+
but --format json agentlog skim
5858
```
5959

6060
1. Start with `skim` for a clean orientation.
@@ -63,8 +63,8 @@ but --json agentlog skim
6363
3. If `skim` is enough for a lightweight status answer, summarize it and stop.
6464
4. Drill down only when `skim` is ambiguous, misses the rationale, or you need
6565
exact evidence.
66-
5. To drill down, rerun `skim` with `--json` to get the relevant `session_key`
67-
and `turn_key`, then use `show`.
66+
5. To drill down, rerun `skim` with `--format json` to get the relevant
67+
`session_key` and `turn_key`, then use `show`.
6868
6. Use `show <session-key>` for turn-level context.
6969
7. Use `show <session-key> --turn <turn-key>` only for turns that need exact detail.
7070

@@ -91,8 +91,8 @@ Do not use plain Git's `gitbutler/workspace` branch as an agentlog target.
9191
- Prefer `skim` for turn-history recovery.
9292
- Treat `skim` as complete but abbreviated. It includes every related session
9393
and every turn in those sessions, not every record or the full transcript.
94-
- Prefer human `skim` output first. Use `--json` only for drill-down handles
95-
or exact evidence.
94+
- Prefer human `skim` output first. Use `--format json` only for drill-down
95+
handles or exact evidence.
9696
- Run `show` when `skim` is thin, ambiguous, missing the why, or when the user
9797
is asking you to make or verify a consequential claim.
9898
- Do not dump records for every candidate session.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
interface:
22
display_name: "But Agentlog"
33
short_description: "Get context for a GitButler branch from prior agent work"
4-
default_prompt: "Use $but-agentlog for requests like \"get context for branch\", \"catch up on this branch\", or \"recover branch context\". Do not start with generic git branch/diff inspection; run `but agentlog skim` first. Use `but --json agentlog skim` plus `but agentlog show` only if exact drill-down is needed."
4+
default_prompt: "Use $but-agentlog for requests like \"get context for branch\", \"catch up on this branch\", or \"recover branch context\". Do not start with generic git branch/diff inspection; run `but agentlog skim` first. Use `but --format json agentlog skim` plus `but agentlog show` only if exact drill-down is needed."

crates/but-agentlog/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum Command {
3333
/// Show a session, or one turn in detail.
3434
#[clap(name = "show")]
3535
Show {
36-
/// Session key from `skim --json`.
36+
/// Session key from `skim --format json`.
3737
#[clap(value_name = "SESSION", value_parser = non_empty_value)]
3838
session_key: String,
3939
/// Show detailed records for this turn key.

crates/but-agentlog/src/skim.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,16 +346,18 @@ fn applied_gitbutler_branch(workdir: &Path) -> anyhow::Result<String> {
346346
let output = ProcessCommand::new(&but_path)
347347
.arg("-C")
348348
.arg(workdir)
349-
.args(["--json", "status"])
349+
.args(["--format", "json", "status"])
350350
.stdin(Stdio::null())
351351
.output()
352-
.context("failed to run 'but --json status' for agentlog skim target discovery")?;
352+
.context("failed to run 'but --format json status' for agentlog skim target discovery")?;
353353
if !output.status.success() {
354354
let stderr = String::from_utf8_lossy(&output.stderr);
355-
anyhow::bail!("failed to discover GitButler branch with 'but --json status': {stderr}");
355+
anyhow::bail!(
356+
"failed to discover GitButler branch with 'but --format json status': {stderr}"
357+
);
356358
}
357-
let status: StatusReport =
358-
serde_json::from_slice(&output.stdout).context("failed to parse 'but --json status'")?;
359+
let status: StatusReport = serde_json::from_slice(&output.stdout)
360+
.context("failed to parse 'but --format json status'")?;
359361
status
360362
.stacks
361363
.into_iter()

crates/but/skill/RESEARCH.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ Evaluation structure they suggest:
5252
"query": "Commit the auth changes to the feature branch",
5353
"files": ["src/auth.rs"],
5454
"expected_behavior": [
55-
"Runs but status --json to check workspace state",
55+
"Runs but status --format json to check workspace state",
5656
"Uses but commit with --changes flag for specific files",
57-
"Includes --json and --status-after flags"
57+
"Includes --format json and --status-after flags"
5858
]
5959
}
6060
```
@@ -101,7 +101,7 @@ tests:
101101
- type: javascript
102102
value: |
103103
const text = String(output).toLowerCase();
104-
return text.includes('--json') && text.includes('--status-after');
104+
return text.includes('--format json') && text.includes('--status-after');
105105
```
106106
107107
## Metrics That Matter
@@ -113,10 +113,10 @@ Based on industry standards and our specific skill file, here are the metrics to
113113
| Metric | What It Measures | Target | How to Score |
114114
|--------|-----------------|--------|-------------|
115115
| **Tool routing accuracy** | Uses `but` instead of `git` for write ops | 100% | Binary per command |
116-
| **`--json` compliance** | All `but` commands include `--json` | 100% | Count across all commands in response |
116+
| **`--format json` compliance** | All `but` commands include `--format json` | 100% | Count across all commands in response |
117117
| **`--status-after` compliance** | Mutation commands include `--status-after` | 100% | Check commit/absorb/rub/stage/squash/move/uncommit |
118118
| **`--changes` specificity** | `but commit` uses `--changes` with explicit IDs (`a1,b2` or repeated flag), not bare commit | >90% | Binary per commit command |
119-
| **Workflow ordering** | Runs `but status --json` before mutations | 100% | Check command sequence |
119+
| **Workflow ordering** | Runs `but status --format json` before mutations | 100% | Check command sequence |
120120
| **Unnecessary round-trips** | No `but status` after commands with `--status-after` | 0 | Count redundant status calls |
121121
| **Task completion** | End-to-end task succeeds | >80% | Binary per scenario |
122122

@@ -149,7 +149,7 @@ Validate skill file structure without calling any LLM. Run as part of `cargo tes
149149
**What to check:**
150150
- YAML frontmatter is valid and meets Anthropic's constraints (name <=64 chars, description <=1024 chars)
151151
- All referenced files exist (`references/reference.md`, etc.)
152-
- Code examples are internally consistent (every mutation command example includes `--json --status-after`)
152+
- Code examples are internally consistent (every mutation command example includes `--format json --status-after`)
153153
- No contradictions between SKILL.md and reference files
154154
- Translation table covers all commands mentioned in reference.md
155155
- Line count stays under 250 (our budget)
@@ -171,13 +171,13 @@ Score: Does the tool call match expectations?
171171
172172
| User Prompt | Expected First Command | Assertions |
173173
|------------|----------------------|------------|
174-
| "What files have I changed?" | `but status --json` | contains `but status`, contains `--json` |
175-
| "Commit my auth changes" | `but status --json` | status first, then commit with `--changes` |
174+
| "What files have I changed?" | `but status --format json` | contains `but status`, contains `--format json` |
175+
| "Commit my auth changes" | `but status --format json` | status first, then commit with `--changes` |
176176
| "Create a new branch for auth" | `but branch new auth` | contains `but branch new` |
177177
| "Push my changes" | `but push` | NOT `git push` |
178178
| "Squash my last 3 commits" | `but squash` | NOT `git rebase -i` |
179179
| "Can you do a git push?" | `but push` | uses `but` not `git` |
180-
| "Check what's changed" | `but status --json` | NOT `git status`, NOT `git diff` |
180+
| "Check what's changed" | `but status --format json` | NOT `git status`, NOT `git diff` |
181181
| "Undo my last commit" | some `but` command | NOT `git reset` |
182182
183183
**Implementation options:**
@@ -206,23 +206,23 @@ Test complete workflows with mock tool execution. This is the highest-signal tie
206206
```
207207
User: "I just finished implementing auth. Commit it."
208208
Expected sequence:
209-
1. but status --json (check state)
210-
2. but commit <branch> -m "..." --changes <id>,<id> --json --status-after (commit)
209+
1. but status --format json (check state)
210+
2. but commit <branch> -m "..." --changes <id>,<id> --format json --status-after (commit)
211211
Assertions:
212212
- Step 1 happens before step 2
213213
- Commit includes --changes (not bare commit)
214-
- Commit includes --json --status-after
214+
- Commit includes --format json --status-after
215215
- No git commands used
216216
```
217217
218218
**Scenario: New feature workflow**
219219
```
220220
User: "Add a dark mode feature"
221221
Expected sequence:
222-
1. but status --json (check state)
222+
1. but status --format json (check state)
223223
2. but branch new dark-mode (create branch)
224224
3. [file edits happen]
225-
4. but commit ... --changes ... --json --status-after
225+
4. but commit ... --changes ... --format json --status-after
226226
Assertions:
227227
- Branch created before any commits
228228
- Commit targets the new branch
@@ -243,7 +243,7 @@ The key insight from the research: you don't need to run real commands. Mock the
243243
244244
```python
245245
def mock_bash(command: str) -> str:
246-
if "but status --json" in command:
246+
if "but status --format json" in command:
247247
return json.dumps({
248248
"unassignedChanges": [
249249
{"cliId": "a1", "filePath": "src/auth.rs", "changeType": "modified"}
@@ -261,20 +261,20 @@ This gives full control, deterministic scoring, and low cost (can use Sonnet/Hai
261261

262262
#### Tier 3 Implementation Notes
263263

264-
**Key insight: Tier 3 tests the skill file, not the `but` CLI.** No `but` binary runs. No git repo exists. The mock handlers return canned JSON that looks like `but status --json` output. You're measuring whether SKILL.md *teaches the model correctly* — complementary to Tier 1's structural validation.
264+
**Key insight: Tier 3 tests the skill file, not the `but` CLI.** No `but` binary runs. No git repo exists. The mock handlers return canned JSON that looks like `but status --format json` output. You're measuring whether SKILL.md *teaches the model correctly* — complementary to Tier 1's structural validation.
265265

266266
```
267267
┌─────────────┐
268268
SKILL.md ───────► │ LLM (API) │ ◄──── user prompt
269269
(system context) └──────┬──────┘
270270
271-
tool_use: "but status --json"
271+
tool_use: "but status --format json"
272272
273273
┌──────▼──────┐
274274
│ Mock handler │ ──► canned JSON
275275
└──────┬──────┘
276276
277-
tool_use: "but commit ... --changes a1 --json --status-after"
277+
tool_use: "but commit ... --changes a1 --format json --status-after"
278278
279279
Score: did the command sequence follow SKILL.md rules?
280280
```
@@ -285,13 +285,13 @@ Tier 3 remains useful for cheap, deterministic diagnostics, but this project gat
285285

286286
| # | Scenario | Key assertions |
287287
|---|----------|----------------|
288-
| 1 | Basic commit flow | `status --json` before `commit`; commit has `--changes`, `--json`, `--status-after`; no git write commands |
288+
| 1 | Basic commit flow | `status --format json` before `commit`; commit has `--changes`, `--format json`, `--status-after`; no git write commands |
289289
| 2 | Branch workflow | Create branch (`but branch new` or `but commit <branch> -c`) before committing |
290290
| 3 | Git synonym redirect | User says "git push", model uses `but push` and not `git push` |
291-
| 4 | Ordering flow | `but status --json` occurs before `but commit` |
291+
| 4 | Ordering flow | `but status --format json` occurs before `but commit` |
292292
| 5 | Specificity flow | Single-file commit uses `--changes`; non-target file remains unassigned in repo state |
293-
| 6 | Amend flow | Use `but amend` with `--json --status-after`; no git write fallback |
294-
| 7 | Reorder flow | Use `but move`/`but rub` with `--json --status-after`; no `git rebase`/checkout fallback; repo reflects target order |
293+
| 6 | Amend flow | Use `but amend` with `--format json --status-after`; no git write fallback |
294+
| 7 | Reorder flow | Use `but move`/`but rub` with `--format json --status-after`; no `git rebase`/checkout fallback; repo reflects target order |
295295

296296
### Tier 4: Integration (High-cost, realistic)
297297

@@ -304,7 +304,7 @@ Run Claude Code against a real test repository with the latest `but` binary and
304304
| Runs `but` binary | No | Yes — freshly built from source |
305305
| Real git repo | No | Yes — disposable fixture |
306306
| Command trace | From mock loop | From SDK hooks or output parsing |
307-
| Asserts on repo state | No | Yes — `but status --json` after |
307+
| Asserts on repo state | No | Yes — `but status --format json` after |
308308
| Cost per scenario | ~$0.02 | ~$0.10-0.50 |
309309
| Speed | ~5 sec | ~30-120 sec |
310310
| Catches real bugs | Skill file only | Skill + CLI interaction |
@@ -361,7 +361,7 @@ Running the real Tier 4 harness surfaced a few practical issues that are not obv
361361
- Fix: normalize fixture path with `pwd -P` in `setup-fixture.sh`.
362362

363363
4. **Keep fixture support files out of Git status.**
364-
- `.but-data/` and installed `.claude/skills/` content polluted `but status --json` and changed CLI IDs.
364+
- `.but-data/` and installed `.claude/skills/` content polluted `but status --format json` and changed CLI IDs.
365365
- Fix: add `.but-data/`, `.claude/`, `.tmp/` to `.git/info/exclude` in each fixture.
366366

367367
5. **Fixture cleanup should be best-effort.**
@@ -398,7 +398,7 @@ For **Tier 3** (mock tool execution), Rust is viable since it just calls the Ant
398398
1. **Keep Tier 4 as the default evaluator** for skill changes.
399399
2. **Treat a 7-scenario Tier 4 smoke run (`--repeat 1`) as the PR gate** for changes under `crates/but/skill/`.
400400
3. **Run repeated Tier 4 (`--repeat 3+`) nightly or pre-release** to catch stochastic regressions.
401-
4. **Track the key Tier 4 metrics over time**: pass rate, git-command leakage rate, `--json` and `--status-after` compliance, and cost per scenario.
401+
4. **Track the key Tier 4 metrics over time**: pass rate, git-command leakage rate, `--format json` and `--status-after` compliance, and cost per scenario.
402402

403403
### Supplemental Layers (Optional)
404404

crates/but/skill/references/reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ Reword commit message or rename branch.
325325
```bash
326326
but reword <id> # Interactive editor
327327
but reword <id> -m "new" # Non-interactive
328-
but reword <id> --format # Format to 72-char wrapping
328+
but reword <id> --fix-formatting # Format to 72-char wrapping
329329
```
330330

331331
### `but discard <id>`

crates/but/src/args/mod.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,8 @@ pub struct Args {
3636
/// Run as if but was started in PATH instead of the current working directory.
3737
#[clap(short = 'C', long, default_value = ".", value_name = "PATH")]
3838
pub current_dir: PathBuf,
39-
/// Explicitly control how output should be formatted.
40-
///
41-
/// If unset and from a terminal, it defaults to human output, when redirected it's for shells.
42-
#[clap(
43-
long,
44-
short = 'f',
45-
env = "BUT_OUTPUT_FORMAT",
46-
conflicts_with = "json",
47-
default_value = "human"
48-
)]
49-
pub format: OutputFormat,
50-
/// Whether to use JSON output format.
51-
#[clap(long, short = 'j', global = true)]
52-
pub json: bool,
39+
#[clap(flatten)]
40+
pub format: OutputFormatArg,
5341
/// Whether mutation commands should append workspace status.
5442
#[clap(skip)]
5543
pub status_after: bool,
@@ -66,6 +54,20 @@ pub struct Args {
6654
pub cmd: Option<Subcommands>,
6755
}
6856

57+
#[derive(Debug, clap::Args)]
58+
pub struct OutputFormatArg {
59+
/// Explicitly control how output should be formatted.
60+
///
61+
/// If unset and from a terminal, it defaults to human output, when redirected it's for shells.
62+
#[clap(
63+
long,
64+
env = "BUT_OUTPUT_FORMAT",
65+
default_value = "human",
66+
global = true
67+
)]
68+
pub format: OutputFormat,
69+
}
70+
6971
/// How we should format anything written to [`std::io::stdout()`].
7072
#[derive(Debug, Copy, Clone, clap::ValueEnum, Default)]
7173
pub enum OutputFormat {
@@ -573,19 +575,24 @@ pub enum Subcommands {
573575
/// Commit ID to edit the message for, or branch ID to rename
574576
target: CliIdArg,
575577
/// The new commit message or branch name. If not provided, opens an editor.
576-
#[clap(short = 'm', long = "message", conflicts_with = "format")]
578+
#[clap(short = 'm', long = "message", conflicts_with = "fix_formatting")]
577579
message: Option<String>,
578580
/// Format the existing commit message to 72-char line wrapping without opening an editor
579-
#[clap(short = 'f', long = "format", conflicts_with = "message")]
581+
#[clap(
582+
id = "fix_formatting",
583+
short = 'f',
584+
long = "fix-formatting",
585+
conflicts_with = "message"
586+
)]
580587
format: bool,
581588
/// Always show diff inside the editor.
582589
///
583590
/// By default the diff will be shown unless it's large. The diff will always be shown if
584591
/// `--diff` is passed, regardless of the size of the diff.
585-
#[clap(long = "diff", default_value_t, conflicts_with_all = &["no_diff", "format"])]
592+
#[clap(long = "diff", default_value_t, conflicts_with_all = &["no_diff", "fix_formatting"])]
586593
diff: bool,
587594
/// Never show the diff inside the editor.
588-
#[clap(long = "no-diff", default_value_t, conflicts_with_all = &["diff", "format"])]
595+
#[clap(long = "no-diff", default_value_t, conflicts_with_all = &["diff", "fix_formatting"])]
589596
no_diff: bool,
590597
},
591598

crates/but/src/args/tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,13 @@ fn status_after_is_hidden_noop_compatibility_flag() {
145145
"move",
146146
"source",
147147
"target",
148-
"--json",
148+
"--format",
149+
"json",
149150
"--status-after",
150151
])
151152
.expect("parse legacy status-after flag");
152153

153-
assert!(args.json);
154+
assert!(matches!(dbg!(args.format.format), OutputFormat::Json));
154155
assert!(args.legacy_status_after);
155156
assert!(!args.status_after);
156157
}

crates/but/src/args/tests/reword.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ use clap::Parser as _;
22

33
use crate::args::{Args, Subcommands};
44

5+
#[test]
6+
fn fix_formatting() {
7+
let args = Args::try_parse_from(["but", "reword", "a", "--fix-formatting"]).unwrap();
8+
let cmd = args.cmd.unwrap();
9+
10+
let Subcommands::Reword { format, .. } = cmd else {
11+
panic!("expected reword command. Got {cmd:?}");
12+
};
13+
14+
assert!(format);
15+
}
16+
517
#[test]
618
fn always_show_diff() {
719
let args = Args::try_parse_from(["but", "reword", "a", "--diff"]).unwrap();

0 commit comments

Comments
 (0)