| name | architect |
| description | Design the sprint from requirements. Decides module boundaries, delegates per-module architecture to sub-architects in parallel, packs closed task specs into dependency-ordered sprints. Produces ARCH.md, decisions index, per-task specs, sprint manifest. Run after /research, before /build. After architect finishes, every remaining question is implementation, not design. |
| version | 1.0.0 |
| schema_version | 1 |
Architect skill
Read this before doing anything
See references/principles.md ## Read This Before Doing Anything (canonical source — the 4-bullet block lives there; this skill cites it by reference).
Task specs you pack must be implementable under references/code-conventions.md (the conventions build's task agents follow): don't prescribe behavioral_pseudocode that forces magic numbers, a framework-coupled core, swallowed errors, or non-atomic critical writes. Design with those conventions in mind — they shape how the spec gets built, never what.
Conduct
Canonical conduct lives at references/principles.md ## Conduct — read it there; it is not duplicated here. The three lines that govern every step of this skill: no shortcuts or deferrals of scope; sub-agents get agency, clear goals, and parallel dispatch; thorough on substance, lean on ceremony.
Operating contract
- Read SPEC.md (required; the complexity assessment in its frontmatter is load-bearing), REQ.md (required when entered from
requirements-ready), and any prior ARCH.md when extending an architecture incrementally.
- Verify
state.phase is one of: requirements-ready, architecture, decomposing.
- Architect is the last phase that can ask the user a design question without violating Front-Loaded-Design. If a decision can't be closed from inputs, ask the user via
AskUserQuestion OR route back to eliciting with a specific addendum request — never push the question to build.
- Use the
essense-flow-tools CLI ops for every state-advancing write and for canonical path lookup. The CLI surface is the only path you, the master, use for state writes — never edit state.yaml directly and never call internal lib helpers for state advancement.
- The decomposition loop has no fixed iteration count. Loop until convergence (no node changes class for two iterations) — convergence is the gate, not a counter.
Pre-flight & finalization checks
Verify before any state-mutating call (Write to canonical paths, state-set-phase, task-spec-write). Read these now; re-check before each state advance.
Phase targets (verbatim from references/transitions.yaml — no synonyms, no English):
requirements-ready → architecture — initial entry from triage / requirements
architecture → decomposing — entering the decomposition loop
decomposing → decomposing — next decomposition iteration
decomposing → architecture — open design decision surfaced; re-decide
architecture → sprinting — sprint manifest closed, task specs closed
decomposing → sprinting — decomposition converged, packing complete
If your target phase is not in the list above, you have invented it — state-set-phase will reject it with exit 3 and the canonical phase list in the error. Common invented values seen in the wild: building, built, done, architected. None are legal.
The exact CLI sequence for the architecture→sprinting transition (replace placeholders, keep the structure):
node plugins/essense-flow/bin/essense-flow-tools.cjs state-set-architecture-completed \
--value "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" --project-root <root>
node plugins/essense-flow/bin/essense-flow-tools.cjs state-set-phase \
--value sprinting --sprint 1 --project-root <root>
node plugins/essense-flow/bin/essense-flow-tools.cjs step-advance \
--skill architect --next-step skill-complete --project-root <root>
Self-check before the call — answer each:
- Is
--value for state-set-phase a string from the legal phases list, spelled exactly?
- Did you
Write one manifest.yaml per sprint at architecture/sprints/<n>/manifest.yaml using the literal sprint number, never the placeholder <n> — and not a single manifest at the architecture root?
- Did you call
task-spec-write for every task in manifest.waves[].tasks? (Not Write directly — task-spec-write does the marker scan + key check + atomic write, always with .yaml extension.)
- Did sub-architects produce the per-module task specs and module-internal decisions? If you authored task specs in main context, the master/sub-architect contract was bypassed — stop and dispatch.
- Are you using
state-set-phase for phase advancement, NOT Write directly on state.yaml? It is the only path that advances phase legally and enforces prerequisites.
- Did you call
step-advance at the start of every step and --next-step skill-complete at the very end? The cursor enforces monotonic step order. align sits between synthesize and pack; skipping it is rejected as non-monotonic AND fails the with sufficient alignment lens dispatch predicate at pack-phase completion.
If any answer is no, stop and fix the gap. The cost of pausing is small; the cost of advancing on a malformed contract is the build skill halting because it cannot find the manifest. state-set-phase rejects the transition outright if any prerequisite artifact is missing — the rejection is your signal to fix, not bypass.
CLI surface (essense-flow-tools)
Path lookups, step bookkeeping, and state advancement go through the CLI. You do not infer paths from prose. You do not write phase: directly. You do not pick task spec extensions or sprint directory names from convention. One block per op below; full flag reference via node plugins/essense-flow/bin/essense-flow-tools.cjs --help.
init architect — canonical paths
At skill-start:
node plugins/essense-flow/bin/essense-flow-tools.cjs init architect --project-root <project-root>
Returns JSON with canonical_paths (arch_md, decisions_yaml, sprint_manifest_template, task_spec_template), ordered_steps ([decide, delegate, synthesize, align, pack, finalize, write-round-close]), sub_agents (the registered essense-flow-sub-architect block), transitions (read-only reference), required_inputs. Parse the JSON. Use the strings verbatim — never construct path or step names from prose.
Placeholders: substitute <n> in sprint_manifest_template (.pipeline/architecture/sprints/<n>/manifest.yaml) yourself at Write time with the literal sprint number; task-spec-write --sprint <n> --task-id <id> --content-file <staged-path> substitutes both placeholders in task_spec_template (.pipeline/architecture/sprints/<n>/tasks/<task-id>.yaml) at write time.
step-advance — per-skill cursor
Before the substantive work of each step in ordered_steps:
node plugins/essense-flow/bin/essense-flow-tools.cjs step-advance --skill architect --next-step <step-name> --project-root <project-root>
Rejects out-of-order or non-monotonic advances — exit 13 with a "not the immediate successor" error. After the final step's work, step-advance --next-step skill-complete deletes the cursor (signals architect finalized cleanly; the next skill can run).
state-set-phase — phase advancement (sole writer)
node plugins/essense-flow/bin/essense-flow-tools.cjs state-set-phase --value <phase> [--sprint <int>] --project-root <project-root>
Phase values are validated against references/transitions.yaml phases: (the canonical names: idle, eliciting, research, triaging, requirements-ready, architecture, decomposing, sprinting, organizing, sprint-complete, glossarying, reviewing, verifying, complete). Invented values are rejected with exit 3 + the legal list. The transition (current.phase → --value) must exist in transitions.yaml's transitions: block; illegal transitions return exit 6. --sprint is required iff target phase is sprinting or sprint-complete — provided otherwise → rejected; required-but-missing → rejected. Writes phase + (when applicable) sprint + auto-stamps last_updated atomically.
For architecture → sprinting (or decomposing → sprinting) the op enforces the prerequisite predicate .pipeline/architecture/sprints/<n>/manifest.yaml exists with all task specs closed: the manifest must exist AND every task id in manifest.waves[].tasks must have a corresponding tasks/<task-id>.yaml written via task-spec-write. Missing manifest → exit 7; missing task specs → exit 7 with the missing ids named.
Timestamp / counter setters (never direct YAML edits)
state-set-architecture-completed --value <iso8601> — stamp BEFORE state-set-phase --value sprinting so the field is in place at phase advance.
state-set-decomposition-round --value <int> — increment per decomposition iteration (decomposing → decomposing); the round counter is that transition's only mutation.
state-set-sprint --value <int|null> — standalone sprint cursor; state-set-phase --sprint covers the normal flow, the null form is heal-territory (complete → idle cycles).
task-spec-write — sole writer of task spec YAML
node plugins/essense-flow/bin/essense-flow-tools.cjs task-spec-write --sprint <n> --task-id <task-id> --content-file <staged-path> --project-root <project-root>
One invocation per spec. Stage each spec at a non-canonical path (e.g. <project-root>/.tmp-task-spec-<task-id>.yaml); the op validates then atomically writes the canonical destination. Validation, in order: forbidden-marker scan — case-insensitive substring match on TBD, [TBD], <TBD>, agent decides, <agent decides>, [agent decides], agent-decides, TODO, [TODO], XXX, FIXME, ???, <choose>, <fill in>, <placeholder>; any hit → exit 15 quoting the marker at line N. YAML parse failure → exit 16. Required keys — the 10 canonical: schema_version, task_id, goal, requirements_traced, file_write_contract, behavioral_pseudocode, test_completion_contract, dependencies, agency_level, agency_rationale (module accepted-but-not-required); missing key → exit 17. Typed-value checks per key. parsed.task_id == --task-id (catches paste-id drift) → exit 18 on mismatch. --task-id must appear in the sprint manifest's waves[].tasks (catches packing-vs-spec drift) → exit 9 if missing. Destination must not already exist → exit 10 (the op never overwrites; re-write is heal-territory).
A rejection points back at the brief that produced the bad spec — re-dispatch the relevant sub-architect with a sharper constraint, or surface to user.
What you write directly with Write
Three artifacts have no dedicated CLI op: ARCH.md (path from canonical_paths.arch_md), decisions.yaml (canonical_paths.decisions_yaml), and each sprints/<n>/manifest.yaml (template with <n> substituted). Their existence is enforced later by state-set-phase's prerequisite predicate at the sprinting transitions.
Core principle
Design closes here, or it doesn't ship. Every task spec architect packages is a closed contract for the build agent — no open questions, no "agent decides X," no "TBD."
Fewest sprints, fewest waves — and no resource caps (see the No Resource Caps section of references/principles.md): the fewest sprints necessary, as many rounds as the substance needs — never a fixed counter. No predetermined agent counts, no turn budgets; give each worker the context necessary and no more. Default to one sprint, one wave. Split only on real data-dependency or real file-conflict. Theme-based splits ("the hooks sprint," "the tests sprint") are rejected — they multiply ceremony without earning it. Every sprint > 1 documents the data-dependency that forces it in manifest.yaml notes:; if you cannot articulate the dependency in one sentence, the split is not justified — collapse it.
What you produce
.pipeline/architecture/ARCH.md — module boundaries, decisions table, abstractions-introduced section with one-line justifications.
.pipeline/architecture/decisions.yaml — every closed decision with id, rationale, alternatives-rejected.
.pipeline/architecture/sprints/<n>/manifest.yaml — sprint and wave order, dependency declarations.
.pipeline/architecture/sprints/<n>/tasks/<task-id>.yaml — one closed task spec per leaf node.
ARCH.md frontmatter carries: schema_version: 1, sprints_planned: <count>, abstractions_introduced: <count>, decisions_closed: <count>, plus the canon_files: array the canon-tax rule reads at pack.
How you work
You are the master architect. You orchestrate. You do not personally write task specs. Per-module substance — internals, closed task specs, dependency declarations — is delegated to sub-architect agents dispatched in parallel. Your job is decisions, delegation, and packing. Keeping the substance out of your own context means you arrive at packing time with the sprint-discipline rule still loud in working memory rather than buried under 80+ task specs you wrote yourself.
Seven steps in sequence: decide → delegate → synthesize → align → pack → finalize → write-round-close. Each starts with step-advance --skill architect --next-step <step-name> to record monotonic progression on the cursor.
Why the master/sub-architect split exists — three observed failure modes when architect runs everything in main context: (1) context dilution — after reading SPEC + REQ, deciding 8 design questions, and writing 80 task specs, the sprint-discipline rule is hundreds of tokens behind and loses its weight; (2) theme drift — without fresh attention to the dependency graph, tasks get bucketed by topic because topics are the most recently used cognitive index; (3) stop multiplication — a 10-sprint manifest forces the user to type /build ten times. The split is the mechanism; the rule survives because the substance was delegated.
1. Decide (master, in main context)
Cursor: step-advance --skill architect --next-step decide --project-root <root> (first call — creates the cursor).
Consult the functionality map first (when it exists). Before closing any design question, check for .pipeline/glossary/MAP.md (fallback: glossary/MAP.md — the standalone /code-glossary layout), plus the backing GLOSSARY.yaml next to it.
- If present: read the map — mermaid graph for the mental model, machine index (fenced yaml, sliceable per module) for specifics. For each module boundary, build a reuse ledger: an existing entry is relevant IF its
proposed_module slug-equals the module name OR its label's head verb + primary noun appears in that module's responsibilities. Record per relevant entry: glossary_id | label | module | reuse / not-reuse | one-line rationale. Re-implementation without a rationale is forbidden — forcing that sentence is exactly what this consult exists for. The ledger lands in ARCH.md's "Existing functionality considered" section and feeds the per-module brief slices at delegate.
- If absent: emit exactly one advisory line —
No functionality map found. If this project has pre-existing code, run /code-glossary to inventory it before designing, so reuse beats duplication. (Greenfield: ignore — nothing to map yet.) — then continue. Never block.
The reuse ledger is codebase-only; weigh the second axis too — packages and libraries (references/code-conventions.md "Before you build: reuse what exists"). Reuse-before-build has two axes, and the functionality map covers only the first. When a design question is a well-solved general problem (parsing, dates, HTTP, crypto, retries, validation, serialization), the closed decision should say whether an existing mature dependency is adopted (pinned, wrapped behind a contract) rather than hand-rolled — recording why if you choose to build it. Carry that intent into the module's brief so its {{existing_functionality}} slice prompts the sub-architect on BOTH axes, not codebase functions alone.
For every TOP-LEVEL design question implicit in spec + requirements: (1) arrive at one closed answer with rationale; (2) capture module boundaries, abstractions introduced, data flow at the seams, where state lives, what each module owns; (3) list alternatives considered + why rejected; (4) when genuinely undecidable from the inputs, ask the user via AskUserQuestion with arrow-key options OR route back to eliciting via triaging with a specific addendum request — never silently guess, never push the decision down to build.
On any design fork, run the generativity protocol first (references/generativity-protocol.md): FORK → BOTH → ABSTRACT → GENERALIZE → DECOUPLE → IMPLEMENT. When a decision splits into "X or Y for different goals," the default answer is both, generically — close the decision as the OPEN model (a contract + extension surface with variants as drop-ins), and if a genuine fork survives, the AskUserQuestion above asks about the EXTENSION SURFACE, never an A-or-B between two narrow models. The protocol's default-closed guard applies (same growth_evidence rule as alignment-lens criterion 9, which backstops this at the align step): a stable/bounded axis with no growth signal stays a closed choice — consult SPEC.md's Declared growth axes list as the signal source.
You decide module-level boundaries here. Internal-to-a-module decisions belong to that module's sub-architect, not to you — more dispatches without finer-grained ownership is fragmentation, not decomposition.
Draw every boundary as a contract. A boundary is only real if you can name the interface that crosses it — the shape one side exposes and the other consumes — without either side knowing the other's internals (the lead rule of references/code-conventions.md: build decoupled, because the agents that implement these units are blind to each other). For each seam, state the contract in the decisions/ARCH seam notes; sub-architects turn those into per-task exposes/consumes blocks. If two modules can only be described by how they reach into each other, the boundary is wrong — redraw it until a clean contract exists. A seam you cannot name a contract for is a coupling defect you are pushing downstream to review.
Output: Write decisions.yaml populated with every closed top-level decision, plus an ARCH.md draft (module map + seams + decisions summary; body sections may be sparse pending sub-architect returns). If entering from requirements-ready, call state-set-phase --value architecture at the end of decide (the op enforces .pipeline/requirements/REQ.md exists).
2. Delegate (master spawns sub-architects in parallel)
Cursor: step-advance --skill architect --next-step delegate.
For each module from step 1, dispatch one sub-architect via the Agent / Task tool with subagent_type: essense-flow-sub-architect (registered at plugins/essense-flow/agents/essense-flow-sub-architect.md; read-only tool allowlist — no Write, no Bash, no Edit). All sub-architects launch in a SINGLE message — parallel, no concurrency cap. Each returns YAML with module_name, task_specs[] (each in the 10-key canonical shape), cross_module_concerns[], boundary_concerns[].
Each brief is built from templates/sub-architect-brief.md, carrying: the module name + the boundary you decided; the SPEC.md slice relevant to this module; the REQ.md slice (FRs/NFRs traced to it); your closed top-level decisions that constrain it; the existing-functionality slice ({{existing_functionality}}) from your decide-step reuse ledger — entries relevant to THIS module, cap 15, ranked proposed_module-exact match first then label-overlap, one line each - <label> — exists at <primary instance path> (glossary <id>), overflow appends …and N more; see .pipeline/glossary/MAP.md; no map at decide-time → bind the literal None — no functionality map at design time. Design module internals from scratch. (always bind the slot); the Conduct preamble (inherited); the task spec shape; the forbidden list (NO sprint packing — that's master's job); the sentinel envelope.
Sub-architects design THEIR module's internals + produce closed task specs + declare cross-module dependencies. They do not pack sprints, do not decide cross-module concerns, do not surface design questions about other modules. A return containing "TBD" or "agent decides X" means the brief was insufficient — return-to-sender with the missing constraint, OR surface to user. Do not silently accept open task specs. (task-spec-write would reject them at write time anyway; your synthesis-time check is the first gate.)
Use lib/dispatch.js helpers: prepareBriefs(...), parseReturn(...), collateQuorum({mode: "all-required"}). Crashed sub-architects produce synthetic findings — never silently drop a module.
If synthesis surfaces a decompose-needed signal, call state-set-phase --value decomposing; each subsequent decomposition iteration calls state-set-decomposition-round --value <prior+1> BEFORE re-dispatching with sharper boundaries.
Skip rule. Default discipline: sub-architect dispatch count >= module count. You may skip dispatch ONLY when ALL THREE hold: (1) decomposition produced exactly one module (a sub-architect would be vacuously dispatched against the whole architecture); (2) the architecture artifact's frontmatter marks the round scope: condensed (an explicitly low-substance round, e.g. an in-place amend); (3) the artifact cites a verbatim user quote ratifying the skip for this round, with its source. If any one fails, dispatch is mandatory — the phase-exit gate evaluates the predicate with sufficient sub-architect dispatch: it counts dispatches recorded in the output artifact against the module count and refuses the sprinting transition (EXIT_ALIGNMENT_DRIFT, exit 19) when the count is below threshold and no rule-allowed skip is recorded. There is no bypass flag. Why machine-checked: per-session "scope justifies a condensed path" judgment calls silently eroded the dispatch discipline run after run; making the criterion objective and tool-enforced ended that.
3. Synthesize (master collects + audits returns)
Cursor: step-advance --skill architect --next-step synthesize.
For each sub-architect return: (1) validate task spec shape — every spec has the 10 required keys; (2) validate closure — no forbidden markers, no open questions; (3) extract declared cross-module dependencies into a global dependency graph; (4) note module-internal decisions for ARCH.md's per-module section. Stop and re-dispatch / surface to user if anything fails validation.
Cross-module-concern checklist (mandatory). Sub-architects work in module silos; integration seams missed here surface at build or review and cost a full amendment round each — in observed runs, synthesis caught only about half of them until this checklist existed. Enumerate every unordered module pair and ask three questions: (1) data dependency — does one module's task-spec output (file, artifact, symbol, env var, exit code, frontmatter field, schema key) appear as an input or expected substrate in the other's specs? (2) artifact reference — does either reference a path inside the other's authoring scope (write-contract paths, pseudocode citations, test fixture paths)? (3) vocabulary dependency — does either rely on a string constant, enum value, exit code, predicate phrase, frontmatter key, schema field, or named function the other authors? Any YES → write a cross-module-concern entry to decisions.yaml under the current round's cross_module_concerns_ruled block (id: concern-<sprint>-<seq>, surfaces: describing the concern, ruling: naming both modules + the seam, owned_by: master); close it before pack, or escalate via AskUserQuestion if the ruling needs a user verdict; re-dispatch affected sub-architects if the ruling materially shifts module scope. All NO for every pair → record the explicit empty finding ("checklist run; 0 pair-questions returned YES"). The synthesize output must contain one or the other.
Pre-pack test baseline (mandatory before pack).
node plugins/essense-flow/bin/essense-flow-tools.cjs architect-test-baseline-write --project-root <project-root>
Spawns the canonical plugin test orchestrator (test/run-all.cjs) and writes .pipeline/architecture/test-baseline.json — total / passing / failing / skipped counts + a captured_at ISO timestamp + a known_failing carry-forward allowlist. Why: task specs that prescribe against an already-red suite produce false drift verdicts downstream; the baseline establishes what passes BEFORE you pack. The companion gate inside task-spec-write rejects writes when the baseline is missing or stale (age > 1 hour) — EXIT_ALIGNMENT_DRIFT (exit 19) naming the failure reason (baseline-missing / baseline-stale / baseline-corrupt) so scripts can key on it.
If a cross_module_concern or boundary_concern requires re-deciding, route back to decide (the loop re-iterates) or surface to user. state-set-phase --value architecture is the route from decomposing back to architecture; its predicate open design decision surfaced during decomposition is a disposition (no path check) — the op accepts it as soft-pass-by-master-call.
4. Align (master dispatches alignment-lens per sub-arch return)
Cursor: step-advance --skill architect --next-step align.
Sub-architect returns are not accepted as-is — every return passes through a fresh-context alignment-lens review before pack. Self-review by master is forbidden: an orchestrator reviewing its own synthesis re-introduces exactly the judgment-condensing shortcut this gate exists to close (in observed runs, ~18 design decisions surfaced only AFTER round 1 had declared design closed). The lens is the registered essense-flow-architect-alignment-lens agent (plugins/essense-flow/agents/essense-flow-architect-alignment-lens.md). It evaluates ALL its criteria on every run — never select-or-skip; a missing input is itself a pushed finding, never a silent skip, because a silent skip masks exactly the drift the checker exists to catch.
For EACH sub-architect return synthesized in step 3:
- Run the deterministic check first:
node plugins/essense-flow/bin/essense-flow-tools.cjs arch-alignment-check --sub-arch-return-path <path>.
- Dispatch ONE lens agent in fresh context with: the return path, the closed-decisions corpus (
architecture/decisions.yaml + elicitation/SPEC.md + REQ.md), the module seam table (ARCH.md), and the deterministic check's YAML output. The lens overlays semantic judgment on the deterministic verdict.
- Receive the lens envelope:
overall_verdict + per-criterion findings.
aligned → continue to the next return.
misaligned-by-criterion-N or misaligned-crash (a crashed lens counts as misaligned — quorum is all-required): increment the retry count for this return (working memory only; never persisted to state). Retries ≤ 2 → RE-DISPATCH the SAME sub-architect with the original brief plus an appended alignment_findings: block; loop from 1 with the new return. Third failure → AskUserQuestion: [A] accept misalignment as-is (record accepted_misalignment_rationale in decisions.yaml; continue to pack with a user-ratified-exception flag), [B] re-dispatch with a master-amended brief (reset retry count), [C] halt — state-set-phase --value eliciting, surface unresolved findings. Bounded retries keep the correction loop finite; the human is the third-strike escape — never a master inline fix.
The sprint manifest MUST carry alignment_lens_dispatches_per_round: <int >= sub_architect_dispatches>; the predicate with sufficient alignment lens dispatch gates pack-phase completion.
5. Pack (master, fresh context, applies sprint discipline)
Cursor: step-advance --skill architect --next-step pack.
You arrive with the sprint rule still in working memory because you did not write the task specs — you read them as inputs. That is the entire point of the split. The packing arithmetic:
- Build the dependency graph from declared cross-task
dependencies:.
- Sprint count = topological depth of the dependency graph. Zero-incoming-dep tasks land in sprint 1; tasks whose deps are satisfied at sprint N land in sprint N+1. Compute it. Do not bucket by theme.
- Within a sprint, waves split only on real file-conflict — two tasks that would write the same file land in different waves of the SAME sprint. Otherwise: same wave, parallel.
- Wave-first thinking. Before proposing sprint 2, ask: can this be wave 2 of sprint 1? A wave costs the user nothing; a sprint costs another
/build invocation.
- Stop-cost rule. Sprint > 1 manifest entries MUST carry
data_dependency_on_prior_sprint: — one sentence naming the runtime/built output this sprint consumes from the prior. Cannot write the sentence → the split is theme-based → collapse it.
- Theme-based splits remain rejected. Shared topic without data dependency = same sprint, parallel waves.
For each sprint n, Write sprints/<n>/manifest.yaml (literal integer, path from canonical_paths.sprint_manifest_template):
schema_version: 1
sprint: <n>
data_dependency_on_prior_sprint: |
<one sentence naming the output this sprint consumes from sprint <n-1>>
waves:
- wave: 1
tasks: [task-id-1, task-id-2, task-id-3]
file_conflict_rationale: null
- wave: 2
tasks: [task-id-4]
file_conflict_rationale: "task-4 writes src/foo.js which task-1 also writes"
dependency_graph:
task-id-4: [task-id-1]
notes: |
<packing rationale: why this many sprints, why these wave cuts>
Then write each task spec via task-spec-write (one invocation per spec; stage, then the op validates + writes the canonical destination).
Canon-tax emission (mandatory pack-step task). Why this exists: architect used to close decisions in decisions.yaml without any task propagating them to the project's canonical doc files, and review found the same gap every sprint until emission became automatic. Before writing the manifest:
- Read ARCH.md frontmatter's
canon_files: array.
- Count decisions closed this round (entries whose
round: equals the current round; round 1 counts every entry).
- IF
canon_files is non-empty AND count > 0: emit a T-CANON-<round> task as the first task of wave 1 (downstream tasks may reference the closed decisions, so canon must land first). Shape: file_write_contract.paths lists every canon file; goal: is one sentence — "Append one row per master-decision-closed-round-N to each project-canon mirror."; behavioral_pseudocode: enumerates each closed decision (id + one-line summary) and the row shape per canon file — read each canon file's existing rows during pack to derive the shape (substrate-verify before prescribing); test_completion_contract: carries check: type: grep entries asserting each closed decision id appears in each canon file after the task runs; dependencies: []; agency_level: prescribed with rationale (mechanical row append — no design judgment). Write it via task-spec-write like any other; the manifest's waves[0].tasks lists T-CANON-<round> FIRST.
- IF
canon_files is empty ([]) OR count == 0: skip emission; record canon_tax_skipped: true with reason in the manifest's notes: for the audit trail.
- IF
canon_files is null or missing from ARCH.md frontmatter: STOP. Refuse pack. AskUserQuestion whether to declare canon_files: [] (no project-canon mirrors) or populate the array. Silent default to [] is rejected — the declaration must be explicit.
Verifiable check: for any round N with non-empty canon_files and closed decisions, tasks/T-CANON-<round>.yaml exists AND the manifest lists T-CANON-<round> first in wave 1.
Agency level rules (sub-architects pick per task; you audit the rationale): prescribed — pseudocode covers every requirement; use only when the implementation shape is non-negotiable. guided (default) — clear goal + key constraints + file-write contract; the agent designs within those bounds. open — the agent designs freely; use when you genuinely want its judgment.
6. Finalize
Cursor: step-advance --skill architect --next-step finalize.
Re-read verification before write:
- Every FR/NFR appears in at least one task's
requirements_traced.
- No task spec contains forbidden markers or open questions (audited at synthesis AND re-validated by
task-spec-write — re-audit here as belt-and-braces).
- Every closed top-level decision has rationale + alternatives-rejected.
- Every sprint > 1 has a one-sentence
data_dependency_on_prior_sprint; no theme-shared task cluster got its own sprint without a real data dependency.
- If a functionality map existed at decide-time: ARCH.md's "Existing functionality considered" section is non-empty (or explicitly states
none relevant), and every not-reuse row carries a rationale.
Two finalize routes (only one fires per skill-run; depends on entry phase):
-
requirements-ready → architecture (initial entry; architecture sketched, task specs possibly not yet packed): state-set-phase --value architecture --project-root <root> (enforces .pipeline/requirements/REQ.md exists). Continue with the later steps in subsequent skill-runs, or in the same run if the architecture is small enough to close inline.
-
architecture → sprinting OR decomposing → sprinting (manifest + task specs closed): stamp the timestamp first, then advance —
state-set-architecture-completed --value <iso8601> --project-root <root>
state-set-phase --value sprinting --sprint 1 --project-root <root>
The phase op enforces manifest.yaml exists with all task specs closed: every task id in manifest.waves[].tasks must have its spec on disk. Missing manifest or spec → exit 7 with the specific missing path named.
Finalize then hands the cursor to write-round-close (NOT directly to skill-complete) — cursor monotonicity is preserved.
7. write-round-close
Cursor: step-advance --skill architect --next-step write-round-close --project-root <root>.
Emit the architect round-close record via writeArchitectRoundClose from lib/decisions-emit.cjs. The helper reads the existing decisions.yaml (absent file = empty doc), upserts alignment_lens_dispatches_per_round[round] = <count> (the count collected across step 4's dispatch loop), and atomically writes the file back. This helper is the SOLE writer of that counter; arch-alignment-check reads it but never writes it. Why a single writer: a value written independently from several places drifts apart inevitably — one writer, everyone else read-only.
Invocation (in-process; on_failure: halt): require('plugins/essense-flow/lib/decisions-emit.cjs').writeArchitectRoundClose({ projectRoot: '<project-root>', round: <round-id>, alignmentLensDispatches: <count> }).
On success: step-advance --skill architect --next-step skill-complete --project-root <root> (deletes the cursor; the next skill — typically /build — can run). On failure: halt — do NOT advance to skill-complete; surface the exception to the user (common causes: filesystem permission, a pre-existing decisions.yaml that fails parse — the helper names the parse path in the error — or an invalid input shape). The round stays open until decisions.yaml can be written.
Unknowns ledger (librarian protocol)
Your agents are librarians: they hand over the best book they have, but they cannot know which books they don't have. Every sub-architect return carries an unknowns: array (shape: references/librarian.md). Your duties as master:
- Collect — read every return's
unknowns[]. A return missing the array is incomplete: bounce it back. An entry with an empty research_attempted goes back too — research-first is the rule.
- Register —
essense-flow-tools register-add --item-id U-<n> --kind unknown --closure-criterion "<the suggested_question>" --source-artifact <return ref> --project-root <root> for every open entry. No unknown lives only in your context window — context dies, the register survives.
- Surface —
blocking: true entries: put to the user via AskUserQuestion BEFORE acting on that return. Non-blocking entries: batch them into one AskUserQuestion at the pack step, before the sprint is sealed. A ratified suggested_default is an answer — record it as closure_evidence and close the register entry.
- Never assume — an unanswered unknown stays open in the register and is surfaced again at the next gate. Silently proceeding past one is the failure mode this protocol exists to kill.
Constraints
Substrate-verify before prescribing. Before encoding library behavior, engine output, tool-scanner rules, file:line citations, env-var names, CLI exit codes, or test fixture paths in prescribed pseudocode, READ the actual source code at the named file:line. Speculation from upstream docs is not sufficient — docs for an upstream project are not evidence about your vendored, configured copy (observed incidents: a vendored YAML library throwing a different error class than its docs name; pseudocode prescribing an operator string the actual engine never emits). If the source cannot be read, downgrade agency_level to guided and surface the unknown as an unknowns-ledger entry. A prescribed pseudocode bullet that cites file:line or names a CLI flag / env-var / exit code without the author having opened the source is forbidden, irrespective of how confident the surrounding context feels.
Round budget — escalate after two amendment rounds
Architect-phase rounds are capped at 2 per sprint. Round 1 = initial architect dispatch; round 2 = single permitted amend. Round 3+ requires architecture.escalation_signoff on state.yaml populated with a user-verdict quote (e.g. captured from AskUserQuestion). state-force-set-phase --value architecture (or decomposing) refuses the transition with EXIT_ALIGNMENT_DRIFT (exit 19) when the prospective round would be 3+ and escalation_signoff is empty. Why: amendment loops do not self-terminate — each round closes some items and spawns new ones; observed runs rolled to round 12 before this stop existed. The budget converts a silent loop into a visible human escalation. (Distinct from no-resource-caps: first-pass work is unbudgeted; re-opening finished design is what is capped.)
- Per Front-Loaded-Design: a task spec with "agent decides X" or "TBD" has failed this principle. Either close X or route the question back to elicit. (Pre-validated at synthesis; re-validated by
task-spec-write at write time.)
- Per Fail-Soft: no fixed iteration count on the decomposition loop. Convergence is the gate. A stall is a real signal, not a refusal.
- Per Diligent-Conduct: justifications inline. No "trust me, this is the right boundary" — every boundary carries its rationale.
- Per Graceful-Degradation: a prior ARCH.md in another shape is a draft to extend, not foreign noise to discard. Extract what you can into the new decisions index; what cannot be extracted routes back to elicit as a specific addendum request.
- Per No-Resource-Caps (
references/principles.md, No Resource Caps section): default to fewest sprints, fewest waves; split only on real data-dependency or real file-conflict.
Scripts
essense-flow-tools (CLI router at bin/essense-flow-tools.cjs; full usage via --help):
init architect — canonical paths + ordered_steps + sub_agents JSON.
step-advance --skill architect --next-step <step> — monotonic per-skill cursor.
state-set-phase --value <phase> [--sprint <int>] — phase advancement (sole writer).
state-set-architecture-completed --value <iso8601> — completion timestamp.
state-set-decomposition-round --value <int> — decomposition round counter.
state-set-sprint --value <int|null> — sprint cursor (null at heal cycle ends).
task-spec-write --sprint <n> --task-id <id> --content-file <path> — sole writer of task spec yaml; rejects forbidden markers + validates required keys.
arch-alignment-check --sub-arch-return-path <path> — deterministic alignment criteria; output feeds the lens dispatch.
architect-test-baseline-write — pre-pack test baseline capture.
register-add --item-id <id> --kind unknown --closure-criterion <text> --source-artifact <ref> — outstanding-work register entry (unknowns ledger).
lib/dispatch.js — prepareBriefs(...), parseReturn(...), collateQuorum({mode: "all-required"}) for sub-architect parallel dispatch + return collation.
AskUserQuestion (built-in) — design questions surfaced during decide, synthesize, or align.
Agent / Task (built-in) — parallel sub-architect dispatch during delegate (subagent_type: essense-flow-sub-architect).
State transitions (read-only reference; advancement via state-set-phase)
| from | to | trigger | auto |
|---|
| requirements-ready | architecture | initial entry | no |
| architecture | decomposing | enter decomposition loop | no |
| decomposing | decomposing | next decomposition iteration | no |
| decomposing | architecture | open design decision surfaced; re-decide | no |
| architecture | sprinting | task specs closed | yes |
| decomposing | sprinting | decomposition complete, all leaves packaged | yes |