Flownix
Разделы
На этой странице

harness-blueprint

Use when a project needs a new multi-agent harness — a council, review board or debate — and no existing one fits. Triggers on "build a council", "set up a review board", "we need agents to deliberate on X", "собери совет", "нужен консилиум

Blueprint a Harness

Take a request like "we need a council to decide X" all the way to a validated, published harness version tailored to this project. Prerequisite: read flownix-basics for the hierarchy, statuses, the project config file and the harness subsystem.

Use / don't use

Use it when a project needs a new council / review board / debate and none of the existing harnesses fit, or when an existing one needs a materially different graph, role set or policy set — that is a new version through this same procedure, not an edit of a published one.

Not this skill: you hold a role in a running harness → flownix-harness-participant; you hold judge/moderator → flownix-harness-moderator; you just want to look at one → harness.get / harness.export.

0. Load project context

shell
cat .flownix 2>/dev/null || cat .ai-flow

Parse the YAML for project_id and org_id. If neither file exists, run flownix-init first.

workspace_id in every harness.* call is the project_id from the project config. harness-service stores workspace_id as a plain UUID with no foreign key and no validation: pass the wrong one and nothing errors — the harness is simply created in a scope nobody looks at. There is no second source for this value; never invent one.

Then get_project_policy({ project_id }). In spec_mode: "strict" any decision this council produces will need a spec delta before the work can be closed — say so in the roles' instructions so the council's output is written to be applicable, not just persuasive.

1. Interview

Ask all questions in one batch, each with its default shown, and let the user answer only what they care about. Never ask about the stack, architecture, conventions or spec mode — those are derived in step 2. Eight questions, no more.

#QuestionDefault if unansweredMaps to
1What is this council for, and what decision must it hand back?(no default — must be answered; without it there is nothing to build)harness.create name + description; picks the graph preset
2Type: review / debate / design / audit?inferred from Q1, else councilharness.create type (single_agent, pipeline, council, review_board, debate, custom); picks the graph preset
3What goes in — a Flownix node, a diff, the spec, an external document?the node the request came from; nothing elseversion input_schema; source_node_ids on harness.start_run
4Who takes part, and what is each participant's specialization?preset's default roles (see step 4)one harness.add_role per participant: key, name, objective, system_instructions, min_instances/max_instances
5How is the winner picked — vote or judge?judge for review/audit, vote for debate/designa vote node (+ voting policy) or a judge node
6Is a human gate needed, and at which step?no gatehuman_gate node + approval policy with effect require_approval
7Any tool or data restrictions?read-only: no file writes, no shelltools / data_access policies via harness.set_policy
8Budget — how many rounds, what cost ceiling?1 round, no explicit ceilingbudget policy; a loop node when rounds > 1

Rules for the interview:

  • Defaults are applied silently, and reported back. After the batch, restate the resolved configuration (all eight lines, defaults marked as such) and get one confirmation before building. A user who answers nothing still gets a working harness.
  • Q1 is the only blocking question. Everything else falls back.
  • Anything derivable is not asked. If you are about to ask what language the project is in, you skipped step 2.
  • The answers, not the preset, win. The preset from Q1/Q2 is a starting layout; Q4–Q8 reshape its nodes, roles and policies.

2. Derive the stack & architecture profile

A council that proposes a Kafka bus to a project that has none is worse than no council. Build the profile once, before any role exists, and paste the identical block into every role's system_instructions.

