Lacunari

Release notes

What a day of real fleet use fixed

Three commits, one day, all responding to the same source: a developer's report after running Lacunari's fleet against real work for real hours, not a test harness.

20 August 2026. Two rounds of feedback landed the same day, then a third commit fixed a bug the first two exposed once the CLI shipped ahead of a schema apply. Here is what changed in each, and why.

A software goal no longer gets a research-shaped plan

lac goal plan decomposes an objective into a research shape by default: scope, gather, corroborate, synthesise. That is right for "trace the provenance of this painting." It is wrong for "ship multi-tenant support," and a brand-new software goal has nothing indexed yet for the planner to infer a different shape from. The code-aware inference in 013_code.sql only works once there is a corpus to read.

lac_goals now carries a kind column: research, software or ops, set with --kind on goal add or corrected later with goal plan <id> --kind software. A software goal now defaults to reproduce, isolate the cause, fix, verify against the reproduction, regression-guard. An ops goal defaults to diagnose, plan, execute, validate, document. The same fix caught every planned subtask inheriting lane_tag= 'research' unconditionally, regardless of what kind the goal actually was.

A lock for a file no task has claimed yet

Task-level locking already worked: claim a task, declare the files it touches, and a second worker is told who holds them. What had no answer was two workers who both need one file without sharing a task, or a worker regenerating an artefact another one depends on. ctask hold <path> --lane <lane> and unhold give that a standalone lock, reusing the conflict views the task-level locks already used.

Wiring it in surfaced a real bug in that shared view. The conflict check compared task_id <> to detect a collision, but a standalone hold has no task, so its task_id is NULL, and NULL <> anything is never true in SQL. Every standalone hold would have been silently exempt from the exact conflict detection it exists to provide. Fixed with IS DISTINCT FROM. A dead worker's standalone hold is reaped by the same keeper, on the same lease, as a task-level one.

Attesting in one step, and validating what actually shipped

lac attest already had --method and --exact, and lac verify already re-ran the method and compared hashes. The feature a report described as missing already existed. What it was actually pointing at was real: recording an attestation meant running the command yourself and piping the output in by hand, two steps with room for the method you typed to drift from the command you actually ran. lac attest --run "cmd" collapses that into one step: it executes the command, hashes stdout, and records that same command as the method.

The second round added a different kind of attestation entirely. --exact means re-derive from scratch and compare bytes, which is wrong for a patch, a config change, or a migration; there is no fresh output to hash against. --validate "cmd" is for something you apply and check. lac verify writes the stored artefact out, runs the command against it, and checks the exit code. A failure here is DISPUTED for a different reason than an --exact failure: "failed validation," not "different bytes." That distinction is the reason it got its own kind instead of overloading the existing one.

Contradictions and findings become something the database tracks

Two agents disagreeing about the state of the world used to be a message someone had to notice and remember to reconcile. lac_findings now has a contradicts_id column, and lac goal finding ... --contradicts <id> records the relationship directly, surfaced through lac_v_goal_contradictions and in lac goal status. Resolving one reuses the confidence column that already existed rather than adding a second resolved flag that could quietly disagree with it. lac goal refute and lac goal corroborate are the first commands that let a finding be updated at all.

lac say also gained somewhere to put a finding that is not just a sentence: optional --file, --severity and --finding on csend, matching filters on cread, and lac_v_bus_findings ranking the worst severity first. Plain chat is unaffected. The new columns show up only once something is actually flagged as a finding.

When the CLI ships ahead of the schema

The third commit is a bug report from running the first two in the wild. lac say started failing intermittently on the fleet, first misdiagnosed as a shell quoting problem. It wasn't. lac_messages predates 023_bus_findings.sql, and csend's insert referenced the new is_finding, file and severity columns unconditionally. Every plain message broke the moment the CLI shipped ahead of a schema apply, not only the structured ones.

The fix is a column-existence check, lac_column_exists in lib/lacdb.sh, checked once per command rather than per flag. Plain chat with none of the new flags never touches the new columns and no longer needs them installed. Only the flags that actually need 023_bus_findings.sql fail now, and they fail by naming that file and pointing at lac doctor, instead of handing back Postgres's raw "column does not exist."

lac doctor had the same blind spot for a different reason. Its layer check looks for a marker relation per migration, but 021_goal_kind.sql and 024_validated_attestations.sql only alter an existing table, so they had no relation to key on and were silently reported as installed even when they weren't. Doctor now checks the column and the constraint directly, and prints a line naming the highest schema file the running CLI ships, so the report reads as a comparison between what the code expects and what the database has, not a one-sided list.

Verified against a throwaway Postgres built with migrations only through 020, the exact shape of the incident: plain csend, cread and --json kept working, structured variants failed naming 023 specifically, and lac doctor reported all five real gaps, including the two that had previously been invisible. core.sh went to 116/116 with 11 new cases, provenance.sh to 46/46, goals.sh to 101/101, locks.sh held at 90/90.

None of this is glamorous. All of it is what "in real use" is supposed to mean: a fleet ran against real work, something broke or read wrong, and the fix landed the same day instead of waiting for someone to file it and forget about it.