Files
max-sixty__worktrunk/docs/static/schema/list-v2.json
Worktrunk Bot 246c6bd919 fix(list): keep [list] columns out of the --format json plan (#3812)
Closes the `[list] columns` half of #3787, per the call in [this
comment](https://github.com/max-sixty/worktrunk/issues/3787#issuecomment-5273942067):
JSON always emits the same shape, and `list.columns` only affects the
actual columns.

Before, `--format json` planned `all_columns` (source `Default`)
*unioned* with the selection's forced-on columns, so the selection
reached JSON in one direction only — it couldn't narrow the emitted
fields, but a listed `ci` did force the forge fetch on without `--full`.
That made a presentation setting decide whether a machine-readable call
talks to GitHub, which is the thing the Neovim plugin in #3787 had to
pin `--config-set 'list.columns=[…]'` against. Now the JSON branch plans
`all_columns` alone; `--full` is the only switch for the gated data, and
it's the one a caller controls.

The table and the `wt switch` picker are untouched — a listed `ci` still
renders the CI column without `--full`, and the picker still unions the
selection in so its table matches `wt list`'s.

Only `ci` and `summary` are affected: every other column is ungated, so
`full_plan()` already covered them, and custom columns require no
background task.

**For the release note — this changes schema 1 too.** A caller with
`[list] columns = […, "ci"]` and no `--full` used to get the `ci` object
in schema-1 JSON and now won't; schema 1 has no `collected` envelope to
say why. The schema-1 `ci` row already documented `` `--full` only ``,
so the docs get *more* accurate, but the observable output changes for
anyone who was relying on the forcing path. Schema 2 reports the same
narrowing through `collected.ci`.

Docs updated in `after_long_help` (the `[list] columns` section plus the
schema-2 `pr`, `summary`, and `checks` rows — `summary` now names
`--full` alongside `[list] summary = true`, and `checks` names the
`--full` gate it shares with `pr`), with the generated mirrors,
`dev/config.example.toml`, and the `--help` snapshots regenerated. The
`CLAUDE.md` network inventory and the `collect` planning comment now
record the exemption too.

<details><summary>Test</summary>

`test_list_json_columns_selection_does_not_force_ci` in
`tests/integration_tests/list_config.rs` asserts schema 2's
`collected.ci` across three configs: unset (false), `columns =
["branch", "ci"]` without `--full` (false — the regression this fixes),
and the same with `--full` (true). `collected` records what the plan
requested rather than what a fetch returned, so the test needs no forge
and no `gh` on PATH. It sits next to
`test_list_json_ignores_columns_selection`, which owns the narrowing
direction, and `test_list_config_listed_column_overrides_full_gate`,
which owns the table's forcing behaviour and still passes unchanged.

Ran locally: full `cargo test --test integration` and `cargo test --lib
--bins`, plus `cargo clippy --all-targets` and `cargo fmt --check`. One
unrelated failure,
`test_copy_ignored_preserves_file_executable_permissions`, is a umask
artifact of this sandbox (expects `0644`, the runner's `umask 002`
produces `0664`); it touches no code in this diff.

The docs-row follow-up in df5c238 re-ran `cargo test --test integration
-- test_help test_docs_are_in_sync` (48 passed) and `cargo fmt --check`.

</details>

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-15 08:00:04 -07:00

831 lines
23 KiB
JSON

{
"$defs": {
"CiSource": {
"description": "Source of CI status (PR/MR vs branch workflow)\n\nSerialized to JSON as \"pr\" or \"branch\" for programmatic consumers.",
"oneOf": [
{
"const": "pr",
"description": "Pull request or merge request",
"type": "string"
},
{
"const": "branch",
"description": "Branch workflow/pipeline (no PR/MR)",
"type": "string"
}
]
},
"Collected": {
"description": "Fact families whose collection is gated (`--full`, `[list] summary`).\nUngated families (working tree, counts, diffs) are always requested.\nSerialized as-is into the schema-2 JSON envelope's `collected` field,\ndisambiguating \"absent because not requested\".",
"properties": {
"ci": {
"description": "Forge CI/PR data was fetched.",
"type": "boolean"
},
"summary": {
"description": "LLM branch summaries were generated.",
"type": "boolean"
}
},
"required": [
"ci",
"summary"
],
"type": "object"
},
"GitRepoInfo": {
"description": "Parsed, provider-neutral repository metadata.\n\nThis is the single shape behind the `repo` / `ci.repo` JSON objects of\n`wt list`; serde controls the field rename/skip rules so there is no\nparallel output-only struct.",
"properties": {
"host": {
"description": "Web host for the repository URL.",
"type": "string"
},
"name": {
"description": "Repository name.",
"type": "string"
},
"owner": {
"description": "Repository owner, organization, or namespace path.",
"type": "string"
},
"project": {
"description": "Azure DevOps project name.",
"type": [
"string",
"null"
]
},
"provider": {
"$ref": "#/$defs/GitRepoProvider",
"description": "Forge provider, or [`GitRepoProvider::Unknown`] for parseable URLs whose\nhost and config do not identify a supported provider."
},
"remote": {
"description": "Local git remote this metadata was derived from. Set only for the\ntop-level `repo` of `wt list`; absent for PR/MR-URL-derived metadata\nsuch as `ci.repo`.",
"type": [
"string",
"null"
]
},
"url": {
"description": "Repository web URL.",
"type": "string"
}
},
"required": [
"url",
"provider",
"host",
"owner",
"name"
],
"type": "object"
},
"GitRepoProvider": {
"description": "Supported forge providers for repository metadata.",
"enum": [
"github",
"gitlab",
"gitea",
"azure-devops",
"unknown"
],
"type": "string"
},
"JsonChanges": {
"description": "Working-tree change facts.",
"properties": {
"conflicted": {
"description": "Tracked files carry merge conflicts; null while unresolved.",
"type": [
"boolean",
"null"
]
},
"deleted": {
"description": "Has deleted files.",
"type": "boolean"
},
"diff": {
"anyOf": [
{
"$ref": "#/$defs/JsonDiff"
},
{
"type": "null"
}
],
"description": "Lines added/deleted vs HEAD; null while unresolved."
},
"modified": {
"description": "Has modified (unstaged) files.",
"type": "boolean"
},
"renamed": {
"description": "Has renamed files.",
"type": "boolean"
},
"staged": {
"description": "Has staged files.",
"type": "boolean"
},
"untracked": {
"description": "Has untracked files.",
"type": "boolean"
}
},
"required": [
"staged",
"modified",
"untracked",
"renamed",
"deleted",
"conflicted",
"diff"
],
"type": "object"
},
"JsonCheckStatus": {
"description": "The pipeline outcomes that reach JSON. [`CiStatus`] carries three more\nthat never appear here, because each is reported by the shape of `checks`\nrather than by a value inside it: `Error` makes both `pr` and `checks`\nunknown, `NoCI` makes `checks` absent, and `Conflicts` leaves `status`\nnull (the conflict itself surfaces as `pr.mergeable`). See\n[`pr_and_checks`].",
"enum": [
"passed",
"running",
"failed"
],
"type": "string"
},
"JsonChecks": {
"description": "CI pipeline facts.",
"properties": {
"source": {
"$ref": "#/$defs/CiSource",
"description": "`\"pr\"` or `\"branch\"` (branch workflow)."
},
"stale": {
"description": "Local HEAD is not what CI ran against.",
"type": "boolean"
},
"status": {
"anyOf": [
{
"$ref": "#/$defs/JsonCheckStatus"
},
{
"type": "null"
}
],
"description": "Pipeline outcome; null when a conflicts report masked it."
}
},
"required": [
"status",
"source",
"stale"
],
"type": "object"
},
"JsonDefaultBranch": {
"description": "Relation to the default branch — independent facts, not the table's\npriority-collapsed symbol (that lives in `display.state`).",
"properties": {
"ahead": {
"description": "Commits ahead; null while unresolved (and for orphans).",
"format": "uint",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"behind": {
"description": "Commits behind; null while unresolved (and for orphans).",
"format": "uint",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"diff": {
"anyOf": [
{
"$ref": "#/$defs/JsonDiff"
},
{
"type": "null"
}
],
"description": "Lines added/deleted vs the default branch; null while unresolved."
},
"integration": {
"$ref": "#/$defs/Nullable_JsonIntegration",
"description": "How committed content is integrated; absent when determined\nnot-integrated, null when undetermined (dirty trees skip the\nexpensive checks)."
},
"merge_conflicts": {
"description": "A merge into the default branch would conflict (local `merge-tree`\nsimulation); null while unresolved.",
"type": [
"boolean",
"null"
]
},
"orphan": {
"description": "No merge-base with the default branch; null while unresolved.",
"type": [
"boolean",
"null"
]
}
},
"required": [
"ahead",
"behind",
"diff",
"orphan",
"merge_conflicts"
],
"type": "object"
},
"JsonDevServer": {
"description": "Dev server facts.",
"properties": {
"listening": {
"description": "The URL's port is listening; null while unresolved.",
"type": [
"boolean",
"null"
]
},
"url": {
"description": "URL from the project's `list.url` template.",
"type": "string"
}
},
"required": [
"url",
"listening"
],
"type": "object"
},
"JsonDiff": {
"description": "Line diff statistics",
"properties": {
"added": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"deleted": {
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"added",
"deleted"
],
"type": "object"
},
"JsonDisplay": {
"description": "Presentation strings. Everything here is a rendering of facts that\nappear elsewhere in the item.",
"properties": {
"columns": {
"additionalProperties": {
"type": "string"
},
"description": "Rendered `[list.custom-columns]` cells keyed by header; empty cells\nomitted.",
"type": "object"
},
"state": {
"anyOf": [
{
"$ref": "#/$defs/JsonMainState"
},
{
"type": "null"
}
],
"description": "The table's collapsed default-branch state (one value per row,\nhighest priority wins); absent when none applies or unresolved."
},
"statusline": {
"description": "Pre-formatted one-line status with ANSI colors, for prompt tools.",
"type": [
"string",
"null"
]
},
"symbols": {
"description": "Raw status glyphs without ANSI (e.g. `\"+!⊂\"`).",
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"JsonHead": {
"description": "HEAD commit facts.",
"properties": {
"committed_at": {
"description": "Committer time, RFC 3339 UTC; null when not loaded.",
"type": [
"string",
"null"
]
},
"sha": {
"description": "Full commit SHA.",
"type": "string"
},
"short_sha": {
"description": "Abbreviated per `core.abbrev`, auto-extended for ambiguous prefixes.",
"type": "string"
},
"subject": {
"description": "Commit subject (first line); null when not loaded (e.g. prunable\nworktrees).",
"type": [
"string",
"null"
]
}
},
"required": [
"sha",
"short_sha",
"subject",
"committed_at"
],
"type": "object"
},
"JsonIntegration": {
"description": "Why committed content counts as integrated.",
"properties": {
"reason": {
"$ref": "#/$defs/JsonIntegrationReason",
"description": "Which check matched."
}
},
"required": [
"reason"
],
"type": "object"
},
"JsonIntegrationReason": {
"description": "`IntegrationReason` wire values. Schema 2 uses snake_case throughout; the\nenum's own serde rename (kebab-case) is shared with other surfaces, so the\nmapping lives here instead of on the enum.",
"enum": [
"same_commit",
"ancestor",
"no_added_changes",
"trees_match",
"merge_adds_nothing",
"patch_id_match"
],
"type": "string"
},
"JsonItemV2": {
"description": "One list row: a worktree, a local branch, or a remote-only branch.",
"properties": {
"branch": {
"description": "Branch name; null for a detached-HEAD worktree. Remote rows carry\nthe bare branch name, with the remote in `remote`.",
"type": [
"string",
"null"
]
},
"checks": {
"$ref": "#/$defs/Nullable_JsonChecks",
"description": "CI pipeline state; absent when no CI exists (or CI wasn't\ncollected), null when the forge fetch failed."
},
"default_branch": {
"$ref": "#/$defs/Nullable_JsonDefaultBranch",
"description": "Relation to the default branch; absent on the default branch itself."
},
"dev_server": {
"anyOf": [
{
"$ref": "#/$defs/JsonDevServer"
},
{
"type": "null"
}
],
"description": "Dev server from the project's `list.url` template; absent when not\nconfigured."
},
"display": {
"$ref": "#/$defs/JsonDisplay",
"description": "Presentation: rendered strings for humans and prompt tools."
},
"head": {
"anyOf": [
{
"$ref": "#/$defs/JsonHead"
},
{
"type": "null"
}
],
"description": "HEAD commit; null for unborn branches."
},
"pr": {
"$ref": "#/$defs/Nullable_JsonPr",
"description": "Open PR/MR; absent when none exists (or CI wasn't collected), null\nwhen the forge fetch failed."
},
"remote": {
"description": "Remote name for remote-only branch rows; absent on local rows.",
"type": [
"string",
"null"
]
},
"summary": {
"$ref": "#/$defs/Nullable_string",
"description": "LLM-generated branch summary; absent when summaries are off or none\nwas produced, null while pending."
},
"upstream": {
"$ref": "#/$defs/Nullable_JsonUpstream",
"description": "Tracking-branch relation; absent when no upstream is configured,\nnull while unresolved."
},
"vars": {
"additionalProperties": {
"type": "string"
},
"description": "Custom variables stored via `wt config state vars`.",
"type": "object"
},
"worktree": {
"anyOf": [
{
"$ref": "#/$defs/JsonWorktreeV2"
},
{
"type": "null"
}
],
"description": "Worktree facts; absent on branch-only rows."
}
},
"required": [
"branch",
"head",
"display"
],
"type": "object"
},
"JsonMainState": {
"description": "[`MainState`] wire values. `MainState::None` renders as no value at all,\nso it has no variant here — `display.state` is absent instead. The\n`Integrated` payload is dropped: the reason it carries is already reported\nas `default_branch.integration.reason`.",
"enum": [
"is_main",
"would_conflict",
"empty",
"same_commit",
"integrated",
"orphan",
"diverged",
"ahead",
"behind"
],
"type": "string"
},
"JsonOperation": {
"description": "[`InProgressOperation`] wire values. The domain enum's `strum` names are\nthe same strings, but going through an exhaustive match is what makes a\nsixth variant a compile error here instead of a value that appears in\npublished output with nothing to catch it.",
"enum": [
"merge",
"rebase",
"cherry_pick",
"revert",
"bisect"
],
"type": "string"
},
"JsonPr": {
"description": "Open PR/MR facts.",
"properties": {
"mergeable": {
"description": "Whether the PR merges cleanly into its target: false when the forge\nreports conflicts, null otherwise (the fetch records only the\nconflicted case).",
"type": [
"boolean",
"null"
]
},
"number": {
"description": "Forge number; null when the forge reported none.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"repo": {
"anyOf": [
{
"$ref": "#/$defs/GitRepoInfo"
},
{
"type": "null"
}
],
"description": "The repository the PR/MR targets (the upstream for fork PRs);\nabsent when the URL doesn't parse."
},
"review": {
"anyOf": [
{
"$ref": "#/$defs/ReviewState"
},
{
"type": "null"
}
],
"description": "Review state; absent when the forge reports no review signal."
},
"url": {
"description": "URL to the PR/MR page; null when the forge reported none.",
"type": [
"string",
"null"
]
}
},
"required": [
"number",
"url",
"mergeable"
],
"type": "object"
},
"JsonReason": {
"description": "Reason payload for `locked` / `prunable`.",
"properties": {
"reason": {
"description": "Reason git records; null when none was given.",
"type": [
"string",
"null"
]
}
},
"required": [
"reason"
],
"type": "object"
},
"JsonRepo": {
"description": "Repo-wide facts.",
"properties": {
"default_branch": {
"description": "The branch every `default_branch` object measures against; absent\nwhen detection failed.",
"type": [
"string",
"null"
]
},
"forge": {
"anyOf": [
{
"$ref": "#/$defs/GitRepoInfo"
},
{
"type": "null"
}
],
"description": "Forge metadata derived from the primary remote; absent when no\nremote URL parses."
}
},
"type": "object"
},
"JsonUpstream": {
"description": "Tracking-branch relation.",
"properties": {
"ahead": {
"description": "Commits ahead of the upstream.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"behind": {
"description": "Commits behind the upstream.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"branch": {
"description": "Branch name on the remote; null when only the remote is known.",
"type": [
"string",
"null"
]
},
"remote": {
"description": "Remote name.",
"type": "string"
}
},
"required": [
"remote",
"branch",
"ahead",
"behind"
],
"type": "object"
},
"JsonWorktreeV2": {
"description": "Worktree facts. Location attributes (`locked`, `prunable`,\n`branch_mismatch`, `duplicate_branch`) are independent fields — unlike\nschema 1's single `state`, they can co-occur.",
"properties": {
"branch_mismatch": {
"description": "The checked-out branch doesn't match the branch this worktree was\ncreated for.",
"type": "boolean"
},
"changes": {
"anyOf": [
{
"$ref": "#/$defs/JsonChanges"
},
{
"type": "null"
}
],
"description": "Working-tree state; null while unresolved."
},
"current": {
"description": "This is the worktree the command ran from.",
"type": "boolean"
},
"detached": {
"description": "HEAD is detached.",
"type": "boolean"
},
"duplicate_branch": {
"description": "Another worktree has the same branch checked out.",
"type": "boolean"
},
"locked": {
"anyOf": [
{
"$ref": "#/$defs/JsonReason"
},
{
"type": "null"
}
],
"description": "Present when the worktree is locked."
},
"main": {
"description": "This is the main worktree.",
"type": "boolean"
},
"operation": {
"$ref": "#/$defs/Nullable_JsonOperation",
"description": "In-progress operation; absent when none, null while unresolved."
},
"path": {
"description": "Filesystem path.",
"type": "string"
},
"previous": {
"description": "This was the previous worktree (`wt switch -`).",
"type": "boolean"
},
"prunable": {
"anyOf": [
{
"$ref": "#/$defs/JsonReason"
},
{
"type": "null"
}
],
"description": "Present when git considers the worktree prunable."
}
},
"required": [
"path",
"main",
"current",
"previous",
"detached",
"branch_mismatch",
"duplicate_branch",
"changes"
],
"type": "object"
},
"Nullable_JsonChecks": {
"anyOf": [
{
"$ref": "#/$defs/JsonChecks"
},
{
"type": "null"
}
]
},
"Nullable_JsonDefaultBranch": {
"anyOf": [
{
"$ref": "#/$defs/JsonDefaultBranch"
},
{
"type": "null"
}
]
},
"Nullable_JsonIntegration": {
"anyOf": [
{
"$ref": "#/$defs/JsonIntegration"
},
{
"type": "null"
}
]
},
"Nullable_JsonOperation": {
"anyOf": [
{
"$ref": "#/$defs/JsonOperation"
},
{
"type": "null"
}
]
},
"Nullable_JsonPr": {
"anyOf": [
{
"$ref": "#/$defs/JsonPr"
},
{
"type": "null"
}
]
},
"Nullable_JsonUpstream": {
"anyOf": [
{
"$ref": "#/$defs/JsonUpstream"
},
{
"type": "null"
}
]
},
"Nullable_string": {
"type": [
"string",
"null"
]
},
"ReviewState": {
"description": "Review state of a PR/MR.\n\nThe vocabulary matches Claude Code's statusline `pr.review_state` field so\nthe two surfaces never disagree on names. A PR with no review signal at all\n(e.g. GitHub's `reviewDecision` is empty on repos without required\nreviewers and no reviews) carries `None` on [`PrStatus`], not `Pending`,\nso unreviewed branches keep their plain CI colors.",
"oneOf": [
{
"enum": [
"approved",
"changes_requested",
"draft"
],
"type": "string"
},
{
"const": "pending",
"description": "Review is required before merge (e.g. branch protection) but not given yet",
"type": "string"
}
]
}
},
"$id": "https://worktrunk.dev/schema/list-v2.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Root envelope.",
"properties": {
"collected": {
"$ref": "#/$defs/Collected",
"description": "Which gated fact families this run requested."
},
"items": {
"description": "One entry per row, in table order.",
"items": {
"$ref": "#/$defs/JsonItemV2"
},
"type": "array"
},
"repo": {
"$ref": "#/$defs/JsonRepo",
"description": "Repo-wide facts, hoisted out of the items."
},
"schema": {
"description": "Output schema version. The unversioned bare-array format is 1.",
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"schema",
"repo",
"collected",
"items"
],
"title": "wt list --format=json (schema 2)",
"type": "object"
}