Flownix
Sections
On this page

flownix-write-spec

Use whenever you are about to record or change described behaviour in a Flownix project — writing a feature's spec, updating a doc node, or preparing the delta a task needs to close. Triggers on "write the spec", "update the spec", "propose

Write a Change Proposal

The one place that says how behaviour is written down in Flownix, and what happens between writing it and being allowed to build it. Every other skill points here instead of restating the format — three copies of a rule drift, and the drifting copy is always the one someone reads.

Prerequisite: flownix-basics.

The format is mandatory

Behaviour is recorded in the OpenSpec format. Not preferred, not the default you may fall back from: it is the only accepted way to describe what the system does. Freeform prose in a doc node is a defect — validate_spec reports it, spec_mode=strict refuses any delta that adds a new violation, and the export to openspec/ has nothing to round-trip.

shell
# <Document title>

## Purpose
One or two sentences: what this part of the system is for.

## Requirements

### Requirement: <name>
The system SHALL <one observable behaviour>.

#### Scenario: <the case this covers>
- GIVEN <starting condition>
- WHEN <what happens>
- THEN <the observable result>

## Non-goals
- <what this deliberately does not do, and why>
  • One requirement, one behaviour, one SHALL. Three "and also" clauses are three requirements. A requirement you cannot fail is not a requirement.
  • SHALL/MUST for a hard requirement, SHOULD where a justified exception exists, MAY for genuinely optional. Uppercase — that is what makes them RFC 2119 keywords.
  • Every requirement needs at least one #### Scenario: with GIVEN/WHEN/THEN. A requirement nobody can check is a wish, not a contract.
  • Keep the how out. Queues, libraries, table schemas, file names belong in the plan. The spec survives three rewrites of the implementation; anything that would not is in the wrong document.
  • Write ## Non-goals when a boundary was a decision. "We deliberately do not do X" is the part future readers most often need and least often find.
  • If you cannot phrase the change as a requirement with a checkable scenario, you have not yet worked out what the behaviour is. That is the thing to fix — not the format.

The procedure

1. Find the document, do not invent one

rag_query for the behaviour you are about to describe, then list_docs / get_node. A second document about the same subject is worse than a stale one: the reader finds one of them and never learns the other exists. Create a doc node only when nothing covers the area (create_doc_node, bound to at least one component).

2. Write the FULL new body

propose_doc_delta takes the complete resulting document, never a patch, never an excerpt, never just the section you touched. Applying a delta replaces the body wholesale, so a fragment silently collapses the spec to that fragment — a data loss noticed weeks later.

Read the current body (get_node), keep everything still true, make your change, send the whole thing back.

3. Validate before you propose

shell
validate_spec(doc_slug: "<DOC>", body: "<your full body>")

Changes nothing, tells you what is wrong and where. In spec_mode=strict a delta that adds a violation the previous version did not have is refused; violations inherited from the previous version come back as warnings and do not block — the format tightens as documents are edited, not in one sweep. Fixing the inherited ones while you are in there is welcome, but never required, and never at the cost of rewriting text you did not come to change.

4. Propose

shell
propose_doc_delta(doc_slug: "<DOC>", source_node_slug: "<TASK or PLAN>", body: "<full body>",
                  reason: "<why the behaviour changes, one or two sentences>")

The response carries ops — which requirements this adds, changes, removes or renames — and spec_issues. Read ops: if it says you removed a requirement you did not mean to touch, you dropped it while rebuilding the body.

In a project with spec_first on, the delta is born review_status: draft. A draft asks nobody anything: it is your working copy, and telling the user their approval is pending would leave you both waiting for each other. The question is put by submit_doc_delta, in step 4a.

4a. Work the draft out (spec-first projects only)

