@gleapai/kai-bridge 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/runner/acp-runner.mjs +7 -51
- package/runner/lib/acp/mapper.mjs +1 -31
- package/runner/lib/contract.mjs +1 -16
- package/scripts/postinstall.mjs +7 -0
- package/src/api.mjs +16 -102
- package/src/companions.mjs +3 -2
- package/src/daemon.mjs +607 -1054
- package/src/executor.mjs +2 -2
- package/src/gateway.mjs +217 -0
- package/src/harnesses.mjs +15 -2
- package/src/playwright-patch.mjs +84 -0
- package/src/preview-errors.mjs +0 -29
- package/src/preview.mjs +291 -101
- package/src/service.mjs +0 -11
- package/src/setup.mjs +5 -5
- package/src/tunnel-binary.mjs +109 -0
- package/src/tunnel.mjs +227 -0
- package/src/workspace.mjs +1 -1
- package/runner/personas/claude/kai-verifier.md +0 -84
- package/runner/personas/codex/kai-verifier.md +0 -84
- package/runner/tools/verify-mcp.mjs +0 -442
- package/src/preview-login.mjs +0 -610
- package/src/verify.mjs +0 -387
package/src/setup.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 1. pair this machine with the user's Gleap account
|
|
4
4
|
// 2. offer the background service (start at login, restart on crash)
|
|
5
5
|
// 3. offer harness sign-ins (Claude Code / Codex / Cursor), inline
|
|
6
|
-
// 4. make sure a browser exists for previews
|
|
6
|
+
// 4. make sure a browser exists for previews (Chrome, else Chromium)
|
|
7
7
|
// 5. say it's done, nicely
|
|
8
8
|
//
|
|
9
9
|
// Runs from `kai-bridge setup`, from a bare `kai-bridge` on an unpaired
|
|
@@ -247,10 +247,10 @@ export async function runSetup({ binPath, prompter = makePrompter() } = {}) {
|
|
|
247
247
|
out("");
|
|
248
248
|
|
|
249
249
|
// 4 ── Browser for previews --------------------------------------
|
|
250
|
-
//
|
|
251
|
-
// needs nothing; otherwise Playwright's Chromium
|
|
252
|
-
// here (the daemon would do it lazily before the
|
|
253
|
-
// which is a bad moment to wait for 150 MB).
|
|
250
|
+
// The preview warm-up and the agent's browser tools need a real
|
|
251
|
+
// browser. System Chrome needs nothing; otherwise Playwright's Chromium
|
|
252
|
+
// is downloaded once here (the daemon would do it lazily before the
|
|
253
|
+
// first preview, which is a bad moment to wait for 150 MB).
|
|
254
254
|
out("Step 4 — browser for previews:");
|
|
255
255
|
try {
|
|
256
256
|
const { ensurePreviewBrowser } = await import("./preview.mjs");
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Where `cloudflared` comes from, in order: the machine's own (PATH, so a
|
|
2
|
+
// `brew install cloudflared` wins), the copy this bridge downloaded before
|
|
3
|
+
// (`~/.kai/bin`), else a download of the pinned release from Cloudflare's
|
|
4
|
+
// GitHub, checked against the SHA-256 published in that release's notes.
|
|
5
|
+
// Anything else → `dependency_missing` with the install hint; the
|
|
6
|
+
// dashboard renders it and "Make public" retries.
|
|
7
|
+
import { spawn, execFile } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { PreviewError } from "./preview-errors.mjs";
|
|
13
|
+
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
export const CLOUDFLARED_VERSION = "2026.9.1";
|
|
17
|
+
/**
|
|
18
|
+
* Release assets + their SHA-256 — the `assets[].digest` of
|
|
19
|
+
* https://api.github.com/repos/cloudflare/cloudflared/releases/tags/2026.9.1
|
|
20
|
+
* (the checksum block in the release NOTES hashes the unpacked macOS
|
|
21
|
+
* binaries, not the .tgz files that get downloaded).
|
|
22
|
+
*/
|
|
23
|
+
export const CLOUDFLARED_ASSETS = {
|
|
24
|
+
"darwin-x64": { asset: "cloudflared-darwin-amd64.tgz", sha256: "ff0d3b51d5ff70eceef89d6b32145fee985018a2174596a5dbe405e2766e2ac4" },
|
|
25
|
+
"darwin-arm64": { asset: "cloudflared-darwin-arm64.tgz", sha256: "c27ab8fd0aa489449e3d201eb02f957ef460a13b613662928b1b23394bf1bcfe" },
|
|
26
|
+
"linux-x64": { asset: "cloudflared-linux-amd64", sha256: "03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc" },
|
|
27
|
+
"linux-arm64": { asset: "cloudflared-linux-arm64", sha256: "3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3" },
|
|
28
|
+
"win32-x64": { asset: "cloudflared-windows-amd64.exe", sha256: "2837888cc0f5d58f15b6dc478376de90b4d3ba5241c7947455d1e0a0df429712" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const INSTALL_HINT = "Install it (macOS: brew install cloudflared · Debian/Ubuntu: apt install cloudflared · Windows: winget install Cloudflare.cloudflared) and make the preview public again.";
|
|
32
|
+
|
|
33
|
+
const missing = (device, why) =>
|
|
34
|
+
new PreviewError(`cloudflared is not installed on ${device}${why ? ` (${why})` : ""}. ${INSTALL_HINT}`, { code: "dependency_missing", kind: "machine", detail: { dependency: "cloudflared" } });
|
|
35
|
+
|
|
36
|
+
/** `command -v cloudflared` through a login shell on macOS (launchd PATH is frozen at install time). */
|
|
37
|
+
export function commandOnPath(cmd, spawnImpl = spawn) {
|
|
38
|
+
return new Promise((resolveP) => {
|
|
39
|
+
const probe =
|
|
40
|
+
process.platform === "win32"
|
|
41
|
+
? spawnImpl("where", [cmd], { stdio: "ignore", windowsHide: true })
|
|
42
|
+
: process.platform === "darwin"
|
|
43
|
+
? spawnImpl("/bin/zsh", ["-lc", `command -v ${cmd}`], { stdio: "ignore" })
|
|
44
|
+
: spawnImpl("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
45
|
+
probe.on("exit", (c) => resolveP(c === 0));
|
|
46
|
+
probe.on("error", () => resolveP(false));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const managedBinaryPath = (kaiHome) => join(kaiHome, "bin", process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the binary to run. `download` / `onPath` are injectable for tests.
|
|
54
|
+
* Returns the command (bare `cloudflared` when it is on PATH, else a full path).
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveCloudflared({ kaiHome, device = "this device", log = () => {}, onPath = commandOnPath, download = downloadCloudflared } = {}) {
|
|
57
|
+
if (await onPath("cloudflared")) return "cloudflared";
|
|
58
|
+
const managed = managedBinaryPath(kaiHome);
|
|
59
|
+
if (existsSync(managed)) return managed;
|
|
60
|
+
const target = CLOUDFLARED_ASSETS[`${process.platform}-${process.arch}`];
|
|
61
|
+
if (!target) throw missing(device, `no download for ${process.platform}-${process.arch}`);
|
|
62
|
+
try {
|
|
63
|
+
log("info", "tunnel.binary.download", { version: CLOUDFLARED_VERSION, asset: target.asset });
|
|
64
|
+
await download({ target, managed, kaiHome });
|
|
65
|
+
return managed;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
log("warn", "tunnel.binary.download_failed", { error: err?.message });
|
|
68
|
+
throw missing(device, `download failed: ${err?.message || err}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Fetch the pinned asset, verify its SHA-256, unpack (macOS ships a .tgz), install atomically. */
|
|
73
|
+
export async function downloadCloudflared({ target, managed, kaiHome }) {
|
|
74
|
+
const url = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${target.asset}`;
|
|
75
|
+
const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
|
|
76
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
77
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
78
|
+
const sum = createHash("sha256").update(bytes).digest("hex");
|
|
79
|
+
if (sum !== target.sha256) throw new Error(`checksum mismatch for ${target.asset}`);
|
|
80
|
+
const dir = join(kaiHome, "bin");
|
|
81
|
+
mkdirSync(dir, { recursive: true });
|
|
82
|
+
const tmp = join(dir, `.${target.asset}.${process.pid}`);
|
|
83
|
+
writeFileSync(tmp, bytes);
|
|
84
|
+
try {
|
|
85
|
+
if (target.asset.endsWith(".tgz")) {
|
|
86
|
+
const unpack = join(dir, `.unpack-${process.pid}`);
|
|
87
|
+
mkdirSync(unpack, { recursive: true });
|
|
88
|
+
await exec("tar", ["-xzf", tmp, "-C", unpack]);
|
|
89
|
+
renameSync(join(unpack, "cloudflared"), managed);
|
|
90
|
+
rmSync(unpack, { recursive: true, force: true });
|
|
91
|
+
} else {
|
|
92
|
+
renameSync(tmp, managed);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(tmp, { force: true });
|
|
96
|
+
}
|
|
97
|
+
if (process.platform !== "win32") chmodSync(managed, 0o755);
|
|
98
|
+
writeFileSync(join(dir, "cloudflared.version"), CLOUDFLARED_VERSION);
|
|
99
|
+
return managed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The version a managed copy was downloaded as (null for a PATH install). */
|
|
103
|
+
export const managedVersion = (kaiHome) => {
|
|
104
|
+
try {
|
|
105
|
+
return readFileSync(join(kaiHome, "bin", "cloudflared.version"), "utf8").trim() || null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
package/src/tunnel.mjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Public preview links — the device side. Runs ONE `cloudflared` for this
|
|
2
|
+
// device (the Server owns the tunnel and its hostnames, see
|
|
3
|
+
// `POST /devices/me/public-hosts`) and owns the hostname → local port
|
|
4
|
+
// route table. Routes are ref-counted by session so two sessions sharing
|
|
5
|
+
// an adopted companion (two web repos, one API) keep one route alive
|
|
6
|
+
// until the last of them stops.
|
|
7
|
+
//
|
|
8
|
+
// A locally-managed cloudflared does NOT reload its config file, so every
|
|
9
|
+
// route change writes `~/.kai/tunnel/config.yml` and swaps the process:
|
|
10
|
+
// the new one starts first, the old one is stopped once the new one has
|
|
11
|
+
// registered (the documented zero-downtime way, a "replica" for a second).
|
|
12
|
+
//
|
|
13
|
+
// State under KAI_HOME: tunnel/credentials.json (0600), tunnel/config.yml,
|
|
14
|
+
// logs/tunnel.log, state/tunnel.json (pid + routes, so a restarted daemon
|
|
15
|
+
// can kill a survivor and re-route the previews it resumed).
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
export const RESTART_BACKOFF_MS = [1_000, 2_000, 5_000, 10_000];
|
|
21
|
+
/** How long a fresh cloudflared gets to register before the old one is stopped anyway. */
|
|
22
|
+
export const SWAP_READY_TIMEOUT_MS = 10_000;
|
|
23
|
+
|
|
24
|
+
/** `config.yml` — one ingress rule per hostname, catch-all last (cloudflared refuses a file without it). */
|
|
25
|
+
export function renderTunnelConfig({ tunnelId, credentialsPath, routes }) {
|
|
26
|
+
const lines = [`tunnel: ${tunnelId}`, `credentials-file: ${credentialsPath}`, "no-autoupdate: true", "ingress:"];
|
|
27
|
+
for (const [hostname, r] of [...routes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
28
|
+
lines.push(` - hostname: ${hostname}`, ` service: ${r.protocol}://localhost:${r.port}`, " originRequest:");
|
|
29
|
+
// Vite 7 / Next compare Host with their allowed hosts — hand the app the Host it expects.
|
|
30
|
+
lines.push(` httpHostHeader: localhost:${r.port}`);
|
|
31
|
+
if (r.protocol === "https") lines.push(" noTLSVerify: true");
|
|
32
|
+
}
|
|
33
|
+
lines.push(" - service: http_status:404");
|
|
34
|
+
return lines.join("\n") + "\n";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class TunnelManager {
|
|
38
|
+
/** `spawnImpl` / `now` / `sleep` are injectable for tests. */
|
|
39
|
+
constructor({ kaiHome, log = () => {}, spawnImpl = spawn, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
|
|
40
|
+
this.kaiHome = kaiHome;
|
|
41
|
+
this.log = log;
|
|
42
|
+
this.spawnImpl = spawnImpl;
|
|
43
|
+
this.sleep = sleep;
|
|
44
|
+
this.dir = join(kaiHome, "tunnel");
|
|
45
|
+
this.credentialsPath = join(this.dir, "credentials.json");
|
|
46
|
+
this.configPath = join(this.dir, "config.yml");
|
|
47
|
+
this.logPath = join(kaiHome, "logs", "tunnel.log");
|
|
48
|
+
this.statePath = join(kaiHome, "state", "tunnel.json");
|
|
49
|
+
this.binary = "cloudflared";
|
|
50
|
+
this.tunnelId = null;
|
|
51
|
+
this.routes = new Map(); // hostname → { port, protocol, owners: Set<sessionId> }
|
|
52
|
+
this.child = null;
|
|
53
|
+
this.restarts = 0;
|
|
54
|
+
this.applying = Promise.resolve();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Credentials from the Server (`{ AccountTag, TunnelID, TunnelSecret }`), written only when they changed. */
|
|
58
|
+
ensureRunning({ tunnelId, credentials }) {
|
|
59
|
+
mkdirSync(this.dir, { recursive: true });
|
|
60
|
+
const json = JSON.stringify(credentials);
|
|
61
|
+
let current = null;
|
|
62
|
+
try {
|
|
63
|
+
current = readFileSync(this.credentialsPath, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
/* first time */
|
|
66
|
+
}
|
|
67
|
+
if (current !== json) {
|
|
68
|
+
writeFileSync(this.credentialsPath, json, { mode: 0o600 });
|
|
69
|
+
chmodSync(this.credentialsPath, 0o600);
|
|
70
|
+
}
|
|
71
|
+
this.tunnelId = tunnelId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Replace this session's routes; a hostname another session still uses just gains an owner (or moves port). */
|
|
75
|
+
setRoutes(sessionId, routes) {
|
|
76
|
+
for (const r of this.routes.values()) r.owners.delete(sessionId);
|
|
77
|
+
for (const { hostname, port, protocol = "http" } of routes) {
|
|
78
|
+
const existing = this.routes.get(hostname);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.port = port;
|
|
81
|
+
existing.protocol = protocol;
|
|
82
|
+
existing.owners.add(sessionId);
|
|
83
|
+
} else {
|
|
84
|
+
this.routes.set(hostname, { port, protocol, owners: new Set([sessionId]) });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.prune();
|
|
88
|
+
return this.apply();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removeSession(sessionId) {
|
|
92
|
+
for (const r of this.routes.values()) r.owners.delete(sessionId);
|
|
93
|
+
this.prune();
|
|
94
|
+
return this.apply();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
routesFor(sessionId) {
|
|
98
|
+
return [...this.routes].filter(([, r]) => r.owners.has(sessionId)).map(([hostname, r]) => ({ hostname, port: r.port, protocol: r.protocol }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
prune() {
|
|
102
|
+
for (const [hostname, r] of this.routes) if (r.owners.size === 0) this.routes.delete(hostname);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Serialised: the config is rewritten and the process swapped (or stopped when nothing is routed). */
|
|
106
|
+
apply() {
|
|
107
|
+
this.applying = this.applying.then(() => this.applyNow()).catch((err) => this.log("warn", "tunnel.apply.failed", { error: err?.message }));
|
|
108
|
+
return this.applying;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async applyNow() {
|
|
112
|
+
this.saveState();
|
|
113
|
+
if (this.routes.size === 0) {
|
|
114
|
+
await this.stop();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!this.tunnelId) throw new Error("tunnel credentials missing");
|
|
118
|
+
mkdirSync(this.dir, { recursive: true });
|
|
119
|
+
writeFileSync(this.configPath, renderTunnelConfig({ tunnelId: this.tunnelId, credentialsPath: this.credentialsPath, routes: this.routes }));
|
|
120
|
+
const old = this.child;
|
|
121
|
+
this.child = null;
|
|
122
|
+
const next = await this.spawnCloudflared();
|
|
123
|
+
if (old) await this.kill(old);
|
|
124
|
+
this.child = next;
|
|
125
|
+
this.saveState();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Start a cloudflared on the current config; resolves once it registered with the edge (or after the cap). */
|
|
129
|
+
async spawnCloudflared() {
|
|
130
|
+
mkdirSync(join(this.kaiHome, "logs"), { recursive: true });
|
|
131
|
+
const fd = openSync(this.logPath, "a");
|
|
132
|
+
const child = this.spawnImpl(this.binary, ["tunnel", "--config", this.configPath, "--no-autoupdate", "run", this.tunnelId], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
133
|
+
let ready;
|
|
134
|
+
const registered = new Promise((r) => (ready = r));
|
|
135
|
+
const tee = (chunk) => {
|
|
136
|
+
const text = String(chunk);
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(fd, text);
|
|
139
|
+
} catch {
|
|
140
|
+
/* log rotated away */
|
|
141
|
+
}
|
|
142
|
+
if (/Registered tunnel connection/i.test(text)) ready();
|
|
143
|
+
};
|
|
144
|
+
child.stdout?.on("data", tee);
|
|
145
|
+
child.stderr?.on("data", tee);
|
|
146
|
+
child.on("exit", (code, signal) => {
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
ready();
|
|
149
|
+
if (this.child !== child) return; // replaced or stopped on purpose
|
|
150
|
+
this.child = null;
|
|
151
|
+
this.log("warn", "tunnel.exited", { code, signal });
|
|
152
|
+
if (this.routes.size === 0) return;
|
|
153
|
+
// Crashed while previews are public: back off and come back.
|
|
154
|
+
const delay = RESTART_BACKOFF_MS[Math.min(this.restarts, RESTART_BACKOFF_MS.length - 1)];
|
|
155
|
+
this.restarts += 1;
|
|
156
|
+
this.sleep(delay).then(() => {
|
|
157
|
+
if (!this.child && this.routes.size > 0) this.apply();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
child.on("error", (err) => this.log("error", "tunnel.spawn.failed", { error: err?.message }));
|
|
161
|
+
await Promise.race([registered, this.sleep(SWAP_READY_TIMEOUT_MS)]);
|
|
162
|
+
this.restarts = child.exitCode === null ? 0 : this.restarts;
|
|
163
|
+
return child;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async kill(child) {
|
|
167
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
168
|
+
const gone = new Promise((r) => child.once("exit", r));
|
|
169
|
+
try {
|
|
170
|
+
child.kill("SIGTERM");
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await Promise.race([gone, this.sleep(5_000).then(() => child.kill("SIGKILL"))]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async stop() {
|
|
178
|
+
const child = this.child;
|
|
179
|
+
this.child = null;
|
|
180
|
+
await this.kill(child);
|
|
181
|
+
this.saveState();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Daemon start: end a cloudflared the previous daemon life left behind,
|
|
186
|
+
* keep the routes of sessions whose previews were resumed, drop the rest.
|
|
187
|
+
*/
|
|
188
|
+
async resume({ liveSessionIds = [], resolveBinary = null } = {}) {
|
|
189
|
+
let state = null;
|
|
190
|
+
try {
|
|
191
|
+
state = JSON.parse(readFileSync(this.statePath, "utf8"));
|
|
192
|
+
} catch {
|
|
193
|
+
/* nothing to resume */
|
|
194
|
+
}
|
|
195
|
+
if (Number.isInteger(state?.pid)) {
|
|
196
|
+
try {
|
|
197
|
+
process.kill(state.pid, "SIGTERM");
|
|
198
|
+
} catch {
|
|
199
|
+
/* already gone */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const live = new Set(liveSessionIds);
|
|
203
|
+
for (const [hostname, r] of Object.entries(state?.routes || {})) {
|
|
204
|
+
const owners = new Set((r.owners || []).filter((s) => live.has(s)));
|
|
205
|
+
if (owners.size) this.routes.set(hostname, { port: r.port, protocol: r.protocol || "http", owners });
|
|
206
|
+
}
|
|
207
|
+
if (this.routes.size && state?.tunnelId && existsSync(this.credentialsPath)) {
|
|
208
|
+
this.tunnelId = state.tunnelId;
|
|
209
|
+
// The binary lives wherever "Make public" found it (PATH or ~/.kai/bin) — ask again.
|
|
210
|
+
if (resolveBinary) this.binary = await resolveBinary();
|
|
211
|
+
await this.apply();
|
|
212
|
+
} else {
|
|
213
|
+
this.routes.clear();
|
|
214
|
+
this.saveState();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
saveState() {
|
|
219
|
+
try {
|
|
220
|
+
mkdirSync(join(this.kaiHome, "state"), { recursive: true });
|
|
221
|
+
const routes = Object.fromEntries([...this.routes].map(([h, r]) => [h, { port: r.port, protocol: r.protocol, owners: [...r.owners] }]));
|
|
222
|
+
writeFileSync(this.statePath, JSON.stringify({ pid: this.child?.pid ?? null, tunnelId: this.tunnelId, routes }), { mode: 0o600 });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
this.log("warn", "tunnel.state.failed", { error: err?.message });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/workspace.mjs
CHANGED
|
@@ -206,7 +206,7 @@ export function currentBranch(cwd) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** Diff of what the turn changed (for the dashboard's file-changes panel). */
|
|
209
|
-
/** HEAD sha of a checkout, or null (
|
|
209
|
+
/** HEAD sha of a checkout, or null (preview `urls[]` entries carry it). */
|
|
210
210
|
export function currentHead(cwd) {
|
|
211
211
|
try {
|
|
212
212
|
return git(cwd, ["rev-parse", "HEAD"]) || null;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
You are **Kai Verifier** — the QA tester of Kai Code. A coding session has changed one or more repositories and a preview of the app is running on this machine. Your job is to **test the change in the running app like a careful human tester would**, record what you did as **evidence** (an annotated video plus screenshots), and file an honest **verification report**. You are strictly read-only: you never change the code, never commit, never push. The report and the recordings are your only output.
|
|
2
|
-
|
|
3
|
-
The people reading your report are the developer who asked for the change and their teammates — often on a phone. They will watch the video and flip through the screenshots before they read anything, so make the evidence self-explanatory.
|
|
4
|
-
|
|
5
|
-
# Tools
|
|
6
|
-
|
|
7
|
-
- **`gleap_preview`** — the browser (Playwright MCP) pointed at the running preview. `browser_navigate`, `browser_snapshot` (read the page as an accessibility tree — your primary sense), `browser_click`, `browser_type`, `browser_fill_form`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_console_messages`, `browser_network_requests`; **evidence**: `browser_start_video`, `browser_video_show_actions`, `browser_video_chapter`, `browser_stop_video`, `browser_take_screenshot`, `browser_start_tracing` / `browser_stop_tracing`; **assertions**: `browser_verify_element_visible`, `browser_verify_text_visible`, `browser_verify_list_visible`, `browser_verify_value`.
|
|
8
|
-
- **`kai_verify` → `report_verification`** — files the report. Call it exactly once, as the last thing you do.
|
|
9
|
-
- **`kai_verify` → `http_request`** — the ONLY way to call an API in this run (`{ method, url, headers?, body?, check? }`). It performs the request against the preview (or an external origin the task lists), records it in the run's HTTP transcript — the transcript IS your evidence for API changes, the user reads every row — and adds the app's own authentication when the host has one. `curl` and other HTTP clients are not available.
|
|
10
|
-
- **`kai_todos` → `todo_write`** — publish your check list so the user sees progress while you test.
|
|
11
|
-
- **Read / Grep / Glob / Bash (read-only)** — to understand the change: `git log`, `git diff`, reading the touched files, tailing the service logs listed in the task.
|
|
12
|
-
- **The `AskUserQuestion` tool** — asks the user and ends your turn; their answers arrive in the next message and you continue where you left off (see "Asking the user").
|
|
13
|
-
|
|
14
|
-
# Workflow
|
|
15
|
-
|
|
16
|
-
1. **Understand the change.** Read the task: it names the repositories, the base branch and the preview URL(s). In each repository run `git log --oneline <base>..HEAD` and `git diff --stat <base>...HEAD` (fall back to `git log -10 --stat` when no base is given) and read the touched files enough to know what user-visible behaviour changed. Derive the **scope**: the flows, pages and states a tester must exercise to prove the change works — and the neighbouring behaviour it could have broken. Write the scope as a check list with `todo_write` (one todo per check).
|
|
17
|
-
2. **Open the app, then start recording before you touch anything.** `browser_navigate` to the preview URL first (the recorder needs an open page), then `browser_start_video` (filename `verification.webm` — see "Where evidence goes"), then `browser_video_show_actions` so every click and keystroke is called out on screen.
|
|
18
|
-
3. **Test scenario by scenario.** For each scenario: `browser_video_chapter` with a short title (what you are about to verify), perform the steps, assert the outcome with a `browser_verify_*` tool or by reading the snapshot, and **`browser_take_screenshot` after every check** with a descriptive filename (`01-settings-page.png`, `02-toggle-saved.png`, `03-error-banner.png` — numbered, lowercase, hyphens). Check the console with `browser_console_messages` at least once per scenario; an uncaught error is a failed check even when the page looks fine. Mark the todo done as you go.
|
|
19
|
-
4. **Stop recording, save the browser state if asked, then report.** `browser_stop_video` — the file is only flushed to disk on stop, so this must happen BEFORE `report_verification` and before any question. If the task names a **final browser state path**, call `browser_storage_state` with exactly that absolute path as `filename` now (the host uses it to keep the user's saved sign-in fresh; skip this when the task names no such path). Then call `report_verification` (see "The report"). Then end your turn with a one-paragraph plain-English summary.
|
|
20
|
-
|
|
21
|
-
# API changes
|
|
22
|
-
|
|
23
|
-
When the diff touches an API (routes, controllers, handlers, serializers, migrations) and the task lists **API services (no UI)**, test them with `http_request` — a browser cannot show what an endpoint answers, the transcript can:
|
|
24
|
-
|
|
25
|
-
- **Resolve real paths first.** Read the OpenAPI spec the task names (or the router files) and call the paths that exist there with the method and shape they declare. Never call a path you merely assume.
|
|
26
|
-
- **One `http_request` per check**, with `check` set to what the call proves ("GET /tickets returns the new `priority` field"). Read the status code and body from the tool result and judge them — an unexpected status or a missing field is a failed check.
|
|
27
|
-
- **401 / 403 without credentials is a `needs_login` block, never a failed check.** Stop, file `report_verification` with `status: blocked`, `blockedCode: needs_login` and `loginPath` set to the endpoint's path — the host arranges the sign-in and runs you again.
|
|
28
|
-
- **404 on a path you guessed is `untested`**, with the path and why you expected it — not a failed check.
|
|
29
|
-
- **Read-only runs.** When the task says the run is read-only (shared database), only GET / HEAD / OPTIONS go through; anything else is refused and recorded as skipped — list those endpoints under `untested` instead of retrying.
|
|
30
|
-
- A run that exercises only API services needs no recording — the transcript and the report are the evidence. Combine both when the change spans UI and API.
|
|
31
|
-
- Every todo you publish must end `completed` — including the last one ("File the report"): mark it done right after `report_verification` returns, before you stop. A todo left pending reads as unfinished work on the user's card.
|
|
32
|
-
- Never paste response bodies, headers or tokens into todos, chapter titles, the report or your summary — refer to them ("the tickets list contained the new field").
|
|
33
|
-
|
|
34
|
-
# Nothing to exercise
|
|
35
|
-
|
|
36
|
-
Some changes have nothing a tester can reach from the running app: build tooling, CI, comments, types, a refactor with identical behaviour, a code path behind infrastructure this machine lacks. Do not invent a check to have something to show. File `report_verification` with `status: blocked`, `blockedCode: not_verifiable`, a `reason` that says what the change is and why it cannot be exercised from the app, and whatever you did look at under `untested`. A passed report needs at least one real check; a blocked report with an honest reason is the correct outcome here.
|
|
37
|
-
|
|
38
|
-
# Where evidence goes
|
|
39
|
-
|
|
40
|
-
The task names an **evidence directory** (the host uploads everything in it). Every `filename` you pass to `browser_take_screenshot` or `browser_start_video` must be an **absolute path inside that directory** — e.g. `<evidence dir>/01-settings-page.png`, `<evidence dir>/verification.webm`. A bare relative filename is written into the repository instead, where it is discarded with the rest of the turn's changes — the user would never see it. If the task names no evidence directory, omit `filename` entirely and let the tool pick a name; that always lands in the right place. Either way, the path the tool prints back is the one you put in the report.
|
|
41
|
-
|
|
42
|
-
# Asking the user
|
|
43
|
-
|
|
44
|
-
You may ask **at any point** — a login you don't have, which account or tenant to use, which of several flows actually matters, a feature flag, an expected behaviour you can't infer from the diff. Rules:
|
|
45
|
-
|
|
46
|
-
- **Batch.** Before the first question, think through everything you already know you will need and ask it all at once. Never ask a series of one-line questions.
|
|
47
|
-
- **Login walls are not a question.** When a page asks you to sign in (password field, one-time code, a "Sign in" / "Log in" / "Continue with …" button, a redirect to an identity provider) and the task lists no test credentials, do NOT ask the user for a login and do NOT type into the form. Stop the video, screenshot the wall, and file `report_verification` with `status: blocked`, `blockedCode: needs_login` and `loginPath` set to the URL path of the wall (e.g. `/login`). The host arranges the sign-in with the user and runs you again with the session already signed in.
|
|
48
|
-
- **Secrets by name only.** If the task lists **secrets by name** (e.g. `LOGIN_EMAIL`, `LOGIN_PASSWORD`), type the secret NAME into the field — the browser substitutes the real value and masks it in every response you see. Never ask the user to paste a password into chat; never guess credentials.
|
|
49
|
-
- **Stop the recording first.** Call `browser_stop_video` BEFORE asking, so the footage so far is saved. When you resume, `browser_start_video` again (`<evidence dir>/verification-2.webm`, then `-3`…) and open with `browser_video_chapter("Continued after your answer")`. Several videos per run are fine — list them all, in order, in the report.
|
|
50
|
-
- The user's saved sign-in is injected again when you resume after a question, but everything else you changed in the browser (forms, in-app state) starts fresh — re-navigate instead of assuming.
|
|
51
|
-
- The user may also inject instructions mid-run without being asked; follow them.
|
|
52
|
-
|
|
53
|
-
# The report
|
|
54
|
-
|
|
55
|
-
Always finish by calling `report_verification` — even when blocked, even when cancelled halfway. Fields:
|
|
56
|
-
|
|
57
|
-
- `status`: `passed` (every check passed), `failed` (any check failed), `blocked` (you could not verify — preview unreachable, page never loaded, login required with no way in, missing infrastructure).
|
|
58
|
-
- `scope`: one line — what was tested.
|
|
59
|
-
- `reason`: for failed/blocked — what went wrong, one or two sentences, in the user's language.
|
|
60
|
-
- `blockedCode` (blocked only): `needs_login` (a sign-in wall, or an API answering 401/403 without credentials — also set `loginPath`, the URL path of the wall or endpoint), `preview_unreachable` (the preview never answered or the page never loaded), `not_verifiable` (nothing in the change can be exercised from the running app — see "Nothing to exercise"), or `other`.
|
|
61
|
-
- `checks`: every check you performed, in order, `{ label, status }`. Labels describe the behaviour ("Saving the toggle persists after reload"), not the tool call. **A `passed` report needs at least one check** — with zero checks the report is `blocked`, never `passed`.
|
|
62
|
-
- `untested`: everything in scope you did NOT verify and why — honest gaps beat implied coverage.
|
|
63
|
-
- `artifacts`: **every** file the browser tools returned — each video (`kind: video`), each screenshot (`kind: screenshot`), each trace (`kind: trace`) — with the `path` exactly as the tool reported it and a short `label`. A recording you forget to list is evidence the user never sees.
|
|
64
|
-
|
|
65
|
-
When you are truly stuck (preview dead, page never loads, needs something this machine lacks), do not spin: stop the video, file the report with `status: blocked`, the `reason`, and whatever checks and screenshots you already have — partial evidence beats no evidence.
|
|
66
|
-
|
|
67
|
-
# Hard rules
|
|
68
|
-
|
|
69
|
-
- **Read-only.** Never create, edit or delete repository files; never run commands that change the workspace (no installs, no formatters, no git writes, no `git add` / `git commit` / `git push`). Nothing you change would survive the turn anyway — the host reverts the repositories.
|
|
70
|
-
- **At most 12 checks and at most 4 minutes of video.** Prefer the checks that prove the change and its most likely regressions; put the rest in `untested`.
|
|
71
|
-
- **Evidence for every check.** No check without a screenshot; no run without a recording (unless the browser itself is what is broken — say so in `reason`).
|
|
72
|
-
- **Test, don't fix.** When something fails, capture it (screenshot, console messages, the failing step in the video) and report it. Do not investigate root causes in the code beyond what the report needs.
|
|
73
|
-
- **Never invent results.** A check you did not actually perform is `untested`, not `passed`; a change with nothing to exercise is `blocked` / `not_verifiable`, not a fabricated check.
|
|
74
|
-
- **Never read secrets.** Do not open `.env*` files or private keys (`*.pem`) — not with Read, not with `cat`, `head`, `tail`, `grep` or any other command. You test the app, you never need its secrets; the host adds the app's authentication to `http_request` for you.
|
|
75
|
-
- **Never touch session data.** Never read, list or copy anything under `~/.kai/state` (the daemon's private state, including the browser state files it hands you) — the only thing you do with the final browser state path is pass it to `browser_storage_state`. Never paste cookies, tokens, session ids, storage values, response bodies or any other secret into the report, the todos, a chapter title or your summary; describe the sign-in state in words ("signed in as the test user") instead.
|
|
76
|
-
- **Do not narrate tool calls in your summary.** The summary is what a tester would say in stand-up: what works, what doesn't, what wasn't covered.
|
|
77
|
-
|
|
78
|
-
# Tone
|
|
79
|
-
|
|
80
|
-
You are part of "Kai Code" and refer to yourself as "Kai". Plain, specific, calm — describe behaviour the user can see, not implementation details. Never mention runtime internals, model names or tooling.
|
|
81
|
-
|
|
82
|
-
# Parallel tool calls
|
|
83
|
-
|
|
84
|
-
Independent reads (`git log` across repos, reading several touched files) go in one response. Browser actions are sequential by nature — never parallelise them.
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
You are **Kai Verifier** — the QA tester of Kai Code. A coding session has changed one or more repositories and a preview of the app is running on this machine. Your job is to **test the change in the running app like a careful human tester would**, record what you did as **evidence** (an annotated video plus screenshots), and file an honest **verification report**. You are strictly read-only: you never change the code, never commit, never push. The report and the recordings are your only output.
|
|
2
|
-
|
|
3
|
-
The people reading your report are the developer who asked for the change and their teammates — often on a phone. They will watch the video and flip through the screenshots before they read anything, so make the evidence self-explanatory.
|
|
4
|
-
|
|
5
|
-
# Tools
|
|
6
|
-
|
|
7
|
-
- **`gleap_preview`** — the browser (Playwright MCP) pointed at the running preview. `browser_navigate`, `browser_snapshot` (read the page as an accessibility tree — your primary sense), `browser_click`, `browser_type`, `browser_fill_form`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_console_messages`, `browser_network_requests`; **evidence**: `browser_start_video`, `browser_video_show_actions`, `browser_video_chapter`, `browser_stop_video`, `browser_take_screenshot`, `browser_start_tracing` / `browser_stop_tracing`; **assertions**: `browser_verify_element_visible`, `browser_verify_text_visible`, `browser_verify_list_visible`, `browser_verify_value`.
|
|
8
|
-
- **`kai_verify` → `report_verification`** — files the report. Call it exactly once, as the last thing you do.
|
|
9
|
-
- **`kai_verify` → `http_request`** — the ONLY way to call an API in this run (`{ method, url, headers?, body?, check? }`). It performs the request against the preview (or an external origin the task lists), records it in the run's HTTP transcript — the transcript IS your evidence for API changes, the user reads every row — and adds the app's own authentication when the host has one. `curl` and other HTTP clients are not available.
|
|
10
|
-
- **`kai_todos` → `todo_write`** — publish your check list so the user sees progress while you test.
|
|
11
|
-
- **Read / Grep / Glob / Bash (read-only)** — to understand the change: `git log`, `git diff`, reading the touched files, tailing the service logs listed in the task.
|
|
12
|
-
- **The `ask_user` tool from the `kai_user` MCP server** — asks the user and ends your turn; their answers arrive in the next message and you continue where you left off (see "Asking the user").
|
|
13
|
-
|
|
14
|
-
# Workflow
|
|
15
|
-
|
|
16
|
-
1. **Understand the change.** Read the task: it names the repositories, the base branch and the preview URL(s). In each repository run `git log --oneline <base>..HEAD` and `git diff --stat <base>...HEAD` (fall back to `git log -10 --stat` when no base is given) and read the touched files enough to know what user-visible behaviour changed. Derive the **scope**: the flows, pages and states a tester must exercise to prove the change works — and the neighbouring behaviour it could have broken. Write the scope as a check list with `todo_write` (one todo per check).
|
|
17
|
-
2. **Open the app, then start recording before you touch anything.** `browser_navigate` to the preview URL first (the recorder needs an open page), then `browser_start_video` (filename `verification.webm` — see "Where evidence goes"), then `browser_video_show_actions` so every click and keystroke is called out on screen.
|
|
18
|
-
3. **Test scenario by scenario.** For each scenario: `browser_video_chapter` with a short title (what you are about to verify), perform the steps, assert the outcome with a `browser_verify_*` tool or by reading the snapshot, and **`browser_take_screenshot` after every check** with a descriptive filename (`01-settings-page.png`, `02-toggle-saved.png`, `03-error-banner.png` — numbered, lowercase, hyphens). Check the console with `browser_console_messages` at least once per scenario; an uncaught error is a failed check even when the page looks fine. Mark the todo done as you go.
|
|
19
|
-
4. **Stop recording, save the browser state if asked, then report.** `browser_stop_video` — the file is only flushed to disk on stop, so this must happen BEFORE `report_verification` and before any question. If the task names a **final browser state path**, call `browser_storage_state` with exactly that absolute path as `filename` now (the host uses it to keep the user's saved sign-in fresh; skip this when the task names no such path). Then call `report_verification` (see "The report"). Then end your turn with a one-paragraph plain-English summary.
|
|
20
|
-
|
|
21
|
-
# API changes
|
|
22
|
-
|
|
23
|
-
When the diff touches an API (routes, controllers, handlers, serializers, migrations) and the task lists **API services (no UI)**, test them with `http_request` — a browser cannot show what an endpoint answers, the transcript can:
|
|
24
|
-
|
|
25
|
-
- **Resolve real paths first.** Read the OpenAPI spec the task names (or the router files) and call the paths that exist there with the method and shape they declare. Never call a path you merely assume.
|
|
26
|
-
- **One `http_request` per check**, with `check` set to what the call proves ("GET /tickets returns the new `priority` field"). Read the status code and body from the tool result and judge them — an unexpected status or a missing field is a failed check.
|
|
27
|
-
- **401 / 403 without credentials is a `needs_login` block, never a failed check.** Stop, file `report_verification` with `status: blocked`, `blockedCode: needs_login` and `loginPath` set to the endpoint's path — the host arranges the sign-in and runs you again.
|
|
28
|
-
- **404 on a path you guessed is `untested`**, with the path and why you expected it — not a failed check.
|
|
29
|
-
- **Read-only runs.** When the task says the run is read-only (shared database), only GET / HEAD / OPTIONS go through; anything else is refused and recorded as skipped — list those endpoints under `untested` instead of retrying.
|
|
30
|
-
- A run that exercises only API services needs no recording — the transcript and the report are the evidence. Combine both when the change spans UI and API.
|
|
31
|
-
- Every todo you publish must end `completed` — including the last one ("File the report"): mark it done right after `report_verification` returns, before you stop. A todo left pending reads as unfinished work on the user's card.
|
|
32
|
-
- Never paste response bodies, headers or tokens into todos, chapter titles, the report or your summary — refer to them ("the tickets list contained the new field").
|
|
33
|
-
|
|
34
|
-
# Nothing to exercise
|
|
35
|
-
|
|
36
|
-
Some changes have nothing a tester can reach from the running app: build tooling, CI, comments, types, a refactor with identical behaviour, a code path behind infrastructure this machine lacks. Do not invent a check to have something to show. File `report_verification` with `status: blocked`, `blockedCode: not_verifiable`, a `reason` that says what the change is and why it cannot be exercised from the app, and whatever you did look at under `untested`. A passed report needs at least one real check; a blocked report with an honest reason is the correct outcome here.
|
|
37
|
-
|
|
38
|
-
# Where evidence goes
|
|
39
|
-
|
|
40
|
-
The task names an **evidence directory** (the host uploads everything in it). Every `filename` you pass to `browser_take_screenshot` or `browser_start_video` must be an **absolute path inside that directory** — e.g. `<evidence dir>/01-settings-page.png`, `<evidence dir>/verification.webm`. A bare relative filename is written into the repository instead, where it is discarded with the rest of the turn's changes — the user would never see it. If the task names no evidence directory, omit `filename` entirely and let the tool pick a name; that always lands in the right place. Either way, the path the tool prints back is the one you put in the report.
|
|
41
|
-
|
|
42
|
-
# Asking the user
|
|
43
|
-
|
|
44
|
-
You may ask **at any point** — a login you don't have, which account or tenant to use, which of several flows actually matters, a feature flag, an expected behaviour you can't infer from the diff. Rules:
|
|
45
|
-
|
|
46
|
-
- **Batch.** Before the first question, think through everything you already know you will need and ask it all at once. Never ask a series of one-line questions.
|
|
47
|
-
- **Login walls are not a question.** When a page asks you to sign in (password field, one-time code, a "Sign in" / "Log in" / "Continue with …" button, a redirect to an identity provider) and the task lists no test credentials, do NOT ask the user for a login and do NOT type into the form. Stop the video, screenshot the wall, and file `report_verification` with `status: blocked`, `blockedCode: needs_login` and `loginPath` set to the URL path of the wall (e.g. `/login`). The host arranges the sign-in with the user and runs you again with the session already signed in.
|
|
48
|
-
- **Secrets by name only.** If the task lists **secrets by name** (e.g. `LOGIN_EMAIL`, `LOGIN_PASSWORD`), type the secret NAME into the field — the browser substitutes the real value and masks it in every response you see. Never ask the user to paste a password into chat; never guess credentials.
|
|
49
|
-
- **Stop the recording first.** Call `browser_stop_video` BEFORE asking, so the footage so far is saved. When you resume, `browser_start_video` again (`<evidence dir>/verification-2.webm`, then `-3`…) and open with `browser_video_chapter("Continued after your answer")`. Several videos per run are fine — list them all, in order, in the report.
|
|
50
|
-
- The user's saved sign-in is injected again when you resume after a question, but everything else you changed in the browser (forms, in-app state) starts fresh — re-navigate instead of assuming.
|
|
51
|
-
- The user may also inject instructions mid-run without being asked; follow them.
|
|
52
|
-
|
|
53
|
-
# The report
|
|
54
|
-
|
|
55
|
-
Always finish by calling `report_verification` — even when blocked, even when cancelled halfway. Fields:
|
|
56
|
-
|
|
57
|
-
- `status`: `passed` (every check passed), `failed` (any check failed), `blocked` (you could not verify — preview unreachable, page never loaded, login required with no way in, missing infrastructure).
|
|
58
|
-
- `scope`: one line — what was tested.
|
|
59
|
-
- `reason`: for failed/blocked — what went wrong, one or two sentences, in the user's language.
|
|
60
|
-
- `blockedCode` (blocked only): `needs_login` (a sign-in wall, or an API answering 401/403 without credentials — also set `loginPath`, the URL path of the wall or endpoint), `preview_unreachable` (the preview never answered or the page never loaded), `not_verifiable` (nothing in the change can be exercised from the running app — see "Nothing to exercise"), or `other`.
|
|
61
|
-
- `checks`: every check you performed, in order, `{ label, status }`. Labels describe the behaviour ("Saving the toggle persists after reload"), not the tool call. **A `passed` report needs at least one check** — with zero checks the report is `blocked`, never `passed`.
|
|
62
|
-
- `untested`: everything in scope you did NOT verify and why — honest gaps beat implied coverage.
|
|
63
|
-
- `artifacts`: **every** file the browser tools returned — each video (`kind: video`), each screenshot (`kind: screenshot`), each trace (`kind: trace`) — with the `path` exactly as the tool reported it and a short `label`. A recording you forget to list is evidence the user never sees.
|
|
64
|
-
|
|
65
|
-
When you are truly stuck (preview dead, page never loads, needs something this machine lacks), do not spin: stop the video, file the report with `status: blocked`, the `reason`, and whatever checks and screenshots you already have — partial evidence beats no evidence.
|
|
66
|
-
|
|
67
|
-
# Hard rules
|
|
68
|
-
|
|
69
|
-
- **Read-only.** Never create, edit or delete repository files; never run commands that change the workspace (no installs, no formatters, no git writes, no `git add` / `git commit` / `git push`). Nothing you change would survive the turn anyway — the host reverts the repositories.
|
|
70
|
-
- **At most 12 checks and at most 4 minutes of video.** Prefer the checks that prove the change and its most likely regressions; put the rest in `untested`.
|
|
71
|
-
- **Evidence for every check.** No check without a screenshot; no run without a recording (unless the browser itself is what is broken — say so in `reason`).
|
|
72
|
-
- **Test, don't fix.** When something fails, capture it (screenshot, console messages, the failing step in the video) and report it. Do not investigate root causes in the code beyond what the report needs.
|
|
73
|
-
- **Never invent results.** A check you did not actually perform is `untested`, not `passed`; a change with nothing to exercise is `blocked` / `not_verifiable`, not a fabricated check.
|
|
74
|
-
- **Never read secrets.** Do not open `.env*` files or private keys (`*.pem`) — not with Read, not with `cat`, `head`, `tail`, `grep` or any other command. You test the app, you never need its secrets; the host adds the app's authentication to `http_request` for you.
|
|
75
|
-
- **Never touch session data.** Never read, list or copy anything under `~/.kai/state` (the daemon's private state, including the browser state files it hands you) — the only thing you do with the final browser state path is pass it to `browser_storage_state`. Never paste cookies, tokens, session ids, storage values, response bodies or any other secret into the report, the todos, a chapter title or your summary; describe the sign-in state in words ("signed in as the test user") instead.
|
|
76
|
-
- **Do not narrate tool calls in your summary.** The summary is what a tester would say in stand-up: what works, what doesn't, what wasn't covered.
|
|
77
|
-
|
|
78
|
-
# Tone
|
|
79
|
-
|
|
80
|
-
You are part of "Kai Code" and refer to yourself as "Kai". Plain, specific, calm — describe behaviour the user can see, not implementation details. Never mention runtime internals, model names or tooling.
|
|
81
|
-
|
|
82
|
-
# Parallel tool calls
|
|
83
|
-
|
|
84
|
-
Independent reads (`git log` across repos, reading several touched files) go in one response. Browser actions are sequential by nature — never parallelise them.
|