Skip to content
This repository was archived by the owner on Jul 24, 2026. It is now read-only.

Commit 71469fe

Browse files
a harness is a record of capabilities, not an enum member (#97)
* a harness is a record of capabilities, not an enum member Two constraints blocked retiring the ordinal launcher aliases. They turned out to be one shape problem: `harness` was an enum whose every consumer branched `if (codex) … else <the claude form>`, so an unlisted harness was silently claude. Codex account selection was mostly already there — decision 0004 put credentials in the spec's `env`, and `env` is spread verbatim into the derived session, so `CODEX_HOME` already reached a codex agent. What was missing is that convoy did not KNOW the config moved: `configDir`, the projection the CLI and pre-trust read, was hardcoded to CLAUDE_CONFIG_DIR. So `--config-dir` on codex set a variable codex does not read (selecting nothing, silently), and pre-trust seeded the ambient config while the agent read the relocated one — and codex's bypass flag does not skip the trust prompt, so that stalls rather than fails. opencode and pi could not be declared. 0005 offered `bin` + "the nearest CLI-compatible flavor" for that, which does not survive contact with the real binaries: opencode's positional is a PROJECT PATH, so the boot prompt would be consumed as a directory name — a wrapper can swallow a flag but cannot rescue that. Neither accepts --permission-mode or --dangerously-bypass-approvals-and- sandbox; --model is not portable either. So: one HarnessDescriptor table (src/harness.ts) carrying session key, config env var, MCP/doctor/auth support, and argv derivation. doctor and auth import the canonical type instead of shadowing it, and their records are Partial because they are partial in fact. Union widened to claude|codex|opencode|pi. `bin` also exposed on `convoy run`, which replaces aliases that were themselves wrappers. opencode and pi are partial citizens BY DECLARATION: they launch with verified flags, and get no account selection, no doctor checkup, no auth probe, and no MCP. harnessLimitations() derives that list from the table so it cannot drift. Flag surfaces read from `opencode 1.18.3 --help` and `pi 0.80.10 --help`. Each of the six defects was reproduced against a clean origin/main checkout first; tests marked LOCK pass on main and pin behavior that was reported missing but was already present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty * make a new harness recognisable to the verbs that manage it, not just launch it The first pass wired every TYPE-coupled harness site but left four plain string literals alone — and those are exactly the ones neither tsc nor the launch tests can catch: usedHarnesses() sniffed "[sessions.claude]"/"[sessions.codex]", so an opencode-only network matched neither and fell back to reporting claude. `convoy remove` matched sessions against a literal ["claude","codex","ding"]. agentShort() and the teardown normalizer stripped /-(claude|codex)$/i, so `alpha-opencode` kept its suffix and the derived session id disagreed with identity matching. Net effect: an opencode/pi agent would have been launchable but not reliably operable — misreported by `ls`, missable by `remove`/`down`. That is precisely the partial-citizen-presented-as-full failure this change is meant to avoid, so it is fixed rather than documented. All four now derive from the harness table (HARNESS_SESSION_KEYS, HARNESS_SUFFIX_RE, harnessesInPtyToml). Verified on a clean origin/main checkout that agentShort("alpha-opencode") returns "alpha-opencode" there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9b87206 commit 71469fe

12 files changed

Lines changed: 716 additions & 72 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ convoy is **declarative** (the Nomad model): **`add` declares** (writes an agent
113113
- `convoy pretrust <dir> [<dir>...] [--config-dir <path>]` — batch **pre-trust** agent working dirs in one atomic write, so a caller that spawns **multiple agents back-to-back** doesn't hit the workspace-trust race (see below). Call it once, with every dir, before the first `convoy add`. Config-dir-agnostic (writes the ambient `~/.claude.json`); pass `--config-dir` only for agents that will run under `CLAUDE_CONFIG_DIR`.
114114
- `convoy install-cli [--bin <dir>]` — symlink `convoy` + `st` + `pty` (from their sibling repos) onto your PATH (default `~/.local/bin`), **reliably + idempotently, without `npm link`** (the global-symlink footgun that once broke ding delivery network-wide). Run it the first time via `node <convoy-clone>/bin/convoy install-cli`; it verifies the links and prints the shell-specific PATH line if the dir isn't on PATH yet. Portable (macOS + Linux).
115115
- `convoy init [dir]` — create + wire a smalltalk network folder (ST_ROOT, bus layout, hooks).
116+
**Harness support tiers.** `claude` and `codex` are fully supported: convoy selects their account (via
117+
`CLAUDE_CONFIG_DIR` / `CODEX_HOME`), pre-trusts their workspace, runs a doctor checkup, and probes their
118+
auth. `opencode` and `pi` **launch** with correct, verified flags but are partial citizens — no account
119+
selection (neither harness has a config-relocation variable), no doctor checkup, no auth probe, no MCP
120+
transport. A green `convoy doctor` does not cover them. See
121+
[decision 0008](context/.decisions/0008-a-harness-is-a-record-not-an-enum-member.md).
122+
116123
- `convoy doctor [--quick] [--full]` — the **setup-readiness suite**: proves your machine can do real agent work. A fast **preflight gate** (`st`/`pty` on PATH, bus round-trips, PTY_ROOT length, hooks discoverable + `/compact`-safe, personas present, and a **real signed-in auth probe** per installed harness — a tiny live call, because a credential can be present on disk but revoked, so a file check would pass while actually signed out) — `--quick` stops here — then the **readiness checks**, each spun in an isolated throwaway network that never touches your prod network (it snapshots prod pty sessions before/after and asserts zero delta): a tmp network stands up + tears down; inbox+ding delivery works end-to-end; agent state externalizes + is reconstructed after a cold restart; inbox processing stays exactly-once across a restart; and a CoS→supervisor→worker tree fixes a real bundled bug, graded held-out (the fix behaves + is mutation-valid, delegation is visible on the bus, only the worker commits). These use **thin deterministic stand-ins** for the two upper tiers — fast per-mechanism health. **`--full`** is the complementary **real-org proof**: your **real** chief-of-staff + supervisor + worker, hosted under `convoy up`, run the whole lifecycle autonomously end-to-end — hands-off bring-up (first-run interview pre-seeded away, dirs pre-trusted, no prompts), an autonomous CoS→supervisor→worker delegation chain visible on the bus, a mutation-valid graded worker fix, restart-continuity (a cold no-`--resume` restart reconstructs from externalized `now.md` state and *straddles* the restart), and a clean teardown that leaves prod **sessions, durable crons, and trust config** untouched. Run the default for fast mechanism health; run `--full` (opt-in — a real multi-agent run takes minutes) when you want the real-org assurance that "when *you* `convoy init` + `convoy up`, it works." Every check is self-cleaning; failures are named + localize to a gate.
117124
- `convoy ls [--live-only]` — list the convoy's members.
118125
- `convoy personas <status|install>` — the base personas convoy installs for roles. `init`/`add`/`cos` auto-install them if missing (footgun-proof setup); this is explicit control.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# 0008 — A harness is a record of capabilities, not an enum member
2+
3+
Status: accepted
4+
5+
Supersedes the "widen the union" option rejected in
6+
[0005](0005-bin-replaces-the-harness-binary.md), and narrows that decision's scope
7+
rather than reversing it.
8+
9+
## Context
10+
11+
`Harness` was `"claude" | "codex"`. Two things were blocked on it.
12+
13+
**Codex account selection.** Reported as "a codex agent has no way to select an
14+
account". This turned out to be mostly wrong: decision 0004 put credential
15+
selection in the spec's `env` block, and `env` is spread verbatim into the
16+
derived harness session, so `CODEX_HOME` already reached a codex agent. The
17+
missing half was that convoy did not *know* the config had moved. `configDir`
18+
the projection of `env` that the imperative CLI and pre-trust both read — was
19+
hardcoded to `CLAUDE_CONFIG_DIR`. Three consequences, all silent:
20+
21+
- `--config-dir` on a codex session set `CLAUDE_CONFIG_DIR`, which codex does not
22+
read. The flag reported success and selected nothing; the agent ran as whatever
23+
account was ambient.
24+
- A codex spec's `CODEX_HOME` never became `configDir`, so pre-trust seeded
25+
`~/.codex/config.toml` while the agent read `$CODEX_HOME/config.toml`. codex's
26+
`--dangerously-bypass-approvals-and-sandbox` does *not* skip the directory-trust
27+
prompt, so the agent stalls on a dialog rather than failing.
28+
- `convoy pretrust` refused `--config-dir` for codex on the grounds that it
29+
"applies only to claude". That was true of the implementation, not of codex.
30+
31+
**opencode and pi could not be declared.** 0005 anticipated this and offered an
32+
escape hatch: set `bin` to the wrapper and `harness` to "the nearest
33+
CLI-compatible flavor", accepting that "a wrapper must accept the harness's
34+
flags."
35+
36+
## Options
37+
38+
**Keep the union closed; rely on `bin` (0005's answer).** Checked against the
39+
real binaries, "nearest compatible flavor" has no referent for these two:
40+
41+
- opencode's positional argument is a **project path**. `exec opencode '<boot
42+
prompt>'` does not pass a prompt opencode ignores — it asks opencode to start
43+
in a directory named after the entire boot ritual. The prompt must ride
44+
`--prompt`.
45+
- Neither opencode nor pi accepts `--permission-mode` or
46+
`--dangerously-bypass-approvals-and-sandbox`.
47+
- `--model` is not portable: opencode wants `provider/model`, pi wants a pattern
48+
plus `--provider`.
49+
50+
A wrapper script could swallow the flags, but it cannot rescue a prompt that has
51+
already been consumed as a path. The escape hatch works for a wrapped *claude* or
52+
*codex* — which is what it was built for, and what dev3's CoS uses today — and
53+
not for a harness whose CLI is a different shape.
54+
55+
**Add members to the union.** Adding a member was silent: exactly one site
56+
(`HARNESS_SESSION_KEY`) was typed tightly enough to fail the build. Every other
57+
branch was `if (harness === "codex") … else <the claude form>`, so a new harness
58+
would have been treated as claude everywhere — and `doctor`/`auth` kept their own
59+
*shadow* copies of the union, so they would not even have failed to compile.
60+
Verified on main: rendering an unlisted harness emits `[sessions.undefined]`.
61+
62+
**Make a harness a record.**
63+
64+
## Decision
65+
66+
A harness is a `HarnessDescriptor` in one table (`src/harness.ts`): its pty
67+
session key, its config-relocation env var (`null` = it has none), whether it
68+
supports MCP, whether convoy can run a doctor checkup or an auth probe for it,
69+
and a function deriving its argument tail. `HARNESSES` is widened to
70+
`claude | codex | opencode | pi`.
71+
72+
Every consumer reads the table. `doctor` and `auth` import the canonical type
73+
instead of shadowing it, and their per-harness records are `Partial` — because
74+
they *are* partial — so an unsupported harness is absent rather than defaulted
75+
into another harness's entry.
76+
77+
`configDir` becomes "the harness's own config dir": `CLAUDE_CONFIG_DIR` for
78+
claude, `CODEX_HOME` for codex, and refused for a harness whose `configEnv` is
79+
`null`. `env` remains the general seam of 0004; `configDir` is the projection of
80+
it convoy needs as a single value.
81+
82+
0005 is otherwise unchanged: `bin` still replaces only the binary name, and every
83+
flag is still derived. `bin` is additionally exposed on `convoy run`, which
84+
replaces the ad-hoc launcher aliases — those aliases *were* wrappers, so the path
85+
replacing them must be able to point at one.
86+
87+
## Consequences
88+
89+
- opencode and pi are **partial citizens, by declaration**. They launch with
90+
correct flags. They get no account selection (neither has a config-relocation
91+
variable, so convoy cannot select one and says so instead of accepting a
92+
`--config-dir` that would do nothing), no doctor checkup, no auth probe, and no
93+
MCP transport. `harnessLimitations()` derives this list from the table, so the
94+
documentation of a limitation cannot drift from the limitation.
95+
- A green `convoy doctor` does **not** cover opencode or pi. This is a real
96+
reduction in what a clean doctor run means, and is reported by name rather than
97+
inferred.
98+
- pi's `--approve` trusts project-local files; it is **not** a tool-permission
99+
bypass. A pi agent may still stall on a tool prompt where a claude or codex
100+
agent under bypass would not.
101+
- Convoy now encodes flag opinions for harnesses it cannot test in CI — the cost
102+
0005 named. It is bounded to one reviewable table, and the alternative was
103+
encoding a *wrong* opinion (the claude branch) for them silently.
104+
- `permissionMode` does not reach opencode or pi. Convoy's posture is currently
105+
the constant `bypassPermissions` for every agent, so nothing is lost today; a
106+
future per-agent posture must not silently map onto `--auto` / `--approve`.
107+
- Adding the next harness is a data change that fails to typecheck until every
108+
capability is decided.
109+
110+
## Evidence
111+
112+
- Flag surfaces read from `opencode 1.18.3 --help` and `pi 0.80.10 --help`, not
113+
from documentation.
114+
- `src/harness.test.ts`. Tests marked `LOCK` pass on main and pin behavior that
115+
was reported missing but was already present; every other test fails on main.
116+
The six current defects were each reproduced against a clean `origin/main`
117+
checkout before being fixed.

src/agent-file.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1010
import { dirname, join } from "node:path";
1111
import { stringify as tomlStringify } from "smol-toml";
1212
import { parseRole, type Role } from "./role.ts";
13-
import { isValidModel, type AgentSpec, type Harness, type Transport } from "./agent-spec.ts";
13+
import { isValidModel, type AgentSpec, type Transport } from "./agent-spec.ts";
14+
import { HARNESS_LIST, harnessDescriptor, isHarness, type Harness } from "./harness.ts";
1415
import { identityErrors, type IdentityContext } from "./identity.ts";
1516
import { asArray, decodeSpecText, formatOfPath, type SpecDoc, type SpecFormat } from "./spec-format.ts";
1617

@@ -26,7 +27,8 @@ export interface AgentFile {
2627
host?: string;
2728
/** The repo/worktree the agent runs in — where render materializes the overlay. `--dir` overrides it. */
2829
workspace?: string;
29-
/** claude | codex. Omit → claude. */
30+
/** claude | codex | opencode | pi. Omit → claude. See src/harness.ts for what convoy supports per
31+
* harness — opencode and pi launch, but get no account selection, doctor checkup, or auth probe. */
3032
harness?: Harness;
3133
/** Per-agent model id (`claude --model <id>` / `codex --model <id>`). Omit → the harness default (today's
3234
* behavior). Free-form (model ids churn) but charset-validated on parse — it lands in the launch command. */
@@ -141,7 +143,7 @@ export function parseAgentFile(text: string, format: SpecFormat = "toml", idCont
141143
if (!role) throw new Error(`invalid \`role\` "${roleRaw}" (want: chief-of-staff | supervisor | worker | technical-manager)`);
142144

143145
const harnessRaw = str("harness");
144-
if (harnessRaw !== undefined && harnessRaw !== "claude" && harnessRaw !== "codex") throw new Error(`invalid \`harness\` "${harnessRaw}" (want: claude | codex)`);
146+
if (harnessRaw !== undefined && !isHarness(harnessRaw)) throw new Error(`invalid \`harness\` "${harnessRaw}" (want: ${HARNESS_LIST})`);
145147
const transportRaw = str("transport");
146148
if (transportRaw !== undefined && transportRaw !== "ding" && transportRaw !== "mcp") throw new Error(`invalid \`transport\` "${transportRaw}" (want: ding | mcp)`);
147149
const strategyRaw = str("strategy");
@@ -294,7 +296,9 @@ export function readAgentFile(path: string, idContext?: IdentityContext): AgentF
294296
/** Compile an AgentFile (+ its network + an optional workspace override) DOWN to the AgentSpec that the
295297
* overlay writers consume. This is render's core mapping. Defaults: harness=claude, transport=ding; host →
296298
* the host-prefix (null → this machine's short hostname); strategy=permanent → permanentOverride. `tier`
297-
* is derived from role by the writers (v1), and `env` is not yet materialized — both carried forward-compat. */
299+
* is derived from role by the writers (v1) and carried forward-compat. `env` IS materialized: it is
300+
* carried onto the spec and spread into the derived harness session (see writePtyToml), which is what
301+
* makes `CODEX_HOME` — and any other harness's credential variable — work with no convoy change. */
298302
export function agentFileToSpec(af: AgentFile, opts: { networkRoot: string | null; workspace?: string | undefined }): AgentSpec {
299303
return {
300304
harness: af.harness ?? "claude",
@@ -306,10 +310,11 @@ export function agentFileToSpec(af: AgentFile, opts: { networkRoot: string | nul
306310
workingDir: opts.workspace ?? af.workspace ?? null,
307311
permanentOverride: af.strategy === "permanent" ? true : null,
308312
prefix: af.prefix ?? af.host ?? null,
309-
// CLAUDE_CONFIG_DIR is CREDENTIAL SELECTION, and credentials ride in `env` — there is no separate
310-
// `account` field to fall out of sync with it. Read it back out of `env` so the derived pty.toml and
311-
// the spec agree by construction.
312-
configDir: af.env?.["CLAUDE_CONFIG_DIR"] ?? null,
313+
// `env` is the GENERAL credential seam (decision 0004) and is carried verbatim below; `configDir` is
314+
// the narrower projection of it that the imperative CLI (`--config-dir`) and pre-trust both need as a
315+
// single value. Read it back out of `env`, per harness, so the declaration and the derived pty.toml
316+
// cannot disagree — and so a codex spec's CODEX_HOME reaches pre-trust the same way claude's does.
317+
configDir: (harnessDescriptor(af.harness ?? "claude").configEnv && af.env?.[harnessDescriptor(af.harness ?? "claude").configEnv!]) || null,
313318
model: af.model ?? null,
314319
bin: af.bin ?? null,
315320
env: af.env ?? null,

src/agent-spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { existsSync, statSync } from "node:fs";
55
import { hostname } from "node:os";
66
import { baseFile } from "./personas.ts";
77
import { permanentByRole, type PermissionMode, type Role } from "./role.ts";
8+
import { harnessDescriptor, HARNESS_SUFFIX_RE, type Harness } from "./harness.ts";
89

910
// Closed enums as const ARRAYS with the type derived from them, so the CLI's `--transport`/`--harness`
1011
// completions can read the real list instead of retyping it (a type alone is not iterable at runtime).
1112
export const TRANSPORTS = ["ding", "mcp"] as const;
1213
export type Transport = (typeof TRANSPORTS)[number];
13-
export const HARNESSES = ["claude", "codex"] as const;
14-
export type Harness = (typeof HARNESSES)[number];
14+
// `Harness` and its capability table live in src/harness.ts — one record per harness, so adding one is a
15+
// data change that fails to typecheck until every field is decided. Re-exported here because this module
16+
// is the established import site for the spec vocabulary.
17+
export { HARNESSES, harnessDescriptor, isHarness, HARNESS_LIST, harnessLimitations, type Harness } from "./harness.ts";
1518

1619
export interface AgentSpec {
1720
harness: Harness;
@@ -64,7 +67,7 @@ export function specPrefix(s: AgentSpec): string {
6467
}
6568
/** The agent's short name — the bus identity minus the harness suffix (`convoy-claude` → `convoy`). */
6669
export function agentShort(identity: string): string {
67-
return identity.replace(/-(claude|codex)$/i, "");
70+
return identity.replace(HARNESS_SUFFIX_RE, "");
6871
}
6972
/** The pinned pty session id for the claude session: `<prefix>.<agentShort>` (e.g. `silber.convoy`).
7073
* The ding session appends `.ding`. Stable across respawns so ding + name refs never drift. */
@@ -140,11 +143,11 @@ export function preflight(s: AgentSpec, existing: string[]): Preflight {
140143
if (s.workingDir !== null && !isDir(s.workingDir)) {
141144
errors.push(`working directory does not exist: ${s.workingDir}`);
142145
}
143-
if (s.harness === "codex" && s.transport === "mcp") {
144-
warnings.push("codex has no MCP transport — it always runs ding-mode; ignoring --transport mcp");
146+
if (!harnessDescriptor(s.harness).supportsMcp && s.transport === "mcp") {
147+
warnings.push(`${s.harness} has no MCP transport — it always runs ding-mode; ignoring --transport mcp`);
145148
}
146149

147-
const effectiveTransport: Transport = s.harness === "codex" ? "ding" : s.transport;
150+
const effectiveTransport: Transport = harnessDescriptor(s.harness).supportsMcp ? s.transport : "ding";
148151
const derived: [string, string][] = [
149152
["harness", s.harness],
150153
["identity", s.identity],

src/command-table.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ const IDENTITY_FLAG: FlagSpec = { name: "identity", desc: "Agent identity", kind
5656
const HARNESS_FLAG: FlagSpec = { name: "harness", desc: "Coding-agent harness", kind: "value", values: HARNESSES };
5757
const TRANSPORT_FLAG: FlagSpec = { name: "transport", desc: "Bus transport", kind: "value", values: TRANSPORTS };
5858
const PERSONA_FLAG: FlagSpec = { name: "persona", desc: "Persona file", kind: "value", takesPath: true };
59-
const CONFIG_DIR_FLAG: FlagSpec = { name: "config-dir", desc: "CLAUDE_CONFIG_DIR for the session", kind: "value", takesPath: true };
59+
// Not "CLAUDE_CONFIG_DIR" — the flag names the HARNESS's own config-relocation dir (CLAUDE_CONFIG_DIR for
60+
// claude, CODEX_HOME for codex), which is how an account is selected (decision 0004).
61+
const CONFIG_DIR_FLAG: FlagSpec = { name: "config-dir", desc: "Config dir for the session (selects the account)", kind: "value", takesPath: true };
6062

6163
const NETWORK_POSITIONAL: PositionalSpec = { desc: "Network", dynamic: "networks" };
6264
const IDENTITY_POSITIONAL: PositionalSpec = { desc: "Agent identity", dynamic: "identities" };
@@ -164,6 +166,10 @@ export const COMMANDS: readonly CommandSpec[] = [
164166
PERSONA_FLAG,
165167
{ name: "prefix", desc: "Session-id prefix (default: short hostname)", kind: "value" },
166168
CONFIG_DIR_FLAG,
169+
// `--bin` on the ad-hoc path too: a deployment that wraps its harness wraps it for ad-hoc sessions
170+
// as well, and `convoy run` is the replacement for exactly those launcher aliases. Without it, the
171+
// ad-hoc path is the one place a convoy session escapes the deployment's boundary.
172+
{ name: "bin", desc: "Run this in place of the bare harness binary", kind: "value", takesPath: true },
167173
DRY_RUN_FLAG,
168174
{ name: "force", desc: "Overwrite a foreign pty.toml in the workspace", kind: "bool" },
169175
],

0 commit comments

Comments
 (0)