Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @bvdm/t3code-cli

`t3code` hands the current folder or Git repository to a new thread in [T3 Code](https://github.com/pingdotgg/t3code).
`t3code` hands the current folder or Git repository to a new thread in [T3 Code](https://github.com/pingdotgg/t3code), and lets automation discover, inspect, and message existing threads.

It does not fake a handover by copying text or opening a generic app URL. It connects to the running local T3 server, resolves the workspace against T3 projects, optionally creates the missing project, creates a fresh thread, and starts its first prompt through T3's orchestration API.

Expand Down Expand Up @@ -97,6 +97,47 @@ Command flags override the CLI config, which overrides the T3 project's saved mo

Speed and thinking effort are stored as model options. T3 applies the option ids supported by the selected provider/model. If `--provider` changes the project's default provider instance, also pass `--model` because provider instance ids can be user-defined and do not imply a model.

## Existing threads

List threads across projects, or restrict discovery by project id or workspace:

```bash
t3code threads list
t3code threads list --status active --cwd .
t3code threads list --status settled --project <project-id>
```

`--status` accepts `active`, `settled`, or `all` (the default). Results include the exact thread id, project, title, model, and update time. Inspect the exact target before sending:

```bash
t3code threads inspect --thread <thread-id>
```

Start a new turn on that thread with one of `--prompt`, `--prompt-file`, or `--stdin`:

```bash
printf '%s' "Review findings from the other thread..." \
| t3code threads send --thread <thread-id> --stdin
```

Sending to a settled thread requires confirmation. Non-interactive and JSON callers must explicitly opt in with `--wake-settled`:

```bash
printf '%s' "New findings that require more work..." \
| t3code --json threads send --thread <thread-id> --stdin --wake-settled
```

The send command does not report success from the HTTP response alone. It waits until the exact message is visible in T3's thread projection. Archived threads are rejected.

Manage settlement explicitly without starting a new turn:

```bash
t3code threads settle --thread <thread-id>
t3code threads unsettle --thread <thread-id>
```

`settle` refuses a thread with a running/starting session or a pending approval or user-input request. `unsettle` marks the thread manually active but does not send a message or start its provider session. Both commands require the server to advertise the `threadSettlement` capability and wait for the requested lifecycle state to appear in T3's projection before succeeding.

## Settings

```bash
Expand Down Expand Up @@ -136,12 +177,17 @@ t3code config path|show|set
t3code projects list
t3code projects resolve --cwd .
t3code projects ensure --cwd . --project-policy create
t3code threads list --status active --cwd .
t3code threads inspect --thread <thread-id>
t3code threads send --thread <thread-id> --stdin
t3code threads settle --thread <thread-id>
t3code threads unsettle --thread <thread-id>
t3code threads create --stdin
t3code handover --stdin
t3code request get /api/orchestration/snapshot
```

Every command supports human-readable output. `--json` produces `{ "ok": true, "data": ... }` on success and a stable error envelope on failure.
Every command supports human-readable output. `--json` produces `{ "ok": true, "data": ... }` on success and a stable error envelope on failure. Thread targeting uses exit code `3` for a missing target, `4` for a lifecycle/confirmation refusal, and `5` when dispatch returned but turn acceptance could not be verified.

## Origin and optional UI example

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "@bvdm/t3code-cli",
"version": "0.1.2",
"description": "Open a folder as a T3 Code project and start a new handover thread.",
"description": "Manage T3 Code projects, handover threads, and cross-thread messages.",
"license": "MIT",
"keywords": [
"t3-code",
"cli",
"handover",
"codex"
"codex",
"threads",
"agents"
],
"repository": {
"type": "git",
Expand Down
37 changes: 36 additions & 1 deletion skills/use-t3code-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: use-t3code-cli
description: Operate the t3code CLI to resolve folders or Git repositories into T3 Code projects, create missing projects according to policy, start new handover threads with prompts, inspect project state, and diagnose the local T3 connection. Use when an agent needs to hand current work to T3 Code or automate T3 project/thread creation from a terminal or application.
description: Operate the t3code CLI to resolve folders or Git repositories into T3 Code projects, create missing projects according to policy, start new handover threads with prompts, inspect project state, and diagnose the local T3 connection. Use when an agent needs to hand current work to T3 Code or automate T3 project/thread creation from a terminal or application. Also use when an agent needs to discover, inspect, message, settle, or unsettle an existing T3 Code thread.
---

# Use T3 Code CLI
Expand Down Expand Up @@ -57,6 +57,35 @@ Use `--project-policy existing` when creating a project is not authorized. The d

Use `--dry-run --open none` to inspect the proposed project and thread commands without changing T3 state.

## Work with existing threads

Discover candidate threads in the relevant project, then inspect the exact target id before changing it:

```bash
t3code --json threads list --cwd . --status all
t3code --json threads inspect --thread "$TARGET_THREAD_ID"
```

Use `--project <project-id>` instead of `--cwd` when the caller provides an exact project id. Filter with `--status active` or `--status settled` when useful. Do not select a target from its title alone because titles are not unique.

Pass messages over stdin:

```bash
printf '%s' "$THREAD_MESSAGE" \
| t3code --json threads send --thread "$TARGET_THREAD_ID" --stdin
```

Sending is an external state change. Keep the target and message within the caller's authorization. A settled thread requires interactive confirmation or `--wake-settled`; JSON and stdin workflows are non-interactive, so use that override only when waking the inspected target is authorized. Archived threads cannot receive a turn.

Manage lifecycle state without sending a message:

```bash
t3code --json threads settle --thread "$TARGET_THREAD_ID"
t3code --json threads unsettle --thread "$TARGET_THREAD_ID"
```

Settle only after the caller authorizes that lifecycle change. T3 refuses settlement while a session is starting/running or the thread has a blocking approval or user-input request. Unsettling marks the thread manually active; it does not start a turn or provider session.

## Optional front-end integration

The CLI can be called from a trusted application backend to power a **Send to T3 Code** button. This pattern was initially built for the [Delano viewer](https://github.com/MajesteitBart/delano). The optional `integrations/` example in this repository includes a React split button and Node bridge; it is not required to install or operate the CLI.
Expand All @@ -67,10 +96,16 @@ Keep the repository root server-owned, pass CLI options as process arguments, an

Read `data.project.id`, `data.thread.id`, `data.projectCreated`, and `data.opened`. A successful current stable desktop reveal can report `opened.exactThread: false`; the thread is still created in the resolved project.

For existing-thread writes, require `data.verification.accepted: true`. Record `data.thread.id` and, for sends, `data.message.messageId` when reporting the result. The CLI verifies the requested projection state rather than treating HTTP submission as success.

On `{ "ok": false }`, report `error.code` and `error.message`. Do not retry write commands blindly. `THREAD_START_FAILED` already attempts to delete the newly-created thread.

`THREAD_TURN_NOT_VERIFIED` or `THREAD_SETTLEMENT_NOT_VERIFIED` means dispatch returned but projection verification timed out. Do not retry automatically because the first operation may still appear later.

## Current compatibility boundary

T3 0.0.28 and later support new-worktree handovers through the atomic bootstrap contract. Worktree creation follows the current installation's explicit `newWorktreesStartFromOrigin` setting. When it is absent, use the installed version's default: `false` on 0.0.28 and `true` on 0.0.29 and later. `WORKTREE_REQUIRES_BRANCH` means the selected folder is not a Git repository on a branch; retry with `--checkout current` only with explicit user or caller authority.

Thread settlement commands require a T3 server that exposes the `threadSettlement` capability. Existing-thread sends preserve the target's saved model, runtime mode, and interaction mode.

Use `t3code --json request get <path>` only as a read-only escape hatch.
4 changes: 2 additions & 2 deletions skills/use-t3code-cli/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface:
display_name: "T3 Code CLI"
short_description: "Create T3 project handover threads from any repo"
default_prompt: "Use $use-t3code-cli to hand this repository over to a new T3 Code thread."
short_description: "Create handovers and manage T3 Code threads"
default_prompt: "Use $use-t3code-cli to hand this repository over to a new T3 Code thread or manage an existing T3 Code thread."
120 changes: 120 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import os from "node:os";
import path from "node:path";

import { afterEach, describe, expect, it, vi } from "vitest";

interface CliResult {
stdout: string;
stderr: string;
exitCode: number | string | undefined;
}

async function runCli(args: string[]): Promise<CliResult> {
const originalArgv = process.argv;
const originalExitCode = process.exitCode;
let stdout = "";
let stderr = "";

process.argv = [process.execPath, "t3code", ...args];
process.exitCode = undefined;
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
stdout += chunk.toString();
return true;
});
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
stderr += chunk.toString();
return true;
});

vi.resetModules();
try {
await import("./cli.js");
return { stdout, stderr, exitCode: process.exitCode };
} finally {
stdoutSpy.mockRestore();
stderrSpy.mockRestore();
process.argv = originalArgv;
process.exitCode = originalExitCode;
}
}

