COSTA
Technical writeups
The problemPipelineTriggerGenerationUIDecisionsQuality
Technical overview

Proposed actions

When a task is added, COSTA generates a proposed action — a draft message, document outline, or checklist — and attaches it to the task. By the time you open the task, the work is already started.

01 · The problem

The gap between capturing and acting

Task lists are good at capturing intent and terrible at reducing the cost of acting on it. A task that says "follow up with Ana about the design review" tells you what to do but none of how. Every time you look at it, you reconstruct: when did you last talk, what was agreed, what does a good follow-up in this context look like? That reconstruction is not thinking — it is overhead that arrives before the actual work begins.

The problem compounds for tasks that require drafting. A message to a colleague, a document outline, a review brief — the task is almost entirely the act of producing text. The block is not knowing what to say but starting from nothing, in a moment when you already have seventeen other things in motion. Most of these tasks are not hard. They are just friction-generating in the worst possible place: the transition between deciding something should happen and making it happen.

Proposed actions are built to eliminate that gap. When a task is added to the system, COSTA reads the surrounding context — your most recent 1-on-1 notes with the person, the org structure, your writing style — and generates the output before you have asked for it. The task is not waiting for you to begin; it is waiting for you to review.

02 · End to end

From task to ready-to-execute

The pipeline from task creation to usable proposed action runs automatically. There is no command to trigger it and no setup required per task. Adding a new task to tasks.md is sufficient.

Trigger-to-stamp pipeline
Task added
tasks.md written to disk
Watcher fires
SSE event · path === tasks.md
Hash check
against task-action-state.json
Agent loop
reads context · generates content
File written
proposed/YYYY-MM-DD-slug.md
Task stamped
> Action: proposed/…

The sequence is: the file watcher detects a change to tasks.md, checks whether any new tasks have appeared by comparing hashes against a state file, and if so calls the /api/tasks/generate-action route. The route runs an agent loop that loads relevant context and writes the proposed action to proposed/YYYY-MM-DD-slug.md. Once the file is written, the route reads tasks.md again and appends an > Action: line to the task entry. The task list is now the index into the proposed action. The UI reads that line to surface the button.

The full cycle — from saving a task to having a proposed action stamped and ready — typically takes fifteen to thirty seconds, depending on the complexity of the task and how much context the agent needs to load.

03 · The trigger

File watcher → hash check → route

The trigger lives in the main app page. A Server-Sent Events connection to /api/watch delivers filesystem events in real time. When an event arrives with path === 'tasks.md' and a type of change or add, the client fires a POST to /api/tasks/generate-action. Nothing else is required: no database polling, no cron job, no manual trigger.

The route's first check is a per-process lock. A module-level generating flag blocks concurrent invocations. If the route is already running — because the file watcher fired a second event during the first agent loop — the second call returns immediately with { skipped: 'busy' }. This prevents file-watcher cascade loops, where a write to tasks.md (the Action stamp at the end of generation) would itself trigger another generation cycle.

The state file at context/task-action-state.json holds a flat array of MD5 hashes, one per processed task. Each hash covers the task's first line and all its continuation lines (the > From: and any other metadata). When the route runs, it parses every open task from tasks.md, computes its hash, and looks it up in the state file. Only tasks with unseen hashes and no existing > Action: line are candidates for generation. The route processes one per invocation and returns — the next watcher event picks up the next pending task.

State file · context/task-action-state.json
{
  "processed": [
    "a3f1c82b",
    "d94e0f17",
    "2b7a5c19"
  ]
}
8-character MD5 prefix per task · computed over the task line plus all continuation lines · written after each successful generation
04 · The agent loop

Context first, then content

The generation step is an agent loop running against a tightly scoped system prompt. The prompt does not try to produce good output from nothing — it instructs the agent to load context first, then generate. This ordering is load-bearing. A draft Slack message to a direct report produced without knowing anything about that person will be generic. The same message produced after reading two months of 1-on-1 notes will be specific.

The agent's prescribed steps, in order: load context/agents/organisation/people.md for org structure, load context/agents/stuart/writing-style.md for voice, then check for task-specific context. If the task mentions a person, it scans 1-1s/ and loads their most recent note. If the task is document- or writing-oriented, it checks notes/ and meeting-notes/ for relevant material. Only then does it generate the proposed action and write it to the target path.

The output format is determined by the task's area tag. The agent is given explicit format instructions per tag, with no default to a generic "here is a draft" framing. The file begins with the task description verbatim as a heading, followed immediately by the content — no preamble, no meta-commentary.

Output format by task area
#meeting · #people
Outreach / message
Draft Slack message or short email
First-name basis, gets to the point in sentence one. No filler.
#document · #writing
Document / draft
Filled-in draft or detailed outline
Not a blank template — actual content based on relevant context.
#admin
Checklist
Numbered step-by-step list
Concrete steps in execution order.
#design
Review brief
Assessment structure with questions
What to look at, what to push on, how to organise feedback.

The target file path is passed in the user message, not computed inside the agent. The route builds proposed/YYYY-MM-DD-slug.md before starting the loop, where the slug is derived from the first six meaningful words of the task text with metadata stripped. The agent's only responsibility is to write to the path it was given and return that path as its final output.

05 · The interface

ACT: one button, one decision

