mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eac11fa2c | ||
|
|
4321aecea3 | ||
|
|
d0da1d2c5e | ||
|
|
199ab3179d | ||
|
|
094747fb59 | ||
|
|
b59c9ab725 | ||
|
|
6ba531722b | ||
|
|
74ee30e6da |
No files matched your search
@@ -32,6 +32,7 @@ function namedStep(source, name) {
|
||||
}
|
||||
|
||||
const ci = read(".github/workflows/ci.yml");
|
||||
const ciSuite = read(".github/workflows/ci-test-suite.yml");
|
||||
const nightly = read(".github/workflows/nightly.yml");
|
||||
const candidate = read(".github/workflows/release-candidate.yml");
|
||||
const artifacts = read(".github/workflows/release-artifacts.yml");
|
||||
@@ -46,28 +47,212 @@ const archiveInstaller = read("scripts/release/install.sh");
|
||||
const cliDispatcher = read("crates/cli/src/lib.rs");
|
||||
const runbook = read("docs/RELEASE_RUNBOOK.md");
|
||||
|
||||
const ciTestJob = ci.slice(ci.indexOf("\n test:\n"));
|
||||
assert.match(
|
||||
ciTestJob,
|
||||
/matrix\.os == 'ubuntu-latest'.*github\.event_name == 'pull_request'/,
|
||||
"heavy pull requests must select the real Ubuntu test lane",
|
||||
);
|
||||
assert.match(
|
||||
ciTestJob,
|
||||
/cargo nextest run --workspace --all-features --locked --profile ci/,
|
||||
"the Ubuntu pull-request lane must run workspace nextest",
|
||||
);
|
||||
assert.match(
|
||||
ciTestJob,
|
||||
/name: Linux test location \(CNB\)/,
|
||||
"the non-PR CNB fallback must be named explicitly",
|
||||
);
|
||||
function jobBlock(source, job) {
|
||||
const match = source.match(new RegExp(`^ ${job}:\\n([\\s\\S]*?)(?=^ \\S|(?![\\s\\S]))`, "m"));
|
||||
assert.ok(match, `missing job: ${job}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function jobIf(source, job) {
|
||||
const lines = jobBlock(source, job).split("\n");
|
||||
const at = lines.findIndex((line) => /^ if:/.test(line));
|
||||
assert.notEqual(at, -1, `${job} must declare an if`);
|
||||
const first = lines[at].replace(/^ if:\s*/, "");
|
||||
if (first !== ">-") return first.replace(/^'(.*)'$/, "$1");
|
||||
const folded = [];
|
||||
for (let i = at + 1; i < lines.length && /^ \S/.test(lines[i]); i += 1) folded.push(lines[i].trim());
|
||||
return folded.join(" ");
|
||||
}
|
||||
|
||||
function evaluate(expression, context) {
|
||||
return Boolean(vm.runInNewContext(expression, context));
|
||||
}
|
||||
|
||||
// The sharded suite: one archive per OS, partitions of it in parallel shards,
|
||||
// doctests once, and a check that the shards add up to the archive's list.
|
||||
assert.match(
|
||||
namedStep(ciTestJob, "Build canonical executable for acceptance tests"),
|
||||
namedStep(ciSuite, "Archive test binaries"),
|
||||
/cargo nextest archive --workspace --all-features --locked --profile ci/,
|
||||
"the suite must archive the same workspace, features and profile the old single run used",
|
||||
);
|
||||
assert.match(
|
||||
namedStep(ciSuite, "Build canonical executable for acceptance tests"),
|
||||
/cargo build -p codewhale-cli --bin codewhale --all-features --locked/,
|
||||
"Engine acceptance must build the canonical executable first",
|
||||
);
|
||||
assert.match(
|
||||
namedStep(ciSuite, "Run doctests"),
|
||||
/cargo test --workspace --all-features --locked --doc/,
|
||||
"nextest does not run doctests; the suite must run them once per OS",
|
||||
);
|
||||
const shardRun = namedStep(ciSuite, "Run tests");
|
||||
assert.match(shardRun, /--partition "count:\$\{\{ matrix\.shard \}\}\/\$\{\{ inputs\.shards \}\}"/);
|
||||
assert.match(shardRun, /-E "not \(\$NATIVE_FILTER\)"/, "every shard must exclude the native tests that shard 1 runs apart");
|
||||
assert.match(namedStep(ciSuite, "Run native containment tests"), /-E "\$NATIVE_FILTER"/);
|
||||
assert.match(ciSuite, /nextest-shard-count\.py check counts/, "the suite must prove the shards add up to the archive's list");
|
||||
for (const name of ["Run tests", "Run native containment tests", "Run doctests"]) {
|
||||
const step = namedStep(ciSuite, name);
|
||||
assert.match(step, /shell: bash/, `${name} must invoke the POSIX helper on Windows too`);
|
||||
}
|
||||
assert.match(ciSuite, /RUST_MIN_STACK: '16777216'/);
|
||||
|
||||
// The native receipt script requires its tests in one JUnit report, so the
|
||||
// shard that runs them must select exactly the script's list.
|
||||
const receiptScript = read("scripts/compiled-host-native-receipt.py");
|
||||
const receiptNames = [
|
||||
receiptScript.match(/^COMPILED = "([^"]+)"$/m)[1],
|
||||
receiptScript.match(/^MEMORY = "([^"]+)"$/m)[1],
|
||||
...[...receiptScript.match(/^WINDOWS = \[([\s\S]*?)^\]/m)[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]),
|
||||
];
|
||||
const nativeFilter = ciSuite.match(/NATIVE_FILTER: >-\n([\s\S]*?)\n steps:/)[1];
|
||||
assert.deepEqual(
|
||||
[...nativeFilter.matchAll(/test\(=([^)]+)\)/g)].map((m) => m[1]).sort(),
|
||||
receiptNames.sort(),
|
||||
"NATIVE_FILTER must list exactly the tests scripts/compiled-host-native-receipt.py requires",
|
||||
);
|
||||
|
||||
// Shard counts: each caller's shards equals the length of its shard_list, the
|
||||
// suite's own list agrees with it, and every OS keeps a pipeline.
|
||||
const suiteCallers = {};
|
||||
for (const job of ["test-ubuntu", "test-macos", "test-windows"]) {
|
||||
const block = jobBlock(ci, job);
|
||||
const os = block.match(/^ os: (\S+)$/m)[1];
|
||||
const shards = Number(block.match(/^ shards: (\d+)$/m)[1]);
|
||||
const list = JSON.parse(block.match(/^ shard_list: '(.+)'$/m)[1]);
|
||||
assert.deepEqual(list, Array.from({ length: shards }, (_, i) => i + 1), `${job} shard_list must be 1..shards`);
|
||||
assert.match(block, /uses: \.\/\.github\/workflows\/ci-test-suite\.yml/);
|
||||
assert.equal(jobIf(ci, job), jobIf(ci, "test-ubuntu"), `${job} must share the suite condition`);
|
||||
suiteCallers[os] = shards;
|
||||
}
|
||||
assert.deepEqual(Object.keys(suiteCallers).sort(), ["macos-latest", "ubuntu-latest", "windows-latest"]);
|
||||
|
||||
// Required contexts. The ruleset requires exactly "Lint" and "Test
|
||||
// (windows-latest)". Only Lint and the compile-only test-windows-pr job may
|
||||
// produce them; a shard, build or suite check must never share a name.
|
||||
const jobNames = (source) => [...source.matchAll(/^ [A-Za-z0-9_-]+:\n name: (.+)$/gm)].map((m) => m[1]);
|
||||
assert.deepEqual(
|
||||
[...jobNames(ci), ...jobNames(ciSuite)].filter((name) => /^Test \(/.test(name) || name === "Lint"),
|
||||
["Lint", "Test (windows-latest)"],
|
||||
"only Lint and the compile-only Windows leg may use the required context names",
|
||||
);
|
||||
assert.match(jobBlock(ci, "test-windows-pr"), /^ name: Test \(windows-latest\)$/m);
|
||||
assert.equal(jobIf(ci, "test-windows-pr"), "github.event_name == 'pull_request'");
|
||||
assert.match(jobBlock(ci, "test-windows-pr"), /cargo check --workspace --all-features --all-targets|cargo check --workspace --all-targets --all-features --locked/);
|
||||
|
||||
// Each gate that pull requests lost on 2026-11-07 (99cb5a0f4f) runs on a heavy
|
||||
// pull request again, plus on dispatch; a docs-only pull request keeps skipping
|
||||
// them; a scoped dispatch skips all of them.
|
||||
const gates = ["safety-gate", "msrv", "test-ubuntu", "test-macos", "test-windows", "portable-boundaries", "npm-wrapper-smoke", "mobile-smoke"];
|
||||
for (const job of gates) {
|
||||
const guard = jobIf(ci, job);
|
||||
const base = { inputs: { workspace_test_mode: "nextest" } };
|
||||
const run = (event, heavy, extra = {}) =>
|
||||
evaluate(guard, {
|
||||
github: { event_name: event },
|
||||
needs: { changes: { outputs: { heavy: String(heavy), mobile: "true", scoped: "false", ...extra } } },
|
||||
...base,
|
||||
});
|
||||
assert.equal(run("pull_request", true), true, `${job} must run on a heavy pull request`);
|
||||
if (job !== "npm-wrapper-smoke") {
|
||||
assert.equal(run("pull_request", false), false, `${job} must skip a docs-only pull request`);
|
||||
}
|
||||
assert.equal(run("workflow_dispatch", true), true, `${job} must run on a full dispatch`);
|
||||
assert.equal(run("workflow_dispatch", true, { scoped: "true" }), false, `${job} must skip a scoped dispatch`);
|
||||
}
|
||||
assert.equal(
|
||||
evaluate(jobIf(ci, "mobile-smoke"), {
|
||||
github: { event_name: "pull_request" },
|
||||
needs: { changes: { outputs: { heavy: "true", mobile: "false", scoped: "false" } } },
|
||||
}),
|
||||
false,
|
||||
"mobile smoke keeps its path filter on pull requests",
|
||||
);
|
||||
for (const job of ["test-ubuntu", "test-macos", "test-windows"]) {
|
||||
assert.equal(
|
||||
evaluate(jobIf(ci, job), {
|
||||
github: { event_name: "workflow_dispatch" },
|
||||
inputs: { workspace_test_mode: "shared-process-twice" },
|
||||
needs: { changes: { outputs: { heavy: "true", scoped: "false" } } },
|
||||
}),
|
||||
false,
|
||||
`${job} must yield to the shared-process qualification`,
|
||||
);
|
||||
}
|
||||
const sharedGuard = jobIf(ci, "test");
|
||||
for (const [event, mode, scoped, expected] of [
|
||||
["workflow_dispatch", "shared-process-twice", "false", true],
|
||||
["workflow_dispatch", "nextest", "false", false],
|
||||
["workflow_dispatch", "shared-process-twice", "true", false],
|
||||
["pull_request", "nextest", "false", false],
|
||||
]) {
|
||||
assert.equal(
|
||||
evaluate(sharedGuard, {
|
||||
github: { event_name: event },
|
||||
inputs: { workspace_test_mode: mode },
|
||||
needs: { changes: { outputs: { heavy: "true", scoped } } },
|
||||
}),
|
||||
expected,
|
||||
`shared-process job: ${event} ${mode} scoped=${scoped}`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
evaluate(jobIf(ci, "scoped-test"), { needs: { changes: { outputs: { scoped: "true" } } } }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
evaluate(jobIf(ci, "scoped-test"), { needs: { changes: { outputs: { scoped: "false" } } } }),
|
||||
false,
|
||||
);
|
||||
assert.match(ci, /^ expected_sha:\n description: .*\n required: true\n type: string$/m, "expected_sha stays required");
|
||||
for (const input of ["crate", "filter", "os"]) {
|
||||
assert.match(ci, new RegExp(`^ ${input}:\\n description: .*\\n required: false\\n`, "m"), `scoped dispatch input ${input}`);
|
||||
}
|
||||
|
||||
// Concurrency: a new push to the same pull request or non-main branch cancels
|
||||
// the older run; runs on main and scoped dispatches never cancel anything.
|
||||
const concurrency = ci.match(/^concurrency:\n group: (.+)\n cancel-in-progress: (.+)$/m);
|
||||
assert.ok(concurrency, "ci.yml must declare its concurrency");
|
||||
const strip = (value) => value.replace(/^\$\{\{ /, "").replace(/ \}\}$/, "");
|
||||
const format = (template, ...args) => template.replace(/\{(\d)\}/g, (_, i) => args[Number(i)]);
|
||||
const groupOf = (context) =>
|
||||
vm.runInNewContext(strip(concurrency[1]), { ...context, format });
|
||||
const cancelOf = (context) => vm.runInNewContext(strip(concurrency[2]), context);
|
||||
const dispatch = (ref, sha, inputs = {}) => ({
|
||||
github: { event_name: "workflow_dispatch", ref, sha, workflow: "CI", run_id: `run-${sha}` },
|
||||
inputs: { crate: "", filter: "", ...inputs },
|
||||
});
|
||||
const pr = (number, sha) => ({
|
||||
github: { event_name: "pull_request", ref: `refs/pull/${number}/merge`, sha, workflow: "CI", event: { pull_request: { number } }, run_id: `run-${sha}` },
|
||||
inputs: {},
|
||||
});
|
||||
assert.equal(groupOf(pr(7, "a")), groupOf(pr(7, "b")), "a new push to a pull request shares its group");
|
||||
assert.notEqual(groupOf(pr(7, "a")), groupOf(pr(8, "a")));
|
||||
assert.equal(cancelOf(pr(7, "a")), true);
|
||||
assert.equal(groupOf(dispatch("refs/heads/ci/x", "a")), groupOf(dispatch("refs/heads/ci/x", "b")), "a new dispatch on the same ci branch shares its group");
|
||||
assert.notEqual(groupOf(dispatch("refs/heads/ci/x", "a")), groupOf(dispatch("refs/heads/ci/y", "a")));
|
||||
assert.equal(cancelOf(dispatch("refs/heads/ci/x", "a")), true);
|
||||
assert.notEqual(groupOf(dispatch("refs/heads/main", "a")), groupOf(dispatch("refs/heads/main", "b")), "runs on main never share a group");
|
||||
assert.equal(cancelOf(dispatch("refs/heads/main", "a")), false, "runs on main never cancel");
|
||||
assert.notEqual(
|
||||
groupOf(dispatch("refs/heads/ci/x", "a", { crate: "codewhale-tui" })),
|
||||
groupOf(dispatch("refs/heads/ci/x", "a")),
|
||||
"a scoped dispatch never shares the full run's group",
|
||||
);
|
||||
assert.equal(cancelOf(dispatch("refs/heads/ci/x", "a", { filter: "test(x)" })), false, "a scoped dispatch cancels nothing");
|
||||
|
||||
// Security for pull requests from forks: the workflows use pull_request (no
|
||||
// pull_request_target), read-only permissions, no secrets, and write caches
|
||||
// only from main.
|
||||
for (const [name, source] of [["ci.yml", ci], ["ci-test-suite.yml", ciSuite]]) {
|
||||
assert.doesNotMatch(source, /pull_request_target/, `${name} must not use pull_request_target`);
|
||||
assert.doesNotMatch(source, /\$\{\{\s*secrets\./, `${name} must not read secrets`);
|
||||
assert.match(source, /^permissions:\n contents: read\n/m, `${name} must keep read-only default permissions`);
|
||||
assert.doesNotMatch(source.replace(/^permissions:\n contents: read\n/m, ""), /^\s+(?:contents|actions|id-token|pull-requests|packages):\s*write/m, `${name} must not grant write permissions`);
|
||||
for (const match of source.matchAll(/Swatinem\/rust-cache@[^\n]*\n(?:\s+(?:with|cache-bin):[^\n]*\n)*\s+save-if: ([^\n]+)/g)) {
|
||||
assert.match(match[1], /refs\/heads\/main|false/, `${name}: rust-cache must save only on main`);
|
||||
}
|
||||
assert.doesNotMatch(source, /actions\/cache(?:\/save)?@/, `${name} must not write caches outside rust-cache and sccache`);
|
||||
}
|
||||
const npmSmokeJob = ci.match(/^ npm-wrapper-smoke:\n([\s\S]*?)(?=^ \S)/m)?.[1];
|
||||
assert.ok(npmSmokeJob, "CI must retain the npm-wrapper job");
|
||||
assert.match(
|
||||
@@ -79,13 +264,13 @@ assert.match(
|
||||
/run: node scripts\/release\/npm-wrapper-smoke\.js/,
|
||||
);
|
||||
const npmSmokeSteps = npmSmokeJob.split(/(?=^ - )/m).slice(1);
|
||||
// Exercise the workflow's actual Boolean guards. The smoke is manual-only
|
||||
// (workflow_dispatch); pull requests, pushes and schedules skip the job.
|
||||
// Exercise the workflow's actual Boolean guards. The smoke runs on pull
|
||||
// requests and dispatches (§180); pushes and schedules skip the job.
|
||||
const npmSmokeCases = [
|
||||
// name, event, heavy, OS, trusted, cache success, execute, Linux deps, CNB
|
||||
["own PR", "pull_request", true, "ubuntu-latest", true, true, false, false, false],
|
||||
["fork PR", "pull_request", true, "ubuntu-latest", false, true, false, false, false],
|
||||
["PR cache failure", "pull_request", true, "ubuntu-latest", false, false, false, false, false],
|
||||
["own PR", "pull_request", true, "ubuntu-latest", true, true, true, true, false],
|
||||
["fork PR", "pull_request", true, "ubuntu-latest", false, true, true, true, false],
|
||||
["PR cache failure", "pull_request", true, "ubuntu-latest", false, false, true, true, false],
|
||||
["light PR", "pull_request", false, "ubuntu-latest", true, true, false, false, false],
|
||||
["manual Ubuntu", "workflow_dispatch", true, "ubuntu-latest", true, true, true, true, false],
|
||||
["main Ubuntu", "push", true, "ubuntu-latest", true, true, false, false, false],
|
||||
@@ -95,15 +280,22 @@ const npmSmokeCases = [
|
||||
["light main", "push", false, "ubuntu-latest", true, true, false, false, false],
|
||||
["schedule", "schedule", true, "ubuntu-latest", true, true, false, false, false],
|
||||
];
|
||||
// The sccache GitHub Actions backend is main-only, mirroring rust-cache's
|
||||
// save-if: pull requests never install or enable it (cache bloat, PLAN D).
|
||||
// The sccache GitHub Actions backend is read-only off main: every run reads
|
||||
// main's cache, and only main writes it (SCCACHE_GHA_RW_MODE), mirroring
|
||||
// rust-cache's save-if (cache bloat, PLAN D).
|
||||
for (const [name, source] of [["ci.yml", ci], ["ci-test-suite.yml", ciSuite]]) {
|
||||
const enabled = source.match(/SCCACHE_GHA_ENABLED=true/g) ?? [];
|
||||
const modes = source.match(/if \[ "\$\{GITHUB_REF\}" = refs\/heads\/main \]; then rw=READ_WRITE; else rw=READ_ONLY; fi\n\s+echo "SCCACHE_GHA_RW_MODE=\$\{rw\}"/g) ?? [];
|
||||
assert.ok(enabled.length > 0, `${name} must enable sccache`);
|
||||
assert.equal(modes.length, enabled.length, `${name}: every sccache enable must be read-only off main`);
|
||||
assert.doesNotMatch(source, /sccache-action[^\n]*\n(?:[^\n]*\n){0,4}\s+if: [^\n]*github\.ref == 'refs\/heads\/main'/, `${name}: sccache must install on every run`);
|
||||
}
|
||||
const sccacheInstallStep = "mozilla-actions/[email protected]";
|
||||
for (const [label, event, heavy, os, trusted, cache, execute, linuxDeps, cnb] of npmSmokeCases) {
|
||||
const ref = event === "pull_request" ? "refs/pull/1/merge" : "refs/heads/main";
|
||||
const onMain = ref === "refs/heads/main";
|
||||
const installed = execute && onMain;
|
||||
const installed = execute;
|
||||
const context = {
|
||||
needs: { changes: { outputs: { heavy: String(heavy), trusted: String(trusted) } } },
|
||||
needs: { changes: { outputs: { heavy: String(heavy), trusted: String(trusted), scoped: "false" } } },
|
||||
github: { event_name: event, ref },
|
||||
matrix: { os },
|
||||
steps: { sccache: { outcome: installed ? (cache ? "success" : "failure") : "skipped" } },
|
||||
@@ -688,7 +880,8 @@ assert.doesNotMatch(
|
||||
// itself runs none: its parity job calls release-parity.yml.
|
||||
let hermeticInvocations = 0;
|
||||
for (const [label, workflow, expected] of [
|
||||
["CI", ci, 6],
|
||||
["CI", ci, 5],
|
||||
["CI test suite", ciSuite, 3],
|
||||
["release", release, 0],
|
||||
["release parity", parityWorkflow, 4],
|
||||
["CNB", cnb, 3],
|
||||
@@ -707,11 +900,6 @@ for (const [label, workflow, expected] of [
|
||||
}
|
||||
hermeticInvocations += commands.length;
|
||||
}
|
||||
for (const name of ["Run tests", "Run doctests"]) {
|
||||
const step = namedStep(ciTestJob, name);
|
||||
assert.match(step, /shell: bash/, `${name} must invoke the POSIX helper on Windows too`);
|
||||
assert.match(step, /RUST_MIN_STACK: '16777216'/);
|
||||
}
|
||||
console.log(`Hermetic Rust workflow invocations OK: ${hermeticInvocations} checks passed.`);
|
||||
|
||||
const nextest = read(".config/nextest.toml");
|
||||
@@ -853,6 +1041,7 @@ for (const [name, source] of [
|
||||
["release-parity.yml", parityWorkflow],
|
||||
["release-republish.yml", republish],
|
||||
["ci.yml", ci],
|
||||
["ci-test-suite.yml", ciSuite],
|
||||
["nightly.yml", nightly],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
name: CI test suite
|
||||
|
||||
# Called by ci.yml, once per operating system, so each OS is its own pipeline:
|
||||
# one build of the whole workspace's test binaries, parallel shards that run
|
||||
# partitions of that one nextest archive, and a check that the shards together
|
||||
# ran exactly the tests the archive lists. Running per OS means Linux reports
|
||||
# as soon as Linux is done instead of waiting for the slowest build.
|
||||
#
|
||||
# Trust boundary: this runs with the caller's event. For a pull request from a
|
||||
# fork that is the read-only token and no secrets; nothing here writes a cache
|
||||
# (rust-cache and sccache save only on main) or uses a write permission. The
|
||||
# artifacts passed between jobs live inside one run.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
description: Hosted runner label
|
||||
required: true
|
||||
type: string
|
||||
shards:
|
||||
description: Number of shards, equal to the length of shard_list
|
||||
required: true
|
||||
type: number
|
||||
shard_list:
|
||||
description: JSON array of shard numbers, for example [1,2,3,4]
|
||||
required: true
|
||||
type: string
|
||||
build_jobs:
|
||||
description: CARGO_BUILD_JOBS for the test build
|
||||
required: true
|
||||
type: string
|
||||
debug:
|
||||
description: Debug-table level for the test build (CARGO_PROFILE_*_DEBUG)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_MIN_STACK: 8388608
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
# A cold hosted Mac compiled for 65 minutes with one Cargo job (run
|
||||
# 43216596942) and the old one-job cap was 210; this job only compiles, so
|
||||
# it keeps real margin without the test run in it.
|
||||
timeout-minutes: 150
|
||||
runs-on: ${{ inputs.os }}
|
||||
env:
|
||||
# Test builds compile all features of the whole workspace. One Cargo job
|
||||
# kept hosted runners inside memory (exit 143 on 2026-10-19 with four);
|
||||
# trimmed debug tables shrink compiler and linker memory so Linux and
|
||||
# Windows can run two. Assertions and every test are unchanged.
|
||||
CARGO_BUILD_JOBS: ${{ inputs.build_jobs }}
|
||||
CARGO_PROFILE_DEV_DEBUG: ${{ inputs.debug }}
|
||||
CARGO_PROFILE_TEST_DEBUG: ${{ inputs.debug }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Test Windows installer PATH helper
|
||||
if: inputs.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/update-user-path.tests.ps1
|
||||
- name: Install NSIS for Windows installer regression
|
||||
if: inputs.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/install-nsis.ps1
|
||||
- name: Test Windows installer PATH regression
|
||||
if: inputs.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install macOS LLVM tools
|
||||
if: inputs.os == 'macos-latest'
|
||||
run: rustup component add llvm-tools --toolchain stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
if: inputs.os == 'ubuntu-latest'
|
||||
run: |
|
||||
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
|
||||
sh scripts/prepare-linux-test-sandbox.sh
|
||||
- name: Ubuntu build headroom
|
||||
# Reuse the same fixed hosted-runner provisioning as Safety and Lint.
|
||||
# Exit 143 is a shutdown observation, not proof of OOM or timeout.
|
||||
if: inputs.os == 'ubuntu-latest'
|
||||
shell: bash
|
||||
run: bash scripts/prepare-ubuntu-build-headroom.sh
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Build canonical executable for acceptance tests
|
||||
shell: bash
|
||||
run: cargo build -p codewhale-cli --bin codewhale --all-features --locked
|
||||
env:
|
||||
RUSTC_WRAPPER: ${{ inputs.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
- name: Windows Terminal clipboard-to-draft acceptance
|
||||
if: inputs.os == 'windows-latest'
|
||||
timeout-minutes: 5
|
||||
shell: pwsh
|
||||
run: node scripts/release/windows-terminal-clipboard.mjs target/debug/codewhale.exe "${{ runner.temp }}/windows-clipboard"
|
||||
- name: Retain Windows Terminal clipboard-to-draft evidence
|
||||
if: ${{ !cancelled() && inputs.os == 'windows-latest' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: windows-clipboard-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/windows-clipboard/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: Archive test binaries
|
||||
# One compile per OS. Shards run this archive with
|
||||
# `cargo nextest run --archive-file`, so the suite is the same test
|
||||
# binaries, built once. Doctests are not in the archive; they run
|
||||
# below, once, while the library artifacts are still warm.
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/test-archive" "$RUNNER_TEMP/test-expected"
|
||||
cargo nextest archive --workspace --all-features --locked --profile ci \
|
||||
--archive-file "$RUNNER_TEMP/test-archive/nextest-archive.tar.zst"
|
||||
exe=codewhale
|
||||
if [ "$RUNNER_OS" = Windows ]; then exe=codewhale.exe; fi
|
||||
cp "target/debug/$exe" "$RUNNER_TEMP/test-archive/$exe"
|
||||
env:
|
||||
RUSTC_WRAPPER: ${{ inputs.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
- name: Count the tests the archive selects
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo nextest list --archive-file "$RUNNER_TEMP/test-archive/nextest-archive.tar.zst" \
|
||||
--workspace-remap . --profile ci --message-format json > "$RUNNER_TEMP/test-listing.json"
|
||||
tests="$(python3 -I scripts/ci/nextest-shard-count.py list "$RUNNER_TEMP/test-listing.json")"
|
||||
printf '{"os": "%s", "tests": %s, "shards": %s}\n' "${{ inputs.os }}" "$tests" "${{ inputs.shards }}" \
|
||||
> "$RUNNER_TEMP/test-expected/expected-${{ inputs.os }}.json"
|
||||
cat "$RUNNER_TEMP/test-expected/expected-${{ inputs.os }}.json"
|
||||
- name: Run doctests
|
||||
# nextest does not run doctests; this keeps them once per OS.
|
||||
shell: bash
|
||||
run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc
|
||||
env:
|
||||
RUSTC_WRAPPER: ${{ inputs.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
RUST_MIN_STACK: '16777216'
|
||||
- name: Lockfile drift guard
|
||||
run: git diff --exit-code -- Cargo.lock
|
||||
- name: sccache stats
|
||||
if: steps.sccache.outcome == 'success'
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: sccache --show-stats
|
||||
- name: Retain test archive for the shards
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-archive-${{ inputs.os }}
|
||||
path: ${{ runner.temp }}/test-archive/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
- name: Retain expected test count
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-expected-${{ inputs.os }}
|
||||
path: ${{ runner.temp }}/test-expected/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
shard:
|
||||
name: Shard ${{ matrix.shard }}/${{ inputs.shards }}
|
||||
needs: build
|
||||
timeout-minutes: 60
|
||||
runs-on: ${{ inputs.os }}
|
||||
strategy:
|
||||
# A failure in one shard must not erase the others' results; the suite
|
||||
# gate needs every conclusion.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJSON(inputs.shard_list) }}
|
||||
env:
|
||||
CODEWHALE_EXT_HOST_TESTS: '1'
|
||||
CODEWHALE_EXT_HOST_BUN_TESTS: '1'
|
||||
# Give test threads the stack the product gives itself; the reasoning is
|
||||
# on the shared-process step above and in scripts/dev-cargo.sh.
|
||||
RUST_MIN_STACK: '16777216'
|
||||
# The exact-image Native containment tests (and, on Windows, the LPAC
|
||||
# owner and profile tests) that scripts/compiled-host-native-receipt.py
|
||||
# requires in ONE JUnit report. Shard 1 runs them in their own nextest
|
||||
# invocation and every shard excludes them from its partition, so each
|
||||
# still runs exactly once. release-workflows.test.js keeps this list equal
|
||||
# to the script's.
|
||||
NATIVE_FILTER: >-
|
||||
package(codewhale-engine) & kind(lib) & (
|
||||
test(=extension_host::tests::compiled_native_host_cannot_read_secrets_or_write_outside_its_data_dir) |
|
||||
test(=extension_host::tests::compiled_native_host_memory_cap_is_enforced) |
|
||||
test(=extension_host::windows::tests::windows_native_lpac_node_owner_boundary) |
|
||||
test(=extension_host::windows::tests::windows_native_lpac_bun_owner_boundary) |
|
||||
test(=extension_host::windows::tests::windows_native_lpac_compiled_owner_boundary) |
|
||||
test(=extension_host::windows::tests::windows_native_profiles_are_distinct_and_acl_grant_refuses_junctions) |
|
||||
test(=extension_host::windows::tests::windows_argv_and_environment_keep_exact_values_and_reject_nul) |
|
||||
test(=extension_host::windows::tests::windows_profile_retirement_removes_only_its_grants_and_inherited_data_on_restarts) |
|
||||
test(=extension_host::windows::tests::windows_profile_directory_budget_and_recorded_identity_refuse_before_overwrite))
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Point terminal failure diagnostics at the runner temp directory
|
||||
shell: bash
|
||||
run: echo "QA_PTY_DIAGNOSTICS_DIR=${RUNNER_TEMP}/pty-failures" >> "${GITHUB_ENV}"
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Linux system dependencies
|
||||
if: inputs.os == 'ubuntu-latest'
|
||||
run: |
|
||||
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
|
||||
sh scripts/prepare-linux-test-sandbox.sh
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: actions/[email protected]
|
||||
# The extension-host integration tests spawn the real bundle under
|
||||
# Node >= 22.19; CODEWHALE_EXT_HOST_TESTS makes a missing Node a
|
||||
# failure instead of a silent skip.
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
|
||||
with:
|
||||
bun-version: 1.4.0
|
||||
- name: Build matching compiled-host input for required Native containment receipts
|
||||
shell: bash
|
||||
run: bash scripts/ci/compiled-host-test-input.sh
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
name: test-archive-${{ inputs.os }}
|
||||
path: ${{ runner.temp }}/test-archive
|
||||
- name: Place the canonical executable for acceptance tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p target/debug
|
||||
cp "$RUNNER_TEMP"/test-archive/codewhale* target/debug/
|
||||
if [ "$RUNNER_OS" != Windows ]; then chmod +x target/debug/codewhale; fi
|
||||
- name: Run native containment tests
|
||||
if: matrix.shard == 1
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/test-count" "$RUNNER_TEMP/nextest-extract"
|
||||
sh scripts/with-hermetic-test-home.sh cargo nextest run \
|
||||
--archive-file "$RUNNER_TEMP/test-archive/nextest-archive.tar.zst" \
|
||||
--extract-to "$RUNNER_TEMP/nextest-extract" --extract-overwrite \
|
||||
--workspace-remap . --profile ci -E "$NATIVE_FILTER"
|
||||
cp target/nextest/ci/junit.xml "$RUNNER_TEMP/native-junit.xml"
|
||||
python3 -I scripts/ci/nextest-shard-count.py junit "$RUNNER_TEMP/native-junit.xml" > "$RUNNER_TEMP/native-count.txt"
|
||||
- name: Run tests
|
||||
# Same test binaries as `cargo test`, run by cargo-nextest: one
|
||||
# process per test, all runner cores busy, slow tests named instead
|
||||
# of stalling the binary. `.config/nextest.toml` serializes the PTY
|
||||
# binary and bounds the integration binary that spawns the real
|
||||
# executable; retries are off, so a flake is a red run, not a hidden
|
||||
# one. This shard runs partition k of N of everything except the
|
||||
# native containment tests; test-suite proves the shards add up to the
|
||||
# archive's full list.
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/test-count" "$RUNNER_TEMP/nextest-extract"
|
||||
monitor=
|
||||
if [ "$RUNNER_OS" = Linux ]; then
|
||||
# Headroom trace: if the runner is terminated again, the last lines
|
||||
# say whether memory or disk ran out.
|
||||
( while sleep 30; do
|
||||
echo "[headroom] $(free -m | awk '/^Mem:/{print "mem used " $3 "/" $2 " MiB"} /^Swap:/{print "swap used " $3 "/" $2 " MiB"}' | paste -sd ' ') disk free $(df -h / | awk 'NR==2{print $4}')"
|
||||
done ) &
|
||||
monitor=$!
|
||||
fi
|
||||
status=0
|
||||
sh scripts/with-hermetic-test-home.sh cargo nextest run \
|
||||
--archive-file "$RUNNER_TEMP/test-archive/nextest-archive.tar.zst" \
|
||||
--extract-to "$RUNNER_TEMP/nextest-extract" --extract-overwrite \
|
||||
--workspace-remap . --profile ci \
|
||||
--partition "count:${{ matrix.shard }}/${{ inputs.shards }}" \
|
||||
-E "not ($NATIVE_FILTER)" || status=$?
|
||||
if [ -n "$monitor" ]; then kill "$monitor" 2>/dev/null || true; fi
|
||||
if [ "$status" -ne 0 ]; then exit "$status"; fi
|
||||
tests="$(python3 -I scripts/ci/nextest-shard-count.py junit target/nextest/ci/junit.xml)"
|
||||
if [ -f "$RUNNER_TEMP/native-count.txt" ]; then tests=$((tests + $(cat "$RUNNER_TEMP/native-count.txt"))); fi
|
||||
printf '{"os": "%s", "shard": %s, "tests": %s}\n' "${{ inputs.os }}" "${{ matrix.shard }}" "$tests" \
|
||||
> "$RUNNER_TEMP/test-count/shard-${{ inputs.os }}-${{ matrix.shard }}.json"
|
||||
cat "$RUNNER_TEMP/test-count/shard-${{ inputs.os }}-${{ matrix.shard }}.json"
|
||||
- name: Retain sealed terminal failure diagnostics
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: terminal-failures-${{ runner.os }}-${{ matrix.shard }}-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/pty-failures/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
- name: Export exact compiled-image Native containment receipt
|
||||
# JUnit is from the same archive's native invocation: no second
|
||||
# compile. The official entire CI run must be green before release can
|
||||
# consume this artifact. Missing/changed inputs or silent test skips
|
||||
# are red.
|
||||
if: matrix.shard == 1
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
|
||||
git diff --exit-code
|
||||
git diff --cached --exit-code
|
||||
python3 scripts/compiled-host-native-receipt.py \
|
||||
--junit "$RUNNER_TEMP/native-junit.xml" \
|
||||
--compiled-image "$CODEWHALE_COMPILED_HOST_TEST_BINARY" \
|
||||
--bundle crates/engine/extension-host/dist/codewhale-extension-host.mjs \
|
||||
--bun "$CODEWHALE_COMPILED_HOST_TEST_BUN" --source-sha "$GITHUB_SHA" \
|
||||
--image-platform "$CODEWHALE_COMPILED_HOST_TEST_PLATFORM" --image-arch "$CODEWHALE_COMPILED_HOST_TEST_ARCH" \
|
||||
--host-sha256 "$CODEWHALE_COMPILED_HOST_TEST_SHA256" \
|
||||
--runtime-sha256 "$CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256" \
|
||||
--bundle-sha256 "$CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256" \
|
||||
--output "$RUNNER_TEMP/native-compiled-host"
|
||||
- name: Retain exact compiled image and Native receipt
|
||||
if: matrix.shard == 1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: native-compiled-host-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/native-compiled-host/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
- name: Lockfile drift guard
|
||||
run: git diff --exit-code -- Cargo.lock
|
||||
- name: Retain shard test count
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-count-${{ inputs.os }}-${{ matrix.shard }}
|
||||
path: ${{ runner.temp }}/test-count/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
check:
|
||||
# The job that answers "did this OS's whole suite pass": the build and every
|
||||
# shard succeeded, and the shards together ran exactly the tests the archive
|
||||
# lists. Not a required check (§180).
|
||||
name: Suite check
|
||||
needs: [build, shard]
|
||||
if: '!cancelled()'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
- name: Every build and shard succeeded
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_RESULT: ${{ needs.build.result }}
|
||||
SHARD_RESULT: ${{ needs.shard.result }}
|
||||
run: |
|
||||
echo "build: ${BUILD_RESULT}"
|
||||
echo "shard: ${SHARD_RESULT}"
|
||||
if [ "${BUILD_RESULT}" != success ] || [ "${SHARD_RESULT}" != success ]; then
|
||||
echo "::error::The sharded suite did not complete successfully."
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
name: test-expected-${{ inputs.os }}
|
||||
path: counts
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
||||
with:
|
||||
pattern: test-count-${{ inputs.os }}-*
|
||||
path: counts
|
||||
merge-multiple: true
|
||||
- name: Shards ran every listed test exactly once
|
||||
run: python3 -I scripts/ci/nextest-shard-count.py check counts
|
||||
+317
-316
@@ -16,13 +16,32 @@ on:
|
||||
options:
|
||||
- nextest
|
||||
- shared-process-twice
|
||||
crate:
|
||||
description: Scoped run, in a few minutes. One workspace package (for example codewhale-tui); with crate or filter set only the scoped test runs, not the full suite
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
filter:
|
||||
description: Scoped run, in a few minutes. A nextest filterset (for example 'test(auto_review)'), alone or with crate; with crate or filter set only the scoped test runs
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
os:
|
||||
description: Operating systems for a scoped run, comma separated (ubuntu-latest, macos-latest, windows-latest, or all); ignored by a full run
|
||||
required: false
|
||||
default: ubuntu-latest
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A new push to a pull request, or a new dispatch on the same non-main branch
|
||||
# (an agent's ci/<task> branch), cancels the older run still in progress.
|
||||
# Runs on main are keyed by SHA and never cancel each other, and a scoped
|
||||
# dispatch gets its own group so it cannot cancel a full run on the same branch.
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || format('ci-{0}-{1}', github.workflow, github.sha) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || (github.event_name == 'workflow_dispatch' && (inputs.crate != '' || inputs.filter != '') && format('ci-scoped-{0}', github.run_id)) || (github.ref != 'refs/heads/main' && format('ci-ref-{0}', github.ref)) || format('ci-{0}-{1}', github.workflow, github.sha) }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !(github.event_name == 'workflow_dispatch' && (inputs.crate != '' || inputs.filter != '')) }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -45,6 +64,8 @@ jobs:
|
||||
mobile: ${{ steps.detect.outputs.mobile }}
|
||||
actions: ${{ steps.detect.outputs.actions }}
|
||||
trusted: ${{ steps.trust.outputs.trusted }}
|
||||
scoped: ${{ steps.scope.outputs.scoped }}
|
||||
scoped_os: ${{ steps.scope.outputs.scoped_os }}
|
||||
steps:
|
||||
- name: Classify event trust
|
||||
id: trust
|
||||
@@ -64,6 +85,48 @@ jobs:
|
||||
else
|
||||
echo "trusted=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Classify scoped dispatch
|
||||
id: scope
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
SCOPE_CRATE: ${{ inputs.crate }}
|
||||
SCOPE_FILTER: ${{ inputs.filter }}
|
||||
SCOPE_OS: ${{ inputs.os }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# A dispatch with a crate or a filter runs only the scoped test job.
|
||||
# Inputs reach the shell only through env, and the crate and OS
|
||||
# names are validated before anything uses them.
|
||||
scoped=false
|
||||
scoped_os='["ubuntu-latest"]'
|
||||
if [[ "${EVENT_NAME}" == "workflow_dispatch" && ( -n "${SCOPE_CRATE}" || -n "${SCOPE_FILTER}" ) ]]; then
|
||||
scoped=true
|
||||
if [[ -n "${SCOPE_CRATE}" && ! "${SCOPE_CRATE}" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
||||
echo "::error::crate must be a package name (letters, digits, - and _)." >&2
|
||||
exit 1
|
||||
fi
|
||||
oses=()
|
||||
IFS=',' read -r -a requested <<< "${SCOPE_OS:-ubuntu-latest}"
|
||||
for name in "${requested[@]}"; do
|
||||
name="${name//[[:space:]]/}"
|
||||
case "${name}" in
|
||||
ubuntu|ubuntu-latest|linux) oses+=("ubuntu-latest") ;;
|
||||
macos|macos-latest) oses+=("macos-latest") ;;
|
||||
windows|windows-latest) oses+=("windows-latest") ;;
|
||||
all) oses+=("ubuntu-latest" "macos-latest" "windows-latest") ;;
|
||||
"") ;;
|
||||
*)
|
||||
echo "::error::os entries must be ubuntu-latest, macos-latest, windows-latest or all." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ "${#oses[@]}" -eq 0 ]]; then oses=("ubuntu-latest"); fi
|
||||
scoped_os="$(printf '%s\n' "${oses[@]}" | sort -u | jq -R . | jq -sc .)"
|
||||
fi
|
||||
echo "scoped=${scoped}" >> "${GITHUB_OUTPUT}"
|
||||
echo "scoped_os=${scoped_os}" >> "${GITHUB_OUTPUT}"
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -197,6 +260,8 @@ jobs:
|
||||
|
||||
versions:
|
||||
name: Version drift
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -274,6 +339,8 @@ jobs:
|
||||
|
||||
plugin-conversion:
|
||||
name: Plugin conversion
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -293,6 +360,8 @@ jobs:
|
||||
|
||||
integrations:
|
||||
name: Integrations
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -349,7 +418,7 @@ jobs:
|
||||
extension-host-runtimes:
|
||||
name: Extension host runtimes (${{ matrix.os }})
|
||||
needs: changes
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -388,6 +457,8 @@ jobs:
|
||||
|
||||
vscode-extension:
|
||||
name: VS Code extension
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -421,7 +492,7 @@ jobs:
|
||||
safety-gate:
|
||||
name: Safety gate
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -431,16 +502,18 @@ jobs:
|
||||
toolchain: stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# The GitHub Actions cache backend is main-only, mirroring
|
||||
# rust-cache's save-if: PR runs wrote thousands of refs/pull/N
|
||||
# entries that pushed the repo past its 10 GiB cache cap.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# Every run reads the cache main wrote; only main writes it
|
||||
# (SCCACHE_GHA_RW_MODE in Enable sccache), mirroring rust-cache's
|
||||
# save-if. PR runs once wrote thousands of refs/pull/N entries that
|
||||
# pushed the repo past its 10 GiB cache cap.
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
@@ -475,6 +548,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true'
|
||||
# Measured on 89ee629e with runner headroom: clippy 10 min, runtime-contract
|
||||
# budget 16 min, and persistence-backlog still running at the old 45-minute
|
||||
# cancellation. Commands, filters and budgets are unchanged.
|
||||
@@ -494,13 +568,15 @@ jobs:
|
||||
# Cache bootstrap failures (e.g. GitHub 504s fetching the sccache
|
||||
# binary) degrade to an uncached build instead of failing product CI.
|
||||
continue-on-error: true
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && github.ref == 'refs/heads/main'
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
@@ -725,7 +801,7 @@ jobs:
|
||||
# fail the build there while stable stays green (#6543).
|
||||
name: MSRV check (1.89)
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -746,7 +822,7 @@ jobs:
|
||||
workflow-rlm-cache:
|
||||
name: Workflow RLM cache
|
||||
needs: changes
|
||||
if: needs.changes.outputs.workflow == 'true'
|
||||
if: needs.changes.outputs.workflow == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -754,14 +830,15 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -772,261 +849,64 @@ jobs:
|
||||
run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-workflow --locked
|
||||
|
||||
test:
|
||||
name: Test
|
||||
# Manual shared-process qualification only (workspace_test_mode =
|
||||
# shared-process-twice): the GH6698 evidence of two libtest passes at one
|
||||
# exact revision on one hosted Ubuntu runner. Every other run executes the
|
||||
# suite through test-build, test-shard and test-suite below.
|
||||
name: Shared-process tests
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
# Contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
|
||||
# "Test (windows-latest)" derive from job name + matrix.os and are
|
||||
# independent of runs-on. For light changes the macOS/Windows legs only
|
||||
# echo a skip line, so run them on ubuntu instead of queueing for scarce
|
||||
# macOS/Windows runners. Pull requests run only the Linux leg here; the
|
||||
# required "Test (windows-latest)" context comes from test-windows-pr.
|
||||
# The ternary is safe: matrix.os is always a non-empty literal, so
|
||||
# runs-on can never evaluate to empty.
|
||||
# A cold GitHub-hosted Mac spent 77-80 minutes in Test on 2026-09-23
|
||||
# (fork PRs #6431, #6417), too close to the old 90-minute limit.
|
||||
# On 2026-10-04 a PR leg spent 111 minutes compiling with no cache and
|
||||
# was cancelled at 120 with 12,356 tests passed and none failed. The
|
||||
# limit stays a literal (release-workflows.test.js, #5496); 210 bounds
|
||||
# cold compilation and the complete suite without disabling test deadlines.
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice' && needs.changes.outputs.heavy == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
# A cold GitHub-hosted runner spent 111 minutes compiling with no cache on
|
||||
# 2026-10-04. The limit stays a literal (release-workflows.test.js, #5496);
|
||||
# 210 bounds cold compilation and both complete passes.
|
||||
timeout-minutes: 210
|
||||
# macOS legs go to the self-hosted Mac ONLY when all three hold: the
|
||||
# change is heavy, the event is trusted (not a fork PR), and the
|
||||
# CW_SELF_HOSTED_MAC repo variable is 'true'. That variable is the kill
|
||||
# switch: unset it and every leg falls back to GitHub-hosted runners
|
||||
# immediately, with no commit — important because an offline
|
||||
# self-hosted runner queues jobs forever, which is worse than a slow one.
|
||||
runs-on: ${{ needs.changes.outputs.heavy != 'true' && 'ubuntu-latest' || (matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true' && fromJSON('["self-hosted","macOS","ARM64","codewhale-mac"]')) || matrix.os }}
|
||||
strategy:
|
||||
# A failure on one desktop platform must not erase evidence from the
|
||||
# other one. We need both conclusions to diagnose and release safely.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Pull requests run the Linux workspace suite only; the macOS and
|
||||
# Windows suites run in final validation. The explicit manual
|
||||
# libtest qualification uses one hosted Ubuntu job, never a fork PR or
|
||||
# self-hosted runner; normal dispatches keep all three legs.
|
||||
os: ${{ fromJSON((github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')) && '["ubuntu-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Skip tests for light change
|
||||
if: needs.changes.outputs.heavy != 'true'
|
||||
run: echo "No executable Rust changes detected; preserving required Test context."
|
||||
- uses: actions/checkout@v7
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- name: Test Windows installer PATH helper
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/update-user-path.tests.ps1
|
||||
- name: Install NSIS for Windows installer regression
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/install-nsis.ps1
|
||||
- name: Test Windows installer PATH regression
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- name: Install macOS LLVM tools
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
run: rustup component add llvm-tools --toolchain stable
|
||||
- name: Omit macOS build debug metadata
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
# Match the existing full-workspace CNB gate. Keep debug assertions and
|
||||
# every test; only disposable build debug tables change on this lane.
|
||||
run: printf '%s\n' 'CARGO_PROFILE_DEV_DEBUG=0' 'CARGO_PROFILE_TEST_DEBUG=0' >> "$GITHUB_ENV"
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
run: |
|
||||
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
|
||||
sh scripts/prepare-linux-test-sandbox.sh
|
||||
- name: Ubuntu build headroom
|
||||
# Reuse the same fixed hosted-runner provisioning as Safety and Lint.
|
||||
# Exit 143 is a shutdown observation, not proof of OOM or timeout.
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
shell: bash
|
||||
run: bash scripts/prepare-ubuntu-build-headroom.sh
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
with:
|
||||
cache-bin: false
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: taiki-e/install-action@nextest
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- name: Check portable config policy boundary
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -B scripts/test_check_command_config_proof.py
|
||||
sh scripts/check-portable-config.sh
|
||||
- name: Check portable core navigation boundary
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -B scripts/test_check_command_core_navigation_proof.py
|
||||
sh scripts/check-portable-core-navigation.sh
|
||||
- uses: actions/[email protected]
|
||||
# The extension-host integration tests spawn the real bundle under
|
||||
# Node >= 22.19; CODEWHALE_EXT_HOST_TESTS below makes a missing Node a
|
||||
# failure instead of a silent skip.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
with:
|
||||
bun-version: 1.4.0
|
||||
- name: Build matching compiled-host input for required Native containment receipts
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bun_binary="$(bun -p 'process.execPath')"
|
||||
host_binary="$(node -p "require('path').join(process.env.RUNNER_TEMP, 'codewhale-extension-host' + (process.platform === 'win32' ? '.exe' : ''))")"
|
||||
compiled_info="$(node crates/engine/extension-host/compile-host.mjs --bun "$bun_binary" --output "$host_binary")"
|
||||
printf 'CODEWHALE_COMPILED_HOST_TEST_BINARY=%s\n' "$host_binary" >> "$GITHUB_ENV"
|
||||
node - "$host_binary" "$bun_binary" "$compiled_info" >> "$GITHUB_ENV" <<'JS'
|
||||
const fs = require('node:fs'), crypto = require('node:crypto');
|
||||
const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const info = JSON.parse(process.argv[4]);
|
||||
if (!info.platform || !info.arch || info.platform !== info.compiler?.platform || info.arch !== info.compiler?.arch) throw new Error('image/compiler identity mismatch');
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_PLATFORM=' + info.platform);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_ARCH=' + info.arch);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_SHA256=' + digest(process.argv[2]));
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_BUN=' + process.argv[3]);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256=' + digest(process.argv[3]));
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256=' + digest('crates/engine/extension-host/dist/codewhale-extension-host.mjs'));
|
||||
JS
|
||||
# The required hermetic full invocation executes the shared actual
|
||||
# compiled Native secret/history/write scenario on each OS; Windows
|
||||
# additionally requires Node/Bun/compiled LPAC owner/reparse probes.
|
||||
# Missing requested compiled input or a failed sandbox is red.
|
||||
- name: Hold this machine's build lock (self-hosted)
|
||||
# The self-hosted Mac is also a developer machine: local agents build
|
||||
# through scripts/dev-cargo.sh, which holds this lock, and two Cargo
|
||||
# builds at once exhaust its memory. Opt-in: the runner's `.env` names
|
||||
# the same file as CODEWHALE_BUILD_LOCK_FILE. The holder is a
|
||||
# background process; the runner kills orphans when the job ends, so a
|
||||
# cancelled or crashed job cannot leave the lock held.
|
||||
# (`env.*` in `if:` cannot see the runner's `.env`, so the opt-in is
|
||||
# checked in the script.)
|
||||
if: needs.changes.outputs.heavy == 'true' && runner.environment == 'self-hosted'
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -z "${CODEWHALE_BUILD_LOCK_FILE:-}" ]; then
|
||||
echo "CODEWHALE_BUILD_LOCK_FILE is not set on this runner; building without the machine lock."
|
||||
exit 0
|
||||
fi
|
||||
hold="$RUNNER_TEMP/cw-build-lock.hold"
|
||||
ready="$RUNNER_TEMP/cw-build-lock.ready"
|
||||
log="$RUNNER_TEMP/cw-build-lock.log"
|
||||
touch "$hold"
|
||||
rm -f "$ready"
|
||||
# The single quotes are deliberate: the inner sh expands $1/$2.
|
||||
# shellcheck disable=SC2016
|
||||
nohup python3 scripts/build-lock.py "$CODEWHALE_BUILD_LOCK_FILE" -- \
|
||||
sh -c 'touch "$1"; while [ -e "$2" ]; do sleep 2; done' _ "$ready" "$hold" \
|
||||
>"$log" 2>&1 &
|
||||
shown=0
|
||||
until [ -e "$ready" ]; do
|
||||
if [ "$shown" -eq 0 ] && [ -s "$log" ]; then cat "$log"; shown=1; fi
|
||||
sleep 2
|
||||
done
|
||||
cat "$log"
|
||||
# Scripts inside this job already run under the lock.
|
||||
echo "CODEWHALE_BUILD_LOCK_HELD=1" >> "$GITHUB_ENV"
|
||||
run: bash scripts/ci/compiled-host-test-input.sh
|
||||
- name: Build canonical executable for acceptance tests
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
shell: bash
|
||||
run: cargo build -p codewhale-cli --bin codewhale --all-features --locked
|
||||
env:
|
||||
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
- name: Windows Terminal clipboard-to-draft acceptance
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
timeout-minutes: 5
|
||||
shell: pwsh
|
||||
run: node scripts/release/windows-terminal-clipboard.mjs target/debug/codewhale.exe "${{ runner.temp }}/windows-clipboard"
|
||||
- name: Retain Windows Terminal clipboard-to-draft evidence
|
||||
if: ${{ !cancelled() && matrix.os == 'windows-latest' && needs.changes.outputs.heavy == 'true' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: windows-clipboard-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/windows-clipboard/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: Run tests
|
||||
# Same test binaries as `cargo test`, run by cargo-nextest: one
|
||||
# process per test, all runner cores busy, slow tests named instead
|
||||
# of stalling the binary. `.config/nextest.toml` serializes the PTY
|
||||
# binary and bounds the integration binary that spawns the real
|
||||
# executable; retries are off, so a flake is a red run, not a hidden
|
||||
# one. nextest does not run doctests — the next step keeps them.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
shell: bash
|
||||
run: |
|
||||
monitor=
|
||||
if [ "$RUNNER_OS" = Linux ]; then
|
||||
# Headroom trace: if the runner is terminated again, the last lines
|
||||
# say whether memory or disk ran out.
|
||||
( while sleep 30; do
|
||||
echo "[headroom] $(free -m | awk '/^Mem:/{print "mem used " $3 "/" $2 " MiB"} /^Swap:/{print "swap used " $3 "/" $2 " MiB"}' | paste -sd ' ') disk free $(df -h / | awk 'NR==2{print $4}')"
|
||||
done ) &
|
||||
monitor=$!
|
||||
fi
|
||||
status=0
|
||||
sh scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci || status=$?
|
||||
if [ -n "$monitor" ]; then kill "$monitor" 2>/dev/null || true; fi
|
||||
exit "$status"
|
||||
env:
|
||||
CARGO_BUILD_JOBS: '1'
|
||||
QA_PTY_DIAGNOSTICS_DIR: ${{ runner.temp }}/pty-failures
|
||||
CODEWHALE_EXT_HOST_TESTS: '1'
|
||||
CODEWHALE_EXT_HOST_BUN_TESTS: '1'
|
||||
# sccache 0.17 panics resolving its config directory under the
|
||||
# isolated Windows home before Cargo can compile or run any test.
|
||||
# Bypass only that optional cache; keep the full suite and isolation.
|
||||
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
# Give test threads the stack the product gives itself. main.rs runs
|
||||
# the owner thread and every tokio worker at
|
||||
# CODEWHALE_MAIN_STACK_BYTES (32 MiB) because the engine and
|
||||
# runtime-thread futures are genuinely deep. `#[tokio::test]` builds
|
||||
# its own runtime and never sees that, so tests ran the same code on
|
||||
# ~2 MiB (~1 MiB on Windows) — a configuration that never ships.
|
||||
# That gap is what aborted the whole Windows test binary with
|
||||
# STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_
|
||||
# environment_id, masking every other Windows result (78afd8d3d4
|
||||
# Box::pin'd that one frame; the mismatch itself remained). std reads
|
||||
# this for any thread spawned without an explicit size, which covers
|
||||
# both libtest's per-test threads and tokio's workers. Test threads
|
||||
# hold 16 MiB: the engine-only chains they run measured a ~2.5 MiB
|
||||
# debug high-water, while the full ~16.5 MiB UI-loop chain that
|
||||
# forced 32 MiB lives in spawned binaries, which size their own
|
||||
# stacks explicitly and never read this variable.
|
||||
RUST_MIN_STACK: '16777216'
|
||||
- name: Retain sealed terminal failure diagnostics
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: terminal-failures-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/pty-failures/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
- name: Shared-process workspace qualification
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice' && matrix.os == 'ubuntu-latest'
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice'
|
||||
shell: bash
|
||||
env:
|
||||
CARGO_BUILD_JOBS: '1'
|
||||
@@ -1128,83 +1008,193 @@ jobs:
|
||||
cat "$evidence/results.txt"
|
||||
exit "$failed"
|
||||
- name: Retain shared-process qualification evidence
|
||||
if: always() && github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice' && matrix.os == 'ubuntu-latest'
|
||||
if: always() && github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shared-process-workspace-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/shared-process-workspace/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
- name: Export exact compiled-image Native containment receipt
|
||||
# JUnit is from the same full invocation: no second compile/test run.
|
||||
# The official entire CI run must be green before release can consume
|
||||
# this artifact. Missing/changed inputs or silent test skips are red.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
|
||||
# One sharded pipeline per OS (ci-test-suite.yml). Shard counts follow the
|
||||
# measured serial test time of each OS (2026-11-12, run 43216596942: Linux 19
|
||||
# min, macOS 24 min, Windows 25 min of test execution, on top of 30 to 65
|
||||
# minutes of single-job compilation), so a shard is five to eight minutes of
|
||||
# tests plus setup. Hosted macOS is the scarce pool, so it gets three shards.
|
||||
# The caller names ("Sharded tests (...)") never equal the required
|
||||
# "Test (windows-latest)" context, which test-windows-pr still produces on
|
||||
# every pull request.
|
||||
test-ubuntu:
|
||||
name: Sharded tests (ubuntu-latest)
|
||||
needs: changes
|
||||
if: >-
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
needs.changes.outputs.scoped != 'true' &&
|
||||
(github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
uses: ./.github/workflows/ci-test-suite.yml
|
||||
with:
|
||||
os: ubuntu-latest
|
||||
shards: 4
|
||||
shard_list: '[1,2,3,4]'
|
||||
build_jobs: '2'
|
||||
debug: line-tables-only
|
||||
|
||||
test-macos:
|
||||
name: Sharded tests (macos-latest)
|
||||
needs: changes
|
||||
if: >-
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
needs.changes.outputs.scoped != 'true' &&
|
||||
(github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
uses: ./.github/workflows/ci-test-suite.yml
|
||||
with:
|
||||
os: macos-latest
|
||||
shards: 3
|
||||
shard_list: '[1,2,3]'
|
||||
build_jobs: '1'
|
||||
debug: '0'
|
||||
|
||||
test-windows:
|
||||
name: Sharded tests (windows-latest)
|
||||
needs: changes
|
||||
if: >-
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
needs.changes.outputs.scoped != 'true' &&
|
||||
(github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') &&
|
||||
!(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
uses: ./.github/workflows/ci-test-suite.yml
|
||||
with:
|
||||
os: windows-latest
|
||||
shards: 4
|
||||
shard_list: '[1,2,3,4]'
|
||||
build_jobs: '2'
|
||||
debug: '0'
|
||||
|
||||
portable-boundaries:
|
||||
name: Portable boundaries
|
||||
needs: changes
|
||||
if: >-
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
needs.changes.outputs.scoped != 'true' &&
|
||||
(github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
run: |
|
||||
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
|
||||
sh scripts/prepare-linux-test-sandbox.sh
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
|
||||
with:
|
||||
bun-version: 1.4.0
|
||||
- name: Check portable config policy boundary
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -B scripts/test_check_command_config_proof.py
|
||||
sh scripts/check-portable-config.sh
|
||||
- name: Check portable core navigation boundary
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -B scripts/test_check_command_core_navigation_proof.py
|
||||
sh scripts/check-portable-core-navigation.sh
|
||||
|
||||
scoped-test:
|
||||
# workflow_dispatch with `crate` and/or `filter`: one package or one nextest
|
||||
# filterset on the chosen OSes (default ubuntu-latest) instead of the full
|
||||
# suite. `changes` skips every other job for such a run. The build uses the
|
||||
# same flags as the full build; only `-p` and `-E` narrow what runs.
|
||||
name: Scoped test (${{ matrix.os }})
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped == 'true'
|
||||
timeout-minutes: 90
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{ fromJSON(needs.changes.outputs.scoped_os) }}
|
||||
env:
|
||||
CARGO_BUILD_JOBS: ${{ matrix.os == 'macos-latest' && '1' || '2' }}
|
||||
CARGO_PROFILE_DEV_DEBUG: ${{ matrix.os == 'ubuntu-latest' && 'line-tables-only' || '0' }}
|
||||
CARGO_PROFILE_TEST_DEBUG: ${{ matrix.os == 'ubuntu-latest' && 'line-tables-only' || '0' }}
|
||||
CODEWHALE_EXT_HOST_TESTS: '1'
|
||||
CODEWHALE_EXT_HOST_BUN_TESTS: '1'
|
||||
RUST_MIN_STACK: '16777216'
|
||||
SCOPE_CRATE: ${{ inputs.crate }}
|
||||
SCOPE_FILTER: ${{ inputs.filter }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Point terminal failure diagnostics at the runner temp directory
|
||||
shell: bash
|
||||
run: echo "QA_PTY_DIAGNOSTICS_DIR=${RUNNER_TEMP}/pty-failures" >> "${GITHUB_ENV}"
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Linux system dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
|
||||
sh scripts/prepare-linux-test-sandbox.sh
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
save-if: false
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
|
||||
with:
|
||||
bun-version: 1.4.0
|
||||
- name: Build matching compiled-host input for required Native containment receipts
|
||||
shell: bash
|
||||
run: bash scripts/ci/compiled-host-test-input.sh
|
||||
- name: Build canonical executable for acceptance tests
|
||||
# Acceptance tests in the TUI, engine and CLI crates, and any run that
|
||||
# names no crate, look for the codewhale binary next to the test
|
||||
# binaries; a narrow run on another crate does not need it.
|
||||
if: env.SCOPE_CRATE == '' || env.SCOPE_CRATE == 'codewhale-tui' || env.SCOPE_CRATE == 'codewhale-engine' || env.SCOPE_CRATE == 'codewhale-cli'
|
||||
shell: bash
|
||||
run: cargo build -p codewhale-cli --bin codewhale --all-features --locked
|
||||
- name: Run the scoped tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
|
||||
git diff --exit-code
|
||||
git diff --cached --exit-code
|
||||
python3 scripts/compiled-host-native-receipt.py \
|
||||
--junit target/nextest/ci/junit.xml \
|
||||
--compiled-image "$CODEWHALE_COMPILED_HOST_TEST_BINARY" \
|
||||
--bundle crates/engine/extension-host/dist/codewhale-extension-host.mjs \
|
||||
--bun "$CODEWHALE_COMPILED_HOST_TEST_BUN" --source-sha "$GITHUB_SHA" \
|
||||
--image-platform "$CODEWHALE_COMPILED_HOST_TEST_PLATFORM" --image-arch "$CODEWHALE_COMPILED_HOST_TEST_ARCH" \
|
||||
--host-sha256 "$CODEWHALE_COMPILED_HOST_TEST_SHA256" \
|
||||
--runtime-sha256 "$CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256" \
|
||||
--bundle-sha256 "$CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256" \
|
||||
--output "$RUNNER_TEMP/native-compiled-host"
|
||||
- name: Retain exact compiled image and Native receipt
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
args=(--all-features --locked --profile ci --no-tests=fail)
|
||||
if [ -n "${SCOPE_CRATE}" ]; then args+=(-p "${SCOPE_CRATE}"); else args+=(--workspace); fi
|
||||
if [ -n "${SCOPE_FILTER}" ]; then args+=(-E "${SCOPE_FILTER}"); fi
|
||||
sh scripts/with-hermetic-test-home.sh cargo nextest run "${args[@]}"
|
||||
- name: Retain sealed terminal failure diagnostics
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: native-compiled-host-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/native-compiled-host/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
- name: Run doctests
|
||||
# The shared-process command already includes every workspace doctest.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
|
||||
shell: bash
|
||||
run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc
|
||||
env:
|
||||
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
RUST_MIN_STACK: '16777216'
|
||||
# The Ubuntu lint lane validates non-RSS backlog fields. Run the same
|
||||
# source-bound measurement on macOS so loss or growth of RSS evidence
|
||||
# fails closed instead of becoming an unsupported-field skip.
|
||||
# Only on the self-hosted Mac: there it reuses the warm build. Every
|
||||
# hosted Mac leg (fork PRs, or any PR with CW_SELF_HOSTED_MAC off) runs
|
||||
# it in the separate `macos-budget` job with its own timeout, because
|
||||
# on a cold hosted Mac it pushed Test past 90 minutes.
|
||||
- name: Check persistence-backlog RSS budget
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'
|
||||
run: python3 scripts/check-persistence-backlog-budget.py
|
||||
- name: Lockfile drift guard
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
run: git diff --exit-code -- Cargo.lock
|
||||
- name: Run Offline Eval Harness
|
||||
# The eval harness is OS-independent prompt/composition checking;
|
||||
# running it once (on the faster macOS leg, warm from the test build)
|
||||
# instead of once per desktop OS keeps the coverage while taking
|
||||
# ~2min off the Windows critical path. Self-hosted Mac only; hosted
|
||||
# Mac legs run it in `macos-budget` (see the RSS step above).
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'
|
||||
run: cargo run -p codewhale-cli --bin codewhale --all-features -- eval
|
||||
- name: sccache stats
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: sccache --show-stats
|
||||
- name: Linux test location (CNB)
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request'
|
||||
run: echo "Linux workspace tests run on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu."
|
||||
- name: Release this machine's build lock (self-hosted)
|
||||
if: always() && runner.environment == 'self-hosted'
|
||||
shell: bash
|
||||
run: rm -f "$RUNNER_TEMP/cw-build-lock.hold"
|
||||
name: terminal-failures-scoped-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/pty-failures/
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
test-windows-pr:
|
||||
name: Test (windows-latest)
|
||||
@@ -1239,7 +1229,7 @@ jobs:
|
||||
echo "## Test (windows-latest) on a pull request"
|
||||
echo
|
||||
echo "Compiled every workspace target for Windows with warnings denied and ran the installer PATH helper tests."
|
||||
echo "The Windows and macOS test suites run in final validation, before a release, not on pull requests."
|
||||
echo "The full Windows test suite runs beside this check as Sharded tests (windows-latest); it is not a required check."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
macos-budget:
|
||||
@@ -1252,15 +1242,15 @@ jobs:
|
||||
# macOS leg runs on the self-hosted Mac (trusted event and
|
||||
# CW_SELF_HOSTED_MAC == 'true'), it runs these two steps on the warm
|
||||
# build instead. The name keeps "(fork PR)" so check contexts stay stable.
|
||||
# Hosted macOS allows only five concurrent jobs per account, and this job
|
||||
# doubled every pull request's claim on them, queueing the required Test
|
||||
# (macos-latest) leg for hours. So pull requests skip it and every push to
|
||||
# main still runs it; a regression shows up on the merge that caused it.
|
||||
# Hosted macOS allows only five concurrent jobs per account, and the macOS
|
||||
# test build and shards already take most of them, so pull requests skip
|
||||
# this job and every dispatch (main-landing issues one per push to main)
|
||||
# still runs it; a regression shows up on the merge that caused it. The
|
||||
# sharded macOS suite never runs on the self-hosted Mac, so these gates no
|
||||
# longer depend on CW_SELF_HOSTED_MAC.
|
||||
name: macOS budget and eval (fork PR)
|
||||
needs: changes
|
||||
# The explicit Ubuntu-only qualification has no warm Test macOS leg, so
|
||||
# retain these gates on the hosted fallback even when self-hosting is on.
|
||||
if: needs.changes.outputs.heavy == 'true' && github.event_name != 'pull_request' && ((github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice') || !(needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'))
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.changes.outputs.scoped != 'true' && github.event_name != 'pull_request'
|
||||
# Cold hosted builds consumed 73 minutes before eval could start (F run
|
||||
# 37497498829). Match Test's compile allowance; RSS/eval limits stay intact.
|
||||
timeout-minutes: 210
|
||||
@@ -1282,9 +1272,10 @@ jobs:
|
||||
npm-wrapper-smoke:
|
||||
name: npm wrapper smoke
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
# Same ternary rationale as the Test job: light legs only echo, so keep
|
||||
# them off macOS/Windows runners. On pull_request the matrix is
|
||||
if: needs.changes.outputs.scoped != 'true' && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')
|
||||
# Light legs only echo, so keep them off macOS/Windows runners. The
|
||||
# ternary is safe: matrix.os is always a non-empty literal. On
|
||||
# pull_request the matrix is
|
||||
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
|
||||
# context is unaffected. Heavy pull requests execute the Ubuntu smoke
|
||||
# here; their branches may not be mirrored to CNB.
|
||||
@@ -1310,13 +1301,15 @@ jobs:
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
|
||||
- name: Enable sccache
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
|
||||
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' && matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- uses: actions/[email protected]
|
||||
@@ -1358,8 +1351,14 @@ jobs:
|
||||
mobile-smoke:
|
||||
name: Mobile runtime smoke
|
||||
needs: changes
|
||||
# Manual (workflow_dispatch) only; the release candidate builds android-arm64.
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
# Not a required PR context. Pull requests run it only when the mobile
|
||||
# runtime surface changed (see the `mobile` filter above); every dispatch,
|
||||
# which main-landing issues for each push to main, runs it unconditionally
|
||||
# as the pre-release safety net.
|
||||
if: >-
|
||||
needs.changes.outputs.scoped != 'true' &&
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
(github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -1367,14 +1366,15 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# Every run reads the GitHub Actions cache main wrote; only main writes (see Enable sccache).
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
|
||||
if [ "${GITHUB_REF}" = refs/heads/main ]; then rw=READ_WRITE; else rw=READ_ONLY; fi
|
||||
echo "SCCACHE_GHA_RW_MODE=${rw}" >> "${GITHUB_ENV}"
|
||||
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
|
||||
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
|
||||
- name: Install Linux system dependencies
|
||||
@@ -1401,7 +1401,7 @@ jobs:
|
||||
actionlint:
|
||||
name: Workflow lint
|
||||
needs: changes
|
||||
if: needs.changes.outputs.actions == 'true'
|
||||
if: needs.changes.outputs.actions == 'true' && needs.changes.outputs.scoped != 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -1418,7 +1418,8 @@ jobs:
|
||||
# Check documentation builds without warnings
|
||||
docs:
|
||||
name: Documentation
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
needs: changes
|
||||
if: needs.changes.outputs.scoped != 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -1439,17 +1440,17 @@ jobs:
|
||||
cargo-deny:
|
||||
name: cargo-deny
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.scoped != 'true'
|
||||
uses: ./.github/workflows/cargo-deny.yml
|
||||
|
||||
security-audit:
|
||||
name: Security audit
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.scoped != 'true'
|
||||
uses: ./.github/workflows/security-audit.yml
|
||||
|
||||
ohos:
|
||||
name: OpenHarmony
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.scoped != 'true'
|
||||
uses: ./.github/workflows/ohos.yml
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
- name: Enable sccache
|
||||
if: steps.sccache.outcome == 'success'
|
||||
if: steps.sccache.outcome == 'success' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
|
||||
@@ -11,11 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
|
||||
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
|
||||
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
|
||||
- **Model thinking:** New installs show model thinking by default: it streams live in the transcript and collapses under a "Thought for …" header when the answer starts. Existing installs keep their saved setting; turn it on under Model reasoning in chat in /config.
|
||||
- **Footer clock:** The footer clock and phase now say "thinking", matching the transcript.
|
||||
|
||||
+16
-9
@@ -165,7 +165,7 @@ before starting a change; preserve any local work when updating your branch.
|
||||
|
||||
- Write tests for new functionality
|
||||
- Run the tests near your change (see [Fast local loop](#fast-local-loop));
|
||||
your pull request runs the Linux workspace suite and a Windows build for
|
||||
your pull request runs the full test suite on Linux, macOS and Windows for
|
||||
you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))
|
||||
- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`
|
||||
modules), and add integration tests under the owning crate's `tests/`
|
||||
@@ -174,8 +174,8 @@ before starting a change; preserve any local work when updating your branch.
|
||||
|
||||
### Pre-push verification
|
||||
|
||||
These are the Rust commands your pull request runs on Linux (the `Lint` and
|
||||
`Test (ubuntu-latest)` checks). You do not need all of them before every
|
||||
These are the Rust commands your pull request runs on Linux (the `Lint` check
|
||||
and the Linux shards of `Sharded tests (ubuntu-latest)`). You do not need all of them before every
|
||||
push: run `cargo fmt`, then check and test the crates you touched (see
|
||||
[Fast local loop](#fast-local-loop)). Run the full set locally when a change
|
||||
spans many crates, or let the pull request run it for you:
|
||||
@@ -215,7 +215,7 @@ cargo install cargo-nextest --locked # once
|
||||
scripts/dev-test.sh tui
|
||||
scripts/dev-cargo.sh nextest run --workspace --all-features --locked
|
||||
|
||||
# 4. The Linux suite your PR runs:
|
||||
# 4. The suite your PR runs on Linux (hosted CI runs it as parallel shards):
|
||||
cargo test --workspace --all-features --locked
|
||||
```
|
||||
|
||||
@@ -340,6 +340,9 @@ maintainer to approve the first run for a brand-new GitHub account.
|
||||
| --- | --- | --- |
|
||||
| `Lint` (required) | `cargo fmt --check`, clippy on every target, and the fast drift checks (locales, READMEs, provider registry, dead-code and blocking-call budgets) | Run `cargo fmt --all` and the clippy command in [Pre-push verification](#pre-push-verification); the log names the file and the check |
|
||||
| `Test (windows-latest)` (required) | Compiles every workspace target for Windows with warnings denied, and the installer PATH helper tests | Usually a `cfg(windows)` path or an import only Windows sees; the log has the compiler error |
|
||||
| `Sharded tests (ubuntu-latest)`, `(macos-latest)`, `(windows-latest)` | The full workspace test suite, built once per OS and run as parallel shards of one nextest archive, plus the doctests; each OS ends with a `Suite check` that fails if the shards did not run every test exactly once | Open the failing `Shard k/N` job; the log names the test. The same test reproduces locally with `cargo nextest run -E 'test(name)'` |
|
||||
| `Safety gate`, `MSRV check (1.89)`, `npm wrapper smoke`, `Portable boundaries` | The hermetic safety and authorization tests, a build on the minimum Rust version, the npm wrapper install smoke and the portable-core boundary checks | The log names the failing test or the compiler error |
|
||||
| `Mobile runtime smoke` | The `codewhale serve --mobile` HTTP and event-stream smoke, only when the mobile runtime surface changed | The log shows the failing request |
|
||||
| `Version drift` | Version files, the `feat:` release-note rule, release helper tests and contributor credit | The credit step only reports: a maintainer adds your credit when the PR merges |
|
||||
|
||||
Smaller checks cover the VS Code extension, integrations, the extension host
|
||||
@@ -347,11 +350,15 @@ and plugin conversion, plus workflow lint when you change `.github/workflows`.
|
||||
A documentation-only change skips the Rust builds but still reports the
|
||||
required checks, so it can merge.
|
||||
|
||||
Your pull request does not run the test suites, the safety gate, MSRV, the npm
|
||||
wrapper smoke, the slow runtime-contract and persistence-backlog measurements,
|
||||
the dependency audits or the OpenHarmony build. Maintainers run those once per
|
||||
release, on the exact commit being released. If that run finds a problem your
|
||||
change introduced, we fix it on `main`; we will not ask you to chase it.
|
||||
Only `Lint` and `Test (windows-latest)` are required. Every other job above is
|
||||
informational for a contributor's pull request: a red one tells you and the
|
||||
maintainers something, and never blocks the merge. Your pull request does not
|
||||
run the slow runtime-contract and persistence-backlog measurements, the macOS
|
||||
budget and eval job, the dependency audits or the OpenHarmony build; those run
|
||||
on `main` and for each release, on the exact commit. If one of them finds a
|
||||
problem your change introduced, we fix it on `main`; we will not ask you to
|
||||
chase it. A newer push to your branch cancels the older run of the same pull
|
||||
request, so the runners always work on your latest commit.
|
||||
|
||||
A red check that your change did not cause is not your job to fix. We rerun it
|
||||
or fix it on `main`, and we never close a PR because CI is slow or flaky. Your
|
||||
|
||||
Generated
-1
@@ -1179,7 +1179,6 @@ dependencies = [
|
||||
"agent-client-protocol",
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -605,7 +605,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
def(
|
||||
"show_thinking",
|
||||
SettingKind::Bool(SHOW_THINKING),
|
||||
"true",
|
||||
"false",
|
||||
ui(
|
||||
TAB_APPEARANCE,
|
||||
"display",
|
||||
@@ -1546,12 +1546,6 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
"false",
|
||||
None,
|
||||
),
|
||||
def(
|
||||
"account_preferences_notice_shown",
|
||||
SettingKind::String,
|
||||
"",
|
||||
None,
|
||||
),
|
||||
// Round 3 work-bar placement migration ran once (`top` → `bottom`).
|
||||
def(
|
||||
"work_surface_bottom_migrated",
|
||||
|
||||
@@ -219,9 +219,6 @@ libc = "0.2"
|
||||
windows-sys = { version = "0.61.2", features = ["Wdk_Storage_FileSystem", "Win32_System_IO", "Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Isolation", "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Pipes", "Win32_System_Threading", "Win32_System_SystemServices", "Win32_System_WindowsProgramming"] }
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows", all(target_os = "linux", not(target_env = "ohos"))))'.dependencies]
|
||||
arboard = { version = "3.4", features = ["wayland-data-control"] }
|
||||
|
||||
[target.'cfg(not(target_env = "ohos"))'.dependencies]
|
||||
portable-pty = "0.9"
|
||||
|
||||
|
||||
@@ -1669,10 +1669,6 @@ pub trait McpTransport: Send + Sync {
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>>;
|
||||
|
||||
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
self.send(msg).await
|
||||
}
|
||||
|
||||
/// Record the protocol revision negotiated at `initialize`. Only the
|
||||
/// Streamable HTTP transport uses it (the `MCP-Protocol-Version` header on
|
||||
/// subsequent requests); stdio and legacy SSE have no header channel, so
|
||||
@@ -3129,30 +3125,6 @@ impl McpConnection {
|
||||
result
|
||||
}
|
||||
|
||||
async fn answer_server_request(&mut self, reply: &serde_json::Value) {
|
||||
let result = match serde_json::to_vec(reply) {
|
||||
Ok(bytes) => {
|
||||
let cancel_token = self.cancel_token.clone();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel_token.cancelled() => {
|
||||
Err(anyhow::anyhow!("MCP connection was cancelled"))
|
||||
}
|
||||
result = self.transport.send_reply(bytes) => result,
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
};
|
||||
if let Err(err) = result {
|
||||
tracing::debug!(
|
||||
target: "mcp",
|
||||
server = %self.name,
|
||||
error = %err,
|
||||
"could not answer an MCP server request"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handshake and discovery receive: each frame wait is bounded by the read
|
||||
/// knob, and a server that stays silent past it is treated as dead.
|
||||
async fn recv(&mut self, expected_id: String) -> Result<serde_json::Value> {
|
||||
@@ -3230,22 +3202,6 @@ impl McpConnection {
|
||||
{
|
||||
self.catalog_stale = true;
|
||||
}
|
||||
if let Some(method) = value.get("method") {
|
||||
if let Some(id) = value.get("id") {
|
||||
let method = method.as_str().unwrap_or_default();
|
||||
let reply = if method == "ping" {
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": id, "result": {}})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {"code": -32601, "message": format!("Method not found: {method}")}
|
||||
})
|
||||
};
|
||||
self.answer_server_request(&reply).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if response_id_matches(value.get("id"), &expected_id) {
|
||||
// Marks the connection stale so it is rebuilt, but this is a
|
||||
// reply to the request, so it never qualifies for a replay.
|
||||
|
||||
@@ -177,21 +177,6 @@ impl McpTransport for HttpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
match &mut self.mode {
|
||||
HttpTransportMode::Streamable(transport) => {
|
||||
transport.send(msg).await.map_err(|err| match err {
|
||||
StreamableSendError::Incompatible(detail)
|
||||
| StreamableSendError::StaleSession(detail) => anyhow::anyhow!(
|
||||
"MCP Streamable HTTP refused a reply to a server request: {detail}"
|
||||
),
|
||||
StreamableSendError::Other(err) => err,
|
||||
})
|
||||
}
|
||||
HttpTransportMode::Sse(transport) => transport.send(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>> {
|
||||
match &mut self.mode {
|
||||
HttpTransportMode::Streamable(transport) => transport.recv().await,
|
||||
|
||||
@@ -3314,80 +3314,6 @@ async fn call_method_skips_notifications_and_unmatched_responses() {
|
||||
assert_eq!(sent[0]["method"], "tools/call");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_answers_server_requests_instead_of_taking_them_as_the_reply() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
let transport = ScriptedValueTransport {
|
||||
sent: Arc::clone(&sent),
|
||||
responses: VecDeque::from([
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": "1", "method": "roots/list"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
|
||||
]),
|
||||
};
|
||||
let mut conn = test_connection(Box::new(transport));
|
||||
|
||||
let result = conn
|
||||
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
let sent = sent.lock().unwrap();
|
||||
assert_eq!(sent.len(), 3);
|
||||
assert_eq!(
|
||||
sent[1],
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}})
|
||||
);
|
||||
assert_eq!(sent[2]["id"], "1");
|
||||
assert_eq!(sent[2]["error"]["code"], -32601);
|
||||
}
|
||||
|
||||
struct ReplyRefusingTransport {
|
||||
sent: Arc<Mutex<Vec<serde_json::Value>>>,
|
||||
responses: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl McpTransport for ReplyRefusingTransport {
|
||||
async fn send(&mut self, msg: Vec<u8>) -> Result<()> {
|
||||
let msg: serde_json::Value = serde_json::from_slice(&msg)?;
|
||||
anyhow::ensure!(msg.get("method").is_some(), "status=405 Method Not Allowed");
|
||||
self.sent.lock().unwrap().push(msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Result<Vec<u8>> {
|
||||
self.responses
|
||||
.pop_front()
|
||||
.context("scripted transport exhausted")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_keeps_its_reply_when_answering_a_server_request_fails() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
let transport = ReplyRefusingTransport {
|
||||
sent: Arc::clone(&sent),
|
||||
responses: VecDeque::from([
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
|
||||
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
|
||||
]),
|
||||
};
|
||||
let mut conn = test_connection(Box::new(transport));
|
||||
|
||||
let result = conn
|
||||
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
assert_eq!(conn.state, ConnectionState::Ready);
|
||||
let sent = sent.lock().unwrap();
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert_eq!(sent[0]["method"], "tools/call");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_method_invalid_json_includes_server_output_preview() {
|
||||
let sent = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -6844,60 +6770,6 @@ async fn custom_headers_applied_to_get_preflight() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refused_reply_to_a_server_request_does_not_fall_back_to_sse() {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let _env = crate::test_support::lock_test_env();
|
||||
let _lock = lock_mcp_loopback_tests().await;
|
||||
let _no_proxy = crate::test_support::EnvVarGuard::set("NO_PROXY", "*,127.0.0.1");
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let requests_srv = Arc::clone(&requests);
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
loop {
|
||||
let (mut socket, _) = listener.accept().await.unwrap();
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = socket.read(&mut buf).await.unwrap();
|
||||
let request_line = String::from_utf8_lossy(&buf[..n])
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
requests_srv.lock().unwrap().push(request_line);
|
||||
socket
|
||||
.write_all(
|
||||
b"HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let url = format!("http://{addr}/mcp");
|
||||
let mut transport = HttpTransport::new(
|
||||
test_mcp_http_client(&url),
|
||||
url,
|
||||
tokio_util::sync::CancellationToken::new(),
|
||||
Duration::from_secs(2),
|
||||
);
|
||||
|
||||
let result = transport
|
||||
.send_reply(json_frame(
|
||||
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}}),
|
||||
))
|
||||
.await;
|
||||
|
||||
server.abort();
|
||||
assert!(result.is_err(), "a refused reply reports the refusal");
|
||||
let requests = requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 1, "requests seen: {requests:?}");
|
||||
assert!(requests[0].starts_with("POST "), "{requests:?}");
|
||||
}
|
||||
|
||||
// === add_runtime_server_config conflict tests ===
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -244,38 +244,22 @@ pub async fn capture(
|
||||
if let Some(snapshot) = supplied {
|
||||
return snapshot.render().map(Some);
|
||||
}
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
if let Some(message) = TEST_ACCOUNT_LOAD_FAILURE.get() {
|
||||
anyhow::bail!(message);
|
||||
}
|
||||
if !account_api_base_is_loopback() {
|
||||
return Ok(crate::prompts::load_user_constitution_block());
|
||||
}
|
||||
}
|
||||
// Unit tests never read an operator's account credentials. Transport tests
|
||||
// exercise load_from with an explicitly isolated store instead.
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
if let Some(snapshot) = load(profile).await? {
|
||||
return snapshot.render().map(Some);
|
||||
}
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
let _ = profile;
|
||||
if let Some(message) = TEST_ACCOUNT_LOAD_FAILURE.get() {
|
||||
anyhow::bail!(message);
|
||||
}
|
||||
}
|
||||
Ok(crate::prompts::load_user_constitution_block())
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn account_api_base_is_loopback() -> bool {
|
||||
std::env::var(codewhale_secrets::account::ACCOUNT_API_BASE_ENV)
|
||||
.ok()
|
||||
.and_then(|value| normalize_runtime_account_api_base(&value))
|
||||
.and_then(|base| reqwest::Url::parse(&base).ok())
|
||||
.and_then(|url| url.host_str().map(str::to_owned))
|
||||
.is_some_and(|host| {
|
||||
host.eq_ignore_ascii_case("localhost")
|
||||
|| host
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.parse::<std::net::IpAddr>()
|
||||
.is_ok_and(|address| address.is_loopback())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
thread_local! {
|
||||
static TEST_ACCOUNT_LOAD_FAILURE: std::cell::Cell<Option<&'static str>> =
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "actiu",
|
||||
"ExtensionPromptUnavailable": "Les instruccions de les extensions no estan disponibles; consulta /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "No s'han pogut carregar les preferències del compte de Codewhale; aquest torn fa servir la configuració local. La clau del proveïdor continua funcionant; executa codewhale login per recuperar les preferències del compte.",
|
||||
"AccountPreferencesNotLoaded": "Compte no carregat. Executa {command}. La clau del proveïdor continua funcionant.",
|
||||
"ComposerOversizedSubmitHeld": "No s’ha enviat: el missatge supera els {limit} caràcters i no s’ha pogut desar en un fitxer de text enganxat ({error}). El text complet continua al camp d’entrada: escurça’l o habilita l’escriptura a .codewhale/pastes i torna a prémer Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "No s’ha enviat: supera els {limit} caràcters; no s’ha desat el fitxer d’enganxat.",
|
||||
"CmdPluginOwnerReport": "Amfitrió d’extensions:\n Estat: {state}\n Eines actives ({count}): {tools}",
|
||||
@@ -1668,7 +1667,7 @@
|
||||
"PhaseIdle": "inactiu",
|
||||
"PhaseDraft": "esborrany",
|
||||
"PhaseWorking": "treballant",
|
||||
"PhaseReasoning": "pensant",
|
||||
"PhaseReasoning": "raonant",
|
||||
"PhaseReading": "llegint",
|
||||
"PhaseUsingTool": "usant una eina",
|
||||
"PhaseSubagents": "subagents en execució",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "aktiv",
|
||||
"ExtensionPromptUnavailable": "Anweisungen von Erweiterungen sind nicht verfügbar; prüfe /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Die Codewhale-Kontoeinstellungen konnten nicht geladen werden; dieser Durchlauf nutzt deine lokalen Einstellungen. Dein Anbieter-Schlüssel funktioniert weiter; führe codewhale login aus, um die Kontoeinstellungen wiederherzustellen.",
|
||||
"AccountPreferencesNotLoaded": "Konto nicht geladen. Führe {command} aus. Dein Anbieter-Schlüssel funktioniert weiter.",
|
||||
"ComposerOversizedSubmitHeld": "Nicht gesendet: Diese Nachricht enthält mehr als {limit} Zeichen und konnte nicht als Einfügedatei gespeichert werden ({error}). Der vollständige Text steht noch im Eingabefeld: Kürze ihn oder mache .codewhale/pastes beschreibbar und drücke dann erneut Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Nicht gesendet: über {limit} Zeichen; Einfügedatei nicht gespeichert.",
|
||||
"CmdPluginOwnerReport": "Erweiterungshost:\n Status: {state}\n Aktive Werkzeuge ({count}): {tools}",
|
||||
@@ -1668,7 +1667,7 @@
|
||||
"PhaseIdle": "ruhend",
|
||||
"PhaseDraft": "entwurf",
|
||||
"PhaseWorking": "arbeitet",
|
||||
"PhaseReasoning": "denkt nach",
|
||||
"PhaseReasoning": "überlegt",
|
||||
"PhaseReading": "liest",
|
||||
"PhaseUsingTool": "nutzt Tool",
|
||||
"PhaseSubagents": "Subagenten laufen",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "live",
|
||||
"ExtensionPromptUnavailable": "Extension instructions are unavailable; inspect /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale account preferences could not be loaded, so this turn uses your local settings. Your provider key still works; run codewhale login to restore account preferences.",
|
||||
"AccountPreferencesNotLoaded": "Account not loaded. Run {command}. Your provider key still works.",
|
||||
"ComposerOversizedSubmitHeld": "Not sent: this message is over {limit} characters and could not be saved as a paste file ({error}). The full text is still in the composer: shorten it, or make .codewhale/pastes writable, then press Enter again.",
|
||||
"ComposerOversizedSubmitHeldShort": "Not sent: over {limit} characters; paste file not saved.",
|
||||
"CmdPluginOwnerReport": "Extension host:\n State: {state}\n Live tools ({count}): {tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "idle",
|
||||
"PhaseDraft": "draft",
|
||||
"PhaseWorking": "working",
|
||||
"PhaseReasoning": "thinking",
|
||||
"PhaseReasoning": "reasoning",
|
||||
"PhaseReading": "reading",
|
||||
"PhaseUsingTool": "using tool",
|
||||
"PhaseSubagents": "agents underway",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "activo",
|
||||
"ExtensionPromptUnavailable": "Las instrucciones de las extensiones no están disponibles; revisa /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "No se pudieron cargar las preferencias de la cuenta de Codewhale; este turno usa tu configuración local. Tu clave del proveedor sigue funcionando; ejecuta codewhale login para restaurar las preferencias de la cuenta.",
|
||||
"AccountPreferencesNotLoaded": "Cuenta no cargada. Ejecuta {command}. Tu clave del proveedor sigue funcionando.",
|
||||
"ComposerOversizedSubmitHeld": "No se envió: este mensaje supera los {limit} caracteres y no se pudo guardar en un archivo de texto pegado ({error}). El texto completo sigue en el campo de entrada: acórtalo o habilita la escritura en .codewhale/pastes y vuelve a presionar Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "No se envió: supera los {limit} caracteres; no se guardó el archivo pegado.",
|
||||
"CmdPluginOwnerReport": "Host de extensiones:\n Estado: {state}\n Herramientas activas ({count}): {tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "inactivo",
|
||||
"PhaseDraft": "borrador",
|
||||
"PhaseWorking": "trabajando",
|
||||
"PhaseReasoning": "pensando",
|
||||
"PhaseReasoning": "razonando",
|
||||
"PhaseReading": "leyendo",
|
||||
"PhaseUsingTool": "usando herramienta",
|
||||
"PhaseSubagents": "subagentes en curso",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "actif",
|
||||
"ExtensionPromptUnavailable": "Les instructions des extensions sont indisponibles ; consultez /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Les préférences du compte Codewhale n'ont pas pu être chargées ; ce tour utilise vos réglages locaux. Votre clé de fournisseur fonctionne toujours ; lancez codewhale login pour rétablir les préférences du compte.",
|
||||
"AccountPreferencesNotLoaded": "Compte non chargé. Lancez {command}. Votre clé de fournisseur fonctionne toujours.",
|
||||
"ComposerOversizedSubmitHeld": "Non envoyé : ce message dépasse {limit} caractères et n’a pas pu être enregistré dans un fichier de texte collé ({error}). Le texte intégral reste dans la zone de saisie : raccourcissez-le ou autorisez l’écriture dans .codewhale/pastes, puis appuyez à nouveau sur Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Non envoyé : plus de {limit} caractères ; fichier collé non enregistré.",
|
||||
"CmdPluginOwnerReport": "Hôte des extensions :\n État : {state}\n Outils actifs ({count}) : {tools}",
|
||||
@@ -1668,7 +1667,7 @@
|
||||
"PhaseIdle": "inactif",
|
||||
"PhaseDraft": "brouillon",
|
||||
"PhaseWorking": "travail en cours",
|
||||
"PhaseReasoning": "réflexion",
|
||||
"PhaseReasoning": "raisonnement",
|
||||
"PhaseReading": "lecture",
|
||||
"PhaseUsingTool": "utilisation d'outil",
|
||||
"PhaseSubagents": "sous-agents en cours",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "सक्रिय",
|
||||
"ExtensionPromptUnavailable": "एक्सटेंशन के निर्देश उपलब्ध नहीं हैं; /plugin show से जाँचें।",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale खाते की प्राथमिकताएँ लोड नहीं हो सकीं, इसलिए यह टर्न आपकी स्थानीय सेटिंग्स का उपयोग करता है। आपकी प्रदाता कुंजी काम करती रहेगी; खाते की प्राथमिकताएँ वापस लाने के लिए codewhale login चलाएँ।",
|
||||
"AccountPreferencesNotLoaded": "खाता लोड नहीं हुआ। {command} चलाएँ। आपकी प्रदाता कुंजी काम करती रहेगी।",
|
||||
"ComposerOversizedSubmitHeld": "नहीं भेजा गया: इस संदेश में {limit} से अधिक वर्ण हैं और इसे पेस्ट फ़ाइल के रूप में सहेजा नहीं जा सका ({error})। पूरा पाठ अभी भी इनपुट बॉक्स में है: इसे छोटा करें या .codewhale/pastes में लिखने की अनुमति दें, फिर Enter दबाएँ।",
|
||||
"ComposerOversizedSubmitHeldShort": "नहीं भेजा गया: {limit} से अधिक वर्ण; पेस्ट फ़ाइल सहेजी नहीं गई।",
|
||||
"CmdPluginOwnerReport": "एक्सटेंशन होस्ट:\n स्थिति: {state}\n सक्रिय टूल ({count}): {tools}",
|
||||
@@ -1668,7 +1667,7 @@
|
||||
"PhaseIdle": "निष्क्रिय",
|
||||
"PhaseDraft": "मसौदा",
|
||||
"PhaseWorking": "काम चल रहा",
|
||||
"PhaseReasoning": "सोच रहा",
|
||||
"PhaseReasoning": "तर्क चल रहा",
|
||||
"PhaseReading": "पढ़ रहा",
|
||||
"PhaseUsingTool": "टूल इस्तेमाल हो रहा",
|
||||
"PhaseSubagents": "सबएजेंट चल रहे हैं",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "aktif",
|
||||
"ExtensionPromptUnavailable": "Instruksi ekstensi tidak tersedia; periksa /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Preferensi akun Codewhale tidak dapat dimuat, jadi giliran ini memakai pengaturan lokal. Kunci penyedia tetap berfungsi; jalankan codewhale login untuk memulihkan preferensi akun.",
|
||||
"AccountPreferencesNotLoaded": "Akun tidak dimuat. Jalankan {command}. Kunci penyedia tetap berfungsi.",
|
||||
"ComposerOversizedSubmitHeld": "Belum terkirim: pesan ini melebihi {limit} karakter dan tidak dapat disimpan sebagai berkas teks tempelan ({error}). Teks lengkap masih ada di kolom input: persingkat teks atau izinkan penulisan ke .codewhale/pastes, lalu tekan Enter lagi.",
|
||||
"ComposerOversizedSubmitHeldShort": "Belum terkirim: melebihi {limit} karakter; berkas tempelan tidak tersimpan.",
|
||||
"CmdPluginOwnerReport": "Host ekstensi:\n Status: {state}\n Alat aktif ({count}): {tools}",
|
||||
@@ -1668,7 +1667,7 @@
|
||||
"PhaseIdle": "diam",
|
||||
"PhaseDraft": "draf",
|
||||
"PhaseWorking": "bekerja",
|
||||
"PhaseReasoning": "berpikir",
|
||||
"PhaseReasoning": "menalar",
|
||||
"PhaseReading": "membaca",
|
||||
"PhaseUsingTool": "memakai tool",
|
||||
"PhaseSubagents": "subagen berjalan",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "稼働中",
|
||||
"ExtensionPromptUnavailable": "拡張機能の指示を利用できません。/plugin show で確認してください。",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale アカウントの設定を読み込めなかったため、このターンはローカル設定を使います。プロバイダーキーはそのまま使えます。アカウント設定を戻すには codewhale login を実行してください。",
|
||||
"AccountPreferencesNotLoaded": "アカウントを読み込めませんでした。{command} を実行してください。プロバイダーキーはそのまま使えます。",
|
||||
"ComposerOversizedSubmitHeld": "未送信:メッセージが {limit} 文字を超えており、貼り付けファイルに保存できませんでした({error})。全文は入力欄に残っています。短くするか .codewhale/pastes を書き込み可能にしてから、もう一度 Enter を押してください。",
|
||||
"ComposerOversizedSubmitHeldShort": "未送信:{limit} 文字超。貼り付けファイルは未保存です。",
|
||||
"CmdPluginOwnerReport": "拡張機能ホスト:\n 状態: {state}\n 有効なツール ({count}): {tools}",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "실행 중",
|
||||
"ExtensionPromptUnavailable": "확장 기능의 지침을 사용할 수 없습니다. /plugin show에서 확인하세요.",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale 계정 설정을 불러오지 못해 이번 턴은 로컬 설정을 사용합니다. 공급자 키는 계속 사용할 수 있으며, 계정 설정을 복원하려면 codewhale login을 실행하세요.",
|
||||
"AccountPreferencesNotLoaded": "계정을 불러오지 못했습니다. {command}을 실행하세요. 공급자 키는 계속 사용할 수 있습니다.",
|
||||
"ComposerOversizedSubmitHeld": "전송하지 못했습니다. 메시지가 {limit}자를 초과하며 붙여넣기 파일로 저장할 수 없습니다({error}). 전체 텍스트는 입력란에 그대로 있습니다. 내용을 줄이거나 .codewhale/pastes에 쓰기 권한을 부여한 뒤 Enter를 다시 누르세요.",
|
||||
"ComposerOversizedSubmitHeldShort": "전송 안 됨: {limit}자 초과, 붙여넣기 파일 저장 실패.",
|
||||
"CmdPluginOwnerReport": "확장 호스트:\n 상태: {state}\n 활성 도구 ({count}): {tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "유휴",
|
||||
"PhaseDraft": "초안",
|
||||
"PhaseWorking": "작업 중",
|
||||
"PhaseReasoning": "생각 중",
|
||||
"PhaseReasoning": "추론 중",
|
||||
"PhaseReading": "읽는 중",
|
||||
"PhaseUsingTool": "도구 사용 중",
|
||||
"PhaseSubagents": "서브에이전트 실행 중",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "ativo",
|
||||
"ExtensionPromptUnavailable": "As instruções das extensões não estão disponíveis; confira /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Não foi possível carregar as preferências da conta Codewhale; este turno usa suas configurações locais. Sua chave do provedor continua funcionando; execute codewhale login para restaurar as preferências da conta.",
|
||||
"AccountPreferencesNotLoaded": "Conta não carregada. Execute {command}. Sua chave do provedor continua funcionando.",
|
||||
"ComposerOversizedSubmitHeld": "Não enviado: esta mensagem excede {limit} caracteres e não pôde ser salva em um arquivo de texto colado ({error}). O texto completo continua no campo de entrada: reduza-o ou permita a gravação em .codewhale/pastes e pressione Enter novamente.",
|
||||
"ComposerOversizedSubmitHeldShort": "Não enviado: acima de {limit} caracteres; arquivo colado não salvo.",
|
||||
"CmdPluginOwnerReport": "Host de extensões:\n Estado: {state}\n Ferramentas ativas ({count}): {tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "ocioso",
|
||||
"PhaseDraft": "rascunho",
|
||||
"PhaseWorking": "trabalhando",
|
||||
"PhaseReasoning": "pensando",
|
||||
"PhaseReasoning": "raciocinando",
|
||||
"PhaseReading": "lendo",
|
||||
"PhaseUsingTool": "usando ferramenta",
|
||||
"PhaseSubagents": "subagentes em andamento",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "активен",
|
||||
"ExtensionPromptUnavailable": "Инструкции расширений недоступны; проверьте /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Не удалось загрузить настройки аккаунта Codewhale, поэтому этот ход использует локальные настройки. Ключ провайдера продолжает работать; выполните codewhale login, чтобы вернуть настройки аккаунта.",
|
||||
"AccountPreferencesNotLoaded": "Аккаунт не загружен. Выполните {command}. Ключ провайдера продолжает работать.",
|
||||
"ComposerOversizedSubmitHeld": "Не отправлено: сообщение длиннее {limit} символов, и его не удалось сохранить в файл вставленного текста ({error}). Полный текст остался в поле ввода: сократите его или разрешите запись в .codewhale/pastes, затем снова нажмите Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Не отправлено: больше {limit} символов; файл вставки не сохранён.",
|
||||
"CmdPluginOwnerReport": "Хост расширений:\n Состояние: {state}\n Активные инструменты ({count}): {tools}",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "активний",
|
||||
"ExtensionPromptUnavailable": "Інструкції розширень недоступні; перевірте /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Не вдалося завантажити налаштування облікового запису Codewhale, тому цей хід використовує локальні налаштування. Ключ провайдера й далі працює; виконайте codewhale login, щоб повернути налаштування облікового запису.",
|
||||
"AccountPreferencesNotLoaded": "Обліковий запис не завантажено. Виконайте {command}. Ключ провайдера й далі працює.",
|
||||
"ComposerOversizedSubmitHeld": "Не надіслано: повідомлення перевищує {limit} символів, і його не вдалося зберегти у файл вставленого тексту ({error}). Повний текст залишається в полі введення: скоротіть його або дозвольте запис у .codewhale/pastes, а потім знову натисніть Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Не надіслано: понад {limit} символів; файл вставки не збережено.",
|
||||
"CmdPluginOwnerReport": "Хост розширень:\n Стан: {state}\n Активні інструменти ({count}): {tools}",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "đang chạy",
|
||||
"ExtensionPromptUnavailable": "Không thể tải hướng dẫn của tiện ích mở rộng; kiểm tra /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Không thể tải tùy chọn tài khoản Codewhale nên lượt này dùng cài đặt cục bộ. Khóa nhà cung cấp của bạn vẫn hoạt động; chạy codewhale login để khôi phục tùy chọn tài khoản.",
|
||||
"AccountPreferencesNotLoaded": "Chưa tải được tài khoản. Chạy {command}. Khóa nhà cung cấp của bạn vẫn hoạt động.",
|
||||
"ComposerOversizedSubmitHeld": "Chưa gửi: tin nhắn vượt quá {limit} ký tự và không thể lưu thành tệp văn bản đã dán ({error}). Toàn bộ nội dung vẫn còn trong ô nhập: hãy rút ngắn hoặc cho phép ghi vào .codewhale/pastes, rồi nhấn Enter lần nữa.",
|
||||
"ComposerOversizedSubmitHeldShort": "Chưa gửi: vượt quá {limit} ký tự; chưa lưu tệp dán.",
|
||||
"CmdPluginOwnerReport": "Máy chủ tiện ích mở rộng:\n Trạng thái: {state}\n Công cụ đang hoạt động ({count}): {tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "rảnh",
|
||||
"PhaseDraft": "nháp",
|
||||
"PhaseWorking": "đang xử lý",
|
||||
"PhaseReasoning": "đang suy nghĩ",
|
||||
"PhaseReasoning": "đang suy luận",
|
||||
"PhaseReading": "đang đọc",
|
||||
"PhaseUsingTool": "đang dùng công cụ",
|
||||
"PhaseSubagents": "đang chạy tác tử con",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "运行中",
|
||||
"ExtensionPromptUnavailable": "扩展指令不可用;请通过 /plugin show 查看。",
|
||||
"ProfileConstitutionUnavailableLocal": "无法加载 Codewhale 账号偏好,本轮改用本地设置。你的提供商密钥仍可正常使用;运行 codewhale login 可恢复账号偏好。",
|
||||
"AccountPreferencesNotLoaded": "账号未加载。请运行 {command}。你的提供商密钥仍可正常使用。",
|
||||
"ComposerOversizedSubmitHeld": "未发送:消息超过 {limit} 个字符,无法保存为粘贴文本文件({error})。完整文本仍保留在输入框中:请缩短内容,或允许写入 .codewhale/pastes,然后再次按 Enter。",
|
||||
"ComposerOversizedSubmitHeldShort": "未发送:超过 {limit} 个字符;粘贴文件未保存。",
|
||||
"CmdPluginOwnerReport": "扩展宿主:\n 状态:{state}\n 活动工具({count}):{tools}",
|
||||
@@ -1691,7 +1690,7 @@
|
||||
"PhaseIdle": "空闲",
|
||||
"PhaseDraft": "草稿",
|
||||
"PhaseWorking": "工作中",
|
||||
"PhaseReasoning": "思考中",
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReading": "读取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "智能体工作中",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"TerminalDockLive": "執行中",
|
||||
"ExtensionPromptUnavailable": "擴充功能指令無法使用;請透過 /plugin show 查看。",
|
||||
"ProfileConstitutionUnavailableLocal": "無法載入 Codewhale 帳號偏好,本輪改用本機設定。你的服務商金鑰仍可正常使用;執行 codewhale login 可恢復帳號偏好。",
|
||||
"AccountPreferencesNotLoaded": "帳號未載入。請執行 {command}。你的服務商金鑰仍可正常使用。",
|
||||
"ComposerOversizedSubmitHeld": "未傳送:訊息超過 {limit} 個字元,無法儲存為貼上文字檔案({error})。完整文字仍保留在輸入框中:請縮短內容,或允許寫入 .codewhale/pastes,然後再次按 Enter。",
|
||||
"ComposerOversizedSubmitHeldShort": "未傳送:超過 {limit} 個字元;貼上檔案未儲存。",
|
||||
"CmdPluginOwnerReport": "擴充功能主機:\n 狀態:{state}\n 活動工具({count}):{tools}",
|
||||
@@ -1689,7 +1688,7 @@
|
||||
"PhaseIdle": "閒置",
|
||||
"PhaseDraft": "草稿",
|
||||
"PhaseWorking": "工作中",
|
||||
"PhaseReasoning": "思考中",
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReading": "讀取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "代理程式工作中",
|
||||
|
||||
@@ -714,7 +714,6 @@ pub enum MessageId {
|
||||
ExtensionsInventorySkills,
|
||||
ExtensionPromptUnavailable,
|
||||
ProfileConstitutionUnavailableLocal,
|
||||
AccountPreferencesNotLoaded,
|
||||
ExtensionsMarketplaceDetail,
|
||||
ExtensionsMarketplaceUnavailable,
|
||||
ExtensionsMcpEmpty,
|
||||
@@ -3652,7 +3651,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ExtensionsInventorySkills,
|
||||
MessageId::ExtensionPromptUnavailable,
|
||||
MessageId::ProfileConstitutionUnavailableLocal,
|
||||
MessageId::AccountPreferencesNotLoaded,
|
||||
MessageId::ExtensionsMarketplaceDetail,
|
||||
MessageId::ExtensionsMarketplaceUnavailable,
|
||||
MessageId::ExtensionsMcpEmpty,
|
||||
|
||||
@@ -255,12 +255,11 @@ impl StdioLspTransport {
|
||||
let child = Arc::new(AsyncMutex::new(Some(child)));
|
||||
let dispatcher_child = child.clone();
|
||||
let dispatcher_pending = pending.clone();
|
||||
let dispatcher_outbound = tx_outbound.clone();
|
||||
let dispatcher_task = spawn_supervised(
|
||||
"lsp-dispatcher",
|
||||
std::panic::Location::caller(),
|
||||
async move {
|
||||
dispatcher_task(rx_inbound, tx_diag, dispatcher_outbound, dispatcher_pending).await;
|
||||
dispatcher_task(rx_inbound, tx_diag, dispatcher_pending).await;
|
||||
// EOF, malformed frames, or an overflowing diagnostics queue
|
||||
// terminate the producer too, so its pipes cannot stay stuck.
|
||||
if let Some(mut child) = dispatcher_child.lock().await.take() {
|
||||
@@ -537,19 +536,15 @@ impl LspTransport for StdioLspTransport {
|
||||
|
||||
/// Send a JSON value as one Content-Length-framed JSON-RPC message.
|
||||
async fn send_message(tx: &mpsc::Sender<Vec<u8>>, value: &Value) -> Result<()> {
|
||||
tx.send(frame_message(value)?)
|
||||
.await
|
||||
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn frame_message(value: &Value) -> Result<Vec<u8>> {
|
||||
let body = serde_json::to_vec(value).context("serialize LSP message")?;
|
||||
let header = format!("Content-Length: {}\r\n\r\n", body.len());
|
||||
let mut frame = Vec::with_capacity(header.len() + body.len());
|
||||
frame.extend_from_slice(header.as_bytes());
|
||||
frame.extend_from_slice(&body);
|
||||
Ok(frame)
|
||||
tx.send(frame)
|
||||
.await
|
||||
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Background task that drains the outbound queue and writes each frame to
|
||||
@@ -643,7 +638,6 @@ fn parse_header(buf: &[u8]) -> Result<Option<(usize, usize)>> {
|
||||
async fn dispatcher_task(
|
||||
mut rx: mpsc::Receiver<Value>,
|
||||
tx_diag: mpsc::Sender<DiagnosticMessage>,
|
||||
tx_outbound: mpsc::Sender<Vec<u8>>,
|
||||
pending: Arc<AsyncMutex<HashMap<i64, oneshot::Sender<Value>>>>,
|
||||
) {
|
||||
while let Some(value) = rx.recv().await {
|
||||
@@ -660,19 +654,6 @@ async fn dispatcher_task(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(method) = method {
|
||||
if let Some(id) = value.get("id") {
|
||||
let reply = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {"code": -32601, "message": format!("unsupported method {method}")},
|
||||
});
|
||||
if let Ok(frame) = frame_message(&reply) {
|
||||
let _ = tx_outbound.try_send(frame);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Replies have an `id` and a `result` or `error`.
|
||||
if let Some(id) = value.get("id").and_then(|v| v.as_i64()) {
|
||||
let mut map = pending.lock().await;
|
||||
@@ -799,31 +780,6 @@ pub(super) mod tests {
|
||||
assert_eq!(diags[0].message, "missing semicolon");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn server_request_sharing_a_pending_id_is_answered_not_taken_as_the_reply() {
|
||||
let (tx_in, rx_in) = mpsc::channel(4);
|
||||
let (tx_out, mut rx_out) = mpsc::channel::<Vec<u8>>(4);
|
||||
let (tx_diag, _rx_diag) = mpsc::channel(4);
|
||||
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
pending.lock().await.insert(2, reply_tx);
|
||||
let dispatcher = tokio::spawn(dispatcher_task(rx_in, tx_diag, tx_out, pending));
|
||||
tx_in
|
||||
.send(json!({"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[]}}))
|
||||
.await
|
||||
.unwrap();
|
||||
tx_in
|
||||
.send(json!({"jsonrpc":"2.0","id":2,"result":"definition"}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(reply_rx.await.unwrap()["result"], "definition");
|
||||
let answer = next_document(&mut rx_out).await;
|
||||
assert_eq!(answer["id"], 2);
|
||||
assert_eq!(answer["error"]["code"], -32601);
|
||||
drop(tx_in);
|
||||
dispatcher.await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_uri_path() {
|
||||
let path = fixture_path("example/foo.rs");
|
||||
@@ -1353,12 +1309,11 @@ while True:
|
||||
let (mut producer, reader) = tokio::io::duplex(MAX_LSP_HEADER_BYTES * 2);
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
let (diag, _diagnostics) = mpsc::channel(8);
|
||||
let (outbound, _outbound_rx) = mpsc::channel(8);
|
||||
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
|
||||
let (reply, receiver) = oneshot::channel();
|
||||
pending.lock().await.insert(1, reply);
|
||||
let read_task = tokio::spawn(reader_task(reader, tx));
|
||||
let dispatch = tokio::spawn(dispatcher_task(rx, diag, outbound, pending.clone()));
|
||||
let dispatch = tokio::spawn(dispatcher_task(rx, diag, pending.clone()));
|
||||
producer.write_all(&frame).await.unwrap();
|
||||
assert!(
|
||||
timeout(Duration::from_secs(1), receiver)
|
||||
|
||||
@@ -492,8 +492,6 @@ pub struct Settings {
|
||||
/// One-time Operate-retirement toast has been shown. Same suppression
|
||||
/// contract as `yolo_deprecation_shown`.
|
||||
pub operate_deprecation_shown: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub account_preferences_notice_shown: Option<String>,
|
||||
/// Round 3 (2026-09-01) moved the work bar under the composer. Every
|
||||
/// settings.toml saved before that carries `work_surface_placement =
|
||||
/// "top"` — the old default, persisted verbatim by ordinary saves, not a
|
||||
@@ -652,7 +650,9 @@ impl Default for Settings {
|
||||
mention_menu_limit: 128,
|
||||
mention_walk_depth: 10,
|
||||
mention_menu_behavior: "fuzzy".to_string(),
|
||||
show_thinking: true,
|
||||
// Reasoning is useful when explicitly requested, but it should
|
||||
// never displace the actual conversation in the default TUI.
|
||||
show_thinking: false,
|
||||
thinking_default_expanded: false,
|
||||
thinking_preview_lines: default_thinking_preview_lines(),
|
||||
thinking_highlight: true,
|
||||
@@ -694,7 +694,6 @@ impl Default for Settings {
|
||||
feature_intro_shown: false,
|
||||
yolo_deprecation_shown: false,
|
||||
operate_deprecation_shown: false,
|
||||
account_preferences_notice_shown: None,
|
||||
legacy_operate_default: false,
|
||||
work_surface_bottom_migrated: false,
|
||||
behavioral_tip_impressions: std::collections::BTreeMap::new(),
|
||||
@@ -3748,36 +3747,6 @@ mod tests {
|
||||
assert_eq!(reloaded.permission_posture.as_deref(), Some("ask"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_shows_by_default_and_a_saved_choice_survives_reload_and_other_saves() {
|
||||
let _g = config_path_test_guard();
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let settings_path = tmp.path().join("settings.toml");
|
||||
let _config_override =
|
||||
EnvVarRestore::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml"));
|
||||
|
||||
let absent = Settings::load().expect("load without a settings file");
|
||||
assert!(absent.show_thinking);
|
||||
assert!(!absent.thinking_default_expanded);
|
||||
assert!(!absent.is_set("show_thinking"));
|
||||
|
||||
std::fs::write(&settings_path, "theme = \"light\"\n").expect("settings without the key");
|
||||
let unnamed = Settings::load().expect("load settings without the key");
|
||||
assert!(unnamed.show_thinking);
|
||||
assert!(!unnamed.is_set("show_thinking"));
|
||||
|
||||
for saved in [false, true] {
|
||||
std::fs::write(&settings_path, format!("show_thinking = {saved}\n"))
|
||||
.expect("saved choice");
|
||||
let loaded = Settings::load().expect("load the saved choice");
|
||||
assert_eq!(loaded.show_thinking, saved);
|
||||
assert!(loaded.is_set("show_thinking"));
|
||||
Settings::transact(|settings| settings.set("locale", "en")).expect("another save");
|
||||
let reloaded = Settings::load().expect("reload after another save");
|
||||
assert_eq!(reloaded.show_thinking, saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_load_keeps_top_placement_chosen_after_the_bottom_migration() {
|
||||
let _g = config_path_test_guard();
|
||||
@@ -3917,7 +3886,7 @@ mod tests {
|
||||
let mut settings = Settings::default();
|
||||
// Density is calm by default; motion is an independent axis.
|
||||
assert!(settings.calm_mode);
|
||||
settings.show_thinking = false;
|
||||
assert!(!settings.show_thinking);
|
||||
|
||||
let changed = settings.apply_preset("CALM").expect("calm preset applies");
|
||||
assert_eq!(
|
||||
@@ -3947,8 +3916,8 @@ mod tests {
|
||||
assert!(settings.fancy_animations);
|
||||
assert_eq!(settings.transcript_spacing, "spacious");
|
||||
assert_eq!(settings.tool_collapse_mode, "compact");
|
||||
assert!(settings.show_thinking);
|
||||
assert!(!settings.thinking_default_expanded);
|
||||
// Thinking is opt-in so the transcript stays focused on the chat.
|
||||
assert!(!settings.show_thinking);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,11 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
|
||||
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
|
||||
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
|
||||
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
|
||||
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
|
||||
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
|
||||
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
|
||||
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
|
||||
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
|
||||
- **Model thinking:** New installs show model thinking by default: it streams live in the transcript and collapses under a "Thought for …" header when the answer starts. Existing installs keep their saved setting; turn it on under Model reasoning in chat in /config.
|
||||
- **Footer clock:** The footer clock and phase now say "thinking", matching the transcript.
|
||||
|
||||
@@ -87,27 +87,6 @@ pub(crate) use types::{
|
||||
WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
|
||||
};
|
||||
|
||||
const ACCOUNT_PREFERENCES_NOTICE: &str = "account-preferences-notice";
|
||||
|
||||
fn account_sign_in_marker(profile: Option<&str>) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
let api_base = codewhale_engine::profile_constitution::runtime_account_api_base();
|
||||
let session = codewhale_secrets::account::AccountSessionStore::new(
|
||||
codewhale_secrets::Secrets::file_backed_read_only(),
|
||||
profile,
|
||||
&api_base,
|
||||
)
|
||||
.runtime_info_at(Utc::now())
|
||||
.ok()
|
||||
.and_then(|info| info.session_id)
|
||||
.unwrap_or_default();
|
||||
Sha256::digest(session.as_bytes())
|
||||
.iter()
|
||||
.take(8)
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// === Types ===
|
||||
|
||||
/// One login owns one mailbox. A cancelled task can only write its abandoned
|
||||
@@ -2469,7 +2448,6 @@ pub struct App {
|
||||
/// re-render per character. `None` means "no bump since the last
|
||||
/// finalize" so the first chunk of a block always renders immediately.
|
||||
pub thinking_revision_last_bump_at: Option<Instant>,
|
||||
pub(crate) thinking_revision_owed: bool,
|
||||
/// Newline-gated streaming collector state.
|
||||
pub streaming_state: StreamingState,
|
||||
/// Live approximate output tokens for the current assistant stream.
|
||||
@@ -3813,38 +3791,6 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn notify_account_preferences_not_loaded(&mut self) {
|
||||
let sign_in = account_sign_in_marker(self.config_profile.as_deref());
|
||||
self.notify_account_preferences_not_loaded_for(&sign_in);
|
||||
}
|
||||
|
||||
fn notify_account_preferences_not_loaded_for(&mut self, sign_in: &str) {
|
||||
let first_for_sign_in = Settings::transact_opt(|settings| {
|
||||
if settings.account_preferences_notice_shown.as_deref() == Some(sign_in) {
|
||||
return Ok(None);
|
||||
}
|
||||
settings.account_preferences_notice_shown = Some(sign_in.to_string());
|
||||
Ok(Some(()))
|
||||
});
|
||||
if matches!(first_for_sign_in, Ok(None)) {
|
||||
return;
|
||||
}
|
||||
let text = self
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
self.push_status_toast_record(StatusToast::standing(
|
||||
text,
|
||||
StatusToastLevel::Warning,
|
||||
ACCOUNT_PREFERENCES_NOTICE,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn dismiss_account_preferences_notice(&mut self) -> bool {
|
||||
let before = self.status_toasts.len();
|
||||
self.retire_event_notices(ACCOUNT_PREFERENCES_NOTICE);
|
||||
self.status_toasts.len() != before
|
||||
}
|
||||
|
||||
/// One-release migration notice for the Shift+Tab/Ctrl+T rebinding: users
|
||||
/// pressing Shift+Tab expecting the old thinking cycle land here first.
|
||||
fn notify_keybinding_migration_once(&mut self) {
|
||||
|
||||
@@ -1142,7 +1142,6 @@ impl App {
|
||||
streaming_thinking_active_entry: None,
|
||||
pending_thinking_translation_cells: HashMap::new(),
|
||||
thinking_revision_last_bump_at: None,
|
||||
thinking_revision_owed: false,
|
||||
streaming_state: StreamingState::new(),
|
||||
streaming_output_token_estimate: 0,
|
||||
reasoning_buffer: String::new(),
|
||||
|
||||
@@ -184,46 +184,6 @@ fn feature_intro_shows_once_persists_then_is_idempotent() {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_preferences_notice_shows_once_per_sign_in_and_can_be_dismissed() {
|
||||
let _env_lock = lock_test_env();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config_path = tmp.path().join("config.toml");
|
||||
let _env = EnvVarGuard::set(
|
||||
"DEEPSEEK_CONFIG_PATH",
|
||||
config_path.to_string_lossy().as_ref(),
|
||||
);
|
||||
let shown = |app: &App| {
|
||||
let notice = app
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
app.status_toasts
|
||||
.iter()
|
||||
.filter(|toast| toast.text == notice && toast.kind == StatusToastKind::Standing)
|
||||
.count()
|
||||
};
|
||||
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
app.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
assert_eq!(shown(&app), 1);
|
||||
assert_eq!(
|
||||
Settings::load()
|
||||
.expect("settings should load")
|
||||
.account_preferences_notice_shown
|
||||
.as_deref(),
|
||||
Some("first-sign-in")
|
||||
);
|
||||
assert!(app.dismiss_account_preferences_notice());
|
||||
assert_eq!(shown(&app), 0);
|
||||
|
||||
let mut later_session = App::new(test_options(false), &Config::default());
|
||||
later_session.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
assert_eq!(shown(&later_session), 0, "the same sign-in is told once");
|
||||
later_session.notify_account_preferences_not_loaded_for("second-sign-in");
|
||||
assert_eq!(shown(&later_session), 1, "a later sign-in is told again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_input_scenario() {
|
||||
// Scenario consolidation of: initial_input_prefill_waits_for_manual_submit, initial_input_submit_marks_startup_dispatch
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
│ Background (default) ✎ │current terminal
|
||||
│ Pet mode Off [ ] │saved terminal
|
||||
│ Quiet transcript On [x] │startup terminal
|
||||
│ Model reasoning in chat On [x] │source settings.toml
|
||||
│ Model reasoning in chat Off [ ] │source settings.toml
|
||||
│ Thinking Default Expanded Off [ ] │scope SAVED
|
||||
│ Thinking Preview Lines 2 ✎ │apply applies on save
|
||||
│ Thinking background highlight On [x] │kind choice
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Background (default) ✎ SAVED █
|
||||
Pet mode Off [ ] SAVED █
|
||||
Quiet transcript On [x] SAVED █
|
||||
Model reasoning in chat On [x] SAVED █
|
||||
Model reasoning in chat Off [ ] SAVED █
|
||||
Thinking Default Expanded Off [ ] SAVED █
|
||||
Thinking Preview Lines 2 ✎ SAVED │
|
||||
Thinking background highlight On [x] SAVED │
|
||||
|
||||
@@ -30,17 +30,15 @@ use crate::tui::history::HistoryCell;
|
||||
/// frame cap (see `frame_rate_limiter`) yet imperceptible as lag.
|
||||
///
|
||||
/// Correctness: this only skips *intermediate* repaints. Appended content is
|
||||
/// never dropped — it lands in the cell immediately — the display beat renders
|
||||
/// a skipped bump once the window has passed, and finalize always forces a
|
||||
/// bump so the final reasoning text is fully rendered.
|
||||
/// never dropped — it lands in the cell immediately — and finalize always
|
||||
/// forces a bump so the final reasoning text is fully rendered.
|
||||
const THINKING_REVISION_THROTTLE: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Bump the active-cell revision for a streaming thinking mutation, but at
|
||||
/// most once per [`THINKING_REVISION_THROTTLE`] window. Returns whether a bump
|
||||
/// was actually emitted. Skipped bumps coalesce into the next one, into
|
||||
/// [`render_owed_revision`] on a later display beat, or into the forced
|
||||
/// finalize bump, so no content is ever lost — only redundant intermediate
|
||||
/// re-wraps are dropped.
|
||||
/// was actually emitted. Skipped bumps coalesce into the next one (or into the
|
||||
/// forced finalize bump), so no content is ever lost — only redundant
|
||||
/// intermediate re-wraps are dropped.
|
||||
fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
|
||||
let due = app
|
||||
.thinking_revision_last_bump_at
|
||||
@@ -49,17 +47,9 @@ fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
|
||||
app.thinking_revision_last_bump_at = Some(now);
|
||||
app.bump_active_cell_revision();
|
||||
}
|
||||
app.thinking_revision_owed = !due;
|
||||
due
|
||||
}
|
||||
|
||||
pub(super) fn render_owed_revision(app: &mut App, now: Instant) -> bool {
|
||||
if app.streaming_thinking_active_entry.is_none() {
|
||||
app.thinking_revision_owed = false;
|
||||
}
|
||||
app.thinking_revision_owed && bump_thinking_revision_throttled(app, now)
|
||||
}
|
||||
|
||||
/// Ensure an in-flight Thinking entry exists in `active_cell` and return its
|
||||
/// entry index. If no thinking entry is currently streaming, push a fresh one.
|
||||
/// P2.3: thinking shares the active cell with subsequent tool calls so the
|
||||
@@ -86,8 +76,7 @@ pub(super) fn ensure_active_entry(app: &mut App) -> usize {
|
||||
/// committed to the cell immediately; the active-cell revision bump that
|
||||
/// triggers a re-wrap of the live tail is debounced to at most one per
|
||||
/// [`THINKING_REVISION_THROTTLE`] window (#1620). Skipped bumps coalesce into
|
||||
/// the next append, a later display beat or the forced finalize bump, so no
|
||||
/// content is ever lost.
|
||||
/// the next append or the forced finalize bump, so no content is ever lost.
|
||||
pub(super) fn append(app: &mut App, entry_idx: usize, text: &str) {
|
||||
append_at(app, entry_idx, text, Instant::now());
|
||||
}
|
||||
@@ -319,7 +308,6 @@ pub(super) fn finalize_active_entry(app: &mut App, duration: Option<f32>, remain
|
||||
// the debounce window so the next thinking block's first chunk renders
|
||||
// immediately rather than being coalesced into a stale window.
|
||||
app.thinking_revision_last_bump_at = None;
|
||||
app.thinking_revision_owed = false;
|
||||
app.bump_active_cell_revision();
|
||||
true
|
||||
}
|
||||
@@ -438,34 +426,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paused_stream_renders_the_reasoning_it_already_received() {
|
||||
let mut app = test_app();
|
||||
let entry = ensure_active_entry(&mut app);
|
||||
app.thinking_revision_last_bump_at = None;
|
||||
let t0 = Instant::now();
|
||||
append_at(&mut app, entry, "first slice ", t0);
|
||||
let shown = app.active_cell_revision;
|
||||
append_at(&mut app, entry, "held back", t0 + Duration::from_millis(16));
|
||||
assert_eq!(app.active_cell_revision, shown);
|
||||
|
||||
assert!(!render_owed_revision(
|
||||
&mut app,
|
||||
t0 + Duration::from_millis(32)
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown);
|
||||
assert!(render_owed_revision(
|
||||
&mut app,
|
||||
t0 + THINKING_REVISION_THROTTLE
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
|
||||
assert!(!render_owed_revision(
|
||||
&mut app,
|
||||
t0 + THINKING_REVISION_THROTTLE * 3
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
|
||||
}
|
||||
|
||||
/// #7123: a reasoning block that opens while the display beat still holds
|
||||
/// an uncommitted slice of prose must not drop that slice. `start_block`
|
||||
/// resets `StreamingState`, so the tail has to be delivered first.
|
||||
|
||||
@@ -7232,16 +7232,14 @@ pub(crate) async fn run_event_loop(
|
||||
continue;
|
||||
}
|
||||
// An idle operator can dismiss the persistent context warning
|
||||
// or the account preferences notice without affecting vim or
|
||||
// attachment handling. While a turn is active Esc retains its
|
||||
// cancellation meaning.
|
||||
// without affecting vim or attachment handling. While a turn
|
||||
// is active Esc retains its cancellation meaning.
|
||||
KeyCode::Esc
|
||||
if !app.is_loading
|
||||
&& app.input.is_empty()
|
||||
&& !slash_menu_open
|
||||
&& !mention_menu_open
|
||||
&& (app.dismiss_context_pressure_warning()
|
||||
|| app.dismiss_account_preferences_notice()) =>
|
||||
&& app.dismiss_context_pressure_warning() =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -9328,10 +9326,14 @@ pub(super) fn apply_engine_status(app: &mut App, message: String) -> bool {
|
||||
app.add_message(HistoryCell::System { content: message });
|
||||
return true;
|
||||
}
|
||||
if app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str() {
|
||||
app.notify_account_preferences_not_loaded();
|
||||
return false;
|
||||
// The account-profile fallback notice is sent once per engine; keep it in
|
||||
// the transcript so the next turn's status line cannot erase it.
|
||||
let retain = app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str();
|
||||
if retain {
|
||||
app.add_message(HistoryCell::System {
|
||||
content: message.clone(),
|
||||
});
|
||||
}
|
||||
app.status_message = Some(message);
|
||||
false
|
||||
retain
|
||||
}
|
||||
@@ -1555,10 +1555,7 @@ pub(crate) fn commit_streaming_display_tick(
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if streaming_thinking::render_owed_revision(app, now) {
|
||||
updated = true;
|
||||
}
|
||||
if app.streaming_state.has_pending_stream_text(0) || app.thinking_revision_owed {
|
||||
if app.streaming_state.has_pending_stream_text(0) {
|
||||
stream_display_clock.note_delta(now);
|
||||
}
|
||||
|
||||
|
||||
@@ -27343,7 +27343,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
|
||||
|
||||
let latest = pager.body_text();
|
||||
assert!(latest.contains("SECOND-PROMPT"), "{latest}");
|
||||
assert!(latest.contains("[∿ thinking 1/1 · running]"), "{latest}");
|
||||
assert!(latest.contains("[∿ reasoning 1/1 · running]"), "{latest}");
|
||||
assert!(latest.contains("SECOND-THOUGHT"), "{latest}");
|
||||
assert!(latest.contains("[◆ · running]"), "{latest}");
|
||||
assert!(latest.contains("SECOND-ANSWER"), "{latest}");
|
||||
@@ -27355,7 +27355,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
|
||||
for expected in [
|
||||
"FIRST-PROMPT",
|
||||
"keep this input line",
|
||||
"[∿ thinking 1/1 · Done]",
|
||||
"[∿ reasoning 1/1 · Done]",
|
||||
"FIRST-THOUGHT",
|
||||
"full reasoning tail",
|
||||
"[⚙ using tool]",
|
||||
@@ -35488,41 +35488,23 @@ fn extension_prompt_origin_keeps_denials_and_other_policy_holds() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_profile_fallback_notice_is_a_standing_notice_outside_the_transcript() {
|
||||
fn account_profile_fallback_notice_stays_in_the_transcript() {
|
||||
let _home = SettingsHomeGuard::new();
|
||||
let mut app = App::new(create_test_options(), &Config::default());
|
||||
let engine_notice = app
|
||||
let notice = app
|
||||
.tr(MessageId::ProfileConstitutionUnavailableLocal)
|
||||
.to_string();
|
||||
let before = app.history.len();
|
||||
let status_before = app.status_message.clone();
|
||||
assert!(!super::event_loop::apply_engine_status(
|
||||
assert!(super::event_loop::apply_engine_status(
|
||||
&mut app,
|
||||
engine_notice
|
||||
notice.clone()
|
||||
));
|
||||
assert_eq!(app.history.len(), before);
|
||||
assert_eq!(app.status_message, status_before);
|
||||
assert!(!super::event_loop::apply_engine_status(
|
||||
&mut app,
|
||||
"Executing tools sequentially".into()
|
||||
));
|
||||
assert_eq!(app.history.len(), before);
|
||||
let notice = app
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
let standing = |app: &App| {
|
||||
app.status_toasts
|
||||
.iter()
|
||||
.find(|toast| toast.text == notice)
|
||||
.cloned()
|
||||
};
|
||||
app.sync_status_message_to_toasts();
|
||||
let shown = standing(&app).expect("a later routine status does not erase the notice");
|
||||
assert_eq!(shown.level, StatusToastLevel::Warning);
|
||||
assert_eq!(shown.ttl_ms, None);
|
||||
assert!(app.dismiss_account_preferences_notice());
|
||||
assert!(standing(&app).is_none());
|
||||
assert!(!app.dismiss_account_preferences_notice());
|
||||
assert_eq!(app.history.len(), before + 1);
|
||||
assert!(matches!(&app.history[before], HistoryCell::System { content } if *content == notice));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -32,8 +32,6 @@ mod plugin_e2e_acceptance;
|
||||
mod screen_mode_inline_pty;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod search_text_pty;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod thinking_stream_pty;
|
||||
mod tool_lifecycle_acceptance;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod work_bar_keys_pty;
|
||||
@@ -1,585 +0,0 @@
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::qa_harness::{
|
||||
harness::{Harness, SealedWorkspace, make_sealed_workspace},
|
||||
keys,
|
||||
};
|
||||
|
||||
const WAIT: Duration = Duration::from_secs(20);
|
||||
const MODEL: &str = "deepseek-v4-pro";
|
||||
const PROMPTS: [&str; 2] = ["first slow fixture prompt", "second slow fixture prompt"];
|
||||
const ANSWER: &str = "Fixture answer after thinking.";
|
||||
const REASONING_LINES: usize = 6;
|
||||
const NOTICE: &str = "Account not loaded. Run codewhale login.";
|
||||
const RETIRED_TRANSCRIPT_NOTICE: &str = "could not be loaded";
|
||||
|
||||
fn reasoning_line(index: usize) -> String {
|
||||
format!("thinking fixture line {index} of {REASONING_LINES}")
|
||||
}
|
||||
|
||||
struct PacedProvider {
|
||||
address: SocketAddr,
|
||||
gate: mpsc::Sender<()>,
|
||||
stop: Arc<AtomicBool>,
|
||||
turns: Arc<AtomicUsize>,
|
||||
profile_requests: Arc<AtomicUsize>,
|
||||
acceptor: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
struct Served {
|
||||
gates: Mutex<mpsc::Receiver<()>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
turns: Arc<AtomicUsize>,
|
||||
profile_requests: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl PacedProvider {
|
||||
fn start() -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("loopback provider");
|
||||
let address = listener.local_addr().expect("loopback address");
|
||||
let (gate, gates) = mpsc::channel();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let turns = Arc::new(AtomicUsize::new(0));
|
||||
let profile_requests = Arc::new(AtomicUsize::new(0));
|
||||
let served = Arc::new(Served {
|
||||
gates: Mutex::new(gates),
|
||||
stop: Arc::clone(&stop),
|
||||
turns: Arc::clone(&turns),
|
||||
profile_requests: Arc::clone(&profile_requests),
|
||||
});
|
||||
let acceptor = std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
if served.stop.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let Ok(stream) = stream else { continue };
|
||||
let served = Arc::clone(&served);
|
||||
std::thread::spawn(move || {
|
||||
let _ = serve(stream, &served);
|
||||
});
|
||||
}
|
||||
});
|
||||
Self {
|
||||
address,
|
||||
gate,
|
||||
stop,
|
||||
turns,
|
||||
profile_requests,
|
||||
acceptor: Some(acceptor),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_base(&self) -> String {
|
||||
format!("http://{}/v1", self.address)
|
||||
}
|
||||
|
||||
fn account_base(&self) -> String {
|
||||
format!("http://{}", self.address)
|
||||
}
|
||||
|
||||
fn release(&self, gates: usize) {
|
||||
for _ in 0..gates {
|
||||
self.gate.send(()).expect("provider gate");
|
||||
}
|
||||
}
|
||||
|
||||
fn turns(&self) -> usize {
|
||||
self.turns.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacedProvider {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Release);
|
||||
let _ = TcpStream::connect(self.address);
|
||||
if let Some(acceptor) = self.acceptor.take() {
|
||||
let _ = acceptor.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, served: &Served) -> std::io::Result<()> {
|
||||
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut stream = stream;
|
||||
let mut request_line = String::new();
|
||||
if reader.read_line(&mut request_line)? == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut content_length = 0usize;
|
||||
loop {
|
||||
let mut header = String::new();
|
||||
if reader.read_line(&mut header)? == 0 {
|
||||
break;
|
||||
}
|
||||
let header = header.trim_end();
|
||||
if header.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some((name, value)) = header.split_once(':')
|
||||
&& name.eq_ignore_ascii_case("content-length")
|
||||
{
|
||||
content_length = value.trim().parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
let mut body = vec![0u8; content_length.min(8 * 1024 * 1024)];
|
||||
reader.read_exact(&mut body)?;
|
||||
let mut parts = request_line.split_whitespace();
|
||||
let method = parts.next().unwrap_or_default();
|
||||
let path = parts.next().unwrap_or_default();
|
||||
if method == "GET" && path.ends_with("/models") {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({"object":"list","data":[{"id":MODEL,"object":"model"}]}),
|
||||
);
|
||||
}
|
||||
if path.starts_with("/api/") {
|
||||
served.profile_requests.fetch_add(1, Ordering::AcqRel);
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"401 Unauthorized",
|
||||
&serde_json::json!({"error":"loopback fixture account is signed out"}),
|
||||
);
|
||||
}
|
||||
if method != "POST" {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"404 Not Found",
|
||||
&serde_json::json!({"error":"not served by the loopback fixture"}),
|
||||
);
|
||||
}
|
||||
let request: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||
let streaming = request["stream"].as_bool().unwrap_or(false);
|
||||
let with_tools = request["tools"]
|
||||
.as_array()
|
||||
.is_some_and(|tools| !tools.is_empty());
|
||||
let text = String::from_utf8_lossy(&body);
|
||||
let turn = PROMPTS
|
||||
.iter()
|
||||
.rposition(|prompt| text.contains(prompt))
|
||||
.map(|index| index + 1);
|
||||
let paced = streaming
|
||||
&& with_tools
|
||||
&& turn.is_some_and(|turn| {
|
||||
served
|
||||
.turns
|
||||
.compare_exchange(turn - 1, turn, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
});
|
||||
if !streaming {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion","model":MODEL,
|
||||
"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}
|
||||
}),
|
||||
);
|
||||
}
|
||||
stream.write_all(
|
||||
b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\nconnection: close\r\n\r\n",
|
||||
)?;
|
||||
stream.flush()?;
|
||||
if paced {
|
||||
wait_for_gate(served)?;
|
||||
for index in 1..=REASONING_LINES {
|
||||
send_delta(
|
||||
&mut stream,
|
||||
serde_json::json!({"reasoning_content": format!("{}\n", reasoning_line(index))}),
|
||||
)?;
|
||||
}
|
||||
wait_for_gate(served)?;
|
||||
send_delta(&mut stream, serde_json::json!({"content": ANSWER}))?;
|
||||
wait_for_gate(served)?;
|
||||
} else {
|
||||
send_delta(&mut stream, serde_json::json!({"content": "ok"}))?;
|
||||
}
|
||||
send_chunk(
|
||||
&mut stream,
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
|
||||
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":8,"completion_tokens":4,"total_tokens":12}
|
||||
}),
|
||||
)?;
|
||||
stream.write_all(b"data: [DONE]\n\n")?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn wait_for_gate(served: &Served) -> std::io::Result<()> {
|
||||
let gates = served.gates.lock().expect("provider gates");
|
||||
loop {
|
||||
if served.stop.load(Ordering::Acquire) {
|
||||
return Err(std::io::ErrorKind::Interrupted.into());
|
||||
}
|
||||
match gates.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
return Err(std::io::ErrorKind::BrokenPipe.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_delta(stream: &mut TcpStream, delta: serde_json::Value) -> std::io::Result<()> {
|
||||
send_chunk(
|
||||
stream,
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
|
||||
"choices":[{"index":0,"delta":delta,"finish_reason":null}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn send_chunk(stream: &mut TcpStream, chunk: &serde_json::Value) -> std::io::Result<()> {
|
||||
stream.write_all(format!("data: {chunk}\n\n").as_bytes())?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn respond_json(
|
||||
stream: &mut TcpStream,
|
||||
status: &str,
|
||||
body: &serde_json::Value,
|
||||
) -> std::io::Result<()> {
|
||||
let body = body.to_string();
|
||||
stream.write_all(
|
||||
format!(
|
||||
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
|
||||
body.len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn prepare(workspace: &SealedWorkspace, provider: &PacedProvider, settings: Option<&str>) {
|
||||
let state = workspace.home().join(".codewhale");
|
||||
std::fs::write(state.join(".onboarded"), "").unwrap();
|
||||
std::fs::write(
|
||||
state.join("config.toml"),
|
||||
format!(
|
||||
"provider = \"deepseek\"\n[providers.deepseek]\nbase_url = {:?}\napi_key = \"loopback-only\"\nmodel = {MODEL:?}\n[notifications]\nmethod = \"off\"\ncompletion_sound = \"off\"\n",
|
||||
provider.model_base()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(settings) = settings {
|
||||
std::fs::write(state.join("settings.toml"), settings).unwrap();
|
||||
}
|
||||
let trust = workspace.workspace().join(".deepseek");
|
||||
std::fs::create_dir_all(&trust).unwrap();
|
||||
std::fs::write(trust.join("trusted"), "").unwrap();
|
||||
}
|
||||
|
||||
fn launch(
|
||||
workspace: &SealedWorkspace,
|
||||
provider: &PacedProvider,
|
||||
(rows, cols): (u16, u16),
|
||||
session: &str,
|
||||
animated: bool,
|
||||
) -> Harness {
|
||||
let mut tui = Harness::builder(Harness::codewhale_binary())
|
||||
.cwd(workspace.workspace())
|
||||
.clear_env()
|
||||
.seal_home(workspace.home())
|
||||
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
|
||||
.env("SHELL", "/bin/sh")
|
||||
.env("CODEWHALE_CLOUD_API_BASE", provider.account_base())
|
||||
.env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
|
||||
.env("CODEWHALE_NO_UPDATE_CHECK", "1")
|
||||
.env("NO_ANIMATIONS", if animated { "0" } else { "1" })
|
||||
.args([
|
||||
"--workspace",
|
||||
workspace.workspace().to_str().unwrap(),
|
||||
"--no-project-config",
|
||||
"--skip-onboarding",
|
||||
session,
|
||||
])
|
||||
.size(rows, cols)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
tui.wait_for_text("accept edits", WAIT)
|
||||
.unwrap_or_else(|error| panic!("the dial chip never painted: {error}"));
|
||||
tui
|
||||
}
|
||||
|
||||
fn expect(tui: &mut Harness, what: &str, predicate: impl FnMut(&super::qa_harness::Frame) -> bool) {
|
||||
if let Err(error) = tui.wait_for(predicate, WAIT) {
|
||||
panic!("waiting for {what}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn settle(tui: &mut Harness) {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let mut quiet_since = Instant::now();
|
||||
while Instant::now() < deadline && quiet_since.elapsed() < Duration::from_millis(300) {
|
||||
if tui.pump() {
|
||||
quiet_since = Instant::now();
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
fn report(tui: &mut Harness, label: &str) {
|
||||
let frame = tui.frame();
|
||||
println!(
|
||||
"--- {label} at {}x{} ---\n{}",
|
||||
frame.cols(),
|
||||
frame.rows(),
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
|
||||
fn footer_row(tui: &mut Harness) -> (u16, String) {
|
||||
let frame = tui.frame();
|
||||
(0..frame.rows())
|
||||
.rev()
|
||||
.map(|row| (row, frame.row(row)))
|
||||
.find(|(_, text)| text.contains("accept edits"))
|
||||
.unwrap_or_else(|| panic!("no dial chip on screen:\n{}", frame.debug_dump()))
|
||||
}
|
||||
|
||||
fn assert_status_and_water(tui: &mut Harness, label: &str) {
|
||||
let (footer, status) = footer_row(tui);
|
||||
let frame = tui.frame();
|
||||
for retired in ["operate", "Operate", "Work bar"] {
|
||||
assert!(
|
||||
!frame.contains(retired),
|
||||
"{label}: `{retired}` is on screen:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
assert!(status.contains("accept edits"), "{label}: {status}");
|
||||
for row in 0..footer.saturating_sub(3) {
|
||||
let text = frame.row(row);
|
||||
let cells: Vec<char> = text.chars().collect();
|
||||
let school = cells.windows(2).any(|pair| {
|
||||
pair.iter()
|
||||
.all(|cell| ('\u{2801}'..='\u{28FF}').contains(cell))
|
||||
});
|
||||
assert!(
|
||||
!school && !text.contains(['˚', '°', '≈', '~']),
|
||||
"{label}: ambient art on transcript row {row} `{text}`:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn thinking_rows(tui: &mut Harness) -> usize {
|
||||
let frame = tui.frame();
|
||||
(0..frame.rows())
|
||||
.filter(|&row| frame.row(row).contains("Thinking…"))
|
||||
.count()
|
||||
}
|
||||
|
||||
fn stream_first_turn(tui: &mut Harness, provider: &PacedProvider, thinking_shown: bool) {
|
||||
tui.type_line(PROMPTS[0]).unwrap();
|
||||
expect(tui, "the first request and its interrupt hint", |frame| {
|
||||
provider.turns() == 1 && frame.contains("Esc to interrupt")
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(900));
|
||||
report(tui, "loading");
|
||||
assert_status_and_water(tui, "loading");
|
||||
|
||||
provider.release(1);
|
||||
let tail = reasoning_line(REASONING_LINES);
|
||||
if thinking_shown {
|
||||
expect(tui, "the live thinking block", |frame| {
|
||||
frame.contains("Thinking…") && frame.contains(&tail)
|
||||
});
|
||||
} else {
|
||||
expect(tui, "the thinking activity line", |frame| {
|
||||
frame.contains("Thinking…")
|
||||
});
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1300));
|
||||
report(tui, "thinking");
|
||||
assert_status_and_water(tui, "thinking");
|
||||
assert_eq!(thinking_rows(tui), 1, "one Thinking line");
|
||||
let (_, status) = footer_row(tui);
|
||||
assert!(
|
||||
status.contains("thinking ") && !status.contains("reasoning"),
|
||||
"the clock names the phase: {status}"
|
||||
);
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("reasoning..."), "{}", frame.debug_dump());
|
||||
assert_eq!(frame.contains(&tail), thinking_shown);
|
||||
assert!(
|
||||
!frame.contains(&reasoning_line(1)),
|
||||
"a long trace stays inside its window:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
|
||||
provider.release(1);
|
||||
expect(tui, "the streamed answer", |frame| frame.contains(ANSWER));
|
||||
report(tui, "answer");
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
|
||||
assert_eq!(frame.contains("Thought for "), thinking_shown);
|
||||
assert!(!frame.contains(&tail), "{}", frame.debug_dump());
|
||||
|
||||
provider.release(1);
|
||||
expect(tui, "the settled turn", |frame| {
|
||||
!frame.contains("Esc to interrupt")
|
||||
});
|
||||
settle(tui);
|
||||
report(tui, "settled");
|
||||
assert_status_and_water(tui, "settled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_streams_stops_and_restores_at_the_shipped_widths() {
|
||||
for size in [(24, 80), (32, 100), (32, 120), (40, 140)] {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
let mut tui = launch(&workspace, &provider, size, "--fresh", true);
|
||||
stream_first_turn(&mut tui, &provider, true);
|
||||
|
||||
tui.type_line(PROMPTS[1]).unwrap();
|
||||
expect(&mut tui, "the second request", |_| provider.turns() == 2);
|
||||
provider.release(1);
|
||||
let tail = reasoning_line(REASONING_LINES);
|
||||
expect(&mut tui, "thinking before the stop", |frame| {
|
||||
frame.contains("Thinking…") && frame.contains(&tail)
|
||||
});
|
||||
tui.send(keys::key::esc()).unwrap();
|
||||
expect(&mut tui, "the stopped turn", |frame| {
|
||||
!frame.contains("Esc to interrupt") && !frame.contains("Thinking…")
|
||||
});
|
||||
settle(&mut tui);
|
||||
report(&mut tui, "stopped");
|
||||
assert_status_and_water(&mut tui, "stopped");
|
||||
provider.release(2);
|
||||
|
||||
tui.send(keys::key::ctrl('u')).unwrap();
|
||||
settle(&mut tui);
|
||||
tui.send(keys::key::ctrl_d()).unwrap();
|
||||
tui.wait_for_exit(Duration::from_secs(10));
|
||||
tui.shutdown();
|
||||
|
||||
let mut tui = launch(&workspace, &provider, size, "--continue", false);
|
||||
expect(&mut tui, "the restored conversation", |frame| {
|
||||
frame.contains(ANSWER)
|
||||
});
|
||||
settle(&mut tui);
|
||||
report(&mut tui, "restored");
|
||||
assert_status_and_water(&mut tui, "restored");
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
|
||||
tui.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_choice_to_hide_thinking_survives_the_new_default() {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, Some("show_thinking = false\n"));
|
||||
let mut tui = launch(&workspace, &provider, (24, 80), "--fresh", false);
|
||||
stream_first_turn(&mut tui, &provider, false);
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thought"), "{}", frame.debug_dump());
|
||||
tui.shutdown();
|
||||
let saved: toml::Value = toml::from_str(
|
||||
&std::fs::read_to_string(workspace.home().join(".codewhale/settings.toml")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(saved["show_thinking"].as_bool(), Some(false));
|
||||
}
|
||||
|
||||
fn sign_in(workspace: &SealedWorkspace, provider: &PacedProvider, session: &str) {
|
||||
let path = workspace.home().join(".codewhale/secrets/secrets.json");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let secrets =
|
||||
codewhale_secrets::Secrets::new(Arc::new(codewhale_secrets::FileKeyringStore::new(path)));
|
||||
let bundle = serde_json::from_value(serde_json::json!({
|
||||
"tokenType": "Bearer",
|
||||
"accessToken": "loopback-access",
|
||||
"refreshToken": "loopback-refresh",
|
||||
"session": {"id": session},
|
||||
"user": {"id": "loopback-account"}
|
||||
}))
|
||||
.unwrap();
|
||||
codewhale_secrets::account::AccountSessionStore::new(secrets, None, &provider.account_base())
|
||||
.save(bundle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn run_ungated_turn(tui: &mut Harness, provider: &PacedProvider) {
|
||||
provider.release(3);
|
||||
tui.type_line(PROMPTS[0]).unwrap();
|
||||
expect(tui, "the answer", |frame| frame.contains(ANSWER));
|
||||
expect(tui, "the settled turn", |frame| {
|
||||
!frame.contains("Esc to interrupt")
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_preferences_notice_is_shown_once_per_sign_in_and_esc_dismisses_it() {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
sign_in(&workspace, &provider, "first-sign-in");
|
||||
|
||||
for cols in [80, 120] {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
sign_in(&workspace, &provider, "first-sign-in");
|
||||
let mut tui = launch(&workspace, &provider, (32, cols), "--fresh", false);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the standing notice", |frame| {
|
||||
frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "account notice");
|
||||
assert!(provider.profile_requests.load(Ordering::Acquire) > 0);
|
||||
let (_, status) = footer_row(&mut tui);
|
||||
assert!(
|
||||
status.contains(NOTICE),
|
||||
"the notice is footer chrome: {status}"
|
||||
);
|
||||
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
|
||||
tui.send(keys::key::esc()).unwrap();
|
||||
expect(&mut tui, "the dismissed notice", |frame| {
|
||||
!frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "account notice dismissed");
|
||||
tui.shutdown();
|
||||
}
|
||||
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the first notice", |frame| frame.contains(NOTICE));
|
||||
tui.shutdown();
|
||||
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
provider.turns.store(0, Ordering::Release);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
tui.wait_for_idle(Duration::from_secs(6), Duration::from_secs(30))
|
||||
.unwrap();
|
||||
report(&mut tui, "same sign-in, later session");
|
||||
assert!(
|
||||
!tui.frame().contains(NOTICE),
|
||||
"the same sign-in is told once"
|
||||
);
|
||||
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
|
||||
tui.shutdown();
|
||||
|
||||
sign_in(&workspace, &provider, "second-sign-in");
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
provider.turns.store(0, Ordering::Release);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the notice for a later sign-in", |frame| {
|
||||
frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "later sign-in");
|
||||
tui.shutdown();
|
||||
}
|
||||
@@ -2230,10 +2230,7 @@ Common settings keys:
|
||||
`@`-mention completions are populated. `fuzzy` searches the workspace and
|
||||
applies mention frecency. `browser` lists only the immediate children of the
|
||||
currently typed directory segment in deterministic alphabetical order.
|
||||
- `show_thinking` (on/off, default on): shows model thinking in the transcript,
|
||||
live while it streams and collapsed under a `Thought for …` header once the
|
||||
answer starts. Off hides it; answers and tools stay visible. A value already
|
||||
saved in `settings.toml` is kept.
|
||||
- `show_thinking` (on/off)
|
||||
- `thinking_default_expanded` (on/off, default off): renders thinking blocks
|
||||
expanded initially when `show_thinking` is enabled. Space still toggles the
|
||||
selected block, and it decides only the blocks you have not touched: a block
|
||||
|
||||
+24
-1
@@ -244,7 +244,30 @@ The manual `ci.yml` path verifies that the dispatch resolved to
|
||||
`expected_sha`, disables light-change shortcuts, and forces the heavy Rust,
|
||||
workflow, mobile, Actions, Linux, macOS, Windows, npm-wrapper, and documentation
|
||||
gates. A mismatch fails before those gates start; it never silently tests a
|
||||
different head.
|
||||
different head. The Linux, macOS and Windows suites run as `Sharded tests (<os>)`
|
||||
(`ci-test-suite.yml`): one test build per OS, parallel nextest shards of that
|
||||
archive, and a `Suite check` job that fails unless the shards together ran
|
||||
exactly the tests the archive lists; read that job, not only the shard jobs.
|
||||
|
||||
`ci.yml` also takes optional scoped-run inputs for a quick answer from an agent
|
||||
or a maintainer. With `crate` (one workspace package, for example
|
||||
`codewhale-tui`) and/or `filter` (a nextest filterset such as
|
||||
`test(auto_review)`) set, only the `Scoped test` job runs, on the operating
|
||||
systems named by `os` (default `ubuntu-latest`; a comma-separated list or
|
||||
`all`), and the full suite and every other gate are skipped. `expected_sha`
|
||||
stays required and is still verified:
|
||||
|
||||
```bash
|
||||
gh workflow run ci.yml --ref ci/my-task \
|
||||
-f expected_sha="$(git rev-parse HEAD)" \
|
||||
-f crate=codewhale-tui -f filter='test(auto_review)'
|
||||
```
|
||||
|
||||
A scoped run proves a slice, never a release; release evidence is a full
|
||||
dispatch with the scoped inputs empty. A newer dispatch on the same non-main
|
||||
branch, and a newer push to a pull request, cancels the older run of the same
|
||||
workflow; runs on `main` are keyed by SHA and never cancel each other, and a
|
||||
scoped run never cancels a full one.
|
||||
|
||||
Verify beforehand that clean local `HEAD`, `main` and `origin/main` agree.
|
||||
Record both run URLs, resolved SHAs and required job results; inspect executed
|
||||
|
||||
+1
-1
@@ -805,7 +805,7 @@ owner = "crates/tui/src/commands/groups/session/export.rs"
|
||||
[[feature]]
|
||||
id = "live-thinking"
|
||||
name = "Live thinking"
|
||||
summary = "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full."
|
||||
summary = "/verbose toggles live model thinking in the transcript."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable" }
|
||||
owner = "crates/tui/src/commands/groups/config/mod.rs"
|
||||
|
||||
@@ -1152,7 +1152,7 @@ codewhale 还把用户偏好存储在:
|
||||
- `mention_menu_limit`(整数,默认 `128`):输入区渲染可见窗口之前保留的 `@`-提及弹出候选最大数。可见行仍取决于终端高度。
|
||||
- `mention_walk_depth`(整数,默认 `10`):`@`-提及补全遍历的最大工作区深度。在深度嵌套的工作区设为 `0` 表示无限深度;在非常大的仓库保持默认,除非需要。
|
||||
- `mention_menu_behavior`(`fuzzy`、`browser`;默认 `fuzzy`):控制 `@`-提及补全如何填充。`fuzzy` 搜索工作区并应用提及 frecency。`browser` 只按确定性字母顺序列出当前输入目录段的直接子项。
|
||||
- `show_thinking`(on/off,默认 on):在对话记录中显示模型思考,流式输出时实时显示,回答开始后折叠到“思考用时 …”标题下。设为 off 则隐藏思考;回答和工具仍然可见。`settings.toml` 中已保存的值保持不变。
|
||||
- `show_thinking`(on/off)
|
||||
- `thinking_default_expanded`(on/off,默认 off):启用 `show_thinking` 时,让思考块初始以展开状态渲染。Space 仍可切换所选块,并且它只决定你尚未动过的块:你自己展开或折叠过的块,即使之后更改这个设置,也会保持该状态。这在 Space 绑定可能被拦截的 SSH/tmux 环境中很有用。
|
||||
- `thinking_preview_lines`(整数,默认 `2`):**折叠**的已完成思考仍显示多少正文行。`0` 仅头部;`10` 是较旧的完整显示。实时流式预览不变。用 Space 展开块,或设置 `thinking_default_expanded` 打开每个块。
|
||||
- `help_expand_groups`(on/off,默认 off):让帮助/快捷键从每个组展开开始。默认折叠长尾(Grok 风格);输入过滤仍展开匹配。
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
bun_binary="$(bun -p 'process.execPath')"
|
||||
host_binary="$(node -p "require('path').join(process.env.RUNNER_TEMP, 'codewhale-extension-host' + (process.platform === 'win32' ? '.exe' : ''))")"
|
||||
compiled_info="$(node crates/engine/extension-host/compile-host.mjs --bun "$bun_binary" --output "$host_binary")"
|
||||
printf 'CODEWHALE_COMPILED_HOST_TEST_BINARY=%s\n' "$host_binary" >> "$GITHUB_ENV"
|
||||
node - "$host_binary" "$bun_binary" "$compiled_info" >> "$GITHUB_ENV" <<'JS'
|
||||
const fs = require('node:fs'), crypto = require('node:crypto');
|
||||
const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const info = JSON.parse(process.argv[4]);
|
||||
if (!info.platform || !info.arch || info.platform !== info.compiler?.platform || info.arch !== info.compiler?.arch) throw new Error('image/compiler identity mismatch');
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_PLATFORM=' + info.platform);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_ARCH=' + info.arch);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_SHA256=' + digest(process.argv[2]));
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_BUN=' + process.argv[3]);
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256=' + digest(process.argv[3]));
|
||||
console.log('CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256=' + digest('crates/engine/extension-host/dist/codewhale-extension-host.mjs'));
|
||||
JS
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Count nextest tests so a sharded run can prove it ran every test once.
|
||||
|
||||
list LISTING_JSON tests the archive selects (nextest list --message-format json)
|
||||
junit JUNIT_XML tests one nextest invocation ran
|
||||
check DIRECTORY per OS, the shard files must cover the expected total exactly
|
||||
|
||||
The sharded CI suite splits one archive across jobs. A shard that is skipped,
|
||||
runs a different filter, or double-counts a partition changes the sum, so the
|
||||
suite gate fails instead of reporting a smaller run as a pass.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def listed(path: str) -> int:
|
||||
suites = json.loads(Path(path).read_text(encoding="utf-8"))["rust-suites"]
|
||||
return sum(
|
||||
1
|
||||
for suite in suites.values()
|
||||
for case in suite["testcases"].values()
|
||||
if case["filter-match"]["status"] == "matches"
|
||||
)
|
||||
|
||||
|
||||
def ran(path: str) -> int:
|
||||
return int(ET.parse(path).getroot().get("tests", "0"))
|
||||
|
||||
|
||||
def check(directory: str) -> int:
|
||||
files = sorted(Path(directory).rglob("*.json"))
|
||||
expected = {}
|
||||
shards: dict[str, dict[int, int]] = {}
|
||||
for file in files:
|
||||
record = json.loads(file.read_text(encoding="utf-8"))
|
||||
if file.name.startswith("expected-"):
|
||||
expected[record["os"]] = record
|
||||
elif file.name.startswith("shard-"):
|
||||
slot = shards.setdefault(record["os"], {})
|
||||
if record["shard"] in slot:
|
||||
print(f"{record['os']}: shard {record['shard']} reported twice")
|
||||
return 1
|
||||
slot[record["shard"]] = record["tests"]
|
||||
if not expected:
|
||||
print("no expected-<os>.json found")
|
||||
return 1
|
||||
failed = False
|
||||
for os_name, record in sorted(expected.items()):
|
||||
got = shards.get(os_name, {})
|
||||
want = set(range(1, record["shards"] + 1))
|
||||
total = sum(got.values())
|
||||
print(f"{os_name}: archive lists {record['tests']}, shards ran {total} {dict(sorted(got.items()))}")
|
||||
if set(got) != want:
|
||||
print(f"{os_name}: expected shards {sorted(want)}, found {sorted(got)}")
|
||||
failed = True
|
||||
if total != record["tests"]:
|
||||
print(f"{os_name}: shard total {total} differs from the archive's {record['tests']}")
|
||||
failed = True
|
||||
if any(count <= 0 for count in got.values()):
|
||||
print(f"{os_name}: a shard ran no tests")
|
||||
failed = True
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) == 3 and argv[1] == "list":
|
||||
print(listed(argv[2]))
|
||||
return 0
|
||||
if len(argv) == 3 and argv[1] == "junit":
|
||||
print(ran(argv[2]))
|
||||
return 0
|
||||
if len(argv) == 3 and argv[1] == "check":
|
||||
return check(argv[2])
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
@@ -273,7 +273,7 @@ class CiWiringTests(unittest.TestCase):
|
||||
block = ci[start:next_step]
|
||||
self.assertIn(
|
||||
"sh scripts/with-hermetic-test-home.sh cargo nextest run "
|
||||
"-p codewhale-tui -p codewhale-runtime --lib --locked --no-tests=fail "
|
||||
"-p codewhale-engine -p codewhale-tui -p codewhale-runtime --lib --locked --no-tests=fail "
|
||||
"-E 'test(auto_review) | test(authority) | test(sandbox)'", block
|
||||
)
|
||||
self.assertIn(
|
||||
@@ -290,27 +290,27 @@ class CiWiringTests(unittest.TestCase):
|
||||
self.assertIn("check-command-migration-manifest.py", ci)
|
||||
self.assertIn("needs.changes.outputs.heavy == 'true'", ci)
|
||||
|
||||
def test_shared_process_mode_is_manual_hosted_and_preserves_default_runner(self) -> None:
|
||||
def test_shared_process_mode_is_manual_hosted_ubuntu_only_and_replaces_the_sharded_suite(self) -> None:
|
||||
ci = load_ci()
|
||||
self.assertIn("default: nextest", ci)
|
||||
matrix = ci.split(" test:\n", 1)[1].split(" steps:\n", 1)[0]
|
||||
self.assertIn(
|
||||
SHARED_PROCESS_MODE + " && '[\"ubuntu-latest\"]' || "
|
||||
"'[\"ubuntu-latest\",\"macos-latest\",\"windows-latest\"]'", matrix,
|
||||
)
|
||||
self.assertIn("needs.changes.outputs.trusted == 'true'", matrix)
|
||||
job = ci.split(" test:\n", 1)[1].split(" steps:\n", 1)[0]
|
||||
self.assertIn("runs-on: ubuntu-latest", job)
|
||||
self.assertIn(SHARED_PROCESS_MODE, job)
|
||||
self.assertNotIn("pull_request", job)
|
||||
for name in ("test-ubuntu", "test-macos", "test-windows"):
|
||||
caller = ci.split(f" {name}:\n", 1)[1].split(" uses:", 1)[0]
|
||||
self.assertIn(f"!({SHARED_PROCESS_MODE})", caller)
|
||||
qualification = named_step(ci, "Shared-process workspace qualification")
|
||||
self.assertIn("if: " + SHARED_PROCESS_MODE + " && matrix.os == 'ubuntu-latest'", qualification)
|
||||
for name, command in (
|
||||
("Run tests", "cargo nextest run --workspace --all-features --locked --profile ci"),
|
||||
("Run doctests", "cargo test --workspace --all-features --locked --doc"),
|
||||
):
|
||||
step = named_step(ci, name)
|
||||
self.assertIn(command, step)
|
||||
self.assertIn(f"!({SHARED_PROCESS_MODE})", step)
|
||||
self.assertIn("if: " + SHARED_PROCESS_MODE, qualification)
|
||||
upload = named_step(ci, "Retain shared-process qualification evidence")
|
||||
self.assertIn("if: always() && " + SHARED_PROCESS_MODE, upload)
|
||||
self.assertIn("if-no-files-found: error", upload)
|
||||
suite = (ROOT / ".github" / "workflows" / "ci-test-suite.yml").read_text(encoding="utf-8")
|
||||
for name, command in (
|
||||
("Run tests", "cargo nextest run"),
|
||||
("Run doctests", "cargo test --workspace --all-features --locked --doc"),
|
||||
):
|
||||
self.assertIn(command, named_step(suite, name))
|
||||
|
||||
def test_shared_process_runs_exact_command_twice_with_fresh_hermetic_homes(self) -> None:
|
||||
proof = run_shared_process_fixture()
|
||||
@@ -332,31 +332,24 @@ class CiWiringTests(unittest.TestCase):
|
||||
for identity in ISSUE_6698_TESTS:
|
||||
self.assertIn(f"test {identity} ... ok", proof["evidence"][f"run-{run}.log"])
|
||||
|
||||
def test_shared_process_keeps_macos_budget_when_self_hosted_test_leg_is_absent(self) -> None:
|
||||
def test_macos_budget_runs_on_every_heavy_dispatch_and_never_on_pull_requests(self) -> None:
|
||||
job = load_ci().split(" macos-budget:\n", 1)[1].split(" steps:\n", 1)[0]
|
||||
condition = next(line.strip()[4:] for line in job.splitlines() if line.strip().startswith("if: "))
|
||||
self.assertIn("runs-on: macos-latest", job)
|
||||
for event in ("pull_request", "push", "schedule", "workflow_dispatch"):
|
||||
for mode in ("nextest", "shared-process-twice"):
|
||||
for heavy, trusted, self_hosted in (
|
||||
(True, True, True), (True, True, False),
|
||||
(True, False, True), (False, True, True),
|
||||
):
|
||||
with self.subTest(event=event, mode=mode, heavy=heavy, trusted=trusted, self_hosted=self_hosted):
|
||||
for heavy in (True, False):
|
||||
for scoped in (True, False):
|
||||
with self.subTest(event=event, heavy=heavy, scoped=scoped):
|
||||
expression = condition
|
||||
for name, value in {
|
||||
"github.event_name": event,
|
||||
"inputs.workspace_test_mode": mode,
|
||||
"needs.changes.outputs.heavy": str(heavy).lower(),
|
||||
"needs.changes.outputs.trusted": str(trusted).lower(),
|
||||
"vars.CW_SELF_HOSTED_MAC": str(self_hosted).lower(),
|
||||
"needs.changes.outputs.scoped": str(scoped).lower(),
|
||||
}.items():
|
||||
expression = expression.replace(name, repr(value))
|
||||
expression = re.sub(r"!(?!=)", "not ", expression.replace("&&", "and").replace("||", "or"))
|
||||
actual = eval(expression, {"__builtins__": {}}, {})
|
||||
previous = heavy and event != "pull_request" and not (trusted and self_hosted)
|
||||
qualification = heavy and event == "workflow_dispatch" and mode == "shared-process-twice"
|
||||
self.assertEqual(actual, previous or qualification)
|
||||
self.assertEqual(actual, heavy and not scoped and event != "pull_request")
|
||||
|
||||
def test_shared_process_retains_failed_first_run_and_still_runs_second_once(self) -> None:
|
||||
proof = run_shared_process_fixture("first_failure")
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { DocArticle } from "../_components/doc-article";
|
||||
import { getDocsFirstTime } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const t = getDocsFirstTime(locale);
|
||||
return buildPageMetadata({
|
||||
path: "/docs/first-time",
|
||||
locale,
|
||||
title: t.metaTitle,
|
||||
description: t.metaDescription,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function FirstTimePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
return <DocArticle t={getDocsFirstTime(locale)} locale={locale} />;
|
||||
}
|
||||
@@ -97,30 +97,6 @@ export default async function InstallPage({ params }: { params: Promise<{ locale
|
||||
<div lang={copyLocale}>
|
||||
{locale === "zh" ? mirrors : null}
|
||||
|
||||
<Block id="install-new-to-terminal" title={t(INSTALL_COPY.newTitle)}>
|
||||
{p(INSTALL_COPY.newLead)}
|
||||
<Channel id="install-new-macos" title={t(INSTALL_COPY.newMacTitle)}>
|
||||
<ol>
|
||||
<li><Rich text={t(INSTALL_COPY.newMac1)} /></li>
|
||||
<li><Rich text={t(INSTALL_COPY.newMac2)} />{code(INSTALL_COMMANDS.shell)}</li>
|
||||
<li><Rich text={t(INSTALL_COPY.newMac3)} /></li>
|
||||
</ol>
|
||||
</Channel>
|
||||
<Channel id="install-new-windows" title={t(INSTALL_COPY.newWinTitle)}>
|
||||
<ol>
|
||||
<li><Rich text={t(INSTALL_COPY.newWin1)} /></li>
|
||||
<li><Rich text={t(INSTALL_COPY.newWin2)} />{code(INSTALL_COMMANDS.windows)}</li>
|
||||
<li><Rich text={t(INSTALL_COPY.newWin3)} /></li>
|
||||
</ol>
|
||||
</Channel>
|
||||
<p>
|
||||
<Link href={`/${locale}/docs/first-time`} className="section-link">
|
||||
{t(INSTALL_COPY.newNext)}
|
||||
<Icon name="arrow-right" className="icon icon-flip" />
|
||||
</Link>
|
||||
</p>
|
||||
</Block>
|
||||
|
||||
<Block id="install-path" title={t(INSTALL_COPY.pathTitle)}>
|
||||
{p(INSTALL_COPY.pathPlaces)}
|
||||
<ul>
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
.install-guide ul { list-style: disc; }
|
||||
.install-guide ol { list-style: decimal; }
|
||||
.install-guide li { margin-block: var(--space-1); }
|
||||
.install-guide li > .code { margin-block: var(--space-3); }
|
||||
.install-guide ::marker { color: var(--muted); }
|
||||
.install-guide a:not(.btn) { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 0.2em; }
|
||||
.install-guide a:not(.btn):hover { text-decoration-color: var(--accent); }
|
||||
|
||||
@@ -1,35 +1,4 @@
|
||||
[
|
||||
{
|
||||
"id": "first-time",
|
||||
"slug": "first-time",
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/GUIDE.md",
|
||||
"sha256": "2fd312552cabc519c990071a16111fe251f902104ebd65f22513039d94e3412d",
|
||||
"content": "# Codewhale User Guide\n\nThis guide is for your first hour with Codewhale. It explains the main\nworkflow, the important safety controls, and where to go next when you need a\ncomplete reference.\n\nCodewhale has deeper reference documents for installation, configuration,\nproviders, modes, keybindings, tools, and operations. Use this page as a guided\nwalkthrough, then follow the \"Next\" links when you need every option.\n\n## 1. Welcome to Codewhale\n\nCodewhale is a terminal coding agent. You run it from a workspace, give it a\ntask, and it can use structured tools to inspect files, run commands, edit\ncode, and report back with evidence.\n\nThe important difference from a normal chat model is that Codewhale is built\naround a harness:\n\n- It keeps the active workspace and session visible.\n- It routes each turn through explicit modes and approval rules.\n- It shows tool calls in the transcript instead of hiding the work.\n- It can preserve sessions, fork conversations, and continue later.\n- It can run sub-agents for focused background work.\n\nYou can use Codewhale for small questions:\n\n```text\nExplain the authentication flow in this repository.\n```\n\nYou can also use it for multi-step work:\n\n```text\nFind the failing validation path, propose a fix, and wait for my approval\nbefore editing files.\n```\n\nFor a new repository, start conservatively. Ask Codewhale to explore and plan\nbefore asking it to change files. That gives you a reviewable path and makes it\neasier to catch wrong assumptions early.\n\nNext: [ARCHITECTURE.md](ARCHITECTURE.md) explains the internal harness and\nruntime model.\n\n## 2. First Launch\n\nFor a new macOS or Linux installation, use the official GitHub release.\nThe installer verifies the release checksums and provides the same runtime\nunder the `codewhale` and `codew` command names:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWindows users should choose the matching installer or archive from\n[GitHub Releases](https://github.com/codewhale-hq/CodeWhale/releases/latest).\nFor an existing direct install, use `codewhale update --check`, then\n`codewhale update`. npm and Cargo remain secondary packaging routes; Cargo\nalso supports source builds where a compatible prebuilt is unavailable.\nFor occupied directories, package-managed installs, and PATH setup, follow\n[the installation and migration guide](INSTALL.md#recommended-official-github-releases).\nAndroid/Termux uses its own [preview archive or source-build path](INSTALL.md#android--termux-arm64).\n\nDocker is also available when you want an isolated runtime. The published\nimage is currently 0.10.0; to test current `main`, [build it from source](DOCKER.md#building-locally).\n\n```bash\ndocker volume create codewhale-home\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nOnce the install directory is on PATH, launch Codewhale from the repository or\ndirectory you want it to work in:\n\n```bash\ncodewhale\n```\n\nFor the default GitHub installer destination, you can use\n`\"$HOME/.local/bin/codewhale\"` until that directory is on PATH.\n\nOn first launch, Codewhale opens the composer, even with no key or route.\nWith no key or route, workspace trust is asked once a provider is connected;\notherwise only when the folder requires a decision. A provider is asked for\nwhen you send your first message: the message stays in the box while you sign\nin or use your own key, and a local model is offered only when a local Ollama\nserver answers. The provider step also has an explicit offline route.\n\nEverything optional stays available after that. Use `/setup` for the\nprogressive setup and repair guide, `/settings` for the full typed editor, and\n`/constitution` when you want your own constitution on top of the bundled one.\nThe localized telemetry choice appears only after the workspace is ready and\ndoes not block the composer.\n\nDeepSeek is the default provider. If you want to configure its key before or\nafter the first launch, the most direct setup path is:\n\n```bash\ncodewhale auth set --provider deepseek\n```\n\nYou can also provide a key through the environment:\n\n```bash\nexport DEEPSEEK_API_KEY=\"your-key\"\ncodewhale\n```\n\nNew Codewhale config is stored under `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` files are still supported for users migrating from\nthe old name.\n\nUse `/constitution` to review or change standing guidance. After setup, run a\ndoctor check:\n\n```bash\ncodewhale doctor\n```\n\nUse the JSON form when you need a machine-readable report for an issue:\n\n```bash\ncodewhale doctor --json\n```\n\nBoth forms are offline by default. They report structural configuration and\nliteral unknown/not-probed credential states without loading workspace `.env`\ncredentials, openiLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
"sha256": "bfd3ed8cae274f9b72d1805232f41d4342cc29b45b53617a26f06c3776941202",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not on the\npermission dial.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nOne dial says how much Codewhale may do without asking. Press `Shift+Tab` to\nstep it: **Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask**. Plan\nis the Plan mode; every other stop is Work with that permission posture. The\nfooter shows the current stop and `Shift+Tab to change`. New installs start on\nAccept Edits when no permission posture is saved. An explicit saved choice,\nincluding Ask from an earlier version, stays in control.\n`Tab` only completes composer menus, commands, file mentions and suggestions;\nit never changes the mode, and never sends or queues composer text (use\n`Enter`).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work` or\n`/mode plan`. `operate` and its spellings still parse, as Work, and say that\nOperate was retired, so an older setting or script never errors.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Accept Edits), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `grep_files`, `bash`, `agent`, `todo_write`, `present`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate is retired.** It was Work with an orchestration contract, and that contract now lives in every session's constitution: make a goal of a durable request, keep the plan visible, fan independent work out to parallel sub-agents, and have an independent reviewer try to refute a non-trivial change. A saved `default_mode = \"operate\"` loads as Work and says so once; a Runtime API request with `mode: \"operate\"` runs as `agent` with the default posture (Accept Edits) and logs one notice per process, never an error. Agents forked from an older Operate conversation do not inherit its contract. Multi-agent work stays on `agent`, `workflow` and `/fleet` in every mode.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work |\n|:---|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated |\n| `bash` | visible name; execution denied | approval- and policy-gated; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same |\n| Paid or external-service tools | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode; fleet profiles never widen it |\n\nMode changes scheduling emphasLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/SANDBOX.md",
|
||||
"sha256": "1d765dc9b89ff76aedd512a49983058fae999e25fc5dd84868f38f268d00b0b1",
|
||||
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository also contains a future Windows helper contract. It is not\nwired into child-command launch, so Codewhale does not advertise it as an\nactive sandbox. Source-only sandbox code is not evidence that a command was\nrestricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). A `workspace-write`\npolicy then binds the host `/tmp` (and `TMPDIR`) over that tmpfs unless it\nexcludes them, so by default a sandboxed command shares the host's `/tmp`,\nwritable, with the usual sticky-bit rules. Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` sheLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/KEYBINDINGS.md",
|
||||
"sha256": "5b3a71595db86ed30eb79675c2b934d60dce897f0202142794be849537a36da9",
|
||||
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | Complete a slash command, `@` file mention or prompt suggestion. Tab never changes the mode |\n| `Shift+Tab` | Step the permission dial: Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask. Plan is the Plan mode; every other stop is Work with that posture. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change, including into or out of Plan, applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "install",
|
||||
"slug": "install",
|
||||
@@ -89,8 +58,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/PROVIDERS.md",
|
||||
@@ -121,8 +90,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/LEGACY_PATHS.md",
|
||||
@@ -153,8 +122,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -296,8 +265,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -328,8 +297,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -408,8 +377,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "CONTRIBUTING.md",
|
||||
"sha256": "7b3e523585eb5535555b1d8aaa66153ce0451b1cedc165dfba212e372dd71d41",
|
||||
"content": "# Contributing to Codewhale\n\nCodewhale is what it is because of the people who used it, reported what broke,\ntranslated it, added their providers and sent fixes. Thank you for being here.\nEveryone is welcome, in English or 中文, with or without code.\n\nAll feedback is a gift to this project, including \"this looked wrong\" or \"I got\nlost here\" with no fix attached. The [roadmap](docs/ROADMAP.md) says where\nCodewhale is going and where help counts most.\n\n## Where things go\n\n| You have | Best place |\n| --- | --- |\n| A bug, a crash, or behavior that surprised you | [Bug report](https://github.com/codewhale-hq/CodeWhale/issues/new?template=bug_report.yml) |\n| An idea or a feature you want | [Feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml), or [Ideas](https://github.com/codewhale-hq/CodeWhale/discussions/categories/ideas) to talk it through first |\n| Feedback on how something looks, reads or feels | [Feedback](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feedback.yml) |\n| A question | [Q&A](https://github.com/codewhale-hq/CodeWhale/discussions/categories/q-a) or [Discord](https://discord.gg/37gfS3ksug) |\n| A security problem | A [private advisory](https://github.com/codewhale-hq/CodeWhale/security/advisories/new), never a public issue ([policy](.github/SECURITY.md)) |\n| A fix for an open issue | A pull request that links the issue |\n| A translation, a provider or model, a docs fix | A pull request, no issue needed ([standing projects](#standing-projects)) |\n| A new feature, already written or not | An issue first ([why](#new-features-start-as-an-issue)) |\n\nIf you are not sure, open an issue. Nothing is filed in the wrong place for\nlong: a maintainer moves it.\n\n## New features start as an issue\n\nIssues are the best place for feature requests, ideas and bugs. A pull request\nis the best place for work we have already agreed on.\n\nA feature that first appears as a pull request takes longer to land than one\nthat starts as an issue, even when we want it. The questions that decide a\nfeature are about the product, not the diff: does it belong in the engine or in\na plugin, what does it do in each permission mode, how does it show up in\nreceipts, what is it called in 19 languages. Answering those in a review means\nrewriting code you already finished. Answering them in an issue costs you a few\nsentences.\n\nSo for a new feature:\n\n1. Open a [feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml).\n Say what you are trying to do and what gets in the way. A proposed design is\n welcome and optional.\n2. Tick \"I would like to open a pull request\" if you want to build it.\n3. A maintainer answers with the shape that fits, or with what is still\n undecided. Then the pull request is small and merges quickly.\n\nA feature pull request that arrives without an issue is still read, and it is\nnever closed for that reason. We will most likely ask the same questions there\nand it will take longer.\n\n## Standing projects\n\nThese are always open, and a pull request needs no issue first:\n\n- **Translations.** You can improve the terminal language packs, the translated\n READMEs in `docs/i18n/`, the website, and the translated docs in\n `docs/zh_hans/` and `docs/id/`. The [localization matrix](docs/LOCALIZATION.md)\n shows what each language has and how to add one, and the\n [Chinese localization group](https://github.com/codewhale-hq/CodeWhale/issues/6804)\n is recruiting.\n- **Providers and models.** You can add a missing provider or a new model, or\n correct a price, a context size or a provider quirk.\n [Providers](docs/PROVIDERS.md) lists what ships, and\n `python3 scripts/check-provider-registry.py` checks your change.\n- **Documentation.** You can fix a typo, an unclear step, a missing example or\n an instruction that no longer matches the product.\n- **Bug fixes.** If there is no issue yet, describe the bug in the pull request\n by saying what you ran, what happened and what you expected.\n- **Planned work.** You can take anything in a\n [milestone](docs/ROADMAP.md#releases) or anything labelled\n [help wanted](https://github.com/codewhale-hq/CodeWhale/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).\n Comment on the issue to claim it so two people do not build the same thing.\n\nLarge changes to planned work are easier in layers; see\n[Layered and EPIC-Sized Work](#layered-and-epic-sized-work).\n\n### What a planned-work issue gives you\n\nEvery issue a maintainer opens for planned work, and every release tracking issue, has the same parts, so you can pick one up without asking first:\n\n- **Status:** what is on `main` already, with commit links, and what is still missing.\n- **Ways to help:** each piece open to contributors, with the files to read, the command to test it locally (usually `scripts/dev-test.sh <crate> <filter>Line truncated
|
||||
"sha256": "096a609f93d82c8bcea9f967072d5306807cd4375390218f9d3784023df0dcb0",
|
||||
"content": "# Contributing to Codewhale\n\nCodewhale is what it is because of the people who used it, reported what broke,\ntranslated it, added their providers and sent fixes. Thank you for being here.\nEveryone is welcome, in English or 中文, with or without code.\n\nAll feedback is a gift to this project, including \"this looked wrong\" or \"I got\nlost here\" with no fix attached. The [roadmap](docs/ROADMAP.md) says where\nCodewhale is going and where help counts most.\n\n## Where things go\n\n| You have | Best place |\n| --- | --- |\n| A bug, a crash, or behavior that surprised you | [Bug report](https://github.com/codewhale-hq/CodeWhale/issues/new?template=bug_report.yml) |\n| An idea or a feature you want | [Feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml), or [Ideas](https://github.com/codewhale-hq/CodeWhale/discussions/categories/ideas) to talk it through first |\n| Feedback on how something looks, reads or feels | [Feedback](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feedback.yml) |\n| A question | [Q&A](https://github.com/codewhale-hq/CodeWhale/discussions/categories/q-a) or [Discord](https://discord.gg/37gfS3ksug) |\n| A security problem | A [private advisory](https://github.com/codewhale-hq/CodeWhale/security/advisories/new), never a public issue ([policy](.github/SECURITY.md)) |\n| A fix for an open issue | A pull request that links the issue |\n| A translation, a provider or model, a docs fix | A pull request, no issue needed ([standing projects](#standing-projects)) |\n| A new feature, already written or not | An issue first ([why](#new-features-start-as-an-issue)) |\n\nIf you are not sure, open an issue. Nothing is filed in the wrong place for\nlong: a maintainer moves it.\n\n## New features start as an issue\n\nIssues are the best place for feature requests, ideas and bugs. A pull request\nis the best place for work we have already agreed on.\n\nA feature that first appears as a pull request takes longer to land than one\nthat starts as an issue, even when we want it. The questions that decide a\nfeature are about the product, not the diff: does it belong in the engine or in\na plugin, what does it do in each permission mode, how does it show up in\nreceipts, what is it called in 19 languages. Answering those in a review means\nrewriting code you already finished. Answering them in an issue costs you a few\nsentences.\n\nSo for a new feature:\n\n1. Open a [feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml).\n Say what you are trying to do and what gets in the way. A proposed design is\n welcome and optional.\n2. Tick \"I would like to open a pull request\" if you want to build it.\n3. A maintainer answers with the shape that fits, or with what is still\n undecided. Then the pull request is small and merges quickly.\n\nA feature pull request that arrives without an issue is still read, and it is\nnever closed for that reason. We will most likely ask the same questions there\nand it will take longer.\n\n## Standing projects\n\nThese are always open, and a pull request needs no issue first:\n\n- **Translations.** You can improve the terminal language packs, the translated\n READMEs in `docs/i18n/`, the website, and the translated docs in\n `docs/zh_hans/` and `docs/id/`. The [localization matrix](docs/LOCALIZATION.md)\n shows what each language has and how to add one, and the\n [Chinese localization group](https://github.com/codewhale-hq/CodeWhale/issues/6804)\n is recruiting.\n- **Providers and models.** You can add a missing provider or a new model, or\n correct a price, a context size or a provider quirk.\n [Providers](docs/PROVIDERS.md) lists what ships, and\n `python3 scripts/check-provider-registry.py` checks your change.\n- **Documentation.** You can fix a typo, an unclear step, a missing example or\n an instruction that no longer matches the product.\n- **Bug fixes.** If there is no issue yet, describe the bug in the pull request\n by saying what you ran, what happened and what you expected.\n- **Planned work.** You can take anything in a\n [milestone](docs/ROADMAP.md#releases) or anything labelled\n [help wanted](https://github.com/codewhale-hq/CodeWhale/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).\n Comment on the issue to claim it so two people do not build the same thing.\n\nLarge changes to planned work are easier in layers; see\n[Layered and EPIC-Sized Work](#layered-and-epic-sized-work).\n\n### What a planned-work issue gives you\n\nEvery issue a maintainer opens for planned work, and every release tracking issue, has the same parts, so you can pick one up without asking first:\n\n- **Status:** what is on `main` already, with commit links, and what is still missing.\n- **Ways to help:** each piece open to contributors, with the files to read, the command to test it locally (usually `scripts/dev-test.sh <crate> <filter>Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/AGENT_ETHOS.md",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"tag": "v0.10.2",
|
||||
"slugs": [
|
||||
"first-time",
|
||||
"install",
|
||||
"guide",
|
||||
"auth",
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
"tag": "v0.10.2",
|
||||
"version": "0.10.2",
|
||||
"topics": [
|
||||
{
|
||||
"id": "first-time",
|
||||
"slug": "first-time",
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/GUIDE.md",
|
||||
"sha256": "43b307ea1fc95898f1ec2484f0b9073b7774d14504d4b12c41e4bf0d5af3b336",
|
||||
"content": "# Codewhale User Guide\n\nThis guide is for your first hour with Codewhale. It explains the main\nworkflow, the important safety controls, and where to go next when you need a\ncomplete reference.\n\nCodewhale has deeper reference documents for installation, configuration,\nproviders, modes, keybindings, tools, and operations. Use this page as a guided\nwalkthrough, then follow the \"Next\" links when you need every option.\n\n## 1. Welcome to Codewhale\n\nCodewhale is a terminal coding agent. You run it from a workspace, give it a\ntask, and it can use structured tools to inspect files, run commands, edit\ncode, and report back with evidence.\n\nThe important difference from a normal chat model is that Codewhale is built\naround a harness:\n\n- It keeps the active workspace and session visible.\n- It routes each turn through explicit modes and approval rules.\n- It shows tool calls in the transcript instead of hiding the work.\n- It can preserve sessions, fork conversations, and continue later.\n- It can run sub-agents for focused background work.\n\nYou can use Codewhale for small questions:\n\n```text\nExplain the authentication flow in this repository.\n```\n\nYou can also use it for multi-step work:\n\n```text\nFind the failing validation path, propose a fix, and wait for my approval\nbefore editing files.\n```\n\nFor a new repository, start conservatively. Ask Codewhale to explore and plan\nbefore asking it to change files. That gives you a reviewable path and makes it\neasier to catch wrong assumptions early.\n\nNext: [ARCHITECTURE.md](ARCHITECTURE.md) explains the internal harness and\nruntime model.\n\n## 2. First Launch\n\nFor a new macOS or Linux installation, use the official GitHub release.\nThe installer verifies the release checksums and provides the same runtime\nunder the `codewhale` and `codew` command names:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWindows users should choose the matching installer or archive from\n[GitHub Releases](https://github.com/codewhale-hq/CodeWhale/releases/latest).\nFor an existing direct install, use `codewhale update --check`, then\n`codewhale update`. npm and Cargo remain secondary packaging routes; Cargo\nalso supports source builds where a compatible prebuilt is unavailable.\nFor occupied directories, package-managed installs, and PATH setup, follow\n[the installation and migration guide](INSTALL.md#recommended-official-github-releases).\nAndroid/Termux uses its own [preview archive or source-build path](INSTALL.md#android--termux-arm64).\n\nDocker is also available when you want an isolated runtime. The published\nimage is currently 0.10.0; to test current `main`, [build it from source](DOCKER.md#building-locally).\n\n```bash\ndocker volume create codewhale-home\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nOnce the install directory is on PATH, launch Codewhale from the repository or\ndirectory you want it to work in:\n\n```bash\ncodewhale\n```\n\nFor the default GitHub installer destination, you can use\n`\"$HOME/.local/bin/codewhale\"` until that directory is on PATH.\n\nOn first launch, Codewhale opens the composer, even with no key or route.\nWith no key or route, workspace trust is asked once a provider is connected;\notherwise only when the folder requires a decision. A provider is asked for\nwhen you send your first message: the message stays in the box while you sign\nin or use your own key, and a local model is offered only when a local Ollama\nserver answers. The provider step also has an explicit offline route.\n\nEverything optional stays available after that. Use `/setup` for the\nprogressive setup and repair guide, `/settings` for the full typed editor, and\n`/constitution` when you want your own constitution on top of the bundled one.\nThe localized telemetry choice appears only after the workspace is ready and\ndoes not block the composer.\n\nDeepSeek is the default provider. If you want to configure its key before or\nafter the first launch, the most direct setup path is:\n\n```bash\ncodewhale auth set --provider deepseek\n```\n\nYou can also provide a key through the environment:\n\n```bash\nexport DEEPSEEK_API_KEY=\"your-key\"\ncodewhale\n```\n\nNew Codewhale config is stored under `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` files are still supported for users migrating from\nthe old name.\n\nUse `/constitution` to review or change standing guidance. After setup, run a\ndoctor check:\n\n```bash\ncodewhale doctor\n```\n\nUse the JSON form when you need a machine-readable report for an issue:\n\n```bash\ncodewhale doctor --json\n```\n\nBoth forms are offline by default. They report structural configuration and\nliteral unknown/not-probed credential states without loading workspace `.env`\ncredentials, opeLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "e8d15c92107272146ad38f66c487bda9c821a4fd85b46c0a1c884907acc6803a",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back]Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
"sha256": "ea1e371421770a8582195f4c80c70fe9d067b3800ef143c7fea1fdcf444f0ecd",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUseLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/SANDBOX.md",
|
||||
"sha256": "1626a34e894f491c15959ebe82b87d2f669c792f9c9457fb912023dcee1a5a33",
|
||||
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository contains a seccomp implementation module plus a future Windows\nhelper contract. They are not wired into child-command launch, so Codewhale\ndoes not advertise them as active sandboxes. Source-only sandbox code is not\nevidence that a command was restricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` shell mode turns it on\nonly in Full Access (see [Subagents](SUBAGENTS.md)); it then shares the host\nnetwork namespace, including loopback and abstract unix sockets, like a\nnetworLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/KEYBINDINGS.md",
|
||||
"sha256": "26d213c47e899e376f240d2f9f08efeac49911a7472ee7d7ed11fea249509ab3",
|
||||
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact that is serving an in-flight turn, Esc stops the turn (the compact is collateral). |\n\n## Composer\n\nEditing the message you're about to send.\n\n| Chord | Action |\n|-----------------------------|---------------------------------------------------------|\n| `Enter` | Send when idle; queue while busLine truncated
|
||||
}
|
||||
],
|
||||
"missing": []
|
||||
},
|
||||
{
|
||||
"id": "install",
|
||||
"slug": "install",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"format": "codewhale.features.v1",
|
||||
"source": {
|
||||
"path": "docs/features.toml",
|
||||
"sha256": "a63042dc25a658f626735cc40e7a7cf4c67eaa51bfbb037dc2820d083d10fe3a"
|
||||
"sha256": "4c4dfe17cea716328253c687d9d0665e1b24fc3b59d00f2a81cf1157dfea7072"
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
@@ -1070,7 +1070,7 @@
|
||||
{
|
||||
"id": "live-thinking",
|
||||
"name": "Live thinking",
|
||||
"summary": "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full.",
|
||||
"summary": "/verbose toggles live model thinking in the transcript.",
|
||||
"status": "stable",
|
||||
"surfaces": {
|
||||
"tui": "stable"
|
||||
|
||||
@@ -2299,279 +2299,6 @@
|
||||
],
|
||||
"sourceNote": "Source documents: docs/public-surface-facts.json (trust), docs/TELEMETRY.md, docs/SANDBOX.md · Update docs-map.ts when changing."
|
||||
},
|
||||
"docs-first-time": {
|
||||
"metaTitle": "Use Codewhale for the first time · Codewhale Docs",
|
||||
"metaDescription": "Open a terminal, install Codewhale, pick the folder it works in, connect a model and finish a first task, with no terminal experience needed.",
|
||||
"bodyClassName": "text-ink-soft leading-relaxed",
|
||||
"title": "Use Codewhale for the first time",
|
||||
"lede": "This page takes you from opening a terminal for the first time to finishing a small task with Codewhale. It also explains which folder to start in, because that folder decides what Codewhale can change.",
|
||||
"sections": [
|
||||
{
|
||||
"id": "what-you-need",
|
||||
"title": "Check what you need",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "You need a Mac, a Windows x64 PC or a Linux computer, and an internet connection for the install. To get answers you also need a model: an API key from a hosted provider such as DeepSeek, or a model running on your own computer with Ollama."
|
||||
},
|
||||
{
|
||||
"p": "Codewhale is free and needs no account. A hosted provider bills you directly for what you use, and a model you run yourself needs no key."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "open-terminal",
|
||||
"title": "Open a terminal",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "A terminal is a window where you type commands instead of clicking. A command runs when you press Return, which is Enter on Windows."
|
||||
},
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"macOS",
|
||||
"Press Cmd+Space, type Terminal and press Return."
|
||||
],
|
||||
[
|
||||
"Windows",
|
||||
"Open the Start menu, type PowerShell and press Enter."
|
||||
],
|
||||
[
|
||||
"Linux",
|
||||
"Open your applications menu and search for Terminal. On Ubuntu, Ctrl+Alt+T opens one."
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "Paste with Cmd+V on a Mac and Ctrl+V on Windows. Most Linux terminals paste with Ctrl+Shift+V."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "install",
|
||||
"title": "Install Codewhale",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "On a Mac or Linux, paste this command and press Return. It downloads Codewhale, checks the download, and adds Codewhale to your PATH, the list of folders your terminal searches for commands."
|
||||
},
|
||||
{
|
||||
"code": "curl -fsSL https://codewhale.net/install.sh | sh",
|
||||
"lang": "macOS · Linux"
|
||||
},
|
||||
{
|
||||
"p": "On Windows, paste this command and press Enter:"
|
||||
},
|
||||
{
|
||||
"code": "winget install --id HunterBown.CodeWhale --exact --source winget",
|
||||
"lang": "Windows"
|
||||
},
|
||||
{
|
||||
"p": "When it finishes, open a new terminal window so it picks up the new PATH. Then check that it worked: a version number means Codewhale is installed."
|
||||
},
|
||||
{
|
||||
"code": "codewhale --version",
|
||||
"lang": "Terminal"
|
||||
},
|
||||
{
|
||||
"p": "If the terminal says `command not found`, follow [the PATH steps on the install page](/install#install-path)."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "choose-folder",
|
||||
"title": "Choose the folder to work in",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "Codewhale works in the folder your terminal is in when you start it. That folder is its workspace: Codewhale reads and changes files there, and its file tools cannot reach other folders unless you allow it."
|
||||
},
|
||||
{
|
||||
"p": "On a Mac, an operating-system sandbox also stops its commands from writing outside the workspace, apart from temporary files. Linux gets the same sandbox when bubblewrap is installed. Windows has no such sandbox yet, so Codewhale relies on asking you first."
|
||||
},
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"A folder for one project",
|
||||
"The best choice for most work. Codewhale sees only that project, and `/undo` can take back what it changed."
|
||||
],
|
||||
[
|
||||
"A new, empty folder",
|
||||
"A good start for something new, such as a website or a script. The next section shows how to make one."
|
||||
],
|
||||
[
|
||||
"A folder that holds several projects",
|
||||
"Lets one task read and change files across all of them. Pick the smallest folder that holds everything the task needs; in a very large folder Codewhale turns snapshots off and tells you."
|
||||
],
|
||||
[
|
||||
"Your home folder, Desktop, Documents or Downloads",
|
||||
"Codewhale runs there, but `/undo` and snapshots are off in these folders themselves because they are too broad to snapshot safely. A folder inside one of them works normally."
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "Snapshots are Codewhale's own copies of the files it changes. It keeps them in its settings folder, not in your project, and they work without git."
|
||||
},
|
||||
{
|
||||
"p": "To let Codewhale reach one more folder, type `/trust add` and that folder's path in Codewhale. Full Access mode reaches every folder and runs commands without the sandbox, so use it only when you want Codewhale to work across your whole computer."
|
||||
},
|
||||
{
|
||||
"p": "By default, Codewhale makes file edits in a git repository without asking and shows you what changed. In a folder without git, it asks before each edit."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "open-in-folder",
|
||||
"title": "Open a terminal in that folder",
|
||||
"blocks": [
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"macOS Finder",
|
||||
"Control-click the folder and choose Services > New Terminal at Folder. If it is missing, turn it on in System Settings > Keyboard > Keyboard Shortcuts > Services > Files and Folders."
|
||||
],
|
||||
[
|
||||
"Windows File Explorer",
|
||||
"Right-click the folder and choose Open in Terminal. On Windows 10, hold Shift while you right-click and choose Open PowerShell window here."
|
||||
],
|
||||
[
|
||||
"Any terminal",
|
||||
"Type `cd` and a space, drag the folder from Finder or File Explorer into the window, and press Return."
|
||||
],
|
||||
[
|
||||
"Without moving",
|
||||
"Start Codewhale with `-C` and the folder's path, for example `codewhale -C ~/Projects/my-site` on a Mac."
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"p": "These commands find your way around from inside a terminal. They work on a Mac, on Linux and in PowerShell."
|
||||
},
|
||||
{
|
||||
"codeTerms": true,
|
||||
"rows": [
|
||||
[
|
||||
"pwd",
|
||||
"Shows the folder you are in."
|
||||
],
|
||||
[
|
||||
"ls",
|
||||
"Lists what is in it."
|
||||
],
|
||||
[
|
||||
"cd Documents",
|
||||
"Moves into the Documents folder inside the current one."
|
||||
],
|
||||
[
|
||||
"cd ..",
|
||||
"Moves up one folder."
|
||||
],
|
||||
[
|
||||
"mkdir my-site",
|
||||
"Makes a new folder called my-site."
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "Type the first letters of a folder name and press Tab, and the terminal finishes the name for you."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "connect",
|
||||
"title": "Start Codewhale and connect a model",
|
||||
"blocks": [
|
||||
{
|
||||
"code": "codewhale",
|
||||
"lang": "Terminal"
|
||||
},
|
||||
{
|
||||
"p": "Codewhale opens a message box right away. When you send your first message, it asks where your model runs: pick a hosted provider and paste its API key, or pick Ollama when it is running on your computer."
|
||||
},
|
||||
{
|
||||
"p": "Keys you enter stay on your computer, in Codewhale's settings folder `~/.codewhale`. Change the provider later with `/provider`."
|
||||
},
|
||||
{
|
||||
"p": "If Codewhale asks whether to work with the instructions and files in this folder, answer yes for a folder you made yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "first-task",
|
||||
"title": "Give it a first task",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "Type what you want in plain words and press Return. A safe first task changes nothing, for example: `Explain what is in this folder and what each file is for. Do not change anything.`"
|
||||
},
|
||||
{
|
||||
"p": "Then try one that makes a file, for example: `Make a file called packing-list.md with a packing list for a weekend trip.`"
|
||||
},
|
||||
{
|
||||
"p": "By default, Codewhale asks before it runs a command that is not read-only. Read what the approval says will run before you allow it."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stop-undo-quit",
|
||||
"title": "Stop, undo and quit",
|
||||
"blocks": [
|
||||
{
|
||||
"codeTerms": true,
|
||||
"rows": [
|
||||
[
|
||||
"Esc",
|
||||
"Stops the task that is running. Ctrl+C does the same."
|
||||
],
|
||||
[
|
||||
"/undo",
|
||||
"Takes back your last request and the file changes it made."
|
||||
],
|
||||
[
|
||||
"/diff",
|
||||
"Shows what changed in this folder."
|
||||
],
|
||||
[
|
||||
"/receipts",
|
||||
"Lists what Codewhale did: files, commands and approvals."
|
||||
],
|
||||
[
|
||||
"/exit",
|
||||
"Quits Codewhale. Ctrl+D also quits when the message box is empty."
|
||||
],
|
||||
[
|
||||
"codewhale --continue",
|
||||
"Run it in the same folder to pick up your last conversation there."
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "help",
|
||||
"title": "Get help",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "Type `/help` in Codewhale to list its commands. [Ask in the Codewhale Discord](https://discord.gg/37gfS3ksug), or read [what to do when something goes wrong](/docs/troubleshooting)."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"next": [
|
||||
{
|
||||
"href": "/docs/guide",
|
||||
"label": "Start your first task",
|
||||
"note": "More first prompts and how to check the answer."
|
||||
},
|
||||
{
|
||||
"href": "/docs/auth",
|
||||
"label": "Connect a provider",
|
||||
"note": "Save a key, check which key is used, or run a local model."
|
||||
},
|
||||
{
|
||||
"href": "/docs/modes",
|
||||
"label": "Set modes and approvals",
|
||||
"note": "Decide which commands and edits stop for your approval."
|
||||
}
|
||||
],
|
||||
"sourceNote": "Source documents: docs/GUIDE.md, docs/INSTALL.md, docs/MODES.md, docs/SANDBOX.md, docs/KEYBINDINGS.md · Update docs-map.ts when changing."
|
||||
},
|
||||
"states": {
|
||||
"loadingLabel": "Loading…",
|
||||
"emptyTitle": "Nothing here yet",
|
||||
|
||||
@@ -2296,279 +2296,6 @@
|
||||
],
|
||||
"sourceNote": "来源文档:docs/public-surface-facts.json(trust)、docs/TELEMETRY.md、docs/SANDBOX.md · 修改时同步更新 docs-map.ts。"
|
||||
},
|
||||
"docs-first-time": {
|
||||
"metaTitle": "第一次使用 Codewhale · Codewhale 文档",
|
||||
"metaDescription": "打开终端、安装 Codewhale、选择它工作的文件夹、连接模型并完成第一个任务,不需要任何终端经验。",
|
||||
"bodyClassName": "text-ink-soft leading-[1.9] tracking-wide",
|
||||
"title": "第一次使用 Codewhale",
|
||||
"lede": "这一页带你从第一次打开终端开始,一直到用 Codewhale 完成一个小任务。它还会说明应该从哪个文件夹启动,因为这个文件夹决定了 Codewhale 能修改什么。",
|
||||
"sections": [
|
||||
{
|
||||
"id": "what-you-need",
|
||||
"title": "确认你需要准备什么",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "你需要一台 Mac、一台 Windows x64 电脑或一台 Linux 电脑,安装时还需要联网。要得到回答,你还需要一个模型:来自 DeepSeek 等托管提供商的 API 密钥,或者用 Ollama 在你自己电脑上运行的模型。"
|
||||
},
|
||||
{
|
||||
"p": "Codewhale 免费,也不需要账号。托管提供商会按你的用量直接向你收费;你自己运行的模型不需要密钥。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "open-terminal",
|
||||
"title": "打开终端",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "终端是一个用输入命令代替点击的窗口。按下回车键后,命令就会运行。"
|
||||
},
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"macOS",
|
||||
"按 Cmd+空格键,输入 Terminal,然后按回车键。"
|
||||
],
|
||||
[
|
||||
"Windows",
|
||||
"打开“开始”菜单,输入 PowerShell,然后按回车键。"
|
||||
],
|
||||
[
|
||||
"Linux",
|
||||
"打开应用程序菜单并搜索“终端”。在 Ubuntu 上,按 Ctrl+Alt+T 也能打开。"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "在 Mac 上用 Cmd+V 粘贴,在 Windows 上用 Ctrl+V 粘贴。大多数 Linux 终端用 Ctrl+Shift+V 粘贴。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "install",
|
||||
"title": "安装 Codewhale",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "在 Mac 或 Linux 上,粘贴下面这条命令并按回车键。它会下载 Codewhale、校验下载内容,并把 Codewhale 加入 PATH,也就是终端查找命令时会搜索的文件夹列表。"
|
||||
},
|
||||
{
|
||||
"code": "curl -fsSL https://codewhale.net/install.sh | sh",
|
||||
"lang": "macOS · Linux"
|
||||
},
|
||||
{
|
||||
"p": "在 Windows 上,粘贴下面这条命令并按回车键:"
|
||||
},
|
||||
{
|
||||
"code": "winget install --id HunterBown.CodeWhale --exact --source winget",
|
||||
"lang": "Windows"
|
||||
},
|
||||
{
|
||||
"p": "安装完成后,打开一个新的终端窗口,让它读取新的 PATH。然后确认安装成功:显示版本号就说明 Codewhale 已经安装好了。"
|
||||
},
|
||||
{
|
||||
"code": "codewhale --version",
|
||||
"lang": "终端"
|
||||
},
|
||||
{
|
||||
"p": "如果终端提示 `command not found`,请按照[安装页面上的 PATH 步骤](/install#install-path)操作。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "choose-folder",
|
||||
"title": "选择工作的文件夹",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "Codewhale 在你启动它时终端所在的文件夹里工作。这个文件夹就是它的工作区:Codewhale 在这里读取和修改文件,除非你允许,它的文件工具无法访问其他文件夹。"
|
||||
},
|
||||
{
|
||||
"p": "在 Mac 上,操作系统沙箱还会阻止它的命令写入工作区以外的位置(临时文件除外)。在 Linux 上,安装 bubblewrap 后也有同样的沙箱。Windows 目前还没有这种沙箱,因此 Codewhale 依靠先征求你的同意。"
|
||||
},
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"只放一个项目的文件夹",
|
||||
"大多数情况下的最佳选择。Codewhale 只看到这个项目,`/undo` 可以撤回它做的修改。"
|
||||
],
|
||||
[
|
||||
"一个新的空文件夹",
|
||||
"适合开始做新东西,比如一个网站或一个脚本。下一节会说明如何创建。"
|
||||
],
|
||||
[
|
||||
"包含多个项目的文件夹",
|
||||
"让一个任务可以读取和修改所有这些项目里的文件。请选择能装下任务所需全部内容的最小文件夹;文件夹非常大时,Codewhale 会关闭快照并告诉你。"
|
||||
],
|
||||
[
|
||||
"你的主文件夹、桌面、文稿或下载",
|
||||
"Codewhale 可以在这里运行,但在这些文件夹本身中 `/undo` 和快照是关闭的,因为它们范围太大,无法安全地做快照。它们里面的子文件夹可以正常使用。"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "快照是 Codewhale 为它修改的文件保存的副本。它们保存在 Codewhale 的设置文件夹里,而不是你的项目里,并且不需要 git。"
|
||||
},
|
||||
{
|
||||
"p": "要让 Codewhale 多访问一个文件夹,请在 Codewhale 中输入 `/trust add` 和该文件夹的路径。Full Access 模式可以访问所有文件夹,并且在没有沙箱的情况下运行命令,所以只有在你希望 Codewhale 在整台电脑范围内工作时才使用它。"
|
||||
},
|
||||
{
|
||||
"p": "默认情况下,在 git 仓库中 Codewhale 会直接修改文件,并向你展示改了什么。在没有 git 的文件夹中,它会在每次修改前询问你。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "open-in-folder",
|
||||
"title": "在那个文件夹中打开终端",
|
||||
"blocks": [
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
"macOS 访达",
|
||||
"按住 Control 点按该文件夹,选择“服务”>“新建位于文件夹位置的终端窗口”。如果没有这一项,请在“系统设置”>“键盘”>“键盘快捷键”>“服务”>“文件和文件夹”中打开它。"
|
||||
],
|
||||
[
|
||||
"Windows 文件资源管理器",
|
||||
"右键点击该文件夹,选择“在终端中打开”。在 Windows 10 上,按住 Shift 再右键点击,选择“在此处打开 PowerShell 窗口”。"
|
||||
],
|
||||
[
|
||||
"任意终端",
|
||||
"输入 `cd` 和一个空格,把文件夹从访达或文件资源管理器拖进窗口,然后按回车键。"
|
||||
],
|
||||
[
|
||||
"不切换文件夹",
|
||||
"启动 Codewhale 时加上 `-C` 和文件夹路径,例如在 Mac 上输入 `codewhale -C ~/Projects/my-site`。"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"p": "下面这些命令可以在终端里切换和查看文件夹。它们在 Mac、Linux 和 PowerShell 中都能用。"
|
||||
},
|
||||
{
|
||||
"codeTerms": true,
|
||||
"rows": [
|
||||
[
|
||||
"pwd",
|
||||
"显示你当前所在的文件夹。"
|
||||
],
|
||||
[
|
||||
"ls",
|
||||
"列出其中的内容。"
|
||||
],
|
||||
[
|
||||
"cd Documents",
|
||||
"进入当前文件夹里的 Documents 文件夹。"
|
||||
],
|
||||
[
|
||||
"cd ..",
|
||||
"回到上一级文件夹。"
|
||||
],
|
||||
[
|
||||
"mkdir my-site",
|
||||
"新建一个名为 my-site 的文件夹。"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"note": "输入文件夹名的前几个字母后按 Tab 键,终端会帮你补全名字。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "connect",
|
||||
"title": "启动 Codewhale 并连接模型",
|
||||
"blocks": [
|
||||
{
|
||||
"code": "codewhale",
|
||||
"lang": "终端"
|
||||
},
|
||||
{
|
||||
"p": "Codewhale 会立即打开一个输入框。你发送第一条消息时,它会询问模型在哪里运行:选择一个托管提供商并粘贴它的 API 密钥,或者在你的电脑上运行着 Ollama 时选择 Ollama。"
|
||||
},
|
||||
{
|
||||
"p": "你输入的密钥保存在你自己的电脑上,位于 Codewhale 的设置文件夹 `~/.codewhale` 中。之后可以用 `/provider` 更换提供商。"
|
||||
},
|
||||
{
|
||||
"p": "如果 Codewhale 询问是否使用这个文件夹中的说明和文件,对于你自己创建的文件夹,回答“是”即可。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "first-task",
|
||||
"title": "交给它第一个任务",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "用平常的话写下你想做的事,然后按回车键。安全的第一个任务不修改任何东西,例如:`说明这个文件夹里有什么,以及每个文件的用途。不要修改任何内容。`"
|
||||
},
|
||||
{
|
||||
"p": "然后试一个会创建文件的任务,例如:`创建一个名为 packing-list.md 的文件,写一份周末出游的行李清单。`"
|
||||
},
|
||||
{
|
||||
"p": "默认情况下,Codewhale 在运行非只读命令前会先询问你。在允许之前,请先看清审批里写明要运行什么。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stop-undo-quit",
|
||||
"title": "停止、撤回和退出",
|
||||
"blocks": [
|
||||
{
|
||||
"codeTerms": true,
|
||||
"rows": [
|
||||
[
|
||||
"Esc",
|
||||
"停止正在运行的任务。Ctrl+C 的作用相同。"
|
||||
],
|
||||
[
|
||||
"/undo",
|
||||
"撤回你的上一个请求及其造成的文件修改。"
|
||||
],
|
||||
[
|
||||
"/diff",
|
||||
"显示这个文件夹中改了什么。"
|
||||
],
|
||||
[
|
||||
"/receipts",
|
||||
"列出 Codewhale 做过的事:文件、命令和审批。"
|
||||
],
|
||||
[
|
||||
"/exit",
|
||||
"退出 Codewhale。输入框为空时,按 Ctrl+D 也会退出。"
|
||||
],
|
||||
[
|
||||
"codewhale --continue",
|
||||
"在同一个文件夹中运行它,可以继续你在那里的上一次对话。"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "help",
|
||||
"title": "获取帮助",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "在 Codewhale 中输入 `/help` 可以列出它的命令。你也可以[在 Codewhale Discord 中提问](https://discord.gg/37gfS3ksug),或阅读[出现问题时怎么办](/docs/troubleshooting)。"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"next": [
|
||||
{
|
||||
"href": "/docs/guide",
|
||||
"label": "开始第一个任务",
|
||||
"note": "更多第一个提示词的示例,以及如何检查回答。"
|
||||
},
|
||||
{
|
||||
"href": "/docs/auth",
|
||||
"label": "连接模型提供商",
|
||||
"note": "保存密钥、查看当前使用的密钥,或运行本地模型。"
|
||||
},
|
||||
{
|
||||
"href": "/docs/modes",
|
||||
"label": "设置模式与审批",
|
||||
"note": "决定哪些命令和修改需要你批准。"
|
||||
}
|
||||
],
|
||||
"sourceNote": "来源文档:docs/GUIDE.md、docs/INSTALL.md、docs/MODES.md、docs/SANDBOX.md、docs/KEYBINDINGS.md · 修改时请同步更新 docs-map.ts。"
|
||||
},
|
||||
"states": {
|
||||
"loadingLabel": "加载中…",
|
||||
"emptyTitle": "这里还没有内容",
|
||||
|
||||
@@ -48,30 +48,6 @@ export const INSTALL_COPY = {
|
||||
scriptLink: { en: "View install script", zh: "查看安装脚本" },
|
||||
releaseLine: { en: "This installs the latest published release, v{version}.", zh: "这会安装最新已发布版本 v{version}。" },
|
||||
|
||||
newTitle: { en: "New to the terminal?", zh: "第一次使用终端?" },
|
||||
newLead: {
|
||||
en: "You install Codewhale by pasting one command into a terminal. These steps show how to open one and how to check that it worked.",
|
||||
zh: "你在终端中粘贴一条命令来安装 Codewhale。下面的步骤说明如何打开终端,以及如何确认安装成功。",
|
||||
},
|
||||
newMacTitle: { en: "macOS", zh: "macOS" },
|
||||
newMac1: { en: "Press Cmd+Space, type Terminal and press Return.", zh: "按 Cmd+空格键,输入 Terminal,然后按回车键。" },
|
||||
newMac2: { en: "Paste this command with Cmd+V and press Return:", zh: "用 Cmd+V 粘贴下面这条命令,然后按回车键:" },
|
||||
newMac3: {
|
||||
en: "When it finishes, open a new window with Cmd+N, type `codewhale --version` and press Return. A version number means it worked.",
|
||||
zh: "安装完成后,按 Cmd+N 打开一个新窗口,输入 `codewhale --version` 并按回车键。显示版本号就说明安装成功了。",
|
||||
},
|
||||
newWinTitle: { en: "Windows", zh: "Windows" },
|
||||
newWin1: { en: "Open the Start menu, type PowerShell and press Enter.", zh: "打开“开始”菜单,输入 PowerShell,然后按回车键。" },
|
||||
newWin2: { en: "Paste this command with Ctrl+V and press Enter:", zh: "用 Ctrl+V 粘贴下面这条命令,然后按回车键:" },
|
||||
newWin3: {
|
||||
en: "When it finishes, close the window, open PowerShell again, type `codewhale --version` and press Enter. A version number means it worked.",
|
||||
zh: "安装完成后,关闭窗口,重新打开 PowerShell,输入 `codewhale --version` 并按回车键。显示版本号就说明安装成功了。",
|
||||
},
|
||||
newNext: {
|
||||
en: "Read the first-time guide: which folder to work in, your first task, and how to undo",
|
||||
zh: "阅读首次使用指南:在哪个文件夹工作、第一个任务,以及如何撤回",
|
||||
},
|
||||
|
||||
pathTitle: { en: "What the installer does to your PATH", zh: "安装程序对 PATH 做了什么" },
|
||||
pathPlaces: {
|
||||
en: "The installer puts `codewhale` and `codew` in `~/.local/bin`. When that folder is not on your PATH, it adds three marked lines to the startup file for your shell:",
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getDocsAuth, getDocsClaudeCode, getDocsConfiguration, getDocsFleet,
|
||||
getDocsHooks, getDocsMcp, getDocsModes, getDocsPet, getDocsReview,
|
||||
getDocsRuntimeApi, getDocsSandbox, getDocsSkills, getDocsSubagents,
|
||||
getDocsTroubleshooting, getDocsTrust, getDocsWeb, getDocsWork, getDocsFirstTime,
|
||||
getDocsTroubleshooting, getDocsTrust, getDocsWeb, getDocsWork,
|
||||
getDocsGuide, getDocsVocabulary, getConstitution, getChrome, getHome, fill, pickText,
|
||||
} from "./i18n/dictionaries";
|
||||
import type { DocsBlock, DocsPageDict } from "./i18n/dictionaries/types";
|
||||
@@ -31,7 +31,7 @@ const ARTICLES: Record<string, (locale: string) => DocsPageDict> = {
|
||||
pet: getDocsPet, review: getDocsReview, "runtime-api": getDocsRuntimeApi,
|
||||
sandbox: getDocsSandbox, skills: getDocsSkills, subagents: getDocsSubagents,
|
||||
troubleshooting: getDocsTroubleshooting, trust: getDocsTrust, web: getDocsWeb,
|
||||
work: getDocsWork, "first-time": getDocsFirstTime,
|
||||
work: getDocsWork,
|
||||
};
|
||||
|
||||
function blockText(block: DocsBlock): string {
|
||||
|
||||
@@ -40,18 +40,6 @@ export const DOC_CATEGORY_LABELS: Record<DocTopic["category"], { en: string; zh:
|
||||
};
|
||||
|
||||
export const DOC_TOPICS: DocTopic[] = [
|
||||
{
|
||||
id: "first-time",
|
||||
slug: "first-time",
|
||||
label: { en: "Use Codewhale for the first time", zh: "第一次使用 Codewhale" },
|
||||
description: {
|
||||
en: "Open a terminal, install, pick the folder Codewhale works in, connect a model and finish a first task.",
|
||||
zh: "打开终端、安装、选择 Codewhale 工作的文件夹、连接模型并完成第一个任务。",
|
||||
},
|
||||
repoSource: ["docs/GUIDE.md", "docs/INSTALL.md", "docs/MODES.md", "docs/SANDBOX.md", "docs/KEYBINDINGS.md"],
|
||||
hasPage: true,
|
||||
category: "getting-started",
|
||||
},
|
||||
{
|
||||
id: "install",
|
||||
slug: "install",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FEATURE_CATALOGUE, type Feature } from "./features";
|
||||
import {
|
||||
getDocsAuth, getDocsClaudeCode, getDocsConfiguration, getDocsFleet, getDocsHooks, getDocsMcp, getDocsModes, getDocsPet,
|
||||
getDocsReview, getDocsRuntimeApi, getDocsSandbox, getDocsSkills, getDocsSubagents, getDocsTroubleshooting, getDocsTrust,
|
||||
getDocsWeb, getDocsWork, getDocsFirstTime,
|
||||
getDocsWeb, getDocsWork,
|
||||
} from "./i18n/dictionaries";
|
||||
import type { DocsBlock, DocsPageDict } from "./i18n/dictionaries/types";
|
||||
import { DOC_TOPICS } from "./docs-map";
|
||||
@@ -50,7 +50,6 @@ const PAGES: Record<string, (locale: string) => DocsPageDict> = {
|
||||
fleet: getDocsFleet, hooks: getDocsHooks, mcp: getDocsMcp, modes: getDocsModes, pet: getDocsPet, review: getDocsReview,
|
||||
"runtime-api": getDocsRuntimeApi, sandbox: getDocsSandbox, skills: getDocsSkills, subagents: getDocsSubagents,
|
||||
troubleshooting: getDocsTroubleshooting, trust: getDocsTrust, web: getDocsWeb, work: getDocsWork,
|
||||
"first-time": getDocsFirstTime,
|
||||
};
|
||||
|
||||
describe("docs describe the published release first", () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
EN_DOCS_PET,
|
||||
EN_DOCS_AUTH,
|
||||
EN_DOCS_TRUST,
|
||||
EN_DOCS_FIRST_TIME,
|
||||
EN_DOCS_REVIEW,
|
||||
EN_DOCS_VOCABULARY,
|
||||
EN_COMPUTER_USE,
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
getDocsPet,
|
||||
getDocsAuth,
|
||||
getDocsTrust,
|
||||
getDocsFirstTime,
|
||||
getComputerUse,
|
||||
getChangelog,
|
||||
getDocsShell,
|
||||
@@ -287,7 +285,6 @@ describe("website dictionaries", () => {
|
||||
["docs-pet", getDocsPet, EN_DOCS_PET],
|
||||
["docs-auth", getDocsAuth, EN_DOCS_AUTH],
|
||||
["docs-trust", getDocsTrust, EN_DOCS_TRUST],
|
||||
["docs-first-time", getDocsFirstTime, EN_DOCS_FIRST_TIME],
|
||||
["docs-review", getDocsReview, EN_DOCS_REVIEW],
|
||||
["docs-vocabulary", getDocsVocabulary, EN_DOCS_VOCABULARY],
|
||||
["changelog", getChangelog, EN_CHANGELOG],
|
||||
@@ -350,7 +347,6 @@ describe("website dictionaries", () => {
|
||||
["docs-subagents", getDocsSubagents],
|
||||
["docs-troubleshooting", getDocsTroubleshooting],
|
||||
["docs-trust", getDocsTrust],
|
||||
["docs-first-time", getDocsFirstTime],
|
||||
["docs-web", getDocsWeb],
|
||||
["docs-work", getDocsWork],
|
||||
["docs-pet", getDocsPet],
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import type { DocsFirstTimeDict } from "../types";
|
||||
|
||||
export const docsFirstTime: DocsFirstTimeDict = {
|
||||
metaTitle: "Use Codewhale for the first time · Codewhale Docs",
|
||||
metaDescription:
|
||||
"Open a terminal, install Codewhale, pick the folder it works in, connect a model and finish a first task, with no terminal experience needed.",
|
||||
bodyClassName: "text-ink-soft leading-relaxed",
|
||||
title: "Use Codewhale for the first time",
|
||||
lede:
|
||||
"This page takes you from opening a terminal for the first time to finishing a small task with Codewhale. It also explains which folder to start in, because that folder decides what Codewhale can change.",
|
||||
sections: [
|
||||
{
|
||||
id: "what-you-need",
|
||||
title: "Check what you need",
|
||||
blocks: [
|
||||
{
|
||||
p: "You need a Mac, a Windows x64 PC or a Linux computer, and an internet connection for the install. To get answers you also need a model: an API key from a hosted provider such as DeepSeek, or a model running on your own computer with Ollama.",
|
||||
},
|
||||
{
|
||||
p: "Codewhale is free and needs no account. A hosted provider bills you directly for what you use, and a model you run yourself needs no key.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "open-terminal",
|
||||
title: "Open a terminal",
|
||||
blocks: [
|
||||
{
|
||||
p: "A terminal is a window where you type commands instead of clicking. A command runs when you press Return, which is Enter on Windows.",
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
["macOS", "Press Cmd+Space, type Terminal and press Return."],
|
||||
["Windows", "Open the Start menu, type PowerShell and press Enter."],
|
||||
["Linux", "Open your applications menu and search for Terminal. On Ubuntu, Ctrl+Alt+T opens one."],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "Paste with Cmd+V on a Mac and Ctrl+V on Windows. Most Linux terminals paste with Ctrl+Shift+V.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "install",
|
||||
title: "Install Codewhale",
|
||||
blocks: [
|
||||
{
|
||||
p: "On a Mac or Linux, paste this command and press Return. It downloads Codewhale, checks the download, and adds Codewhale to your PATH, the list of folders your terminal searches for commands.",
|
||||
},
|
||||
{ code: "curl -fsSL https://codewhale.net/install.sh | sh", lang: "macOS · Linux" },
|
||||
{ p: "On Windows, paste this command and press Enter:" },
|
||||
{ code: "winget install --id HunterBown.CodeWhale --exact --source winget", lang: "Windows" },
|
||||
{
|
||||
p: "When it finishes, open a new terminal window so it picks up the new PATH. Then check that it worked: a version number means Codewhale is installed.",
|
||||
},
|
||||
{ code: "codewhale --version", lang: "Terminal" },
|
||||
{
|
||||
p: "If the terminal says `command not found`, follow [the PATH steps on the install page](/install#install-path).",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "choose-folder",
|
||||
title: "Choose the folder to work in",
|
||||
blocks: [
|
||||
{
|
||||
p: "Codewhale works in the folder your terminal is in when you start it. That folder is its workspace: Codewhale reads and changes files there, and its file tools cannot reach other folders unless you allow it.",
|
||||
},
|
||||
{
|
||||
p: "On a Mac, an operating-system sandbox also stops its commands from writing outside the workspace, apart from temporary files. Linux gets the same sandbox when bubblewrap is installed. Windows has no such sandbox yet, so Codewhale relies on asking you first.",
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
["A folder for one project", "The best choice for most work. Codewhale sees only that project, and `/undo` can take back what it changed."],
|
||||
["A new, empty folder", "A good start for something new, such as a website or a script. The next section shows how to make one."],
|
||||
["A folder that holds several projects", "Lets one task read and change files across all of them. Pick the smallest folder that holds everything the task needs; in a very large folder Codewhale turns snapshots off and tells you."],
|
||||
["Your home folder, Desktop, Documents or Downloads", "Codewhale runs there, but `/undo` and snapshots are off in these folders themselves because they are too broad to snapshot safely. A folder inside one of them works normally."],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "Snapshots are Codewhale's own copies of the files it changes. It keeps them in its settings folder, not in your project, and they work without git.",
|
||||
},
|
||||
{
|
||||
p: "To let Codewhale reach one more folder, type `/trust add` and that folder's path in Codewhale. Full Access mode reaches every folder and runs commands without the sandbox, so use it only when you want Codewhale to work across your whole computer.",
|
||||
},
|
||||
{
|
||||
p: "By default, Codewhale makes file edits in a git repository without asking and shows you what changed. In a folder without git, it asks before each edit.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "open-in-folder",
|
||||
title: "Open a terminal in that folder",
|
||||
blocks: [
|
||||
{
|
||||
rows: [
|
||||
["macOS Finder", "Control-click the folder and choose Services > New Terminal at Folder. If it is missing, turn it on in System Settings > Keyboard > Keyboard Shortcuts > Services > Files and Folders."],
|
||||
["Windows File Explorer", "Right-click the folder and choose Open in Terminal. On Windows 10, hold Shift while you right-click and choose Open PowerShell window here."],
|
||||
["Any terminal", "Type `cd` and a space, drag the folder from Finder or File Explorer into the window, and press Return."],
|
||||
["Without moving", "Start Codewhale with `-C` and the folder's path, for example `codewhale -C ~/Projects/my-site` on a Mac."],
|
||||
],
|
||||
},
|
||||
{ p: "These commands find your way around from inside a terminal. They work on a Mac, on Linux and in PowerShell." },
|
||||
{
|
||||
codeTerms: true,
|
||||
rows: [
|
||||
["pwd", "Shows the folder you are in."],
|
||||
["ls", "Lists what is in it."],
|
||||
["cd Documents", "Moves into the Documents folder inside the current one."],
|
||||
["cd ..", "Moves up one folder."],
|
||||
["mkdir my-site", "Makes a new folder called my-site."],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "Type the first letters of a folder name and press Tab, and the terminal finishes the name for you.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "connect",
|
||||
title: "Start Codewhale and connect a model",
|
||||
blocks: [
|
||||
{ code: "codewhale", lang: "Terminal" },
|
||||
{
|
||||
p: "Codewhale opens a message box right away. When you send your first message, it asks where your model runs: pick a hosted provider and paste its API key, or pick Ollama when it is running on your computer.",
|
||||
},
|
||||
{
|
||||
p: "Keys you enter stay on your computer, in Codewhale's settings folder `~/.codewhale`. Change the provider later with `/provider`.",
|
||||
},
|
||||
{
|
||||
p: "If Codewhale asks whether to work with the instructions and files in this folder, answer yes for a folder you made yourself.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "first-task",
|
||||
title: "Give it a first task",
|
||||
blocks: [
|
||||
{
|
||||
p: "Type what you want in plain words and press Return. A safe first task changes nothing, for example: `Explain what is in this folder and what each file is for. Do not change anything.`",
|
||||
},
|
||||
{
|
||||
p: "Then try one that makes a file, for example: `Make a file called packing-list.md with a packing list for a weekend trip.`",
|
||||
},
|
||||
{
|
||||
p: "By default, Codewhale asks before it runs a command that is not read-only. Read what the approval says will run before you allow it.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "stop-undo-quit",
|
||||
title: "Stop, undo and quit",
|
||||
blocks: [
|
||||
{
|
||||
codeTerms: true,
|
||||
rows: [
|
||||
["Esc", "Stops the task that is running. Ctrl+C does the same."],
|
||||
["/undo", "Takes back your last request and the file changes it made."],
|
||||
["/diff", "Shows what changed in this folder."],
|
||||
["/receipts", "Lists what Codewhale did: files, commands and approvals."],
|
||||
["/exit", "Quits Codewhale. Ctrl+D also quits when the message box is empty."],
|
||||
["codewhale --continue", "Run it in the same folder to pick up your last conversation there."],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
title: "Get help",
|
||||
blocks: [
|
||||
{
|
||||
p: "Type `/help` in Codewhale to list its commands. [Ask in the Codewhale Discord](https://discord.gg/37gfS3ksug), or read [what to do when something goes wrong](/docs/troubleshooting).",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
next: [
|
||||
{
|
||||
href: "/docs/guide",
|
||||
label: "Start your first task",
|
||||
note: "More first prompts and how to check the answer.",
|
||||
},
|
||||
{
|
||||
href: "/docs/auth",
|
||||
label: "Connect a provider",
|
||||
note: "Save a key, check which key is used, or run a local model.",
|
||||
},
|
||||
{
|
||||
href: "/docs/modes",
|
||||
label: "Set modes and approvals",
|
||||
note: "Decide which commands and edits stop for your approval.",
|
||||
},
|
||||
],
|
||||
sourceNote:
|
||||
"Source documents: docs/GUIDE.md, docs/INSTALL.md, docs/MODES.md, docs/SANDBOX.md, docs/KEYBINDINGS.md · Update docs-map.ts when changing.",
|
||||
};
|
||||
@@ -37,7 +37,6 @@ import type {
|
||||
DocsSubagentsDict,
|
||||
DocsTroubleshootingDict,
|
||||
DocsTrustDict,
|
||||
DocsFirstTimeDict,
|
||||
DocsWebDict,
|
||||
DocsWorkDict,
|
||||
DocsPetDict,
|
||||
@@ -101,8 +100,6 @@ import { docsAuth as enDocsAuth } from "./en/docs-auth";
|
||||
import { docsAuth as zhDocsAuth } from "./zh/docs-auth";
|
||||
import { docsTrust as enDocsTrust } from "./en/docs-trust";
|
||||
import { docsTrust as zhDocsTrust } from "./zh/docs-trust";
|
||||
import { docsFirstTime as enDocsFirstTime } from "./en/docs-first-time";
|
||||
import { docsFirstTime as zhDocsFirstTime } from "./zh/docs-first-time";
|
||||
import { docsReview as enDocsReview } from "./en/docs-review";
|
||||
import { docsReview as zhDocsReview } from "./zh/docs-review";
|
||||
import { docsVocabulary as enDocsVocabulary } from "./en/docs-vocabulary";
|
||||
@@ -340,10 +337,6 @@ const DOCS_TRUST: Record<string, DocsTrustDict> = {
|
||||
zh: zhDocsTrust,
|
||||
};
|
||||
|
||||
const DOCS_FIRST_TIME: Record<string, DocsFirstTimeDict> = {
|
||||
zh: zhDocsFirstTime,
|
||||
};
|
||||
|
||||
const DOCS_REVIEW: Record<string, DocsReviewDict> = {
|
||||
zh: zhDocsReview,
|
||||
};
|
||||
@@ -499,10 +492,6 @@ export function getDocsTrust(locale: string): DocsTrustDict {
|
||||
return DOCS_TRUST[locale] ?? enDocsTrust;
|
||||
}
|
||||
|
||||
export function getDocsFirstTime(locale: string): DocsFirstTimeDict {
|
||||
return DOCS_FIRST_TIME[locale] ?? enDocsFirstTime;
|
||||
}
|
||||
|
||||
export function getDocsReview(locale: string): DocsReviewDict {
|
||||
return DOCS_REVIEW[locale] ?? enDocsReview;
|
||||
}
|
||||
@@ -585,7 +574,6 @@ export const EN_DOCS_WORK = enDocsWork;
|
||||
export const EN_DOCS_PET = enDocsPet;
|
||||
export const EN_DOCS_AUTH = enDocsAuth;
|
||||
export const EN_DOCS_TRUST = enDocsTrust;
|
||||
export const EN_DOCS_FIRST_TIME = enDocsFirstTime;
|
||||
export const EN_DOCS_REVIEW = enDocsReview;
|
||||
export const EN_DOCS_VOCABULARY = enDocsVocabulary;
|
||||
export const EN_COMPUTER_USE = enComputerUse;
|
||||
|
||||
@@ -442,8 +442,6 @@ export type DocsAuthDict = DocsPageDict;
|
||||
|
||||
export type DocsTrustDict = DocsPageDict;
|
||||
|
||||
export type DocsFirstTimeDict = DocsPageDict;
|
||||
|
||||
/** `app/[locale]/changelog/page.tsx` — version-aware release record. */
|
||||
export interface ChangelogDict {
|
||||
metaTitle: string;
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import type { DocsFirstTimeDict } from "../types";
|
||||
|
||||
export const docsFirstTime: DocsFirstTimeDict = {
|
||||
metaTitle: "第一次使用 Codewhale · Codewhale 文档",
|
||||
metaDescription:
|
||||
"打开终端、安装 Codewhale、选择它工作的文件夹、连接模型并完成第一个任务,不需要任何终端经验。",
|
||||
bodyClassName: "text-ink-soft leading-[1.9] tracking-wide",
|
||||
title: "第一次使用 Codewhale",
|
||||
lede:
|
||||
"这一页带你从第一次打开终端开始,一直到用 Codewhale 完成一个小任务。它还会说明应该从哪个文件夹启动,因为这个文件夹决定了 Codewhale 能修改什么。",
|
||||
sections: [
|
||||
{
|
||||
id: "what-you-need",
|
||||
title: "确认你需要准备什么",
|
||||
blocks: [
|
||||
{
|
||||
p: "你需要一台 Mac、一台 Windows x64 电脑或一台 Linux 电脑,安装时还需要联网。要得到回答,你还需要一个模型:来自 DeepSeek 等托管提供商的 API 密钥,或者用 Ollama 在你自己电脑上运行的模型。",
|
||||
},
|
||||
{
|
||||
p: "Codewhale 免费,也不需要账号。托管提供商会按你的用量直接向你收费;你自己运行的模型不需要密钥。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "open-terminal",
|
||||
title: "打开终端",
|
||||
blocks: [
|
||||
{
|
||||
p: "终端是一个用输入命令代替点击的窗口。按下回车键后,命令就会运行。",
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
["macOS", "按 Cmd+空格键,输入 Terminal,然后按回车键。"],
|
||||
["Windows", "打开“开始”菜单,输入 PowerShell,然后按回车键。"],
|
||||
["Linux", "打开应用程序菜单并搜索“终端”。在 Ubuntu 上,按 Ctrl+Alt+T 也能打开。"],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "在 Mac 上用 Cmd+V 粘贴,在 Windows 上用 Ctrl+V 粘贴。大多数 Linux 终端用 Ctrl+Shift+V 粘贴。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "install",
|
||||
title: "安装 Codewhale",
|
||||
blocks: [
|
||||
{
|
||||
p: "在 Mac 或 Linux 上,粘贴下面这条命令并按回车键。它会下载 Codewhale、校验下载内容,并把 Codewhale 加入 PATH,也就是终端查找命令时会搜索的文件夹列表。",
|
||||
},
|
||||
{ code: "curl -fsSL https://codewhale.net/install.sh | sh", lang: "macOS · Linux" },
|
||||
{ p: "在 Windows 上,粘贴下面这条命令并按回车键:" },
|
||||
{ code: "winget install --id HunterBown.CodeWhale --exact --source winget", lang: "Windows" },
|
||||
{
|
||||
p: "安装完成后,打开一个新的终端窗口,让它读取新的 PATH。然后确认安装成功:显示版本号就说明 Codewhale 已经安装好了。",
|
||||
},
|
||||
{ code: "codewhale --version", lang: "终端" },
|
||||
{
|
||||
p: "如果终端提示 `command not found`,请按照[安装页面上的 PATH 步骤](/install#install-path)操作。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "choose-folder",
|
||||
title: "选择工作的文件夹",
|
||||
blocks: [
|
||||
{
|
||||
p: "Codewhale 在你启动它时终端所在的文件夹里工作。这个文件夹就是它的工作区:Codewhale 在这里读取和修改文件,除非你允许,它的文件工具无法访问其他文件夹。",
|
||||
},
|
||||
{
|
||||
p: "在 Mac 上,操作系统沙箱还会阻止它的命令写入工作区以外的位置(临时文件除外)。在 Linux 上,安装 bubblewrap 后也有同样的沙箱。Windows 目前还没有这种沙箱,因此 Codewhale 依靠先征求你的同意。",
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
["只放一个项目的文件夹", "大多数情况下的最佳选择。Codewhale 只看到这个项目,`/undo` 可以撤回它做的修改。"],
|
||||
["一个新的空文件夹", "适合开始做新东西,比如一个网站或一个脚本。下一节会说明如何创建。"],
|
||||
["包含多个项目的文件夹", "让一个任务可以读取和修改所有这些项目里的文件。请选择能装下任务所需全部内容的最小文件夹;文件夹非常大时,Codewhale 会关闭快照并告诉你。"],
|
||||
["你的主文件夹、桌面、文稿或下载", "Codewhale 可以在这里运行,但在这些文件夹本身中 `/undo` 和快照是关闭的,因为它们范围太大,无法安全地做快照。它们里面的子文件夹可以正常使用。"],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "快照是 Codewhale 为它修改的文件保存的副本。它们保存在 Codewhale 的设置文件夹里,而不是你的项目里,并且不需要 git。",
|
||||
},
|
||||
{
|
||||
p: "要让 Codewhale 多访问一个文件夹,请在 Codewhale 中输入 `/trust add` 和该文件夹的路径。Full Access 模式可以访问所有文件夹,并且在没有沙箱的情况下运行命令,所以只有在你希望 Codewhale 在整台电脑范围内工作时才使用它。",
|
||||
},
|
||||
{
|
||||
p: "默认情况下,在 git 仓库中 Codewhale 会直接修改文件,并向你展示改了什么。在没有 git 的文件夹中,它会在每次修改前询问你。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "open-in-folder",
|
||||
title: "在那个文件夹中打开终端",
|
||||
blocks: [
|
||||
{
|
||||
rows: [
|
||||
["macOS 访达", "按住 Control 点按该文件夹,选择“服务”>“新建位于文件夹位置的终端窗口”。如果没有这一项,请在“系统设置”>“键盘”>“键盘快捷键”>“服务”>“文件和文件夹”中打开它。"],
|
||||
["Windows 文件资源管理器", "右键点击该文件夹,选择“在终端中打开”。在 Windows 10 上,按住 Shift 再右键点击,选择“在此处打开 PowerShell 窗口”。"],
|
||||
["任意终端", "输入 `cd` 和一个空格,把文件夹从访达或文件资源管理器拖进窗口,然后按回车键。"],
|
||||
["不切换文件夹", "启动 Codewhale 时加上 `-C` 和文件夹路径,例如在 Mac 上输入 `codewhale -C ~/Projects/my-site`。"],
|
||||
],
|
||||
},
|
||||
{ p: "下面这些命令可以在终端里切换和查看文件夹。它们在 Mac、Linux 和 PowerShell 中都能用。" },
|
||||
{
|
||||
codeTerms: true,
|
||||
rows: [
|
||||
["pwd", "显示你当前所在的文件夹。"],
|
||||
["ls", "列出其中的内容。"],
|
||||
["cd Documents", "进入当前文件夹里的 Documents 文件夹。"],
|
||||
["cd ..", "回到上一级文件夹。"],
|
||||
["mkdir my-site", "新建一个名为 my-site 的文件夹。"],
|
||||
],
|
||||
},
|
||||
{
|
||||
note: "输入文件夹名的前几个字母后按 Tab 键,终端会帮你补全名字。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "connect",
|
||||
title: "启动 Codewhale 并连接模型",
|
||||
blocks: [
|
||||
{ code: "codewhale", lang: "终端" },
|
||||
{
|
||||
p: "Codewhale 会立即打开一个输入框。你发送第一条消息时,它会询问模型在哪里运行:选择一个托管提供商并粘贴它的 API 密钥,或者在你的电脑上运行着 Ollama 时选择 Ollama。",
|
||||
},
|
||||
{
|
||||
p: "你输入的密钥保存在你自己的电脑上,位于 Codewhale 的设置文件夹 `~/.codewhale` 中。之后可以用 `/provider` 更换提供商。",
|
||||
},
|
||||
{
|
||||
p: "如果 Codewhale 询问是否使用这个文件夹中的说明和文件,对于你自己创建的文件夹,回答“是”即可。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "first-task",
|
||||
title: "交给它第一个任务",
|
||||
blocks: [
|
||||
{
|
||||
p: "用平常的话写下你想做的事,然后按回车键。安全的第一个任务不修改任何东西,例如:`说明这个文件夹里有什么,以及每个文件的用途。不要修改任何内容。`",
|
||||
},
|
||||
{
|
||||
p: "然后试一个会创建文件的任务,例如:`创建一个名为 packing-list.md 的文件,写一份周末出游的行李清单。`",
|
||||
},
|
||||
{
|
||||
p: "默认情况下,Codewhale 在运行非只读命令前会先询问你。在允许之前,请先看清审批里写明要运行什么。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "stop-undo-quit",
|
||||
title: "停止、撤回和退出",
|
||||
blocks: [
|
||||
{
|
||||
codeTerms: true,
|
||||
rows: [
|
||||
["Esc", "停止正在运行的任务。Ctrl+C 的作用相同。"],
|
||||
["/undo", "撤回你的上一个请求及其造成的文件修改。"],
|
||||
["/diff", "显示这个文件夹中改了什么。"],
|
||||
["/receipts", "列出 Codewhale 做过的事:文件、命令和审批。"],
|
||||
["/exit", "退出 Codewhale。输入框为空时,按 Ctrl+D 也会退出。"],
|
||||
["codewhale --continue", "在同一个文件夹中运行它,可以继续你在那里的上一次对话。"],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
title: "获取帮助",
|
||||
blocks: [
|
||||
{
|
||||
p: "在 Codewhale 中输入 `/help` 可以列出它的命令。你也可以[在 Codewhale Discord 中提问](https://discord.gg/37gfS3ksug),或阅读[出现问题时怎么办](/docs/troubleshooting)。",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
next: [
|
||||
{
|
||||
href: "/docs/guide",
|
||||
label: "开始第一个任务",
|
||||
note: "更多第一个提示词的示例,以及如何检查回答。",
|
||||
},
|
||||
{
|
||||
href: "/docs/auth",
|
||||
label: "连接模型提供商",
|
||||
note: "保存密钥、查看当前使用的密钥,或运行本地模型。",
|
||||
},
|
||||
{
|
||||
href: "/docs/modes",
|
||||
label: "设置模式与审批",
|
||||
note: "决定哪些命令和修改需要你批准。",
|
||||
},
|
||||
],
|
||||
sourceNote:
|
||||
"来源文档:docs/GUIDE.md、docs/INSTALL.md、docs/MODES.md、docs/SANDBOX.md、docs/KEYBINDINGS.md · 修改时请同步更新 docs-map.ts。",
|
||||
};
|
||||
@@ -16,7 +16,6 @@ export const DOCS_TOPICS = [
|
||||
"computers",
|
||||
"configuration",
|
||||
"constitution",
|
||||
"first-time",
|
||||
"fleet",
|
||||
"guide",
|
||||
"hooks",
|
||||
|
||||
@@ -96,23 +96,6 @@ function installLines(facts: RepoFacts): string[] {
|
||||
"",
|
||||
...link(I.scriptLink, `${SITE_URL}/install.sh`),
|
||||
...(version ? p(I.releaseLine) : []),
|
||||
...h(I.newTitle),
|
||||
...p(I.newLead),
|
||||
...h(I.newMacTitle, 3),
|
||||
`1. ${t(I.newMac1)}`,
|
||||
`2. ${t(I.newMac2)}`,
|
||||
"",
|
||||
...code(INSTALL_COMMANDS.shell),
|
||||
`3. ${t(I.newMac3)}`,
|
||||
"",
|
||||
...h(I.newWinTitle, 3),
|
||||
`1. ${t(I.newWin1)}`,
|
||||
`2. ${t(I.newWin2)}`,
|
||||
"",
|
||||
...code(INSTALL_COMMANDS.windows),
|
||||
`3. ${t(I.newWin3)}`,
|
||||
"",
|
||||
...link(I.newNext, `${SITE_URL}/en/docs/first-time`),
|
||||
...h(I.pathTitle),
|
||||
...p(I.pathPlaces),
|
||||
...bullets([I.pathZsh, I.pathBashLinux, I.pathBashMac, I.pathOther, I.pathFish]),
|
||||
|
||||
@@ -35,6 +35,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
|
||||
"@luochen211",
|
||||
"@gannp",
|
||||
"@Felixkw12",
|
||||
"@MannXo",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
|
||||
* one. scripts/check-contributor-credit.py requires them here now; the release
|
||||
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
|
||||
*/
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord", "@MannXo"];
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord"];
|
||||
|
||||
/**
|
||||
* Contributors who helped with reports, reproductions, and verification.
|
||||
|
||||
@@ -49,7 +49,6 @@ const STEMS = {
|
||||
"docs-web": { exportName: "docsWeb", typeName: "DocsWebDict" },
|
||||
"docs-auth": { exportName: "docsAuth", typeName: "DocsAuthDict" },
|
||||
"docs-trust": { exportName: "docsTrust", typeName: "DocsTrustDict" },
|
||||
"docs-first-time": { exportName: "docsFirstTime", typeName: "DocsFirstTimeDict" },
|
||||
states: { exportName: "states", typeName: "StatesDict" },
|
||||
changelog: { exportName: "changelog", typeName: "ChangelogDict" },
|
||||
"computer-use": { exportName: "computerUse", typeName: "ComputerUseDict" },
|
||||
|
||||
Reference in new issue
Block a user