afterEach(() => {
vi.restoreAllMocks();
});

describe.sequential("CLI parsing", () => {
it("writes a JSON usage envelope for a missing required option", async () => {
const result = await runCli(["--json", "threads", "inspect"]);

expect(result.exitCode).toBe(2);
expect(result.stdout).toBe("");
expect(JSON.parse(result.stderr)).toEqual({
ok: false,
error: {
code: "INVALID_USAGE",
message: "required option '--thread <thread-id>' not specified",
},
});
});

it("writes a JSON usage envelope for an invalid choice", async () => {
const result = await runCli(["--json", "threads", "list", "--status", "archived"]);

expect(result.exitCode).toBe(2);
expect(result.stdout).toBe("");
expect(JSON.parse(result.stderr)).toEqual({
ok: false,
error: {
code: "INVALID_USAGE",
message: "option '--status <status>' argument 'archived' is invalid. Allowed choices are active, settled, all.",
},
});
});

it("writes a JSON usage envelope for an unknown option", async () => {
const result = await runCli(["--json", "threads", "inspect", "--thread", "thread-1", "--bogus"]);

expect(result.exitCode).toBe(2);
expect(result.stdout).toBe("");
expect(JSON.parse(result.stderr)).toEqual({
ok: false,
error: {
code: "INVALID_USAGE",
message: "unknown option '--bogus'",
},
});
});

it("keeps human-readable usage errors", async () => {
const result = await runCli(["threads", "inspect"]);

expect(result.exitCode).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toBe("t3code: required option '--thread <thread-id>' not specified\n");
});

it("keeps help and version successful", async () => {
const help = await runCli(["--help"]);
const version = await runCli(["--version"]);

expect(help.exitCode).toBe(0);
expect(help.stderr).toBe("");
expect(help.stdout).toContain("Usage: t3code [options] [command]");
expect(version).toEqual({ stdout: "0.1.2\n", stderr: "", exitCode: 0 });
});

it("leaves action-level JSON errors unchanged", async () => {
const missingConfig = path.join(os.tmpdir(), "t3code-cli-cli-test-missing.json");
const result = await runCli(["--json", "--config", missingConfig, "handover"]);

expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("");
expect(JSON.parse(result.stderr)).toEqual({
ok: false,
error: {
code: "PROMPT_SOURCE_REQUIRED",
message: "Use exactly one of --prompt, --prompt-file, or --stdin.",
},
});
});
});
Loading