Repository navigation
SPEC-018: Keep one source of truth for the workflow and roles - #21
Merged
TheJisus28 merged 5 commits intoSep 20, 2026
Merged
Conversation
TheJisus28
deleted the
spec/018-keep-one-source-of-truth-for-the-workflow-and
branch
September 20, 2026 17:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Spec: .forge/specs/SPEC-018-keep-one-source-of-truth-for-the-workflow-and/spec.md
Problem
The loop is written down six times —
AGENTS.md,kit/AGENTS.md,docs/workflow.md,kit/machine/WORKFLOW.md, theforge-workskill andthe roles — and they have already drifted.
conductoris a frontmatterfield and a role in
docs/teams.mdand in theblockedmessage, butforge roleslists four roles and none is named conductor.docs/cli.mdsays
forge startcreatesplan.mdandtasks.md; the code only createsthe folder.
forge archivematches Spanish headings (Convenciones propuestas) by literal string in a machine contract.Acceptance criteria
in the binary (
forge workflow,forge roles); the Markdown pages linkto it instead of restating it, and a test fails if a page's state list
diverges.
roles
forge roleslists, and it is used consistently in frontmatter,help text and docs.
docs/cli.mdmatches behaviour: eitherforge startcreatesplan.mdandtasks.md, or the page no longer claims it does.that fails if a second-language alias is relied on.
docs/does not restate the state machine; it links toforge workflow.Contract
Decisions
1.
internal/workflowis the only declaration of the states and theirorder;
forge workflowrenders the table from it.internal/workflow/workflow.gokeepsAll()and thetransitionsslice andgains
Meaning(State) string, the one-line meaning that today lives only inthe Markdown.
kit/machine/WORKFLOW.mdloses its hand-written## Statestable and carries the marker
<!-- forge:states -->where the table was;cmdWorkflowininternal/cli/machine.goreplaces that marker with a tablebuilt from
workflow.All(),workflow.Meaning()andworkflow.WaitingFor(). A missing marker is an error namingkit/machine/WORKFLOW.md, so the source cannot silently lose the table.Discards: the second copy in Markdown (it already drifted:
blockedsaidconductor) and any second state list in Go.2. Every Markdown page stops restating the state list and points at
forge workflow. The loop diagram and the## Statestable are deleted fromdocs/workflow.md, and the loop line is deleted fromAGENTS.md,kit/AGENTS.mdandREADME.md; each says the states and transitions live inthe binary.
docs/workflow.mdkeeps what is explanation — open questions,criteria, hierarchy, coverage, dependencies, drift — but no ordered state
list. The
kit/copy is the source;forge updaterefreshes the planted.claude/and.opencode/copies. Discards: keeping a copy in a page andtesting it for equality (a copy is a second source even when a test guards
it), and deleting
docs/workflow.mdentirely (the rationale is worthkeeping).
3.
kit/machine/roles/is the only list of roles;workflow.WaitingFormay only name a role from it.
forge rolesalready lists the role files,so no role list is added to Go.
WaitingFor(Blocked)changes from"conductor: clear the blocker"to"orchestrator: clear the blocker". Atest walks every state and fails when the word before
:is notanyone,nobody, or a member ofkit.Roles(). Discards: a fifthconductorrole(the driver already exists as
orchestrator) and a Go role-name constantthat could drift from the files.
4. The driver is
orchestratoreverywhere; theconductorfrontmatter keyis retired.
project.Spec.ConductorbecomesSpec.Orchestrator;project.Savewritesorchestratorand deletes a legacyconductor;FromDocreadsorchestratorand falls back toconductor, so specswritten before this change keep their recorded actor.
cmdStart's--byhelp in
internal/cli/work.gosays "who orchestrates this spec";internal/view/view.goprintsorchestratorinBriefandDetail;docs/teams.md,docs/cli.mdand the release step inAGENTS.mdsayorchestrator. Delivered
.forge/specs/history and.forge/decisions/records are left as written; decision 0002's rule is unchanged by the role's
name. Discards: keeping
conductoras a second name (AC2 asks for one) andrenaming the existing
orchestratorrole instead (that would touch everyhost adapter and leave old history lines contradicting the new name).
Expensive, hard to reverse: it renames a frontmatter key, so any tool
that reads
conductormust move toorchestrator; the fallback keeps oldrecords readable until their next save, and no migration command is added.
5.
docs/cli.mdmatchesforge start; the command is not changed.forge startrecords the orchestrator and@contractfingerprints and doesnot create
plan.mdortasks.md; those are written during planning, afterforge approve. Theforge startparagraph is corrected to say so.Discards: making
forge startcreate emptyplan.md/tasks.md(artifactsbefore approval, and
forge validateexpects them only atimplementing).6. The CLI reads English section headings only.
project.Criteria,project.Contractandproject.OpenQuestionsstop falling back toCriterios de aceptación,ContratoandPreguntas abiertas;pendingConventionsininternal/cli/work.gostops readingConvenciones propuestas.working_languagestill governs the prose inside a spec,decision or convention; only the
##headings are fixed English(
Acceptance criteria,Contract,Open questions,Proposed conventions,Existing state).docs/customizing.mdis corrected.Discards: the Spanish heading aliases and the claim that both are accepted.
The
- ningunavalueisNoneaccepts is content, not a heading, and isunchanged (SPEC-019, decision 2).
Interfaces other specs build against
internal/workflow:All(),Next(),WaitingFor()keep theirsignatures; new
Meaning(State) string.WaitingForreturns<role>: <action>(oranyone/nobody), and the role token is a name fromkit.Roles(). SPEC-015 (renamespecifying→contracting) and SPEC-016(fast lane) change
All()/transitions/Meaningin this one package;forge workflowand the tests follow with no Markdown edit.forge workflow: printskit/machine/WORKFLOW.mdwith<!-- forge:states -->replaced by the generated## Statestable.kit.Workflow()stays theraw embedded copy; callers that need the rendered text go through
cmdWorkflowor the same renderer ininternal/cli/machine.go.kit.Roles()— the.mdfiles inkit/machine/roles/— is thenormative list;
forge rolesand the "who acts next" column offorge workflowboth derive from it.orchestrator;conductoris read-only legacy andis never written. New readers use
orchestrator.Acceptance criteria,Contract,Open questions,Proposed conventionsandExisting state; headings are nevertranslated.
forge check,criterion coverage) do not touch these surfaces; SPEC-015 and SPEC-016
change contents inside
internal/workflow, not this mechanism.Tests
internal/workflow/workflow_test.go:Meaningreturns a non-empty linefor every state in
All();WaitingFor(Blocked)starts withorchestrator:and never containsconductor.kit/machine_test.go:kit.Workflow()contains<!-- forge:states -->and no
| \proposed`table row; everyWaitingForrole token isanyone,nobody, or inkit.Roles(), andkit.Roles()isarchitect,implementer,orchestrator,reviewer`.internal/cli/machine_test.go:forge workflowlistsworkflow.All()inorder with each state's
MeaningandWaitingFor, and two runs arebyte-identical.
internal/cli/machine_internal_test.gocoversrenderWorkflowreturning an error when the marker is absent.internal/cli/cli_test.go: scanningAGENTS.md,kit/AGENTS.md,README.mdanddocs/*.mdfinds nostate → statechain and no| \state` |row;docs/workflow.mdpoints atforge workflow. Noconductorindocs/*.md,AGENTS.md,kit/AGENTS.md,kit/machine/WORKFLOW.mdorforge rolesoutput (the.forge/records are excluded). Afternew/accept/start --by ana, the spec carriesorchestrator:and noconductor:, andforge status SPEC-001showsorchestrator ana; the spec folder holds onlyspec.md(plan.mdandtasks.mdare absent), anddocs/cli.mdno longer claimsforge startcreates them. A real proposal under## Convenciones propuestasdoes not blockforge archive, while one under## Proposed conventions` does.internal/project/project_test.go: a spec with legacyconductor: analoads
Orchestrator == "ana"and the nextSavewritesorchestrator:and deletes
conductor:;Criteria,ContractandOpenQuestionsareempty for a body with only
## Criterios de aceptación,## Contratoand## Preguntas abiertas.internal/view/view_test.go:Detailprintsorchestrator anafor aspec with an orchestrator.
docs: a test readsdocs/customizing.mdand fails if it still claims theparser accepts
## Criterios de aceptación.Out of scope of this contract
SPEC-016).
forge checkand criterion-to-task/evidence coverage (SPEC-017)..forge/specs/*history or.forge/decisions/*records to say
orchestrator.conductorkey; the read fallback plus thenext-save rewrite is the whole mechanism.
working_language.