Files
6krrt/deploy/opencode-plugin/guardrails.contract.test.mjs
adlee-was-taken ddb1ebb377 fix(opencode-plugin): task_worktree_line rewrites output.args and parses the real line forms
Defects fixed:
- (k) Rewrite targets output.args.prompt, not output.prompt
- (l) Prepended line uses actual main checkout directory, not parent of worktree
- (m) Parser accepts 'path. cd there', 'path -- cd there', 'path' forms;
      dotted paths (e.g. feat.v2) are no longer truncated
2026-10-04 05:56:15 -04:00

830 lines
25 KiB
JavaScript

/**
* Contract tests for guardrails.js
*
* Drives the plugin with opencode's real hook shapes: (input, output) where
* input has {tool, sessionID, callID} and output has {args}. Builds a client
* whose session.get returns the real key set, writes a real .omo/guardrails.json
* (based on guardrails.example.json) and real .omo/boulder.json into a temp dir.
*
* Acceptance probe — every rule at default, real hook shape.
* Per-rule — one positive (blocks) and one negative (allows) case each.
* Only the tick gate test may use the real scripts/verify_commit.py.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
copyFileSync,
writeFileSync,
readFileSync,
existsSync,
mkdirSync,
} from "node:fs";
import { join } from "node:path";
import { mkdtempSync } from "node:fs";
import os from "node:os";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Guardrails } from "./guardrails.js";
const execFileAsync = promisify(execFile);
const __dirname = fileURLToPath(new URL(".", import.meta.url));
// ---------------------------------------------------------------------------
// Constants — real guardrails.example.json content (Design C defaults)
// ---------------------------------------------------------------------------
/** Mirrors the default guardrails.example.json shipped in deploy/. */
const EXAMPLE_CONFIG = {
rules: {
task_needs_agent: "block",
task_banned_agent: "block",
task_worktree_line: "block",
write_outside_worktree: "block",
bash_main_checkout: "block",
bash_banned: "block",
bash_protected_port: "block",
plan_tick_gate: "block",
},
log: ".omo/guardrails.log",
protected_ports: [8080],
tick_gate: {
cmd: [
"python3",
"{directory}/scripts/verify_commit.py",
"--repo",
"{worktree}",
"--require-clean",
"HEAD",
],
timeout_s: 900,
},
};
const SCRIPT_DIR = join(__dirname, "..", "..", "scripts");
/** Full path to python3 — required by tick_gate because guardrails checks
* existsSync on resolvedArgs[0], which must be a file path, not a PATH command. */
const PYTHON3 = execFileSync("which", ["python3"], { encoding: "utf-8" }).trim();
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function newDir() {
return mkdtempSync(join(os.tmpdir(), "guardrails-contract-"));
}
function writeConfig(dir, configObj) {
const cfgDir = join(dir, ".omo");
mkdirSync(cfgDir, { recursive: true });
writeFileSync(join(cfgDir, "guardrails.json"), JSON.stringify(configObj));
return cfgDir;
}
function writeBoulder(dir, boulderObj) {
const boulderDir = join(dir, ".omo");
if (!existsSync(boulderDir)) {
mkdirSync(boulderDir, { recursive: true });
}
writeFileSync(join(boulderDir, "boulder.json"), JSON.stringify(boulderObj));
}
/**
* Build a client whose session.get returns the real key set.
* The guardrails plugin reads result.data.parentID for scope walk.
*/
function realClient(extraData) {
return {
session: {
get: async ({ path }) => ({
data: {
agent: "Sisyphus-ultraworker",
cost: 0.12,
directory: "/home/test/project",
id: path?.id ?? "ses_contract",
model: "gpt-5.6",
path: "/home/test/project/main",
projectID: "6krrt",
slug: "6krrt",
summary: "Refactor router dispatch",
time: "2026-10-04T12:00:00Z",
title: "R6 — contract test",
tokens: 4821,
version: "2.0.0",
...extraData,
},
}),
},
};
}
/**
* Drive the hook with opencode's real shape:
* await hooks["tool.execute.before"]({tool, sessionID, callID}, {args})
*
* All tests use this function — never build input.args.
* The {args} object is always on the OUTPUT parameter.
*/
async function hookDrive(hooks, tool, sessionID, args) {
return hooks["tool.execute.before"](
{ tool, sessionID, callID: "call_contract" },
{ args },
);
}
// ---------------------------------------------------------------------------
// Loader contract
// ---------------------------------------------------------------------------
describe("loader contract", () => {
it("every named export from guardrails.js is a function", async () => {
const mod = await import("./guardrails.js");
for (const [name, value] of Object.entries(mod)) {
assert.equal(
typeof value,
"function",
`export "${name}" must be a function`,
);
}
});
});
// ===========================================================================
// Acceptance probes — every rule at default (block), real hook shape
// ===========================================================================
describe("Acceptance probes (every rule at default, real hook shape)", () => {
const SESSION = "ses_accept";
it("task with no category, subagent_type or task_id → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "task", SESSION, {}),
{ message: /task\/call_omo_agent needs category or subagent_type \(or task_id for resume\)/ },
);
});
it("task with subagent_type oh-my-claudecode:writer → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "oh-my-claudecode:writer",
prompt: "hello",
}),
{ message: /subagent_type must not start with oh-my-claudecode/ },
);
});
it("bash git push origin x → BLOCK (bash_banned)", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await assert.rejects(
hookDrive(hooks, "bash", SESSION, { command: "git push origin x" }),
{ message: /bash\/banned: blocked/ },
);
});
it("bash git stash → BLOCK (bash_banned)", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await assert.rejects(
hookDrive(hooks, "bash", SESSION, { command: "git stash" }),
{ message: /bash\/banned: blocked/ },
);
});
it("bash curl -s localhost:8080/health → BLOCK (bash_protected_port)", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await assert.rejects(
hookDrive(hooks, "bash", SESSION, {
command: "curl -s localhost:8080/health",
}),
{ message: /bash\/protected_port: blocked/ },
);
});
it("bash ls → ALLOW", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await hookDrive(hooks, "bash", SESSION, { command: "ls" });
// No throw → ALLOW
});
it("task with category:quick and correct WORKTREE line → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
// Test multiple valid forms of the WORKTREE line
const validLines = [
`WORKTREE: ${wt}. cd there first; never edit under ${dir}/.`,
`WORKTREE: ${wt}. cd there`,
`WORKTREE: ${wt} -- cd there`,
`WORKTREE: ${wt}`,
];
for (const line of validLines) {
const prompt = line + "\nRefactor this function";
await hookDrive(hooks, "task", SESSION, {
category: "quick",
prompt,
});
}
});
it("task with dotted path in WORKTREE line → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt.v2");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
const correctLine = `WORKTREE: ${wt}. cd there first; never edit under ${dir}/.`;
await hookDrive(hooks, "task", SESSION, {
category: "quick",
prompt: correctLine + "\nRefactor this function",
});
});
it("task with no WORKTREE line → REWRITE (verify using hookDrive as a wrapper)", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
const args = { category: "quick", prompt: "hello" };
// hookDrive passes { args } as output, so the hook mutates args.prompt
await hooks["tool.execute.before"](
{ tool: "task", sessionID: SESSION, callID: "call_rewrite" },
{ args },
);
const expectedLine = `WORKTREE: ${wt}. cd there first; never edit under ${dir}/.`;
assert.equal(args.prompt, expectedLine + "\nhello");
});
it("task with WORKTREE line and warn mode → ALLOW and LOG", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, { ...EXAMPLE_CONFIG, rules: { task_worktree_line: "warn" } });
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
const badLine = `WORKTREE: /wrong/path. cd there first.`;
await hookDrive(hooks, "task", SESSION, {
category: "quick",
prompt: badLine + "\nhello",
});
const logContent = readFileSync(join(dir, ".omo/guardrails.log"), "utf-8");
assert.ok(logContent.includes("task_worktree_line"));
});
});
// ===========================================================================
// Per-rule positive (blocks) and negative (allows)
// ===========================================================================
describe("task_needs_agent (R1)", () => {
const SESSION = "ses_tna";
it("positive: bare task without category/subagent_type/task_id → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "task", SESSION, {}),
{ message: /task\/call_omo_agent needs category or subagent_type/ },
);
});
it("negative: task with category only → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await hookDrive(hooks, "task", SESSION, {
category: "quick",
prompt: "hello",
});
});
it("negative: task with subagent_type only → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await hookDrive(hooks, "task", SESSION, {
subagent_type: "explore",
prompt: "hello",
});
});
it("negative: task with task_id only → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await hookDrive(hooks, "task", SESSION, {
task_id: "task_123",
prompt: "hello",
});
});
it("negative: task with category + subagent_type → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "explore",
prompt: "hello",
});
});
});
describe("task_banned_agent (R2)", () => {
const SESSION = "ses_tba";
it("positive: oh-my-claudecode:writer subagent → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "oh-my-claudecode:writer",
prompt: "hello",
}),
{ message: /subagent_type must not start with oh-my-claudecode/ },
);
});
it("negative: normal subagent_type → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
await hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "build",
prompt: "hello",
});
});
});
describe("task_worktree_line (R3)", () => {
const SESSION = "ses_twl";
it("positive: WORKTREE line with wrong path → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
const badLine = `WORKTREE: /some/other/path. cd there first; never edit under /some/other/.`;
await assert.rejects(
hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "explore",
prompt: badLine + "\nhello",
}),
{ message: /WORKTREE line path.*does not match/ },
);
});
it("negative: correct WORKTREE line in prompt → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null, id: SESSION }),
directory: dir,
});
const dirPath = wt.slice(0, wt.lastIndexOf("/")) || wt;
const correctLine =
`WORKTREE: ${wt}. cd there first; never edit under ${dirPath}/.`;
await hookDrive(hooks, "task", SESSION, {
category: "quick",
subagent_type: "build",
prompt: correctLine + "\nRefactor this function",
});
});
});
describe("write_outside_worktree (R4)", () => {
const SESSION = "ses_wow";
it("positive: write to directory outside worktree → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "write", SESSION, {
filePath: join(dir, "main.py"),
content: "hello",
}),
{ message: /write\/outside_worktree/ },
);
});
it("negative: write inside worktree → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await hookDrive(hooks, "write", SESSION, {
filePath: join(wt, "a.py"),
content: "hello",
});
});
});
describe("bash_main_checkout (R5a)", () => {
const SESSION = "ses_bmc";
it("positive: git operation in main checkout → BLOCK", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "bash", SESSION, { command: "git add ." }),
{ message: /bash\/main_checkout/ },
);
});
it("negative: git -C worktree → ALLOW", async () => {
const dir = newDir();
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
writeConfig(dir, EXAMPLE_CONFIG);
writeBoulder(dir, {
status: "active",
worktree_path: wt,
session_ids: [`opencode:${SESSION}`],
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await hookDrive(hooks, "bash", SESSION, {
command: `git -C ${wt} add .`,
});
});
});
describe("bash_banned (R5b)", () => {
const SESSION = "ses_bb";
it("positive: banned git push → BLOCK", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await assert.rejects(
hookDrive(hooks, "bash", SESSION, { command: "git push origin main" }),
{ message: /bash\/banned: blocked/ },
);
});
it("negative: safe command ls → ALLOW", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await hookDrive(hooks, "bash", SESSION, { command: "ls -la" });
});
});
describe("bash_protected_port (R5c)", () => {
const SESSION = "ses_bpp";
it("positive: curl to localhost:8080 → BLOCK", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await assert.rejects(
hookDrive(hooks, "bash", SESSION, {
command: "curl -s http://127.0.0.1:8080/health",
}),
{ message: /bash\/protected_port/ },
);
});
it("negative: curl to non-protected port → ALLOW", async () => {
const dir = newDir();
writeConfig(dir, EXAMPLE_CONFIG);
const hooks = await Guardrails({ client: realClient(), directory: dir });
await hookDrive(hooks, "bash", SESSION, {
command: "curl -s http://127.0.0.1:9999/health",
});
});
});
// ===========================================================================
// Tick gate — real scripts/verify_commit.py (only test that may use it)
// ===========================================================================
describe("plan_tick_gate with real verify_commit.py", () => {
const SESSION = "ses_ptg";
/**
* Set up a minimal git repo in wtDir with a commit and a plan file.
*/
async function setupTickRepo(wtDir, planPath) {
await execFileAsync("git", ["-C", wtDir, "init"]);
await execFileAsync("git", ["-C", wtDir, "config", "user.email", "test@test.com"]);
await execFileAsync("git", ["-C", wtDir, "config", "user.name", "Test"]);
writeFileSync(join(wtDir, "README.md"), "# Test repo");
await execFileAsync("git", ["-C", wtDir, "add", "README.md"]);
await execFileAsync("git", ["-C", wtDir, "commit", "-m", "init"]);
mkdirSync(join(wtDir, ".omo"), { recursive: true });
writeFileSync(planPath, "- [ ] 1. do it");
// Commit plan so HEAD has a valid commit tree with it
await execFileAsync("git", ["-C", wtDir, "add", ".omo/PLAN.md"]);
await execFileAsync("git", ["-C", wtDir, "commit", "-m", "add plan"]);
}
it("allows tick when verify_commit passes (clean tree)", async () => {
const dir = newDir();
const wtDir = join(dir, "wt");
mkdirSync(wtDir, { recursive: true });
// Copy verify_commit.py so the default tick gate command resolves
const scriptsDest = join(dir, "scripts");
mkdirSync(scriptsDest, { recursive: true });
copyFileSync(
join(SCRIPT_DIR, "verify_commit.py"),
join(scriptsDest, "verify_commit.py"),
);
const planPath = join(wtDir, ".omo", "PLAN.md");
await setupTickRepo(wtDir, planPath);
// Temporarily override tick_gate to point at our copied script
writeConfig(dir, {
...EXAMPLE_CONFIG,
tick_gate: {
cmd: [
PYTHON3,
"{directory}/scripts/verify_commit.py",
"--repo",
"{worktree}",
"--require-clean",
"HEAD",
],
timeout_s: 30,
},
});
writeBoulder(dir, {
status: "active",
worktree_path: wtDir,
session_ids: [`opencode:${SESSION}`],
active_plan: planPath,
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
// Tick the todo: - [ ] 1. do it → - [x] 1. do it
await hookDrive(hooks, "write", SESSION, {
filePath: planPath,
oldString: "- [ ] 1. do it",
newString: "- [x] 1. do it",
});
// No throw → ALLOW
});
it("blocks tick when verify_commit fails (dirty tree)", async () => {
const dir = newDir();
const wtDir = join(dir, "wt");
mkdirSync(wtDir, { recursive: true });
const scriptsDest = join(dir, "scripts");
mkdirSync(scriptsDest, { recursive: true });
copyFileSync(
join(SCRIPT_DIR, "verify_commit.py"),
join(scriptsDest, "verify_commit.py"),
);
const planPath = join(wtDir, ".omo", "PLAN.md");
await setupTickRepo(wtDir, planPath);
// Make the tree dirty (modify a tracked file without committing)
writeFileSync(join(wtDir, "README.md"), "# Dirty");
writeConfig(dir, {
...EXAMPLE_CONFIG,
tick_gate: {
cmd: [
PYTHON3,
"{directory}/scripts/verify_commit.py",
"--repo",
"{worktree}",
"--require-clean",
"HEAD",
],
timeout_s: 30,
},
});
writeBoulder(dir, {
status: "active",
worktree_path: wtDir,
session_ids: [`opencode:${SESSION}`],
active_plan: planPath,
});
const hooks = await Guardrails({
client: realClient({ parentID: null }),
directory: dir,
});
await assert.rejects(
hookDrive(hooks, "write", SESSION, {
filePath: planPath,
oldString: "- [ ] 1. do it",
newString: "- [x] 1. do it",
}),
{ message: /plan_tick_gate/ },
);
});
});