Clawsy Codebase Audit

August 11, 2026 · repo ~/code/agents and workspace ~/.clawsy

On 2026-08-11, a 15-agent automated audit of the Clawsy codebase found four competing representations of a task; the markdown files in the workspace tasks folder are canonical. Ten findings were confirmed at high severity, none refuted. Among them: the iOS app read a legacy status field instead of the current posture field, so finished tasks appeared open. It also silently dropped 17 of 39 tasks whose title lives in the body rather than a dedicated field. Global search plus the @-mention menu queried a SQLite table frozen since July 21, leaving three weeks of work invisible to both features. A schema validator existed but was never wired to run. All issues were fixed the same day: iOS now derives task lifecycle the same way the web app does, search reads the live index, the dead pre-v3 code was deleted, and the validator CLI now runs nightly with a full baseline scan. Current state: zero findings across 660 files.

10 / 10
findings confirmed under adversarial verification, 0 refuted
−12,383
lines deleted (963 added) across 7 commits
905 + 217
web + iOS tests passing after the change
660 files, 0
validator findings on a full baseline scan

One task structure, one read path

The audit's central question was how many shapes of “task” the system carries. The answer was four. Three are now gone as code paths; the markdown store is the only one anything reads.

RepresentationWho read itOutcome
Markdown files in tasks/, lifecycle in posture, validated by TaskSchema Web Tasks page and Day views via /api/v3/entities canonical, unchanged
SQLite tasks table in agents.db, frozen 2026-07-21 (287 of its 289 rows already existed as archived markdown) Cmd-K search, @-mention menu, 6 API routes, lib/todos.ts (~2,500 lines), lib/planner.ts readers rewired or deleted
iOS decode of legacy status frontmatter, requiring a frontmatter title The whole iOS task list and notification planner now posture-aware, title falls back to the body heading
Python writers with their own status vocabulary (cx.py task, clawsy_tasks.py) Nothing scheduled; write-only into the frozen table

What was broken, what happened to it

FindingStatusCommit
iOS showed finished tasks as open (read status, never posture) and silently dropped 17 of 39 tasks lacking a frontmatter titlefixed85d91d0
Cmd-K search and @-mentions were blind to every task created or edited after 2026-07-21 and emitted dead linksfixed0b99fe2
iOS Disposition Review posted to /api/todos routes that no longer exist; every use failed silently and it always reviewed zero tasksfixed, now uses v3 verbs85d91d0
Brief routes (/api/brief/today, hard-thing), iOS's notification source, read the frozen tablerewired to markdown, same JSON shapeb929d9d
Schema validator fully built and tested but nothing ever ran itCLI added, wired into the nightly janitor373e4d7
Validator would falsely quarantine every typeless task file, contradicting the schema's own optional-type contractfixed, type inferred from directory373e4d7
Six monologue memos had unparseable YAML from hand-written quotes; the prompt that writes them invited the bugfiles healed, prompt correctede7cf9f4 (workspace)
Live idea files failed their schema under three drifted conventions (raw/open status, missing source, lax timestamps)schema aligned with the ratified convention, 5 files healedff31114, 9ed8672
Dead pre-v3 tree: 5 workspace components, 6 orphaned API routes, dead libsdeleted after per-file caller verificationb929d9d
Two independently configured data-root resolvers with different safety guards

The validator, now real

Run it any time from web/:

npm run validate:workspace

Near-miss worth remembering: the validator's very first live run exposed a latent bug in its filename-drift rule. It did not know tasks/archive/ is a legitimate home and renamed 287 archived tasks back into tasks/. The change was caught in review of the diff, reverted cleanly (the workspace is git-versioned), root-caused, fixed, and covered by a regression test before the validator was wired to run nightly. Had the validator been scheduled before this audit, that rename would have run unattended.

Workspace data healed

Open follow-ups

Filed as one-click task chips in the session; none block daily use.

Detail per confirmed finding

iOS task lifecycle and title decoding

ClawsyTask(v3Entity:) read fm["status"] ?? "active" and required a frontmatter title. The convention (ratified 2026-07-30) makes posture the lifecycle field and puts the title in the body heading. Live files such as tasks/2026-08-04-fix-the-irrigation.md carry posture: done and no status at all, so they rendered as open; 17 of 39 live task files had no frontmatter title and never appeared at all. The decoder now ports taskStatus()/taskTitle() from @clawsy/core verbatim, including the rule that a contradicting legacy status loses to posture. posture: dropped maps to archived visibility, matching the web. New unit tests mirror the real file shapes.

Global search and @-mention menu

workspace-search.ts dynamically imported searchTodos and queried agents.db, whose newest row is 2026-07-21, while the real store has files updated daily. Result hrefs used a #todos/<id> format the router truncates, so even hits never deep-linked. It now queries the same derived FTS index /api/v3/search uses, filtered to tasks, with an empty-query fallback to Workspace.list("task"), and emits #tasks?task=<id> links that the Tasks surface actually reads. Tests rewritten against real markdown files in a temp workspace.

Disposition Review and the brief routes

The evening Disposition Review pulled its task list from an endpoint that was retired server-side, swallowed the 404, and therefore always reviewed an empty list. It now reviews the same tasks the rest of the app fetches, and its four actions map to the exact verbs the web tasks surface calls: carry to update_task active, tomorrow to snooze_task until the next day, park to update_task snoozed, drop to update_task archived. The two brief routes iOS calls kept their JSON contract but read the markdown store and the TodayCommit entity now. One judgment call flagged for review: park has no exact web analog; it shelves without a resurface date.

Validator machinery and its two latent bugs

The validation stack (write-path schema checks, a reconciliation validator for direct file edits, a janitor) was complete and unit-tested, but the only callers of runValidator/runJanitor were their own tests; the actual nightly janitor job is a prose-driven agent that never touched schema validation. Wiring it up surfaced two latent bugs: the unknown-type gate quarantined typeless tasks that the schema deliberately allows (fixed by directory-based type inference, the same rule the live read path already used), and the filename-drift rule did not recognize tasks/archive/ (the 287-file near-miss described above). tasks/README.md was also being scanned as if it were a task entity; convention docs are now excluded.

Idea schema drift

Three writing conventions had drifted from IdeaSchema: agents write status: raw/open with no source (12 files), two old files used type: doc, kind: idea, and stamps appeared as plain dates or Python's space-separated isoformat, both of which TaskSchema already treats as canonical. The schema now accepts the live status convention and the same lax stamps tasks use; the files missing required fields were healed rather than the requirements dropped.

Dead code removed

Deleted after per-file, repo-wide caller verification (web, iOS, scripts, packages, prompts): the SQL task system (lib/todos.ts, lib/planner.ts, routes /api/tasks, /api/today/plan, /api/cx-tasks both routes), the orphaned lib/todo-attachment-url.ts, and the unreachable pre-v3 tree: repo-workspace, review-workspace with trust-panel, journal-workspace with journal-sidebar, voice-memos-workspace, notes-context-nav, workspace-view-state, lib/journal.ts, lib/goals.ts, lib/projects.ts, lib/milestones.ts, plus their tests. Two audit claims did not survive verification and were kept: /api/today/reminder is a live, unrelated feature (daily quote), and lib/db.ts has many live non-task consumers.

Environment fix worth knowing about

Two different shells on this machine resolve different Node binaries: /opt/homebrew (arm64) and /usr/local (x64 under Rosetta). A better-sqlite3 built under the wrong one fails at load with an architecture error, which had silently broken all derived-index search and intermittently fails test runs. The module was rebuilt for arm64; if a wall of sqlite dlopen errors ever appears in tests again, check node -p process.arch first.