update_doc_delta(revision_id, body: "<full body>") replaces the body in place, keeping the delta's identity and its source_node. Call it as many times as the document needs.

  • body is the full document here too. The temptation is stronger than when proposing, because "update" sounds like it should take the paragraph you fixed. It does not: applying a delta replaces the document wholesale.
  • Format issues do not block the edit, even new ones. A draft is written in passes, and a requirement whose scenario is not written yet is a normal intermediate state. The ratchet applies at submit_doc_delta and at apply_doc_delta.
  • Record rejected decisions as [decision] comments on the source node. What a discussion threw away is the part a future reader most needs and least often finds.

Then submit_doc_delta(revision_id) — the only draft → pending transition. After it the delta is in the review queue, holds the gates, and can no longer be edited in place. The whole procedure around this, from opening a change to cutting tasks, is spec-first-change.

5. STOP if approval is required

Read get_project_policy. When require_proposal_approval is on (it only means anything with spec_mode: strict), the proposal is a gate, not a step:

  • The delta comes back review_status: pending.
  • Until a human makes it approved, the server refuses to move the covered task or plan into in_progress, and refuses apply_doc_delta. Both refusals are FailedPrecondition prefixed proposal_not_approved. The gate looks up the tree: a delta proposed from a feature or plan blocks the tasks underneath it.

You cannot approve it. approve_doc_delta and reject_doc_delta refuse every call from an agent session with PermissionDenied — they exist in your toolchain so you can name the step, not take it.

So: stop, tell the user, wait. Name the delta and the document, say what the proposal changes in one or two lines, and say plainly that it needs their approval in the web UI before you can start. Then stop — do not fill the wait with adjacent work you were not asked for.

What not to do while waiting, all of which look like progress and are not:

  • re-proposing the same delta (a second pending row, same gate);
  • editing the draft after submit_doc_delta — the server refuses it, and the point is that you would be changing the text the human is looking at while they look at it;
  • applying it (refused);
  • moving the task into in_progress anyway (refused);
  • deleting it and starting over — a rejected delta can still be approved later, and deleting it destroys the review trail;
  • switching to spec_mode: off or turning the policy off to get past the gate. Changing the project's policy to avoid its policy is not a workaround, it is the failure.

Resume when list_doc_deltas shows review_status: approved.

6. When it comes back rejected

Read review_note — the reason is mandatory on the server precisely so it exists. Rebuild the spec on that feedback. Do not argue with the rejection in a comment and re-send the same body.

Where you put the rebuilt text depends on the project:

  • spec_first on — fix the same delta with update_doc_delta. It returns to draft, the rejected body and the reason stay readable in its round history, and submit_doc_delta sends it back. A new delta would break that thread: the point of the history is that "what we discarded and why" reads as one story on one row.
  • spec_first off — there is no draft state and no round history, so propose a new delta, as before.

7. Apply with the work, not before it

In spec_mode: strict, apply_doc_delta belongs with closing the task: the spec says what the system does, and it starts saying it when the system actually does it. A version conflict (version_conflict) means the document moved under you — reread it, rebuild your change on the current body, propose again.

When the change is not a spec change

Not every task touches described behaviour. declare_no_spec_impact exists for exactly that, and it wants a sentence naming what you changed and why no documented behaviour moved — a placeholder is refused. If you cannot write that sentence honestly, the change probably does affect the spec, and this section is the wrong exit.

Checklist

  • Searched for an existing document before creating one.
  • Body is the FULL document, not a fragment.
  • ## Purpose, ### Requirement: with SHALL/MUST, #### Scenario: with GIVEN/WHEN/THEN.
  • validate_spec run on the body; no new issues.
  • propose_doc_delta with source_node_slug and a real reason.
  • In a spec_first project: the delta came back draft, and the user was not told it awaits their approval.
  • Draft worked out with update_doc_delta (full body each time), rejected decisions written down as [decision] comments.
  • submit_doc_delta called deliberately — that is the moment the question reaches a human.
  • ops in the response matches what you meant to change.
  • If require_proposal_approval: told the user, named the delta, and stopped.
  • Applied only after review_status: approved, together with closing the work.