Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(tasks): head a task's rationale for what it carries, not for its column

closed#2682CedarCopilot

CedarCopilot wants to merge 4 commits into staging from fix/task-rationale-header

Not deployedTimeline and evidence
  1. Opened
    Sep 3, 2026, 9:25 PM
  2. Sep 3, 2026, 10:45 PM
  3. Merged, live on staging, live on prod, observed
    Pending
  4. Not deployed

    Not deployed

    Pending

Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.

Note on the evidence, added after a second look at the run data. An earlier version of this description claimed this change lifts retirement recall 81% → 90%. That claim does not hold , it rests on 7 cases of which 3 moved (sign test p = 0.25) and it did not replicate. The change still ships, on narrower and better-stated grounds: it is never worse than the baseline on any axis in either run, it carries the experiment's only sub-0.05 signal (less sibling over-cancelling, p = 0.038, caveated below), and it costs one string. See Every recall difference here is within noise.

What ships

One string. <existing_tasks> headed a task's stored rationale as Notes:; it now reads Why this exists / satisfied when:. Same text, same field, no schema change, no new capture.

notes is the column's name; it is not what the column carries. 133 of one rep's 137 open non-approval tasks hold ~340 characters there stating why the task exists and what would settle it , "Do not send if they've already responded", "only execute if the Sep 8 meeting is cancelled", "do not send until after the event reply goes out". Under Notes: that reads as trailing detail.

The header is now one helper shared by the two renderers of this block , listTasksTool for the real run and existing-tasks-block.ts for the read-only preview , which previously wrote the string out twice. field-approval and crm-opportunity tasks store a machine payload in notes rather than a rationale, and keep the neutral Notes: label.

The measurement

apps/server/.claude/skills/playbook-instruction-eval/task-retirement-eval.ts, a new confusion-matrix harness. 14 labelled cases built from real production rows on one rep's account (7 must-retire, 7 must-keep), 5 variants, 6 trials each = 420 model calls, 2126s wall clock at 8 in flight, 0 errored trials. Record-only tool stubs; zero writes anywhere. The committed fixture is pseudonymised , invented names on .example domains, same prose and structure. The two variants that matter were re-run on the pseudonymised fixture, and that replication is what the analysis below turns on.

"Retired" = cancel-task on the task under test or update-task {status: "done"} , the system prompt deliberately splits those and a variant that picks the right one has not made a mistake.

Confusion matrix (expected counts over trials) , the run the decision was made on

VariantTPFPFNTNprecisionrecallF0.5chandlermerrick grovecollateral/run
V0 today's Notes: (baseline)5.670.001.337.00100%81%0.9550% ✅0% ✅12.33
V1 Why this exists / satisfied when:6.330.000.677.00100%90%0.9790% ✅0% ✅9.17
V2 forced per-task KEEP/DONE/CANCEL verdict5.670.171.336.8397%81%0.9340% ✅0% ✅10.00
V3 explicit synthesised satisfiedWhen line5.000.002.007.00100%71%0.9260% ✅0% ✅9.67
V4 V1 + V2 + a clock-starter guard5.500.001.507.00100%79%0.9480% ✅0% ✅9.17

Do not read the recall column on its own , every difference in it is inside the noise. The analysis below is the honest version, and it changes the argument for this PR.

Every recall difference here is within noise

The unit of analysis is the case, not the trial. Six trials on one case are repeats of one prompt: they measure the model's consistency on that case, not six independent observations. That leaves 7 independent units per side, not 42.

Per-case retire counts (out of 6) for the two variants that matter:

CaseV0V1Δpseudonymised V0V1Δ
harlowfinch66065−1
bramley56+1660
calder45+136+3
trelawney44052−3
vela660
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/.claude/skills/playbook-instruction-eval/SKILL.mdno production surface mapped
  • apps/server/.claude/skills/playbook-instruction-eval/fixtures/next-steps-agent-instructions.mdno production surface mapped
  • apps/server/.claude/skills/playbook-instruction-eval/fixtures/task-retirement-cases.jsonno production surface mapped
  • apps/server/.claude/skills/playbook-instruction-eval/task-retirement-eval.tsno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/existing-task-notes-header.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/__tests__/existing-tasks-block.test.tsno production surface mapped
  • apps/server/src/mastra/tools/task/existing-tasks-block.tsno production surface mapped
  • apps/server/src/mastra/tools/task/listTasksTool.tsno production surface mapped