@wardrail/plugin 0.1.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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "wardrail",
3
+ "displayName": "Wardrail",
4
+ "version": "0.1.0",
5
+ "description": "Keep your coding agent on the rails: consult your project's Wardrail contract while it writes code, plus a checkpoint->clear->resume workflow with checkpoints that are machine-verified, not self-reported.",
6
+ "author": { "name": "Ghostables Ltd", "url": "https://wardrail.io" },
7
+ "homepage": "https://wardrail.io",
8
+ "license": "UNLICENSED",
9
+ "keywords": ["wardrail", "guardrails", "mcp", "checkpoint", "context", "ai"],
10
+ "userConfig": {
11
+ "WARDRAIL_URL": {
12
+ "type": "string",
13
+ "title": "Wardrail URL",
14
+ "description": "Your Wardrail server. Leave the default unless self-hosting.",
15
+ "default": "https://wardrail.io",
16
+ "required": false
17
+ },
18
+ "WARDRAIL_INGEST_TOKEN": {
19
+ "type": "string",
20
+ "title": "Wardrail ingest token",
21
+ "description": "Project-scoped token from Wardrail -> Trust -> Attest from CI. Scopes reads to exactly one project; never grants account access.",
22
+ "sensitive": true,
23
+ "required": true
24
+ },
25
+ "ANTHROPIC_API_KEY": {
26
+ "type": "string",
27
+ "title": "Anthropic API key (optional)",
28
+ "description": "Only needed for wardrail_review_diff. Stays on your machine and goes straight to Anthropic; Wardrail never sees it.",
29
+ "sensitive": true,
30
+ "required": false
31
+ }
32
+ }
33
+ }
package/.mcp.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "wardrail": {
4
+ "command": "npx",
5
+ "args": ["-y", "@wardrail/mcp"],
6
+ "env": {
7
+ "WARDRAIL_URL": "${user_config.WARDRAIL_URL}",
8
+ "WARDRAIL_INGEST_TOKEN": "${user_config.WARDRAIL_INGEST_TOKEN}",
9
+ "ANTHROPIC_API_KEY": "${user_config.ANTHROPIC_API_KEY}"
10
+ }
11
+ }
12
+ }
13
+ }
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @wardrail/plugin
2
+
3
+ The **Wardrail Claude Code plugin**. One install gives your coding agent two things:
4
+
5
+ 1. **The Wardrail contract, while it codes** — the four [`@wardrail/mcp`](https://www.npmjs.com/package/@wardrail/mcp)
6
+ tools (`get_contract`, `check_path`, `get_findings`, `review_diff`), so the agent stays
7
+ on the rails *before* a violation lands.
8
+ 2. **A context-saving workflow** — `/task` and `/checkpoint` slash commands, a SessionStart
9
+ resume listing, and a machine **verifier** so a checkpoint *can't lie*: it reconciles a
10
+ task file's claims against `git`, a real test run, and a diff scan before it's allowed to
11
+ be marked done.
12
+
13
+ This is the same anti-drift, verify-don't-trust thesis Wardrail applies to your *code*,
14
+ turned on the agent's own working memory.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ # add the marketplace (served by Wardrail), then install the plugin
20
+ claude plugin marketplace add https://wardrail.io/marketplace.json
21
+ claude plugin install wardrail@wardrail
22
+ ```
23
+
24
+ At enable time you'll be asked for:
25
+
26
+ | Value | Required | What it is |
27
+ |---|---|---|
28
+ | `WARDRAIL_URL` | no | Your Wardrail server. Defaults to `https://wardrail.io`. |
29
+ | `WARDRAIL_INGEST_TOKEN` | **yes** | Project-scoped token from Wardrail → **Trust → Attest from CI**. Scopes reads to one project; never grants account access. |
30
+ | `ANTHROPIC_API_KEY` | no | Only for `wardrail_review_diff`. Stays on your machine; Wardrail never sees it. |
31
+
32
+ These feed the bundled MCP server's environment via `${user_config.*}`. The `@wardrail/mcp`
33
+ server is fetched on demand by `npx`.
34
+
35
+ ## What's in the box
36
+
37
+ ```
38
+ .claude-plugin/plugin.json manifest + userConfig prompts
39
+ skills/task/SKILL.md /task new|resume <slug>
40
+ skills/checkpoint/SKILL.md /checkpoint (runs the verifier)
41
+ hooks/hooks.json SessionStart -> resume listing
42
+ hooks/tasks-session-start.mjs
43
+ hooks/verify-checkpoint.mjs the machine verifier
44
+ .mcp.json the Wardrail MCP server (npx -y @wardrail/mcp)
45
+ ```
46
+
47
+ ## The workflow
48
+
49
+ `/checkpoint` → `/clear` → `/task resume <slug>`. Task files live per-project in
50
+ `./tasks/<slug>.md` and carry a small, honest snapshot so a fresh session rehydrates from
51
+ ~3k tokens instead of a 150k transcript. `/checkpoint` stamps a tamper-evident
52
+ `## Verification` block; `status: done` over a failed check is downgraded to FAIL.
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node",
9
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/tasks-session-start.mjs"]
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook for the tasks plugin.
3
+ // Prints active task files in the current project's ./tasks/ so resuming is one step.
4
+ // Silent (no output) when there is no ./tasks/ dir or no active task — costs nothing
5
+ // in projects that don't use the workflow.
6
+
7
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ function readStdin() {
11
+ try {
12
+ return readFileSync(0, "utf8");
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ let cwd = process.cwd();
19
+ try {
20
+ const input = JSON.parse(readStdin() || "{}");
21
+ if (input.cwd) cwd = input.cwd;
22
+ } catch {
23
+ // no/!json stdin — fall back to process.cwd()
24
+ }
25
+
26
+ const tasksDir = join(cwd, "tasks");
27
+ if (!existsSync(tasksDir)) process.exit(0);
28
+
29
+ const active = [];
30
+ for (const name of readdirSync(tasksDir)) {
31
+ if (!name.endsWith(".md")) continue;
32
+ let text;
33
+ try {
34
+ text = readFileSync(join(tasksDir, name), "utf8");
35
+ } catch {
36
+ continue;
37
+ }
38
+ const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
39
+ if (!fm || !/^status:\s*active\s*$/m.test(fm[1])) continue;
40
+
41
+ const slug = name.replace(/\.md$/, "");
42
+ const next = text.match(/##\s*Next step\s*\r?\n+([^\r\n]+)/);
43
+ // Last machine verdict from the checkpoint verifier, if any — surfaced so a resume
44
+ // sees up front whether the prior checkpoint left open issues (WARN/FAIL).
45
+ const verdict = text.match(/^-\s*Verdict:\s*(\S+)\s*(PASS|WARN|FAIL)/m);
46
+ active.push({ slug, next: next ? next[1].trim() : "", verdict: verdict ? `${verdict[1]} ${verdict[2]}` : "" });
47
+ }
48
+
49
+ if (active.length === 0) process.exit(0);
50
+
51
+ const lines = ["Active task file(s) in ./tasks/ — resume cheaply with `/task resume <slug>`:"];
52
+ for (const t of active) {
53
+ let line = `- ${t.slug}`;
54
+ if (t.verdict) line += ` [last checkpoint: ${t.verdict}]`;
55
+ if (t.next) line += ` — next: ${t.next}`;
56
+ lines.push(line);
57
+ }
58
+ process.stdout.write(lines.join("\n") + "\n");
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ // verify-checkpoint.mjs — machine-produced verification for the tasks plugin.
3
+ //
4
+ // Reconciles a task file's CLAIMS against ground truth, then stamps a tamper-evident
5
+ // `## Verification` block into the file. The model does not narrate this — the script
6
+ // observes git, runs the tests, and scans the diff itself, so a checkpoint cannot claim
7
+ // work that didn't happen. A `status: done` task with any failed check is downgraded to
8
+ // FAIL.
9
+ //
10
+ // Usage: node verify-checkpoint.mjs [slug]
11
+ // cwd must be the project root. With no slug, auto-detects the single active task.
12
+ //
13
+ // v1 checks: (1) git Files-touched reconciliation, (2) real test run
14
+ // (tasks/.verify.json override, else package.json `test`), (3) honesty scan of the diff
15
+ // for TODO/FIXME/placeholder. Each check degrades gracefully and never reports a pass
16
+ // it didn't observe.
17
+
18
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
19
+ import { execSync } from "node:child_process";
20
+ import { join, isAbsolute } from "node:path";
21
+ import { homedir } from "node:os";
22
+
23
+ const cwd = process.cwd();
24
+ const tasksDir = join(cwd, "tasks");
25
+
26
+ function fail(msg) {
27
+ process.stderr.write(msg + "\n");
28
+ process.exit(1);
29
+ }
30
+
31
+ // ---- locate the task file -------------------------------------------------
32
+ let slug = process.argv[2];
33
+ if (!slug) {
34
+ if (!existsSync(tasksDir)) fail("No ./tasks/ directory and no slug given.");
35
+ const active = readdirSync(tasksDir).filter((n) => {
36
+ if (!n.endsWith(".md")) return false;
37
+ const fm = readFileSync(join(tasksDir, n), "utf8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
38
+ return fm && /^status:\s*active\s*$/m.test(fm[1]);
39
+ });
40
+ if (active.length === 1) slug = active[0].replace(/\.md$/, "");
41
+ else fail(`Need a slug — ${active.length} active tasks found.`);
42
+ }
43
+ const taskPath = join(tasksDir, `${slug}.md`);
44
+ if (!existsSync(taskPath)) fail(`No task file at tasks/${slug}.md`);
45
+ let doc = readFileSync(taskPath, "utf8");
46
+
47
+ // ---- parse claims ---------------------------------------------------------
48
+ const status = (doc.match(/^status:\s*(\w+)/m) || [, "active"])[1];
49
+
50
+ const ftSection = doc.match(/##\s*Files touched\s*\r?\n([\s\S]*?)(?:\r?\n##\s|\s*$)/);
51
+ const claimedFiles = [];
52
+ if (ftSection) {
53
+ for (const line of ftSection[1].split(/\r?\n/)) {
54
+ const m = line.match(/^\s*-\s+(.+?)(?:\s+[—-]\s.*)?$/);
55
+ if (!m) continue;
56
+ const p = m[1].trim();
57
+ if (!p || p.startsWith("(")) continue; // skip parenthetical notes / placeholders — real paths never start with "("
58
+ claimedFiles.push(p.replace(/\\/g, "/"));
59
+ }
60
+ }
61
+
62
+ // ---- helpers --------------------------------------------------------------
63
+ const git = (cmd) => execSync(`git ${cmd}`, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
64
+ const isRepo = (() => {
65
+ try {
66
+ return git("rev-parse --is-inside-work-tree").trim() === "true";
67
+ } catch {
68
+ return false;
69
+ }
70
+ })();
71
+
72
+ // Resolve a claimed path to an absolute, forward-slashed path (expanding a leading ~).
73
+ const resolveClaim = (claim) => {
74
+ let p = claim;
75
+ if (p === "~" || p.startsWith("~/")) p = join(homedir(), p.slice(1));
76
+ else if (!isAbsolute(p)) p = join(cwd, p);
77
+ return p.replace(/\\/g, "/");
78
+ };
79
+ // Repo root (toplevel) — used to tell in-repo claims from ones the verifier can't see.
80
+ const repoRoot = (() => {
81
+ if (!isRepo) return cwd.replace(/\\/g, "/");
82
+ try { return git("rev-parse --show-toplevel").trim().replace(/\\/g, "/"); }
83
+ catch { return cwd.replace(/\\/g, "/"); }
84
+ })();
85
+ const insideRepo = (abs) => (abs + "/").startsWith(repoRoot.replace(/\/+$/, "") + "/");
86
+
87
+ // ---- check 1: files-touched reconciliation --------------------------------
88
+ let filesLine, filesOk = true;
89
+ if (!isRepo) {
90
+ filesLine = "⏭️ not a git repo — file claims unverified";
91
+ } else {
92
+ const changed = [];
93
+ for (const raw of git("status --porcelain").split(/\r?\n/)) {
94
+ if (!raw.trim()) continue;
95
+ let p = raw.slice(3).trim().replace(/^"|"$/g, "");
96
+ if (p.includes(" -> ")) p = p.split(" -> ")[1];
97
+ p = p.replace(/\\/g, "/");
98
+ if (p.startsWith("tasks/")) continue; // ignore the task file & its sidecars
99
+ changed.push(p);
100
+ }
101
+ const matches = (claim, real) => real === claim || real.endsWith("/" + claim) || claim.endsWith("/" + real) || real.endsWith(claim);
102
+ const unmetAll = claimedFiles.filter((c) => !changed.some((r) => matches(c, r)));
103
+ // A claim git can't reconcile is only a contradiction if it's missing or in-repo-but-unchanged.
104
+ // One that exists on disk but outside this repo is unverifiable here, not a lie — don't fail on it.
105
+ const unmet = [], unverifiable = [];
106
+ for (const c of unmetAll) {
107
+ const abs = resolveClaim(c);
108
+ if (existsSync(abs) && !insideRepo(abs)) unverifiable.push(c);
109
+ else unmet.push(c);
110
+ }
111
+ const unclaimed = changed.filter((r) => !claimedFiles.some((c) => matches(c, r)));
112
+ const parts = [];
113
+ if (claimedFiles.length === 0) parts.push("no files claimed");
114
+ if (unmet.length) { parts.push("❌ claimed but not changed: " + unmet.join(", ")); filesOk = false; }
115
+ if (unverifiable.length) parts.push("⏭️ claimed but outside this repo — unverifiable here: " + unverifiable.join(", "));
116
+ if (unclaimed.length) parts.push("⚠️ changed but not claimed: " + unclaimed.join(", "));
117
+ if (filesOk && !unverifiable.length && !unclaimed.length && claimedFiles.length)
118
+ parts.push(`✅ all ${claimedFiles.length} claimed path(s) present in the working tree`);
119
+ filesLine = parts.join("; ");
120
+ }
121
+
122
+ // ---- check 2: tests -------------------------------------------------------
123
+ let testCmd = null;
124
+ const cfgPath = join(tasksDir, ".verify.json");
125
+ if (existsSync(cfgPath)) {
126
+ try {
127
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
128
+ if (cfg.test === false) testCmd = false;
129
+ else if (typeof cfg.test === "string") testCmd = cfg.test;
130
+ } catch { /* ignore malformed config */ }
131
+ }
132
+ if (testCmd === null && existsSync(join(cwd, "package.json"))) {
133
+ try {
134
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
135
+ if (pkg.scripts && pkg.scripts.test) testCmd = "npm test";
136
+ } catch { /* ignore */ }
137
+ }
138
+
139
+ let testsLine, testsOk = true;
140
+ if (testCmd === false) {
141
+ testsLine = "⏭️ tests skipped (disabled in tasks/.verify.json)";
142
+ } else if (!testCmd) {
143
+ testsLine = "⏭️ no test command found — test status unverified";
144
+ } else {
145
+ try {
146
+ execSync(testCmd, { cwd, encoding: "utf8", timeout: 120000, stdio: ["ignore", "pipe", "pipe"] });
147
+ testsLine = `✅ \`${testCmd}\` passed`;
148
+ } catch (e) {
149
+ testsOk = false;
150
+ const tail = String(e.stdout || e.stderr || "").trim().split(/\r?\n/).filter(Boolean).pop() || "";
151
+ testsLine = e.killed
152
+ ? `❌ \`${testCmd}\` timed out after 120s`
153
+ : `❌ \`${testCmd}\` failed (exit ${e.status})${tail ? ": " + tail.slice(0, 160) : ""}`;
154
+ }
155
+ }
156
+
157
+ // ---- check 3: honesty scan of the diff ------------------------------------
158
+ let honestyLine, honestyOk = true;
159
+ if (!isRepo) {
160
+ honestyLine = "⏭️ not a git repo — diff not scanned";
161
+ } else {
162
+ let diff = "";
163
+ try { diff = git("diff HEAD"); } catch { try { diff = git("diff"); } catch { /* no diff */ } }
164
+ const pat = /\b(TODO|FIXME|XXX|HACK)\b|not implemented|placeholder/i;
165
+ const hits = [];
166
+ let file = "";
167
+ for (const line of diff.split(/\r?\n/)) {
168
+ if (line.startsWith("+++ b/")) { file = line.slice(6); continue; }
169
+ if (line.startsWith("+") && !line.startsWith("+++") && pat.test(line)) {
170
+ hits.push(`${file}: ${line.slice(1).trim().slice(0, 80)}`);
171
+ }
172
+ }
173
+ if (hits.length) {
174
+ honestyOk = false;
175
+ honestyLine = `❌ ${hits.length} unfinished marker(s) in diff — ` + hits.slice(0, 3).join(" | ") + (hits.length > 3 ? " …" : "");
176
+ } else {
177
+ honestyLine = "✅ no TODO/FIXME/placeholder in the diff";
178
+ }
179
+ }
180
+
181
+ // ---- verdict --------------------------------------------------------------
182
+ const allOk = filesOk && testsOk && honestyOk;
183
+ let verdict;
184
+ if (allOk) verdict = "✅ PASS";
185
+ else if (status === "done") verdict = "❌ FAIL — marked `done` but checks above did not pass";
186
+ else verdict = "⚠️ WARN — open issues above (acceptable while `status: active`)";
187
+
188
+ const stamp = new Date().toISOString();
189
+ const block =
190
+ `## Verification (machine-checked ${stamp})\n` +
191
+ `- Files touched: ${filesLine}\n` +
192
+ `- Tests: ${testsLine}\n` +
193
+ `- Honesty scan: ${honestyLine}\n` +
194
+ `- Verdict: ${verdict}\n`;
195
+
196
+ // replace any existing Verification section, else append
197
+ doc = doc.replace(/\n##\s*Verification[\s\S]*?(?=\n##\s|\s*$)/, "").replace(/\s*$/, "\n");
198
+ writeFileSync(taskPath, doc + "\n" + block, "utf8");
199
+
200
+ process.stdout.write(`Verification stamped into tasks/${slug}.md\n\n${block}`);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@wardrail/plugin",
3
+ "version": "0.1.0",
4
+ "description": "Wardrail Claude Code plugin — consult your project's contract while coding (MCP) plus a checkpoint->clear->resume workflow with machine-verified checkpoints.",
5
+ "type": "module",
6
+ "files": [
7
+ ".claude-plugin",
8
+ "skills",
9
+ "hooks",
10
+ ".mcp.json",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "wardrail",
18
+ "claude-code",
19
+ "claude-code-plugin",
20
+ "mcp",
21
+ "guardrails",
22
+ "checkpoint"
23
+ ],
24
+ "license": "UNLICENSED",
25
+ "private": false
26
+ }
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: checkpoint
3
+ description: "Write the current working state into the active task file right before a /clear, so a fresh session can resume cheaply. This is the critical step in the checkpoint→clear→resume workflow — run it whenever a long session is getting expensive and the work isn't finished. Pairs with the `task` skill."
4
+ ---
5
+
6
+ # Checkpoint
7
+
8
+ Capture everything the next session needs into `./tasks/<slug>.md`, then hand off. The
9
+ flow this completes is: **`/checkpoint` → `/clear` → `/task resume <slug>`**. After the
10
+ checkpoint, the transcript is disposable; the task file carries the state.
11
+
12
+ ## Steps
13
+
14
+ 1. **Find the active task.** Look in `./tasks/` for files with `status: active`.
15
+ - Exactly one → use it.
16
+ - Several → ask which one (or checkpoint the one this session was actually working
17
+ on, if that's unambiguous).
18
+ - None → offer to run `/task new <slug>` first.
19
+
20
+ 2. **Update the file to reflect reality now.** Edit in place:
21
+ - **Objective** — sharpen only if it genuinely changed; don't churn it.
22
+ - **Decisions** — append choices settled this session, each with its one-line reason,
23
+ so the next session doesn't relitigate them.
24
+ - **Files touched** — add/adjust the paths changed this session with a short note.
25
+ - **Next step** — the single most important thing: the exact next concrete action,
26
+ specific enough to start cold. One action, not a list.
27
+
28
+ 3. **Be honest.** Write only what is actually true right now. Do not record planned
29
+ work as done, do not claim a passing test you didn't run. A false checkpoint is
30
+ worse than none — it makes the next session build on a lie.
31
+
32
+ 4. **Redirect heavy outputs.** If this session generated large tool outputs (build logs,
33
+ test dumps) worth keeping, note their file path in the task file and `tail` them —
34
+ don't paste them in. Big outputs are a top context cost.
35
+
36
+ 5. **Verify against ground truth — do not self-attest.** From the project root, run:
37
+ `node "${CLAUDE_PLUGIN_ROOT}/hooks/verify-checkpoint.mjs" <slug>`. It reconciles the
38
+ file's claims against `git`, a real test run, and a diff scan, then writes a
39
+ `## Verification` block into the task file itself. Then:
40
+ - Read the verdict. If it is **not PASS**, the checkpoint is not done: either fix the
41
+ underlying issue and re-run, or keep `status: active` and point Next step at exactly
42
+ what the verdict flagged. **Never set `status: done` over a FAIL.**
43
+ - Do not hand-edit the `## Verification` block — it is the machine's record, not
44
+ yours. Surface the verdict to the user in your handoff.
45
+ - If verification can't apply (not a git repo, files live outside this repo, no
46
+ tests), say so plainly rather than implying a pass.
47
+
48
+ 6. **Drop the clear-ready sentinel.** Write the active task's slug into
49
+ `./tasks/.clear-ready` (overwrite; create if missing). This is a transient marker the
50
+ `Stop` hook consumes once to back up the clear nudge — it deletes itself, so it
51
+ normally won't linger in the working tree.
52
+
53
+ 7. **Hand off.** Confirm the path written, the verification verdict, and the Next step,
54
+ then tell the user it's safe to `/clear` and later run `/task resume <slug>`.
55
+
56
+ ## Why manual
57
+
58
+ Auto-clearing is unsafe: judging whether work is at a clean stopping point needs
59
+ judgment. Two keystrokes (`/checkpoint`, then `/clear`) is the target — deliberate, not
60
+ automatic. This cannot beat `/compact` *within* a session; it makes the boundary
61
+ between sessions cheap. See [task] for the file format and resume step.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: task
3
+ description: "Manage long-session task files for the checkpoint→clear→resume workflow. Invoke as `/task new <slug>` to scaffold a task file, or `/task resume <slug>` to rehydrate a fresh session from one. Task files live in ./tasks/ in the current project. The point is to make /clear cheap: resume reloads a ~3k-token brief instead of carrying a 150k transcript."
4
+ ---
5
+
6
+ # Task files
7
+
8
+ A task file at `./tasks/<slug>.md` (relative to the current project) is a small,
9
+ honest snapshot of in-flight work. It exists so a long session can be `/clear`-ed and
10
+ a fresh one rehydrated from ~3k tokens instead of the whole transcript. The savings
11
+ happen at the boundary between sessions, not within one.
12
+
13
+ The model is stateless and the harness re-sends the full transcript every turn — you
14
+ cannot make the AI "read less from the top." The only lever is a smaller window. Task
15
+ files are that lever: cheap rehydration after `/clear`.
16
+
17
+ ## File format
18
+
19
+ ```markdown
20
+ ---
21
+ status: active # active | done
22
+ related: [] # other task slugs this depends on, e.g. [auth-rework]
23
+ ---
24
+
25
+ # <One-line objective title>
26
+
27
+ ## Objective
28
+ What "done" means, stated so it can be verified (a test, a command, an observable
29
+ behaviour). Not "make it work."
30
+
31
+ ## Decisions
32
+ - Settled choices not to relitigate, each with the one-line reason.
33
+
34
+ ## Files touched
35
+ - path/to/file — what changed / what it's for
36
+
37
+ ## Next step
38
+ The single next concrete action. One thing, not a list.
39
+ ```
40
+
41
+ ## Dispatch on the argument
42
+
43
+ ### `/task new <slug>`
44
+
45
+ 1. Ensure `./tasks/` exists (create it if not).
46
+ 2. If `./tasks/<slug>.md` already exists, stop and say so — do not overwrite.
47
+ 3. Write the scaffold above. Fill **Objective** from the current conversation if there
48
+ is enough context to state it verifiably; otherwise leave a one-line prompt for the
49
+ user to complete. Leave Decisions / Files touched / Next step minimal but real —
50
+ never invent progress that hasn't happened.
51
+ 4. Tell the user the path and that `/checkpoint` will keep it current before a `/clear`.
52
+
53
+ ### `/task resume <slug>`
54
+
55
+ 1. Read `./tasks/<slug>.md`. If `status: done`, say so and ask whether to reopen.
56
+ 2. Read each task listed in `related:` (those files only).
57
+ 3. Read **only** the files named under "Files touched" that you actually need for the
58
+ Next step — not the whole repo. CLAUDE.md and memory are already auto-loaded; do not
59
+ re-read them.
60
+ 4. Give the user a 3–5 line orientation: the Objective, the key Decisions, and the
61
+ Next step you're about to take. Then proceed with that Next step.
62
+
63
+ Load nothing beyond the above. Pulling in extra context defeats the purpose.
64
+
65
+ ## Honesty
66
+
67
+ A stale or optimistic task file is worse than none — it misleads the next session into
68
+ building on work that didn't happen. Everything written must be true at write time.
69
+ See [checkpoint] for the write-before-clear step.