In the task list, tasks with a proposed action show a small ↗ ACT button alongside the task text. Clicking it opens the proposed action document in the content panel — the same panel used for notes, briefs, and 1-on-1s. Nothing is sent or executed automatically. The button presents the work for review; what happens next is up to you.

The button only appears when three conditions are met: the task has an > Action: line in tasks.md, an onOpenFile handler is present in the panel, and the task is not already marked done. Completed tasks do not show the button — there is no point reviewing an action for something that is finished.

Task entry before and after stamping
Before
- [ ] [M] Reach out to Ana re
  design review @work #people
  [est:15m]
  > From: 1-1s/Ana Peleteiro
    - May 6 2026.md
After stamping
- [ ] [M] Reach out to Ana re
  design review @work #people
  [est:15m]
  > From: 1-1s/Ana Peleteiro
    - May 6 2026.md
  > Action: proposed/2026-05-06
    -reach-out-to-ana-re.md
The Action line is appended to the continuation block — after From: and any other metadata. The UI reads this line to surface the ACT button.

The parsing happens in ContentPanel.tsx inside the parseSections function. As it iterates task lines, it looks ahead at continuation lines matching /^ > /. A line starting with Action: is parsed into the task's action field; everything else goes into note. The action field is a relative path — the button passes it directly to onOpenFile, which handles the file fetch.

The proposed action file itself is plain markdown. When opened in the content panel it renders like any other document. You can edit it inline, copy from it, or use it as the basis for something longer. After you have acted on it — sent the message, created the document — you mark the task done and move on. The proposed action file stays in proposed/ but does not clutter the notes view, because that directory is not in the documents page allowlist.

06 · Design decisions

The choices that make it work

Several of these decisions were not obvious upfront. They emerged from running the system and noticing where it broke.

Design decisions
Files live in proposed/, not documents
The proposed/ directory is not in the documents page allowlist. Actions are ephemeral work product — generated for a task, reviewed, used, then superseded. They should not accumulate alongside permanent notes.
Per-process lock prevents cascade loops
The generating flag in the route module ensures only one action is generated at a time. Without it, rapid file-watcher events (e.g. an editor autosave) would stack concurrent agent calls. A single boolean is sufficient because the flag lives per-process.
One action per invocation, not batch
Each POST call processes at most one pending task. If multiple new tasks appear simultaneously, the watcher fires once per change event and each call picks up the next unprocessed task. This keeps latency predictable and avoids one long loop blocking the interface.
Existing tasks pre-seeded on first run
On first use, all tasks already in tasks.md have their hashes written to the state file. Only tasks added after that point trigger generation. This prevents the system from generating actions retroactively for an existing backlog when the feature is first deployed.
Slug derived from task text, not random
The file slug is built from the first six meaningful words of the task. This makes proposed/ directory contents readable without opening files — you can see what each action is for at a glance.
07 · Quality

What good proposed actions look like

A proposed action is good when you can act on it without editing it first. That is the bar. Not "useful as a starting point" — that describes most first drafts. The standard is a message you would actually send, a checklist you would actually follow, a review brief you would actually open in the design critique.

Specificity comes from context depth

The difference between a generic draft and a usable one is almost entirely explained by what context the agent loaded before generating. A draft message to a colleague that opens "Hey, following up on our last conversation about the design review" is generic — the agent loaded nothing. The same task after loading the most recent 1-on-1 note produces something that references the actual work in flight, uses the right framing for that relationship, and matches the communication register.

This means keeping 1-on-1 notes current has a direct payoff in action quality. A stale note from three months ago produces a less specific draft than a note from last week. The system is not magic — it surfaces whatever signal is actually there.

The format constraint is doing real work

Forcing the agent to produce a draft message rather than "thoughts on a draft message" removes a layer of indirection that would otherwise sit between you and acting. If the output were a summary of what a good message might include, you would still have to write the message. The constraint — produce the exact artefact the task requires — is what makes the action genuinely proposed rather than advisory.

The same logic applies to the no-preamble rule in the file format. An action file that starts "Here is a draft Slack message for your review:" adds nothing. You know it is a draft. You know it is for review. Starting with the content immediately is faster to scan and easier to copy from.

The review step is intentional

Proposed actions are designed to require review, not to be sent as-is. This is deliberate. A system that automatically dispatches messages or creates documents would be fast but dangerous — the model does not have full context for every interpersonal nuance or organisational sensitivity. The ACT button opens the action for your eyes before anything goes anywhere. The value is in eliminating the blank-slate problem, not in removing human judgment from the loop.

The standard: would you send this as-is?
Good signal
Opens with the specific thing in flight, not a generic opener
Matches the register of the relationship (direct report vs peer vs leadership)
Proposes a concrete next step, not "let me know your thoughts"
Length matches the situation — two sentences for a check-in, more for a substantive ask
Weak signal
Generic opener that could be addressed to anyone
Correct in structure but vague in content
Missing what the person actually needs to do or decide
Context clearly wasn't loaded — reads like a template, not a message

When the quality is consistently weak for a particular type of task, the fix is almost always in the context rather than the prompt. Richer 1-on-1 notes, more specific writing style guidance, better org context — these are the inputs that determine the output ceiling. The agent is a function of what it knows.

Also in this series
Below the WaterlineOrganisational MemoryContext ArchitectureKnowledge AgentsBrief GenerationColour System
COSTA · Proposed Actions← Back to app