Extraction order — stop as soon as a line is answered, and prefer the cheap source:

  1. get_project_policy({ project_id }) — spec mode. In strict, every role's instructions must also say: the decision has to be expressible as a spec delta on an existing doc node.
  2. list_docs({ project_id }) plus rag_context / rag_query on "architecture", "stack", "conventions". If bootstrap-spec has been run on this project, the profile comes almost entirely from these doc nodes and the repo is only spot-checked.
  3. The repository, which wins on any disagreement with the docs — docs go stale, manifests do not:
    • manifests: go.work, each go.mod, each package.json;
    • service layout: services/**, top-level apps (web, admin, landing, …);
    • contracts: proto/** plus buf.yaml / buf.gen.yaml;
    • storage and schema history: migrations/**;
    • deploy: k3s/**, Dockerfile*, docker-compose.yml, CI workflows;
    • build and test commands: the root Makefile.

When the repo and the docs disagree, record the repo's answer in the profile and flag the stale doc to the user — it is a finding, not noise to swallow.

The block

Fill this template and reuse it verbatim across all roles of the harness. Same text everywhere: two roles working from different pictures of the same system is exactly the failure this section exists to prevent.

shell
## Project stack & architecture (authoritative)
- Languages & runtimes: <…>
- Services & boundaries: <…>
- Transport & contracts: <…>
- Storage & migrations: <…>
- Build, test, deploy: <…>
- Conventions that constrain solutions: <…>

Propose solutions that fit this stack. A proposal that requires a new language,
framework, datastore, or transport must be marked as an explicit alternative and
carry a justification plus a migration cost estimate.

Rules for filling it:

  • Every line is backed by a file or a node. A category you could not confirm is written as unknown — not found in <where you looked>, never guessed and never quietly dropped.
  • The closing paragraph is a constraint on the role, not advice. Keep it verbatim; it is what makes an off-stack proposal a labelled alternative instead of the default answer.
  • Keep it to structure and contracts. No source code, no file-by-file inventory — the roles get a map, not the territory.

Worked example — this repository, filled from go.work, services/**, proto/** + buf.*, migrations/**, k3s/** and the root Makefile:

shell
## Project stack & architecture (authoritative)
- Languages & runtimes: Go (go.work workspace, one module per service); TypeScript/Next.js
  for the three frontends.
- Services & boundaries: seven Go services under services/** — core-service (nodes, docs,
  spec mode), auth-service, gateway (HTTP edge), mcp-server, harness-service, rag-service,
  notification-service. Frontends: web, admin, landing.
- Transport & contracts: gRPC between services, proto/** generated with buf; HTTP/SSE at the
  gateway and the MCP server.
- Storage & migrations: PostgreSQL with per-service SQL in migrations/**; Qdrant for RAG.
- Build, test, deploy: root Makefile targets, Go tests per module, k3s/** manifests, CI images.
- Conventions that constrain solutions: contract changes start in proto/** and are regenerated
  with buf, never hand-edited; schema changes ship as a new migration, never as an edit to an
  applied one; spec_mode=strict means closing a task needs an applied doc delta or an explicit
  declare_no_spec_impact.

3. Build

Fixed order — each call needs an ID the previous one returned:

shell
// 1. The definition. workspace_id === project_id from the project config.
harness.create({ workspace_id, name, description, type })            // → harness_id

// 2. A draft version. Everything below hangs off this version.
harness.create_version({ harness_id, changelog: "initial blueprint",
                         input_schema })                             // → harness_version_id

// 3. One call per participant from Q4. Paste the step-2 profile block into
//    system_instructions, identically for every role.
harness.add_role({ harness_version_id, key: "architect", name: "Architect",
                   objective: "…", system_instructions: "…profile block…",
                   responsibilities: [...], prohibited_actions: [...],
                   min_instances: 1, max_instances: 3 })             // → role_id

// 4. Nodes, then edges.
harness.add_workflow_node({ harness_version_id, type: "agent_task",
                            name: "proposals", config: { role_key: "architect" } })
harness.connect_workflow_nodes({ harness_version_id, from_node_id, to_node_id })

// 5. Policies from Q6–Q8.
harness.set_policy({ harness_version_id, category: "budget", effect: "allow", … })

Things that bite:

  • Nodes reference roles by role_key inside config, not by role_id. config is free-form JSON with no schema validation — a typo'd key is not rejected here, it surfaces as a validation error in step 5 (or, worse, at run time).
  • Node types are exactly: agent_task, parallel_group, review, debate, vote, judge, human_gate, transform, create_flow_nodes, condition, loop, end.
  • Policy categories: tools, data_access, voting, budget, execution, approval, retention, privacy. Effects: allow, deny, require_approval. deny beats allow — a broad allow plus a narrow deny is the way to express "everything except this".
  • Role instances: max_instances > 1 is what makes a parallel_group actually parallel; leaving both at 1 yields a group of one.
  • Every graph ends at an end node. A branch that just stops is a validation error, not a graceful finish.

4. Graph presets

Pick by the answers to Q1/Q2, then reshape with Q4–Q8. These are starting layouts, not doctrine: drop a node the interview didn't ask for, add the human gate where the user wanted it.

Architecture decision (type: council / debate) — several proposals, cross-examined:

shell
parallel_group(proposals) → review(cross-review) → debate → vote | judge → end

Default roles: two-plus proposers with different angles, one critic, plus judge if Q5 said judge.

Review board (type: review_board) — one artifact, several independent lenses:

shell
parallel_group(reviewers, one role per dimension) → judge → end

Default roles: correctness, security, performance reviewers + judge. Dimensions come from Q4; reviewers must not see each other's findings before the judge — that is the point of the parallel group.

Spec audit (type: pipeline) — turn a spec into tracked work:

shell
agent_task(analyze spec) → human_gate → create_flow_nodes → end

The human_gate is mandatory here even if Q6 said no gate: create_flow_nodes writes real nodes into the project, and that is not something a council does unattended.

5. Validate & publish

shell
harness.validate({ harness_version_id })   // errors[] must be empty
harness.publish({ harness_version_id })    // only after a clean validate

harness.validate is not optional and not advisory. Read the result: errors block publishing, warnings need a conscious decision. Common failures and their fix:

Validation errorWhat actually happenedFix
role referenced by a node does not existconfig.role_key typo, or the role was never addedfix the key, or harness.add_role
node unreachable / no path to endan edge was never created, or points the wrong wayharness.connect_workflow_nodes
circular edgesa feedback loop expressed as a back-edgemodel repetition with a loop node + max_iterations, not a cycle
unfulfilled role bindingnode needs more instances than the role's max_instances allowsraise max_instances via harness.update_role
policy conflictoverlapping allow/deny on the same actionremember deny wins; delete the allow that is now dead

Only a draft can be published, and publishing makes it the single active version.

shell
harness.start_run({ workspace_id, harness_id, harness_version_id,
                    title, input, source_node_ids: ["<node the run is about>"] })

start_run also advances the graph immediately: start nodes get step runs, and node types needing an agent get work items visible through work.list_available(run_id, role_key).

Then link the run back by hand. source_node_ids is plain JSONB with no foreign key, and there is no reverse lookup from a node to its runs — a run recorded only there is invisible from the ticket. Always:

shell
add_comment({ slug: "<source node>", body: "[progress] Harness run started: run_id=<…>, harness_id=<…>, version=<…>" })

Optionally set_references as well, but the comment is the part that survives.

Rules

  • workspace_id is always project_id. No validation, no error, no second guess.
  • Never invent tools. Only the harness.* calls above exist for building; work.* and council.* belong to participants (flownix-harness-participant) and the judge (flownix-harness-moderator), not to this skill.
  • Publish only what validated clean. Errors are blocking; warnings are a decision to state out loud, not to skip.
  • Never edit a published version. Changes go into a new draft via harness.create_version; exactly one version is active at a time.
  • Ask Q1, derive the rest. Anything you could have read from the spec, the docs or the repo is not a question.
  • One profile block, all roles. Divergent role instructions produce a council arguing about the system rather than about the decision.