Your coding agent
Claude Code, Codex, Copilot, OpenCode — the skill ships inside the binary. It drafts the specs; the kernel enforces the gates.
walden skill install <agent>
skill drafts · kernel enforces
Spec-driven delivery kernel · Open source
Walden turns ideas into reviewed feature specifications and executes approved work through a deterministic, gated workflow. A Go CLI enforces the rules. An optional AI skill drafts the specs.
curl -fsSL https://raw.githubusercontent.com/andrearaponi/walden/main/install.sh | sh
Plan modes draft. Checklists suggest. Analysis advises. Walden enforces.
Blocking gates with exit codes — validation, proofs, freshness — independent of any model’s judgment.
Walden draws a hard line between what a machine should decide and what a human should author. The non-deterministic work is drafted. The rules are enforced.
The skill drafts
non-deterministic · proposes, never approves
The CLI enforces
deterministic · the same answer every time
Every feature moves through four phases. Each one has an approval gate that must pass before the next begins. Phases cannot be skipped.
What the feature must do — EARS acceptance criteria with stable IDs like R1.AC1.
How it gets built — architecture, alternatives considered, tradeoffs, a coverage matrix.
validate · review · approveWhat code to write, in order. Every leaf task traces to an AC and carries a proof.
validate · review · approveBuild it. The CLI runs each verification proof — a task closes only when its proof passes.
verify · completeChange an already-approved document and everything downstream resets to draft. Execution stays blocked until the chain is reconciled. No code outruns its spec.
Every acceptance criterion takes exactly one of six EARS forms. The CLI validates
the grammar — a single SHALL, the right keywords in the right place.
WHEN a task’s verification proof passes, the system SHALL mark the task complete.
One prompt in. A reviewed spec tree out — every gate passed, every proof recorded, nothing typed but the intent.
you
We need a small, single-user todo app on the command line — add a task, list what’s still pending, mark one done, all kept in a plain-text file, POSIX shell only. Let’s use Walden.
The skill asks its questions, then drafts every artifact and stops at each gate for your approval. This is what it left behind — open a spec file ↓
Every spec above carries its own frontmatter — status: approved,
timestamps, the staleness chain. walden feature init,
walden validate, walden review approve,
walden task complete ran underneath to earn each of those
badges. You never typed them.
The feature is a toy; the ceremony isn’t — the same gates hold
for the stack you actually ship on.
Walden doesn’t replace your agent, your IDE, or your spec tool. It is the gate they call — anything that runs a command and reads an exit code gets hard gates.
Claude Code, Codex, Copilot, OpenCode — the skill ships inside the binary. It drafts the specs; the kernel enforces the gates.
walden skill install <agent>
skill drafts · kernel enforces
Gate every pull request that touches a spec. Validation runs headless and the exit code decides — no model in the loop.
walden validate <feature> --all --json
exit ≠ 0 → merge blocked
Kiro’s hooks honor exit codes. Run the gate as a pre‑tool‑use hook: when validation fails, the action is blocked.
walden validate <feature> --all
exit ≠ 0 → action blocked
Spec Kit workflows run shell steps. Put the gate between phases: the pipeline advances only when the kernel agrees.
walden validate <feature> --all
exit ≠ 0 → workflow halts
The same gate returns the same answer in every host. Enforcement stops being a feature of your editor and becomes a property of the spec.
One command. The latest release binary lands in ~/.local/bin/walden,
checksum-verified — then it installs its own AI skill for Claude Code, Codex,
Copilot, or OpenCode. No Go toolchain, no clone.
curl -fsSL https://raw.githubusercontent.com/andrearaponi/walden/main/install.sh | sh
Prefer the toolchain? go install github.com/andrearaponi/walden/cmd/walden@latest
· then walden skill install <agent>. Building from a clone?
./setup.sh still works.
I went to the woods because I wished to live deliberately, to front only the essential facts of life.
— Henry David Thoreau, Walden, or Life in the Woods
Do fewer things, but do them with full attention. Software rarely does. Walden is an attempt to apply that discipline — to require intention before code, and proof before completion.
todo-cli / .walden / specs / todo-cli
A single-user command-line todo manager for POSIX systems. One person can add tasks, list the tasks still pending, and mark a task as done. All state lives in a single plain-text file so it works with zero dependencies on any POSIX shell and remains readable and hand-editable.
As a user, I want to add a task from the command line, so that I can capture something I need to do.
R1.AC1 WHEN the user runs
todo add <text> with non-empty text, the system SHALL
append a new task with status pending to the storage file.
R1.AC2 WHEN the user runs
todo add <text>, the system SHALL assign the new task
an identifier that is unique among all tasks in the storage file.
R1.AC3 WHEN a task is successfully added, the system SHALL print a confirmation that includes the new task's identifier.
R1.AC4 IF the user runs
todo add with missing or empty text, THEN the system
SHALL print an error message and leave the storage file unchanged.
As a user, I want to list the tasks that are still pending, so that I can see what is left to do.
R2.AC1 WHEN the user runs
todo list, the system SHALL display every task whose
status is pending.
R2.AC2 WHEN the user runs
todo list, the system SHALL omit from the output every
task whose status is done.
R2.AC3 WHEN the user runs
todo list for a pending task, the system SHALL show that
task's identifier and its text.
R2.AC4 WHILE the storage file
contains no pending tasks, WHEN the user runs todo list,
the system SHALL print a message indicating there are no pending tasks.
As a user, I want to mark a task as done by its identifier, so that it stops appearing in my pending list.
R3.AC1 WHEN the user runs
todo done <id> for a pending task, the system SHALL
set that task's status to done in the storage file.
R3.AC2 WHEN a task is successfully marked done, the system SHALL print a confirmation that identifies the task.
R3.AC3 IF the user runs
todo done <id> with an id that matches no task, THEN
the system SHALL print an error message and leave the storage file unchanged.
R3.AC4 IF the user runs
todo done without an id, THEN the system SHALL print
an error message and leave the storage file unchanged.
R3.AC5 IF the user runs
todo done <id> for a task already marked done, THEN
the system SHALL print a message that the task is already done and leave
the storage file unchanged.
As a user, I want my tasks stored in one plain-text file, so that they persist between runs and I can inspect or edit them by hand.
R4.AC1 The system SHALL persist all
tasks in a single plain-text file located at the path given by
TODO_FILE, defaulting to $HOME/.todo.
R4.AC2 WHEN the user runs a task-modifying command and the storage file does not yet exist, the system SHALL create the storage file.
R4.AC3 IF a write to the storage file fails or is interrupted, THEN the system SHALL preserve the previous contents of the storage file.
R4.AC4 IF the storage file exists but cannot be read or written, THEN the system SHALL print an error message and exit without further changes.
As a user, I want clear usage guidance, so that I know which subcommands exist and how to invoke them.
R5.AC1 WHEN the user runs the program with no arguments, the system SHALL print usage information that lists the available subcommands.
R5.AC2 IF the user runs the program with an unrecognized subcommand, THEN the system SHALL print an error message and the usage information.
NFR1 The system SHALL run under any
POSIX-compliant /bin/sh without relying on Bash-specific features.
NFR2 The system SHALL depend only on the POSIX shell and standard POSIX utilities, with no third-party runtime dependencies.
NFR3 The system SHALL tolerate interruption of a task-modifying command without corrupting or partially writing the storage file.
NFR4 The system SHALL produce error messages that name the problem and show the correct usage.
C1 Implementation is POSIX shell only; no Bash-specific syntax.
C2 Storage is a single plain-text file; no database or structured store.
C3 Single-user and local; no concurrent access or network is assumed or supported.
A single self-contained POSIX shell script named
todo implements the whole tool. It dispatches on the first argument
(add, list, done, or help) and reads/writes
one plain-text file. Each task is one line with three tab-separated fields: a
stable integer id, a status word, and the free-form text. Modifications are
written to a temporary file in the same directory and then renamed over the
storage file, so a failed or interrupted write never corrupts existing data.
Single process, no daemon, no network. One script, invoked per command:
todo <subcommand> [args]
|
v
dispatch (case on $1)
| | |
add list done
\ | /
storage layer ---> $TODO_FILE (plain text, tab-delimited records)
(ensure / read / atomic write)
The storage layer is the only code that touches the file. Command functions build the new set of lines and hand them to an atomic-write helper; they never edit the file in place.
Summary: One sh script. Records are
id<TAB>status<TAB>text. Reads use read -r
with IFS set to tab; writes go to a mktemp file in the
storage directory and are mv'd over the target. The next id is
max(existing id) + 1.
Why chosen: Minimum moving parts that still satisfy every
requirement. Tab-delimited records parse safely with pure POSIX read
(text is the last field, so spaces and even tabs inside it survive).
Temp-plus-rename gives atomic writes with only standard utilities, satisfying
NFR2 and NFR3. Stable ids fall out naturally because ids are stored, not positional.
Summary: Keep tasks as JSON, or keep pending and done tasks in two separate files.
Why rejected: JSON needs jq or hand-rolled
parsing, breaking the zero-dependency and plain-text constraints (C2, NFR2) and
hurting hand-editability (R4). Splitting into two files multiplies the write
paths and the ways they can drift out of sync, for no user-visible benefit
against a single-user tool.
Summary: Do not store ids; number pending tasks 1..N at list time.
Why rejected: The user explicitly chose stable ids; positional numbering makes a task's id change whenever another task is completed, which would break R1.AC2's "unique, stable" intent.
$TODO_FILE; no in-place edits.main / case "$1")Purpose: Route the subcommand; print usage when none or unknown.
Inputs/Outputs: Reads $1..$n; exits non-zero with
usage on unknown/no command, zero on success.
Requirements: R5.AC1, R5.AC2
cmd_addPurpose: Append a new pending task with a fresh id.
Inputs/Outputs: Task text from "$*" after the
subcommand; prints confirmation with the new id; error on empty text.
Requirements: R1.AC1, R1.AC2,
R1.AC3, R1.AC4
cmd_listPurpose: Print pending tasks, or an empty-state message.
Inputs/Outputs: No args; prints id text per
pending task to stdout.
Requirements: R2.AC1, R2.AC2,
R2.AC3, R2.AC4
cmd_donePurpose: Flip a task's status to done by id.
Inputs/Outputs: One id arg; prints confirmation; distinct errors for missing id, unknown id, and already-done.
Requirements: R3.AC1, R3.AC2,
R3.AC3, R3.AC4, R3.AC5
ensure_file, read loop, atomic_write)Purpose: Own all file access. Create the file on demand; read records; replace the file atomically.
Inputs/Outputs: ensure_file creates
$TODO_FILE and parent dir if absent; atomic_write
takes new content on stdin, writes a sibling mktemp, then
mvs it over $TODO_FILE.
Requirements: R4.AC1, R4.AC2,
R4.AC3, R4.AC4, NFR1, NFR2,
NFR3
Storage file: ${TODO_FILE:-$HOME/.todo}. One record per line:
<id>\t<status>\t<text>
id: positive integer, unique, never reused. Next id = highest
existing id + 1 (0 when the file is empty).status: the literal word pending or done.text: everything after the second tab; may contain spaces. Read
with IFS=<tab> read -r id status text, so text
absorbs the remainder intact.1 pending buy milk 2 done call bank 3 pending write report
Input handling: task text is taken verbatim except that any embedded tab or newline in the provided text is replaced with a space before storage, to keep one record on one line and protect the field delimiter.
R1.AC4): print
error: task text is required plus usage to stderr, exit 1, no write.R3.AC4): print
error: task id is required, exit 1.R3.AC3): print
error: no task with id <id>, exit 1, no write.R3.AC5): print
task <id> is already done, exit 0, no write.R4.AC4): test readability/writability
before acting; on failure print error: cannot access <path>
to stderr, exit 1.R5): print usage, exit non-zero.set -eu; a trap removes the
temp file on any exit so a failed write leaves the original file untouched
(R4.AC3).Local single-user tool; no network, no privilege boundary. The
temp file is created with mktemp in the same directory as the storage
file (mode 600 by default) so task text is not exposed via a world-readable temp path.
mv; the original is
replaced only if the temp write fully succeeded. Tradeoff: needs free space for
a second copy of the file momentarily — acceptable for a personal todo list.todo invocations write concurrently. Mitigation: none
beyond atomic rename; last writer wins, but neither leaves a corrupt file.
Tradeoff: no locking, matching the single-user constraint (C3).A single POSIX test script tests/run.sh drives the
real todo script against an isolated TODO_FILE in a temp
directory (no writes to $HOME). It runs end-to-end scenarios and
asserts on output and on file contents. Tests run under dash
(/bin/dash) to enforce POSIX behavior (NFR1). No third-party test
framework is used (NFR2); assertions are plain sh with a small
pass/fail helper.
tests/run.sh; running the suite under dash
demonstrates R1–R5.dash tests/run.sh exits 0 with all scenarios
passing; the atomic-write scenario proves R4.AC3/NFR3 by forcing a write error
and re-reading the untouched file.| R1 | cmd_add |
| R2 | cmd_list |
| R3 | cmd_done |
| R4 | Storage layer |
| R5 | Dispatcher |
| R1.AC1 | cmd_add + atomic append |
| R1.AC2 | cmd_add next-id = max+1 |
| R1.AC3 | cmd_add confirmation output |
| R1.AC4 | cmd_add empty-text guard |
| R2.AC1 | cmd_list pending filter |
| R2.AC2 | cmd_list pending filter |
| R2.AC3 | cmd_list id + text formatting |
| R2.AC4 | cmd_list empty-state branch |
| R3.AC1 | cmd_done status flip + atomic write |
| R3.AC2 | cmd_done confirmation output |
| R3.AC3 | cmd_done unknown-id branch |
| R3.AC4 | cmd_done missing-id branch |
| R3.AC5 | cmd_done already-done branch |
| R4.AC1 | Storage layer $TODO_FILE path |
| R4.AC2 | ensure_file |
| R4.AC3 | atomic_write temp + rename, trap cleanup |
| R4.AC4 | Storage layer readability/writability check |
| R5.AC1 | Dispatcher usage on no args |
| R5.AC2 | Dispatcher usage on unknown subcommand |
| NFR1 | Runs under dash; test suite enforces POSIX |
| NFR2 | Only POSIX utilities; no framework/deps |
| NFR3 | atomic_write + trap; atomic-write test |
| NFR4 | Error messages name problem + usage |
✓1.1 — Create the todo
script at the repo root with a #!/bin/sh shebang,
set -eu, TODO_FILE resolution
(${TODO_FILE:-$HOME/.todo}), a usage() function
listing add/list/done, and a
case "$1" dispatcher that prints usage and exits non-zero on no
argument or an unknown subcommand. Make the file executable.
command: ["sh", "-c", "d=$(mktemp -d); dash ./todo >\"$d/o\" 2>&1 && exit 1; grep -qi usage \"$d/o\" || exit 1; dash ./todo nope >\"$d/e\" 2>&1 && exit 1; grep -qi usage \"$d/e\""]
add command✓2.1 — Implement the storage
layer (ensure_file to create $TODO_FILE and its
parent dir on demand, and atomic_write that reads new content on
stdin, writes a sibling mktemp file, then mvs it over
$TODO_FILE, with a trap cleaning the temp on exit)
and cmd_add: reject empty text with an error to stderr and no
write; otherwise compute the next id as highest-existing-id + 1, append an
id<TAB>pending<TAB>text record atomically, and print a
confirmation containing the new id. Strip embedded tabs/newlines from the text
before storing.
command: ["sh", "-c", "d=$(mktemp -d); export TODO_FILE=\"$d/t\"; dash ./todo add \"buy milk\" >\"$d/a1\" || exit 1; test -f \"$TODO_FILE\" || exit 1; grep -q \"buy milk\" \"$TODO_FILE\" || exit 1; grep -q 1 \"$d/a1\" || exit 1; dash ./todo add \"call bank\" >\"$d/a2\" || exit 1; grep -q 2 \"$d/a2\" || exit 1; b=$(cat \"$TODO_FILE\"); dash ./todo add \"\" >\"$d/a3\" 2>&1 && exit 1; [ \"$b\" = \"$(cat \"$TODO_FILE\")\" ]"]
list command✓3.1 — Implement
cmd_list: read records with
IFS=<tab> read -r id status text, print id and
text for every pending record, skip malformed lines,
and print a "no pending tasks" message when none are pending.
command: ["sh", "-c", "d=$(mktemp -d); export TODO_FILE=\"$d/t\"; dash ./todo list >\"$d/l0\" 2>&1 || exit 1; grep -qi \"no pending\" \"$d/l0\" || exit 1; dash ./todo add \"buy milk\" >/dev/null || exit 1; dash ./todo add \"write report\" >/dev/null || exit 1; dash ./todo list >\"$d/l1\" || exit 1; grep -q \"buy milk\" \"$d/l1\" || exit 1; grep -q \"write report\" \"$d/l1\" || exit 1; grep -q 1 \"$d/l1\""]
done command✓4.1 — Implement
cmd_done: require an id (error on missing), error when no record
matches the id, print "already done" and exit 0 (no write) when the matched
record is already done, otherwise flip its status to done via an
atomic write and print a confirmation naming the id. Verify a subsequently
listed task marked done no longer appears in list (R2.AC2).
command: ["sh", "-c", "d=$(mktemp -d); export TODO_FILE=\"$d/t\"; dash ./todo add \"buy milk\" >/dev/null || exit 1; dash ./todo add \"call bank\" >/dev/null || exit 1; dash ./todo done >\"$d/e1\" 2>&1 && exit 1; grep -qi id \"$d/e1\" || exit 1; dash ./todo done 999 >\"$d/e2\" 2>&1 && exit 1; grep -qi \"no task\" \"$d/e2\" || exit 1; dash ./todo done 1 >\"$d/d1\" || exit 1; grep -q 1 \"$d/d1\" || exit 1; dash ./todo list >\"$d/l\" || exit 1; grep -q \"buy milk\" \"$d/l\" && exit 1; grep -q \"call bank\" \"$d/l\" || exit 1; dash ./todo done 1 >\"$d/d2\" 2>&1 || exit 1; grep -qi \"already done\" \"$d/d2\""]
✓5.1 — Add a
readability/writability guard so that when $TODO_FILE exists but
cannot be read or written the tool prints
error: cannot access <path> to stderr and exits non-zero
without changes, and confirm the atomic write leaves the prior contents intact
when the write cannot complete (temp/rename failure).
command: ["sh", "-c", "d=$(mktemp -d); f=\"$d/t\"; printf '1\\tpending\\tx\\n' >\"$f\"; chmod 000 \"$f\"; TODO_FILE=\"$f\" dash ./todo list >\"$d/o\" 2>&1 && { chmod 644 \"$f\"; exit 1; }; chmod 644 \"$f\"; grep -qi \"cannot access\" \"$d/o\""]
command: ["sh", "-c", "d=$(mktemp -d); s=\"$d/s\"; mkdir \"$s\"; f=\"$s/t\"; printf '1\\tpending\\tbuy milk\\n' >\"$f\"; export TODO_FILE=\"$f\"; b=$(cat \"$f\"); chmod 500 \"$s\"; dash ./todo done 1 >\"$d/o\" 2>&1; rc=$?; chmod 700 \"$s\"; [ \"$b\" = \"$(cat \"$f\")\" ] || exit 1; [ \"$rc\" -ne 0 ]"]
✓6.1 — Create
tests/run.sh, a POSIX sh script with a small
pass/fail helper that consolidates the scenarios from tasks 1–5 (add/list/done
happy paths, empty text, missing/unknown/already-done ids, empty-state, done
hidden from list, file created on first use, write-failure preservation)
against an isolated TODO_FILE under a mktemp -d,
exiting non-zero on any failed assertion.
command: ["dash", "tests/run.sh"]