Clawsy Codebase Audit
~/code/agents and workspace ~/.clawsyOn 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.
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.
| Representation | Who read it | Outcome |
|---|---|---|
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 | follow-up chip filed (test fixtures entangled) |
What was broken, what happened to it
| Finding | Status | Commit |
|---|---|---|
iOS showed finished tasks as open (read status, never posture) and silently dropped 17 of 39 tasks lacking a frontmatter title | fixed | 85d91d0 |
| Cmd-K search and @-mentions were blind to every task created or edited after 2026-07-21 and emitted dead links | fixed | 0b99fe2 |
iOS Disposition Review posted to /api/todos routes that no longer exist; every use failed silently and it always reviewed zero tasks | fixed, now uses v3 verbs | 85d91d0 |
Brief routes (/api/brief/today, hard-thing), iOS's notification source, read the frozen table | rewired to markdown, same JSON shape | b929d9d |
| Schema validator fully built and tested but nothing ever ran it | CLI added, wired into the nightly janitor | 373e4d7 |
| Validator would falsely quarantine every typeless task file, contradicting the schema's own optional-type contract | fixed, type inferred from directory | 373e4d7 |
| Six monologue memos had unparseable YAML from hand-written quotes; the prompt that writes them invited the bug | files healed, prompt corrected | e7cf9f4 (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 healed | ff31114, 9ed8672 |
| Dead pre-v3 tree: 5 workspace components, 6 orphaned API routes, dead libs | deleted after per-file caller verification | b929d9d |
| Two independently configured data-root resolvers with different safety guards | follow-up chip filed | — |
The validator, now real
Run it any time from web/:
npm run validate:workspace
- Resolves the workspace root the same way the API does, scans changed files since the last validated marker, and prints findings by rule. It is a report, not a gate: exit 0 on any completed scan.
- The nightly 2 AM janitor prompt now runs it and logs the counts.
- A full baseline scan (all 660 covered files) reports zero findings.
- New coverage: monologue memos got their own registered schema derived from all 83 live files, so the sync shape validates instead of being permanently foreign.
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
- 6 monologue memos:
outcome:re-quoted so the YAML parses; text preserved verbatim. - 5 idea files: missing
title/statusadded (titles taken from each file's own heading). - 2 idea files migrated off the superseded
type: doc, kind: ideaconvention. docs/summaries/2026-W32.md:created/updatedbackfilled from git history.prompts/afternoon.md: the instruction that produced the broken YAML now specifies safe quoting.
Open follow-ups
Filed as one-click task chips in the session; none block daily use.
- Retire the last legacy SQL task writers (
cx.py task,clawsy_tasks.py); blocked on reworking fixtures in a 1,018-line integration test. Also fixcx.py's calls to two retired chat endpoints. - Unify the two data-root resolvers (
CLAWSY_WORKSPACE_ROOTvsCLAWSY_DATA_ROOT). - Extend validator coverage to
persons/,goals/,habits/,hypotheses/,research/(currently unvalidated). - Delete
CardRenderer.swift(1,176 dead lines in iOS). - Sweep migration debris:
clawsy-migrate,clawsy-sheet-sync, stray scratch JSONs, stale audit scripts. - Decision needed: the orphaned
taskstable inagents.dbnow has zero readers and zero writers in the live app. Dropping it requires an explicit go-ahead; until then it is inert.
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.