@clemrig/factory 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -17
- package/dist/app/prototype.d.ts +15 -0
- package/dist/app/prototype.js +65 -0
- package/dist/app/prototype.js.map +1 -0
- package/dist/app/review.js +3 -2
- package/dist/app/review.js.map +1 -1
- package/dist/app/spec-page.d.ts +15 -2
- package/dist/app/spec-page.js +45 -10
- package/dist/app/spec-page.js.map +1 -1
- package/dist/app/workflows/issue-save.js +63 -20
- package/dist/app/workflows/issue-save.js.map +1 -1
- package/dist/app/workflows/stages/agents.d.ts +5 -0
- package/dist/app/workflows/stages/agents.js +5 -0
- package/dist/app/workflows/stages/agents.js.map +1 -1
- package/dist/app/workflows/stages/deliver.js +11 -3
- package/dist/app/workflows/stages/deliver.js.map +1 -1
- package/dist/app/workflows/stages/implement.js +8 -2
- package/dist/app/workflows/stages/implement.js.map +1 -1
- package/dist/app/workflows/stages/plan.js +2 -0
- package/dist/app/workflows/stages/plan.js.map +1 -1
- package/dist/app/workflows/stages/prepare.js +15 -1
- package/dist/app/workflows/stages/prepare.js.map +1 -1
- package/dist/app/workflows/stages/review.js +9 -2
- package/dist/app/workflows/stages/review.js.map +1 -1
- package/dist/app/workflows/stages/run.d.ts +14 -1
- package/dist/app/workflows/stages/run.js.map +1 -1
- package/dist/cli/ui.js +3 -2
- package/dist/cli/ui.js.map +1 -1
- package/dist/domain/pull-request.d.ts +9 -2
- package/dist/domain/pull-request.js +12 -2
- package/dist/domain/pull-request.js.map +1 -1
- package/dist/domain/spec-page.d.ts +73 -17
- package/dist/domain/spec-page.js +93 -6
- package/dist/domain/spec-page.js.map +1 -1
- package/dist/domain/spec.d.ts +13 -1
- package/dist/domain/spec.js +60 -8
- package/dist/domain/spec.js.map +1 -1
- package/dist/domain/tidy.d.ts +2 -1
- package/dist/domain/tidy.js +5 -1
- package/dist/domain/tidy.js.map +1 -1
- package/dist/system/git.d.ts +4 -0
- package/dist/system/git.js +13 -0
- package/dist/system/git.js.map +1 -1
- package/dist/system/images.d.ts +16 -0
- package/dist/system/images.js +51 -0
- package/dist/system/images.js.map +1 -0
- package/dist/system/issues.d.ts +18 -1
- package/dist/system/issues.js +63 -3
- package/dist/system/issues.js.map +1 -1
- package/dist/system/run-files.d.ts +0 -8
- package/dist/system/run-files.js +1 -28
- package/dist/system/run-files.js.map +1 -1
- package/package.json +4 -4
- package/prompts/implement.md +11 -1
- package/prompts/plan.md +18 -6
- package/prompts/review.md +13 -4
- package/templates/init/factory.ts +18 -0
- package/templates/skills/issue/SKILL.md +48 -18
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ factory doctor
|
|
|
34
34
|
prompts/answer.md the prompt of its answer stage
|
|
35
35
|
tsconfig.json editor settings only
|
|
36
36
|
.gitignore node_modules/, runs/, .env, issues/*/spec-answers.json*
|
|
37
|
-
issues/<slug>/ issue.md, spec.md, spec-page.json
|
|
37
|
+
issues/<slug>/ issue.md, spec.md, spec-page.json and spec-page/, plan.md (committed)
|
|
38
38
|
runs/ one folder per run (ignored)
|
|
39
39
|
.env optional secrets (ignored)
|
|
40
40
|
.agents/skills/issue/ the issue skill, read by Codex and Pi
|
|
@@ -46,36 +46,36 @@ The global `factory` command uses the nearest `.factory/` holding a `package.jso
|
|
|
46
46
|
## From an idea to a pull request
|
|
47
47
|
|
|
48
48
|
1. **Backlog.** In Claude Code, Codex or Pi, `/issue` asks for a title and a description, writes a draft outside the repository and saves it with `factory run issue-save --issue <slug> --kind issue --file <draft>`. The command checks the draft and writes `.factory/issues/<slug>/issue.md` itself: the agent chooses neither the path nor the format.
|
|
49
|
-
2. **Specifying.** `/issue` again: the agent reads the code, asks questions until it can fill every section without inventing anything, including the issues this one depends on, and proposes the spec; once the operator agrees, `--kind spec` writes `spec.md`. The agent then presents it, with a mockup for each change that brings an interface, and `--kind page` keeps the presentation as `spec-page.json` (see The spec page). When a run refuses to start because of the spec, `/issue` fixes it from the problems the run listed.
|
|
49
|
+
2. **Specifying.** `/issue` again: the agent reads the code, and the prototype you validated when the need comes from one, asks questions until it can fill every section without inventing anything, including the issues this one depends on, and proposes the spec; once the operator agrees, `--kind spec` writes `spec.md`. The agent then presents it, with a mockup for each change that brings an interface, a capture of the prototype's screen when the prototype shows it, and `--kind page` keeps the presentation as `spec-page.json` (see The spec page). When a run refuses to start because of the spec, `/issue` fixes it from the problems the run listed.
|
|
50
50
|
3. Validate the spec on its page in `factory ui` (the Spec link of its card): the message of the cards you send back goes to `/issue`, which updates the spec and presents it again. Then commit the spec and its presentation and push them on the main branch.
|
|
51
51
|
4. **Executing.** `factory run implement --issue <slug>`: starting the run approves the spec. It runs without the operator; follow it in `factory ui`.
|
|
52
52
|
5. **To Approve → Done.** Review and merge the pull request. After `git pull`, `plan.md` is on the main branch: the issue is done. Its folder stays.
|
|
53
53
|
|
|
54
54
|
A failed run stops; past `prepare`, it leaves the repository on the branch `<slug>`, with the agent's changes, for inspection. When the cause has passed (a timeout, the budget, the network, a Git hook), or when the checks or the review stopped it at their maximum of corrections and you grant one more round, `factory resume <RUN_ID>` starts the stopped stage again (each resumption allows that loop one more correction), its agent in the same conversation: the repository must still be on `<slug>` at the commit where the run stopped (the plan's commit, or the starting commit before it), with its changes in progress and none in `.factory/`; a delivery that stopped after its commit resumes once `git reset --soft <plan commit>` puts the changes back in progress, as the refusal says, a run whose pull request is recorded in its state, stopped once its delivery was done, does not resume, and neither does, past `prepare`, a run started with an earlier version of the factory. Otherwise, clean up, fix what is needed (often the spec, committed and pushed again) and start a new run: it starts again from the main branch, recreates the branch and replaces the pull request's content. Requested changes on a pull request are fixed the same way, or by hand on the branch. An issue is abandoned by deleting its folder (and closing its pull request, if any).
|
|
55
55
|
|
|
56
|
-
`issue-save` lists every problem of a draft's format at once, then, for a valid spec, its dependencies that are no issue, and for a presentation, what does not present the saved spec; it refuses a spec for an issue that has no `issue.md` and a presentation for an issue whose `spec.md` is missing or invalid, replaces the file if it exists and commits nothing; each save is a run. The skill tells the agent never to write in `.factory/issues/` directly, commit or run `implement`. Pi reads a project's skills once the project is trusted; Codex ignores a `SKILL.md` that is itself a link, hence the folder link.
|
|
56
|
+
`issue-save` lists every problem of a draft's format at once, then, for a valid spec, its dependencies that are no issue and a prototype `origin` does not hold, and for a presentation, what does not present the saved spec and the captures that are no image; it refuses a spec for an issue that has no `issue.md` and a presentation for an issue whose `spec.md` is missing or invalid, replaces the file if it exists and commits nothing; each save is a run. The skill tells the agent never to write in `.factory/issues/` directly, commit or run `implement`. Pi reads a project's skills once the project is trusted; Codex ignores a `SKILL.md` that is itself a link, hence the folder link.
|
|
57
57
|
|
|
58
58
|
### Issues
|
|
59
59
|
|
|
60
60
|
An issue is a folder `.factory/issues/<slug>/` holding at least one of the files below (any other folder there is ignored), identified by its slug alone: lowercase letters and digits in words separated by hyphens, which also names its branch and its `--issue` input.
|
|
61
61
|
|
|
62
62
|
- `issue.md`: a `# ` title, then a description.
|
|
63
|
-
- `spec.md`: a `# ` title, then exactly `## Objective`, `## Scope`, `## Out of scope`, `## Acceptance criteria`, `## Constraints` and `## Dependencies`, in this order, with nothing before the first and none empty (`None.`). Acceptance criteria holds only `- AC<n>: <criterion>` items numbered from AC1 without gaps; an indented line continues an item. Dependencies holds only `- <slug>` items, each another issue of the repository that must be delivered first, or `None.`. The spec says what and why, not how; headings stay in English, the content may be in any language. `issue-save` and `implement` read it the same way; `issue-save` also refuses a dependency that is no issue of the repository.
|
|
63
|
+
- `spec.md`: a `# ` title, then exactly `## Objective`, `## Scope`, `## Out of scope`, `## Acceptance criteria`, `## Constraints` and `## Dependencies`, in this order, with nothing before the first and none empty (`None.`). Acceptance criteria holds only `- AC<n>: <criterion>` items numbered from AC1 without gaps; an indented line continues an item. Dependencies holds only `- <slug>` items, each another issue of the repository that must be delivered first, or `None.`. A spec that draws on a prototype ends with `## Prototype`: `- Branch: <branch>` then `- Commit: <full commit id>`, the commit you validated, as `git rev-parse` prints it, with the prototype's notes, on a branch named with a slash such as `prototype/<slug>` (no run takes such a branch), which `origin` must hold, never rewritten (commits on top are fine), until the issue is delivered; a commit the main branch already has is refused; a spec without a prototype leaves the section out. The spec says what and why, not how; headings stay in English, the content may be in any language. `issue-save` and `implement` read it the same way; `issue-save` also refuses a dependency that is no issue of the repository, and a prototype whose branch `origin` does not have with that commit (it reads the local copy of `origin`'s branch, and fetches it only when the commit is missing).
|
|
64
64
|
- `plan.md`: written by the run, in the language of the spec's content: the approach, what the code already does that the change must keep (Existing), steps that each name the criteria they serve, their files, the cases they must handle besides their main path and their tests, how it read the spec where the spec reads more than one way, and the risks.
|
|
65
65
|
|
|
66
|
-
Beside `spec.md`, `spec-page.json` keeps the presentation of the spec for its page, as `issue-save` checked it, with the spec it presents word for word (see The spec page), and `spec-answers.json` where the operator is on that page, forgotten when a new presentation is saved; Git ignores the latter. A repository initialized before factory 0.20.0 adds `issues/*/spec-answers.json*` to its `.factory/.gitignore`: otherwise the answers are changes in progress, and `implement` refuses to start.
|
|
66
|
+
Beside `spec.md`, `spec-page.json` keeps the presentation of the spec for its page, as `issue-save` checked it, with the spec it presents word for word (see The spec page), `spec-page/` the images of its captures, replaced with each presentation, and `spec-answers.json` where the operator is on that page, forgotten when a new presentation is saved; Git ignores the latter. A repository initialized before factory 0.20.0 adds `issues/*/spec-answers.json*` to its `.factory/.gitignore`: otherwise the answers are changes in progress, and `implement` refuses to start.
|
|
67
67
|
|
|
68
68
|
### The `implement` run
|
|
69
69
|
|
|
70
70
|
| Stage | What it does |
|
|
71
71
|
|---|---|
|
|
72
|
-
| `prepare` | Refuses to start, listing every reason, unless the repository is on the main branch without any change in progress, the main branch matches `origin` after `git fetch`, `spec.md` is valid (otherwise it says to fix it with `/issue <slug>`), each of its dependencies is delivered (its `plan.md` is on the main branch), the issue has no `plan.md` (an issue already delivered is never restarted), its slug does not name the main branch (its branch would replace it), `FACTORY_UI_URL`, when set, is an http or https address, and `gh` is logged in and finds the repository on GitHub; then the repository's checks must pass on the main branch, before any agent; then, under a lock of the working tree (`factory-implement.lock` in its Git directory, held for that step only) and unless the repository moved meanwhile (another run is using it: the run stops), creates (or recreates) the branch `<slug>` from the approved start commit. Branches are read and pushed by their full reference: a tag of the same name never stands in for them |
|
|
73
|
-
| `plan`, `planned` | The `plan` agent receives the slug and
|
|
72
|
+
| `prepare` | Refuses to start, listing every reason, unless the repository is on the main branch without any change in progress, the main branch matches `origin` after `git fetch`, `spec.md` is valid (otherwise it says to fix it with `/issue <slug>`), each of its dependencies is delivered (its `plan.md` is on the main branch), `origin` holds its prototype, when it names one (fetched when missing: the agents that plan, implement and review, and their sub-agents, read the prototype at its commit, with the captures of its screens you validated on the spec page unless that page presents another text of the spec, and the instruction that the spec decides and its code is not to be copied), the issue has no `plan.md` (an issue already delivered is never restarted), its slug does not name the main branch (its branch would replace it), `FACTORY_UI_URL`, when set, is an http or https address, and `gh` is logged in and finds the repository on GitHub; then the repository's checks must pass on the main branch, before any agent; then, under a lock of the working tree (`factory-implement.lock` in its Git directory, held for that step only) and unless the repository moved meanwhile (another run is using it: the run stops), creates (or recreates) the branch `<slug>` from the approved start commit. Branches are read and pushed by their full reference: a tag of the same name never stands in for them |
|
|
73
|
+
| `plan`, `planned` | The `plan` agent receives the slug, the spec and its prototype, when it names one. It first explores the code where the spec changes it (mechanisms, invariants, conventions, test tools), handing each area of a spec that touches several to a sub-agent when its profile allows it, then answers with a plan: what the code already does that the change must keep, and steps with their criteria, the files expected, the situations to handle besides the main path and their tests. Where the spec reads more than one way, or its letter would give a behavior its objective would not want, the plan records the reading it chose, apart from its risks: the pull request asks you to confirm it. The plan says what must be true, not how to write it; the implementing agent may take a better way and records it as a deviation; every criterion must be served by a step, otherwise the plan goes back to it (2 times at most) |
|
|
74
74
|
| `commit-plan` | Writes `plan.md` and commits it alone (`docs: plan <slug>`): the run's reference commit |
|
|
75
|
-
| `implement`, `implemented` | The `implement` agent receives the spec, the plan and the run's browser, in one conversation for the run, and answers with evidence for each criterion and its deviations from the plan. It fixes what the review asks for test first: a test that reproduces the trigger and fails, the fix, then a test for each situation around it that the fix changes; what no test can show, it checks in the browser before and after; the run stops if it committed, switched branches or changed `.factory/`, whatever it answered |
|
|
75
|
+
| `implement`, `implemented` | The `implement` agent receives the spec, its prototype, the plan and the run's browser, in one conversation for the run, and answers with evidence for each criterion and its deviations from the plan. It fixes what the review asks for test first: a test that reproduces the trigger and fails, the fix, then a test for each situation around it that the fix changes; what no test can show, it checks in the browser before and after; the run stops if it committed, switched branches or changed `.factory/`, whatever it answered |
|
|
76
76
|
| `verify` | The repository's checks, in order, a check with `when` only if a file changed since the reference commit matches it; a failure, a timeout or a command that cannot start goes back to `implement` with the command, the cause and the end of its output (3 times at most). What each check wrote is kept in the run's folder, for the review, and a failed check's whole output in `checks/failed-<n>-<name>.log`, the run's `n`th failure, resumptions included, whose path the run journals (over 2 MiB per stream, its end, marked as truncated) |
|
|
77
77
|
| `diff` | Every change since the reference commit, new files included, with each file's added and removed lines; the run stops if it is over 2 MiB. Over 1,500 lines of diff, the change is large: its diff is kept in the run's folder instead of being sent in the review's request. |
|
|
78
|
-
| `review`, `reviewed` | The `review` agent, in a new conversation for each review, receives the spec, the changes (the diff, or for a large change where to read it), the checks and where their output is, and the run's browser, never the implementing agent's answer. It judges each criterion `met` or `not_met`, with its decisive evidence, and gives each finding a nature, only on what it established: a test it ran, the application it tried or a code path it followed to its end; a risk it could not establish is at most a `weak_test`. From the second round on, once it has answered on its own with a review the run can judge (an answer it cannot, such as a note sent while its sub-agents still run, goes back to it as an invalid review), it receives in the same conversation the previous review's unmet criteria and findings and checks them itself, without sub-agents: it reads and runs the tests of each fix and answers on them alone, with each criterion judged again, the findings a faithful test proves fixed (one that reproduces the trigger, would fail without the fix and covers the situations around it), those its first answer reports again, and what the fixes broke (a fix that breaks what worked is a `regression`). The run keeps its first answer whole and adds that check to it: a previous finding stays as it was until its fix is proven, unless the first answer reports it again with a nature that blocks as much, and a criterion is met only when both answers say so. Every review judged is kept in the run. An unmet criterion or a blocking finding goes back to `implement` (4 times at most) with the other findings, to fix as well when that is safe, and the implementing agent fixes the mechanism that let a defect happen, wherever the change has it, rather than adding a guard per case |
|
|
78
|
+
| `review`, `reviewed` | The `review` agent, in a new conversation for each review, receives the spec, its prototype, the changes (the diff, or for a large change where to read it), the checks and where their output is, and the run's browser, never the implementing agent's answer. It judges each criterion `met` or `not_met`, with its decisive evidence, and gives each finding a nature, only on what it established: a test it ran, the application it tried or a code path it followed to its end; a risk it could not establish is at most a `weak_test`. From the second round on, once it has answered on its own with a review the run can judge (an answer it cannot, such as a note sent while its sub-agents still run, goes back to it as an invalid review), it receives in the same conversation the previous review's unmet criteria and findings and checks them itself, without sub-agents: it reads and runs the tests of each fix and answers on them alone, with each criterion judged again, the findings a faithful test proves fixed (one that reproduces the trigger, would fail without the fix and covers the situations around it), those its first answer reports again, and what the fixes broke (a fix that breaks what worked is a `regression`). The run keeps its first answer whole and adds that check to it: a previous finding stays as it was until its fix is proven, unless the first answer reports it again with a nature that blocks as much, and a criterion is met only when both answers say so. Every review judged is kept in the run. An unmet criterion or a blocking finding goes back to `implement` (4 times at most) with the other findings, to fix as well when that is safe, and the implementing agent fixes the mechanism that let a defect happen, wherever the change has it, rather than adding a guard per case |
|
|
79
79
|
| `describe`, `described` | Once the change is approved, the `describe` agent explains it for the review page (see below), takes its screenshots, and writes the decisions and the checks only a person can do. It receives the spec, the plan, its interpretations numbered `I1`…, the changes, the checks, the approved review, the deviations from the plan numbered `D1`…, the screenshots folder and the run's browser, never the implementing agent's summary. Its guide must name changed files only, each once, its screenshots must be distinct images of the folder, 10 MB at most, and each decision must name what exists: a criterion of the spec, an interpretation or a deviation, a changed file, lines the diff changes (60 at most), screenshots of the description; otherwise its answer goes back to it (2 times at most) |
|
|
80
80
|
| `deliver` | Commits the changes with the plan's conventional commit subject, force-pushes `<slug>` to `origin`, updates the branch's open pull request or opens one with `gh`, then returns to the main branch |
|
|
81
81
|
|
|
@@ -109,11 +109,11 @@ The `implement`, `review` and `describe` agents check a change that shows in a w
|
|
|
109
109
|
|
|
110
110
|
#### The pull request
|
|
111
111
|
|
|
112
|
-
Its title is the commit subject. Its body is short: a warning first when the operator delivered the change despite its review (see below), a caution when merging is a one-way door (reverting does not walk it back: a migration that transforms data, a published contract, an external effect), then the link to the change's review page with what there is to decide, and a table at a glance: the files changed with their lines, links to the spec at the starting commit and to `plan.md`, the run's ID; the door and the blast radius; the criteria met, the checks and the screenshots; how the review ended and what it left. The link is `FACTORY_UI_URL` followed by `review?run=<RUN_ID>`; without it, the body says to open `/review?run=<RUN_ID>` in `factory ui` where the run ran.
|
|
112
|
+
Its title is the commit subject. Its body is short: a warning first when the operator delivered the change despite its review (see below), a caution when merging is a one-way door (reverting does not walk it back: a migration that transforms data, a published contract, an external effect), then the link to the change's review page with what there is to decide, and a table at a glance: the files changed with their lines, links to the spec at the starting commit and to `plan.md`, the run's ID; the door and the blast radius; the criteria met, the checks and the screenshots; how the review ended and what it left; last, when the spec drew on a prototype, what to do with it once merged: remove its branch (the command is given) and, where it was made, its working tree, or keep it while the spec of another issue still to deliver draws on it, as the main branch has the specs. The link is `FACTORY_UI_URL` followed by `review?run=<RUN_ID>`; without it, the body says to open `/review?run=<RUN_ID>` in `factory ui` where the run ran.
|
|
113
113
|
|
|
114
114
|
#### The spec page
|
|
115
115
|
|
|
116
|
-
`factory ui` serves, for each issue whose spec is presented, a page to validate it before any run, linked from the issue's card: a deck of cards, with the same piles, drags and keys as the review page below. The agent of `/issue` writes the presentation in plain words, in the spec's language: the need; a card per change, with a mockup when it brings an interface
|
|
116
|
+
`factory ui` serves, for each issue whose spec is presented, a page to validate it before any run, linked from the issue's card: a deck of cards, with the same piles, drags and keys as the review page below. The agent of `/issue` writes the presentation in plain words, in the spec's language: the need; a card per change, with a mockup when it brings an interface, shown at the card's width and at full size on a click: a capture of the screen in the spec's prototype (a PNG, JPEG or WebP image of 2 MiB at most, not a link, which the draft gives by its absolute path and `issue-save` keeps in `spec-page/`, each refusal with its reason), or else a whole HTML document drawn at a given size, in a frame without script, network or anything but its own HTML and inline styles; a card per choice the spec made, with the options it weighed and the one it took, where taking another sends the card back; a card per group of acceptance criteria, each in plain words with how it will be checked; and what the spec leaves out. `issue-save` checks that it presents every criterion of the saved spec once and that each choice's options hold, and keeps the spec it presents: once the spec changes, the page says it is stale. The need card says which prototype and commit the spec draws on, when it names one. The verdict is spec validated, with the command to run next, or changes requested, with a message made of the cards you sent back, your notes and the options you took instead, to give to the agent. Each card but the choices and the verdict opens the spec's own words. Your answers are kept in `.factory/issues/<slug>/spec-answers.json`, so that another device resumes them. Validating gates nothing yet: `implement` starts on any valid spec.
|
|
117
117
|
|
|
118
118
|
#### The review page
|
|
119
119
|
|
|
@@ -150,7 +150,7 @@ The pull request is your approval: merge it, or close it, and the scout does not
|
|
|
150
150
|
| To Approve | the last `implement` run completed, and `plan.md` is not on the main branch |
|
|
151
151
|
| Done | `plan.md` is on the main branch, a merged cleanup's too |
|
|
152
152
|
|
|
153
|
-
The files are read in the working tree, the main branch is the local branch `origin/HEAD` names, and runs are those of `.factory/runs/`. A card shows the issue's title (the `# ` line of `issue.md`; its slug without one), its slug, the dependencies of its spec not delivered yet (none once the issue is delivered), the newest pull request its `implement` runs recorded (a new run updates the same one), with a Review link to its review page when the run described it, a Spec link to its spec page when its spec is presented and, for each workflow shown on cards, its last run on the issue: its steps (done, current, failed or to come, each with the corrections that sent the run back into it), its duration and its estimated cost. `implement` shows six steps: prepare, plan, implement, verify, review and deliver (the description included). The title opens the newest of these runs in the runs dashboard. The most recently active issue comes first in its column; the page refreshes every 2 seconds. What may leave the board incomplete is listed above it: a `factory.ts` that does not load (`factory ui` loads it when it starts: restart it once fixed), an unreadable `issue.md` or run, an undelivered issue's `spec.md` that cannot be read or is invalid (its dependencies are then not shown), or a main branch Git cannot read (`origin/HEAD` missing: `git remote set-head origin --auto` sets it).
|
|
153
|
+
The files are read in the working tree, the main branch is the local branch `origin/HEAD` names, and runs are those of `.factory/runs/`. A card shows the issue's title (the `# ` line of `issue.md`; its slug without one), its slug, the dependencies of its spec not delivered yet (none once the issue is delivered), the newest pull request its `implement` runs recorded (a new run updates the same one), with a Review link to its review page when the run described it, a Spec link to its spec page when its spec is presented, and, for each workflow shown on cards, its last run on the issue: its steps (done, current, failed or to come, each with the corrections that sent the run back into it), its duration and its estimated cost. `implement` shows six steps: prepare, plan, implement, verify, review and deliver (the description included). The title opens the newest of these runs in the runs dashboard. The most recently active issue comes first in its column; the page refreshes every 2 seconds. What may leave the board incomplete is listed above it: a `factory.ts` that does not load (`factory ui` loads it when it starts: restart it once fixed), an unreadable `issue.md` or run, an undelivered issue's `spec.md` that cannot be read or is invalid (its dependencies are then not shown), or a main branch Git cannot read (`origin/HEAD` missing: `git remote set-head origin --auto` sets it).
|
|
154
154
|
|
|
155
155
|
## Adapt the factory
|
|
156
156
|
|
|
@@ -162,7 +162,19 @@ import ask from "./workflows/ask/index.ts";
|
|
|
162
162
|
|
|
163
163
|
const minutes = 60_000;
|
|
164
164
|
const opus = { runtime: "claude-code", model: "claude-opus-5-5", thinking: "high" } as const;
|
|
165
|
-
const planner = {
|
|
165
|
+
const planner = {
|
|
166
|
+
...opus,
|
|
167
|
+
tools: ["Read", "Grep", "Glob", "Agent"],
|
|
168
|
+
agents: {
|
|
169
|
+
Explore: {
|
|
170
|
+
description: "Read-only explorer of one area of the code. Give it the area, the questions to answer about it and …",
|
|
171
|
+
prompt: "You explore one area of this repository for an agent that plans a change, …",
|
|
172
|
+
tools: ["Read", "Grep", "Glob"],
|
|
173
|
+
omitClaudeMd: true,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
timeoutMs: 30 * minutes,
|
|
177
|
+
} satisfies AgentProfile;
|
|
166
178
|
const checker = {
|
|
167
179
|
...opus,
|
|
168
180
|
tools: ["Read", "Grep", "Glob", "Bash"],
|
|
@@ -191,7 +203,7 @@ export default defineFactory({
|
|
|
191
203
|
- `verify`: the repository's checks, at least one: `{ name, command, timeoutMs }`, the command an executable and its arguments, run without a shell in the repository. They run in order and stop at the first failure: on the main branch before the run's agents, then after each implementation. A check may add `when`, globs of the files whose change makes it run (`when: ["scripts/*deploy*", "caddy/**"]`): it is for a long validation, such as a rehearsal, that only some changes call for; it never runs on the main branch, and after an implementation only if a file changed since the plan matches it.
|
|
192
204
|
- `maxCostUsd`: the budget of an `implement` run, in USD; `factory run implement --issue <slug> --max-cost-usd 40` changes it for one run. Codex announces no cost, so the agents of `implement` and `tidy` (`plan`, `implement`, `review`, `describe`) cannot run with Codex.
|
|
193
205
|
- `tidy` (optional): `{ maxCostUsd }`, the budget of a `tidy` run, that of `implement` when absent; `--max-cost-usd` changes it for one run.
|
|
194
|
-
- `profiles`: the agents of `plan`, `implement` and `review`, and optionally `describe`, which writes the pull request (the review's profile without it; see Profiles below); `tidy` runs its scout with the `plan` profile. Each profile decides what its agent may do: here, `plan` only reads the repository, handing areas of a large spec to sub-agents (`Agent`); `implement` also edits it and runs commands, and `permissionMode: "dontAsk"` refuses any other tool instead of waiting for a permission prompt; `review` and `describe` read it and run commands, such as tests, reproductions or the application in a browser (they must not change the repository: the run puts back what they change). `Agent` in the review's tools lets it split the review of a large change between sub-agents. Without a way to start the application and a browser, `describe` takes no screenshot and says why.
|
|
206
|
+
- `profiles`: the agents of `plan`, `implement` and `review`, and optionally `describe`, which writes the pull request (the review's profile without it; see Profiles below); `tidy` runs its scout with the `plan` profile. Each profile decides what its agent may do: here, `plan` only reads the repository, handing areas of a large spec to sub-agents (`Agent`), its `Explore` in place of Claude Code's: read-only, reporting facts with their file and symbol and what it did not find, on the planner's model and effort; `implement` also edits it and runs commands, and `permissionMode: "dontAsk"` refuses any other tool instead of waiting for a permission prompt; `review` and `describe` read it and run commands, such as tests, reproductions or the application in a browser (they must not change the repository: the run puts back what they change). `Agent` in the review's tools lets it split the review of a large change between sub-agents. Without a way to start the application and a browser, `describe` takes no screenshot and says why.
|
|
195
207
|
- `app` (optional): how to run the repository's application for the agents that check a change: `{ setup?, start, teardown?, ports?, url }`, `url` a function of its variables that gives its address (see The application above).
|
|
196
208
|
- `workflows` (optional): the repository's own workflows.
|
|
197
209
|
|
|
@@ -302,10 +314,10 @@ Workflows are TypeScript run directly by Node: use erasable syntax only (no `enu
|
|
|
302
314
|
| `model` | model name or alias | model name | model name |
|
|
303
315
|
| `thinking` | effort: `low` … `max` | reasoning effort of the model (`low`, `medium`, `high`…) | `off` … `max` |
|
|
304
316
|
| `tools` | optional: exact names (`Read`, `Grep`, `Bash`…) and `mcp__<server>__<tool>`; limits the built-in tools and lets the listed ones run | — | optional: exact names (`read`, `bash`…); `[]` for none |
|
|
305
|
-
| other | `permissionMode` (a `--permission-mode` choice), `mcpServers: { name: { command, args? } }` | `sandbox`: `read-only`, `workspace-write` or `danger-full-access` | `skills`, `extensions` (paths) |
|
|
317
|
+
| other | `permissionMode` (a `--permission-mode` choice), `mcpServers: { name: { command, args? } }`, `agents: { name: { description, prompt, model?, thinking?, tools?, omitClaudeMd? } }` (sub-agents, see below) | `sandbox`: `read-only`, `workspace-write` or `danger-full-access` | `skills`, `extensions` (paths) |
|
|
306
318
|
| `timeoutMs` | default 15 minutes, no maximum | default 15 minutes, no maximum | default 15 minutes, no maximum |
|
|
307
319
|
|
|
308
|
-
Each agent runs with this machine's and this repository's configuration, as you would run it yourself: Claude Code with its settings, `CLAUDE.md`, plugins, hooks, skills, MCP servers and memory; Codex with its `config.toml`, `AGENTS.md` files, skills and MCP servers; Pi with its extensions, skills, context files and MCP servers. The profile only adds its settings; without `tools`, `permissionMode` or `sandbox`, the agent's own configuration decides. A Claude Code call waits for its agent's sub-agents until they end or the profile's timeout stops the call: print mode alone would stop them after 10 minutes without a turn. The agent's answer ends the call: Claude Code is stopped then, with any command the agent left running in the background, such as the application it started, and the stage's journal counts them; whatever a Claude Code agent's commands leave running, detached with `&` or `nohup` included, ends with the call too. The same workflow can therefore behave differently on another machine, and a repository's own hooks and MCP servers run with its agents. Relative skill, extension and MCP command paths are relative to `.factory/`; a bare command is looked up in the `PATH`. Runtimes are not sandboxes unless one is enabled: their tools act with your rights.
|
|
320
|
+
Each agent runs with this machine's and this repository's configuration, as you would run it yourself: Claude Code with its settings, `CLAUDE.md`, plugins, hooks, skills, MCP servers and memory; Codex with its `config.toml`, `AGENTS.md` files, skills and MCP servers; Pi with its extensions, skills, context files and MCP servers. The profile only adds its settings; without `tools`, `permissionMode` or `sandbox`, the agent's own configuration decides. A Claude Code profile's `agents` defines sub-agents for its sessions, in place of the machine's or the built-in ones of the same name: `Explore`, say, for the code exploration Claude Code would otherwise run at the agent's effort, on the agent's model or, when the agent runs on Fable with a Claude subscription, a Console account or a gateway, on Opus. Each runs on its `model` (the agent's model without one, Fable included) at its `thinking` effort (the agent's when it runs on the agent's model) with its `tools` (those of the agent without a list), and `omitClaudeMd: true` starts it without the user's and the repository's `CLAUDE.md`, as the built-in `Explore` does; the run records them by name. A Claude Code call waits for its agent's sub-agents until they end or the profile's timeout stops the call: print mode alone would stop them after 10 minutes without a turn. The agent's answer ends the call: Claude Code is stopped then, with any command the agent left running in the background, such as the application it started, and the stage's journal counts them; whatever a Claude Code agent's commands leave running, detached with `&` or `nohup` included, ends with the call too. The same workflow can therefore behave differently on another machine, and a repository's own hooks and MCP servers run with its agents. Relative skill, extension and MCP command paths are relative to `.factory/`; a bare command is looked up in the `PATH`. Runtimes are not sandboxes unless one is enabled: their tools act with your rights.
|
|
309
321
|
|
|
310
322
|
**Prompts.** An agent's prompt is `.factory/workflows/<workflow>/prompts/<name>.md`, where the name is the stage's unless `agent({ prompt })` names another; the factory's own workflows read theirs from the package. It is appended to the runtime's system prompt (Codex receives it as the session's `developer_instructions`); the request (context and output JSON Schema) follows on standard input. Prompts are read when a run starts: an edit applies to the next run, without building anything.
|
|
311
323
|
|
|
@@ -333,7 +345,7 @@ The run executes in the foreground. The terminal first shows the launch (flow's
|
|
|
333
345
|
|
|
334
346
|
## Data
|
|
335
347
|
|
|
336
|
-
Issues live in `.factory/issues/` and are committed: their specs and plans are part of the repository's history. Each run keeps `state.json`, `events.jsonl` and, for each agent call, a record in `calls/<n>.json` (the workflow's prompt, the request as the runtime read it, the command with the prompt and the MCP servers' configuration replaced by what names them, and the agent's final text) in `.factory/runs/<RUN_ID>/`, plus its Pi sessions and Claude Code and Codex session markers; Claude Code and Codex keep their transcripts in their own storage (`~/.claude/projects/…`, `~/.codex/sessions/…`). Runs hold inputs, what flow sent the agents, their outputs and the journal: they stay private and are ignored by Git. `.factory/.env` is loaded automatically, without overriding variables already set in the shell; runtimes, MCP servers and commands inherit the environment. `factory run` executes the code of `.factory/`: trust it as you trust `npm run`. The Kanban and the runs dashboard show the same data to any local process that can reach `127.0.0.1`.
|
|
348
|
+
Issues live in `.factory/issues/` and are committed: their specs and plans are part of the repository's history. Each run keeps `state.json`, `events.jsonl` and, for each agent call, a record in `calls/<n>.json` (the workflow's prompt, the request as the runtime read it, the command with the prompt, the sub-agents' definitions and the MCP servers' configuration replaced by what names them, and the agent's final text) in `.factory/runs/<RUN_ID>/`, plus its Pi sessions and Claude Code and Codex session markers; Claude Code and Codex keep their transcripts in their own storage (`~/.claude/projects/…`, `~/.codex/sessions/…`). Runs hold inputs, what flow sent the agents, their outputs and the journal: they stay private and are ignored by Git. `.factory/.env` is loaded automatically, without overriding variables already set in the shell; runtimes, MCP servers and commands inherit the environment. `factory run` executes the code of `.factory/`: trust it as you trust `npm run`. The Kanban and the runs dashboard show the same data to any local process that can reach `127.0.0.1`.
|
|
337
349
|
|
|
338
350
|
Tokens and costs are the figures each runtime announces: Claude Code announces at the end of a call its session's total, earlier calls included, and its tokens during a call are provisional; Codex announces at the end of each turn its thread's total tokens, earlier turns included, without a cost (its input includes the cached tokens, which flow takes out so that input counts the same tokens for every runtime); Pi announces each response with its cost. A run's total is the sum of its sessions, with the cost they announced: a call without a cost makes the cost partial, unknown while no call announced one. The estimated cost is the runtime's public-price estimate, not a bill.
|
|
339
351
|
|
|
@@ -344,6 +356,7 @@ Tokens and costs are the figures each runtime announces: Claude Code announces a
|
|
|
344
356
|
- The repository is used in place: one `implement` run at a time, since it needs a clean tree and stops if another run moved the repository during its starting checks; after a failure, the operator cleans up, or leaves the branch as it is to resume the run. The repository's Git hooks run on the run's commits, and a failing hook fails the run.
|
|
345
357
|
- The factory's own agents (`plan`, `implement`, `review`, `describe`) cannot run on Codex, since `maxCostUsd` is required and Codex announces no cost; the repository's own workflows and the `issue` skill can use Codex.
|
|
346
358
|
- Runtimes are not sandboxes: the implementing agent's tools can run any command its profile allows, bounded only by its prompt, the repository's agent instructions and the run's checks.
|
|
359
|
+
- A prototype's branch is pushed to `origin` like any branch: the repository's CI or preview deployments may run on it.
|
|
347
360
|
- The review page lives on the machine of the run: elsewhere, the pull request shows only its header. A spec page shows the working tree of the machine that serves it: a spec written elsewhere is validated there once pulled. Both are served without sign-in: anyone who reaches `factory ui`, on that machine or through `FACTORY_UI_URL`, can read them and change their answers. Validating a spec does not gate `implement` yet. The factory does not check the Mermaid syntax: the page shows the text of a diagram it cannot draw. Mermaid, built into the page, brings `@clemrig/factory-ui` to about 5.5 MB.
|
|
348
361
|
- Screenshots show the new state only, never the previous one; no videos.
|
|
349
362
|
- `tidy` has no scheduler of its own: cron, launchd or a CI runner starts it. Its scout reads at most 200 recent commits and the latest 100 open and 100 closed pull requests, with the first 100 files of each open one, as `gh` lists them, and a defect it finds is only named in the cleanup's issue.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Prototype } from "../domain/spec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Why origin does not hold the prototype a spec draws on, for every run and
|
|
4
|
+
* every machine: on a branch Git accepts, other than the main one `base`,
|
|
5
|
+
* with the commit the spec names. Origin's copy is read as the repository
|
|
6
|
+
* has it, and fetched only when it lacks that commit: a spec whose prototype
|
|
7
|
+
* was pushed from here saves without reaching origin, and a run's machine
|
|
8
|
+
* gets the commit its agents read.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prototypeProblems(dir: string, { branch, commit }: Prototype, base: string): Promise<string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* The other issues to deliver, as the working tree has them, whose spec
|
|
13
|
+
* draws on prototype branch `branch`: until they are delivered, it stays.
|
|
14
|
+
*/
|
|
15
|
+
export declare function issuesDrawingOn(dir: string, branch: string, except: string): Promise<string[]>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { parseSpec } from "../domain/spec.js";
|
|
2
|
+
import * as git from "../system/git.js";
|
|
3
|
+
import { hasArtifact, listIssueFolders, readArtifact, } from "../system/issues.js";
|
|
4
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
5
|
+
/** Whether origin's copy of `branch`, as the repository has it, holds `commit`, named exactly: no tree, no abbreviation. */
|
|
6
|
+
async function isOnOrigin(dir, branch, commit) {
|
|
7
|
+
const ref = git.remoteRef(branch);
|
|
8
|
+
if ((await git.commitOf(dir, ref).catch(() => undefined)) === undefined)
|
|
9
|
+
return false;
|
|
10
|
+
const found = await git
|
|
11
|
+
.commitOf(dir, `${commit}^{commit}`)
|
|
12
|
+
.catch(() => undefined);
|
|
13
|
+
return found === commit && git.isAncestor(dir, commit, ref);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Why origin does not hold the prototype a spec draws on, for every run and
|
|
17
|
+
* every machine: on a branch Git accepts, other than the main one `base`,
|
|
18
|
+
* with the commit the spec names. Origin's copy is read as the repository
|
|
19
|
+
* has it, and fetched only when it lacks that commit: a spec whose prototype
|
|
20
|
+
* was pushed from here saves without reaching origin, and a run's machine
|
|
21
|
+
* gets the commit its agents read.
|
|
22
|
+
*/
|
|
23
|
+
export async function prototypeProblems(dir, { branch, commit }, base) {
|
|
24
|
+
if (!(await git.isBranchName(dir, branch)))
|
|
25
|
+
return [`Prototype: "${branch}" is not a branch name`];
|
|
26
|
+
if (branch === base)
|
|
27
|
+
return [
|
|
28
|
+
`Prototype: ${branch} is the main branch: keep the prototype on a branch of its own`,
|
|
29
|
+
];
|
|
30
|
+
if (!(await isOnOrigin(dir, branch, commit)))
|
|
31
|
+
try {
|
|
32
|
+
await git.fetchBranch(dir, branch);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return [
|
|
36
|
+
`Prototype: origin/${branch} cannot be fetched: push the prototype's branch, and check that origin can be reached (${message(error)})`,
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
if (!(await isOnOrigin(dir, branch, commit)))
|
|
40
|
+
return [
|
|
41
|
+
`Prototype: commit ${commit.slice(0, 7)} is not on origin/${branch}. The spec names the commit the operator validated: push it, and never rewrite the branch before the issue is delivered; to draw on another commit, have the operator validate it, then save the spec again`,
|
|
42
|
+
];
|
|
43
|
+
// A commit the main branch already has holds nothing of the prototype.
|
|
44
|
+
if (await git.isAncestor(dir, commit, git.remoteRef(base)))
|
|
45
|
+
return [
|
|
46
|
+
`Prototype: commit ${commit.slice(0, 7)} is on ${base}, which holds nothing of the prototype: name the prototype's own commit, the one the operator validated`,
|
|
47
|
+
];
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The other issues to deliver, as the working tree has them, whose spec
|
|
52
|
+
* draws on prototype branch `branch`: until they are delivered, it stays.
|
|
53
|
+
*/
|
|
54
|
+
export async function issuesDrawingOn(dir, branch, except) {
|
|
55
|
+
const drawing = [];
|
|
56
|
+
for (const { slug, hasSpec } of await listIssueFolders(dir)) {
|
|
57
|
+
if (slug === except || !hasSpec || (await hasArtifact(dir, slug, "plan")))
|
|
58
|
+
continue;
|
|
59
|
+
const spec = parseSpec((await readArtifact(dir, slug, "spec")) ?? "", slug);
|
|
60
|
+
if (spec.ok && spec.value.prototype?.branch === branch)
|
|
61
|
+
drawing.push(slug);
|
|
62
|
+
}
|
|
63
|
+
return drawing.sort();
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=prototype.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prototype.js","sourceRoot":"","sources":["../../src/app/prototype.ts"],"names":[],"mappings":"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"}
|
package/dist/app/review.js
CHANGED
|
@@ -3,7 +3,8 @@ import { maxScreenshotBytes } from "../domain/description.js";
|
|
|
3
3
|
import { reviewAnswers, reviewPage, } from "../domain/review-page.js";
|
|
4
4
|
import { runDir } from "../system/factory.js";
|
|
5
5
|
import { fileAnchor } from "../system/github.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readImageIn } from "../system/images.js";
|
|
7
|
+
import { keepAnswers, readAnswers } from "../system/run-files.js";
|
|
7
8
|
import { loadRun } from "./runs.js";
|
|
8
9
|
import { diffOf } from "./workflows/stages/deliver.js";
|
|
9
10
|
import { facts, specCommitOf } from "./workflows/stages/run.js";
|
|
@@ -65,7 +66,7 @@ export async function readReviewScreenshot(factory, id, file) {
|
|
|
65
66
|
const { description, screenshotsDir } = facts(state);
|
|
66
67
|
if (!description.screenshots.some((shot) => shot.file === file))
|
|
67
68
|
return undefined;
|
|
68
|
-
return
|
|
69
|
+
return readImageIn(screenshotsDir, file, maxScreenshotBytes);
|
|
69
70
|
}
|
|
70
71
|
/** Where a person is in the run's review; `undefined` before the first answer, or when what is kept no longer reads. */
|
|
71
72
|
export async function readReviewAnswers(factory, id) {
|
package/dist/app/review.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,YAAY,EAAE,eAAe,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAGL,aAAa,EACb,UAAU,GACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAgB,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"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"}
|
package/dist/app/spec-page.d.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { type SpecAnswers, type SpecPage } from "../domain/spec-page.js";
|
|
2
2
|
import type { Factory } from "../system/factory.js";
|
|
3
|
+
import type { Image } from "../system/images.js";
|
|
3
4
|
/**
|
|
4
5
|
* The page of issue `slug`'s spec, from its presentation; `undefined` when
|
|
5
|
-
* its spec is not presented, an unknown issue included.
|
|
6
|
+
* its spec is not presented, an unknown issue included. `captureUrl` says
|
|
7
|
+
* where the page loads a capture.
|
|
6
8
|
*/
|
|
7
|
-
export declare function readSpecPage(factory: Factory, slug: string): Promise<SpecPage | undefined>;
|
|
9
|
+
export declare function readSpecPage(factory: Factory, slug: string, captureUrl: (file: string) => string): Promise<SpecPage | undefined>;
|
|
10
|
+
/** One of the captures issue `slug`'s presented spec shows; `undefined` for any other file. */
|
|
11
|
+
export declare function readSpecCapture(factory: Factory, slug: string, file: string): Promise<Image | undefined>;
|
|
8
12
|
/** Where the operator is in issue `slug`'s spec validation; `undefined` before the first answer, or when what is kept no longer reads. */
|
|
9
13
|
export declare function readSpecAnswers(factory: Factory, slug: string): Promise<SpecAnswers | undefined>;
|
|
10
14
|
/** Keeps the answers the page sent for issue `slug`'s spec; returns why they are refused, if they are. */
|
|
11
15
|
export declare function saveSpecAnswers(factory: Factory, slug: string, answers: unknown): Promise<string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* The captures of the screens the operator validated on the page of issue
|
|
18
|
+
* `slug`'s spec, `spec`, with their absolute paths: none when its
|
|
19
|
+
* presentation shows another text of it, or no longer reads.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readValidatedCaptures(dir: string, slug: string, spec: string): Promise<{
|
|
22
|
+
caption: string;
|
|
23
|
+
file: string;
|
|
24
|
+
}[]>;
|
package/dist/app/spec-page.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { z } from "@clemrig/flow";
|
|
2
2
|
import { isSlug } from "../domain/issue.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
export async function readSpecPage(factory, slug) {
|
|
3
|
+
import { parseSpec } from "../domain/spec.js";
|
|
4
|
+
import { capturesOf, maxCaptureBytes, validatedCaptures, presentedSpec, specAnswers, specPage, } from "../domain/spec-page.js";
|
|
5
|
+
import { keepSpecAnswers, readArtifact, readSpecAnswers as readKeptAnswers, readSpecCapture as readKeptCapture, readSpecPage as readPresented, specCapturePath, } from "../system/issues.js";
|
|
6
|
+
/** Issue `slug`'s presented spec, as kept; `undefined` when its spec is not presented, an unknown issue included. */
|
|
7
|
+
async function presented(factory, slug) {
|
|
10
8
|
if (!isSlug(slug))
|
|
11
9
|
return undefined;
|
|
12
10
|
const kept = await readPresented(factory.dir, slug);
|
|
@@ -15,18 +13,41 @@ export async function readSpecPage(factory, slug) {
|
|
|
15
13
|
const parsed = presentedSpec.safeParse(kept);
|
|
16
14
|
if (!parsed.success)
|
|
17
15
|
throw new Error(`The presentation of ${slug}'s spec no longer reads:\n${z.prettifyError(parsed.error)}`);
|
|
18
|
-
return
|
|
16
|
+
return parsed.data;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The page of issue `slug`'s spec, from its presentation; `undefined` when
|
|
20
|
+
* its spec is not presented, an unknown issue included. `captureUrl` says
|
|
21
|
+
* where the page loads a capture.
|
|
22
|
+
*/
|
|
23
|
+
export async function readSpecPage(factory, slug, captureUrl) {
|
|
24
|
+
const kept = await presented(factory, slug);
|
|
25
|
+
return (kept &&
|
|
26
|
+
specPage(slug, kept, await readArtifact(factory.dir, slug, "spec"), captureUrl));
|
|
27
|
+
}
|
|
28
|
+
/** Whether issue `slug`'s spec has a page, its presentation of a spec that reads: only then are answers kept. */
|
|
29
|
+
async function hasPage(factory, slug) {
|
|
30
|
+
const kept = await presented(factory, slug);
|
|
31
|
+
return kept !== undefined && parseSpec(kept.spec, slug).ok;
|
|
32
|
+
}
|
|
33
|
+
/** One of the captures issue `slug`'s presented spec shows; `undefined` for any other file. */
|
|
34
|
+
export async function readSpecCapture(factory, slug, file) {
|
|
35
|
+
const kept = await presented(factory, slug);
|
|
36
|
+
if (!kept ||
|
|
37
|
+
!capturesOf(kept.presentation).some(({ image }) => image === file))
|
|
38
|
+
return undefined;
|
|
39
|
+
return readKeptCapture(factory.dir, slug, file, maxCaptureBytes);
|
|
19
40
|
}
|
|
20
41
|
/** Where the operator is in issue `slug`'s spec validation; `undefined` before the first answer, or when what is kept no longer reads. */
|
|
21
42
|
export async function readSpecAnswers(factory, slug) {
|
|
22
|
-
if (!(await
|
|
43
|
+
if (!(await hasPage(factory, slug)))
|
|
23
44
|
return undefined;
|
|
24
45
|
const kept = specAnswers.safeParse(await readKeptAnswers(factory.dir, slug));
|
|
25
46
|
return kept.success ? kept.data : undefined;
|
|
26
47
|
}
|
|
27
48
|
/** Keeps the answers the page sent for issue `slug`'s spec; returns why they are refused, if they are. */
|
|
28
49
|
export async function saveSpecAnswers(factory, slug, answers) {
|
|
29
|
-
if (!(await
|
|
50
|
+
if (!(await hasPage(factory, slug)))
|
|
30
51
|
return `Issue ${slug} has no presented spec`;
|
|
31
52
|
const parsed = specAnswers.safeParse(answers);
|
|
32
53
|
if (!parsed.success)
|
|
@@ -34,4 +55,18 @@ export async function saveSpecAnswers(factory, slug, answers) {
|
|
|
34
55
|
await keepSpecAnswers(factory.dir, slug, parsed.data);
|
|
35
56
|
return undefined;
|
|
36
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The captures of the screens the operator validated on the page of issue
|
|
60
|
+
* `slug`'s spec, `spec`, with their absolute paths: none when its
|
|
61
|
+
* presentation shows another text of it, or no longer reads.
|
|
62
|
+
*/
|
|
63
|
+
export async function readValidatedCaptures(dir, slug, spec) {
|
|
64
|
+
const kept = presentedSpec.safeParse(await readPresented(dir, slug));
|
|
65
|
+
if (!kept.success)
|
|
66
|
+
return [];
|
|
67
|
+
return validatedCaptures(kept.data, spec).map(({ caption, image }) => ({
|
|
68
|
+
caption,
|
|
69
|
+
file: specCapturePath(dir, slug, image),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
37
72
|
//# sourceMappingURL=spec-page.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-page.js","sourceRoot":"","sources":["../../src/app/spec-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EACL,aAAa,EAGb,WAAW,EACX,QAAQ,GACT,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"spec-page.js","sourceRoot":"","sources":["../../src/app/spec-page.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { defineWorkflow, END, node, step, z } from "@clemrig/flow";
|
|
2
2
|
import { isSlug, parseIssue } from "../../domain/issue.js";
|
|
3
3
|
import { parseSpec } from "../../domain/spec.js";
|
|
4
|
-
import { parsePresentation } from "../../domain/spec-page.js";
|
|
4
|
+
import { captureName, capturesOf, maxCaptureBytes, namingCaptures, parsePresentation, } from "../../domain/spec-page.js";
|
|
5
|
+
import { defaultBranch } from "../../system/git.js";
|
|
6
|
+
import { readDraftImage } from "../../system/images.js";
|
|
5
7
|
import { forgetSpecAnswers, hasArtifact, isIssue, readArtifact, readDraft, writeArtifact, writeSpecPage, } from "../../system/issues.js";
|
|
8
|
+
import { prototypeProblems } from "../prototype.js";
|
|
6
9
|
const input = z.object({
|
|
7
10
|
issue: z
|
|
8
11
|
.string()
|
|
@@ -10,7 +13,11 @@ const input = z.object({
|
|
|
10
13
|
kind: z.enum(["issue", "spec", "page"]),
|
|
11
14
|
file: z.string().min(1),
|
|
12
15
|
});
|
|
13
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* What is wrong with a draft of issue.md or spec.md: its format, and for a
|
|
18
|
+
* spec, dependencies that are no issue of the repository and a prototype
|
|
19
|
+
* origin does not hold.
|
|
20
|
+
*/
|
|
14
21
|
async function draftProblems(dir, { issue, kind }, text) {
|
|
15
22
|
if (kind === "issue") {
|
|
16
23
|
const parsed = parseIssue(text);
|
|
@@ -23,12 +30,32 @@ async function draftProblems(dir, { issue, kind }, text) {
|
|
|
23
30
|
for (const dependency of parsed.value.dependencies)
|
|
24
31
|
if (!(await isIssue(dir, dependency)))
|
|
25
32
|
problems.push(`Dependencies: there is no issue ${dependency} in .factory/issues/`);
|
|
33
|
+
if (parsed.value.prototype) {
|
|
34
|
+
let base;
|
|
35
|
+
try {
|
|
36
|
+
base = await defaultBranch(dir);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return [
|
|
40
|
+
...problems,
|
|
41
|
+
`Prototype: the main branch cannot be read, to tell it from the prototype's (origin/HEAD names it: git remote set-head origin --auto sets it): ${error instanceof Error ? error.message : String(error)}`,
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
problems.push(...(await prototypeProblems(dir, parsed.value.prototype, base)));
|
|
45
|
+
}
|
|
26
46
|
return problems;
|
|
27
47
|
}
|
|
48
|
+
function invalidDraft(file, kind, problems) {
|
|
49
|
+
return new Error([
|
|
50
|
+
`Invalid ${kind} draft ${file}:`,
|
|
51
|
+
...problems.map((problem) => `- ${problem}`),
|
|
52
|
+
].join("\n"));
|
|
53
|
+
}
|
|
28
54
|
/**
|
|
29
55
|
* Checks the presentation of issue `issue`'s spec an agent drafted, then
|
|
30
|
-
* keeps it with the spec it presents, word for word, as `spec-page.json
|
|
31
|
-
* the operator's answers on the
|
|
56
|
+
* keeps it with the spec it presents, word for word, as `spec-page.json`,
|
|
57
|
+
* and the images of its captures beside it; the operator's answers on the
|
|
58
|
+
* previous one are forgotten.
|
|
32
59
|
*/
|
|
33
60
|
async function savePresentation(dir, issue, file) {
|
|
34
61
|
const spec = await readArtifact(dir, issue, "spec");
|
|
@@ -39,16 +66,28 @@ async function savePresentation(dir, issue, file) {
|
|
|
39
66
|
throw new Error(`Issue ${issue}'s spec.md is invalid: save a valid spec first`);
|
|
40
67
|
const parsed = parsePresentation(await readDraft(file), parsedSpec.value);
|
|
41
68
|
if (!parsed.ok)
|
|
42
|
-
throw
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
throw invalidDraft(file, "presentation", parsed.problems);
|
|
70
|
+
const problems = [];
|
|
71
|
+
const captures = [];
|
|
72
|
+
for (const { change, image } of capturesOf(parsed.value)) {
|
|
73
|
+
const where = `changes[${change}].mockup.image: ${image}`;
|
|
74
|
+
const read = await readDraftImage(image, maxCaptureBytes);
|
|
75
|
+
if (typeof read === "string")
|
|
76
|
+
problems.push(`${where} ${read}`);
|
|
77
|
+
else {
|
|
78
|
+
const name = captureName(change, read.type);
|
|
79
|
+
if (name)
|
|
80
|
+
captures.push({ change, name, body: read.body });
|
|
81
|
+
else
|
|
82
|
+
problems.push(`${where} is a GIF: a capture is a PNG, JPEG or WebP image`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (problems.length > 0)
|
|
86
|
+
throw invalidDraft(file, "presentation", problems);
|
|
87
|
+
const presentation = namingCaptures(parsed.value, new Map(captures.map(({ change, name }) => [change, name])));
|
|
88
|
+
const saved = await writeSpecPage(dir, issue, { spec, presentation }, captures);
|
|
50
89
|
await forgetSpecAnswers(dir, issue);
|
|
51
|
-
return
|
|
90
|
+
return saved;
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
93
|
* Checks a draft written by an agent, then writes it as the issue's
|
|
@@ -61,21 +100,25 @@ export const issueSave = defineWorkflow({
|
|
|
61
100
|
nodes: {
|
|
62
101
|
save: node(step(async ({ dir, state }) => {
|
|
63
102
|
const { issue, kind, file } = state.input;
|
|
64
|
-
if (kind === "page")
|
|
65
|
-
|
|
103
|
+
if (kind === "page") {
|
|
104
|
+
const { page, captures } = await savePresentation(dir, issue, file);
|
|
105
|
+
return { path: page, ...(captures ? { captures } : {}) };
|
|
106
|
+
}
|
|
66
107
|
if (kind === "spec" && !(await hasArtifact(dir, issue, "issue")))
|
|
67
108
|
throw new Error(`Issue ${issue} does not exist: save its issue.md first`);
|
|
68
109
|
const text = await readDraft(file);
|
|
69
110
|
const problems = await draftProblems(dir, { issue, kind }, text);
|
|
70
111
|
if (problems.length > 0)
|
|
71
|
-
throw
|
|
72
|
-
`Invalid ${kind}.md draft ${file}:`,
|
|
73
|
-
...problems.map((problem) => `- ${problem}`),
|
|
74
|
-
].join("\n"));
|
|
112
|
+
throw invalidDraft(file, `${kind}.md`, problems);
|
|
75
113
|
return { path: await writeArtifact(dir, issue, kind, text) };
|
|
76
114
|
}), { ok: END }),
|
|
77
115
|
},
|
|
78
116
|
// Also called for a failed run, which saved nothing.
|
|
79
|
-
presentResult: ({ data }) =>
|
|
117
|
+
presentResult: ({ data }) => [
|
|
118
|
+
...(typeof data.path === "string" ? [`Saved ${data.path}`] : []),
|
|
119
|
+
...(typeof data.captures === "string"
|
|
120
|
+
? [`Saved its captures in ${data.captures}`]
|
|
121
|
+
: []),
|
|
122
|
+
],
|
|
80
123
|
});
|
|
81
124
|
//# sourceMappingURL=issue-save.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue-save.js","sourceRoot":"","sources":["../../../src/app/workflows/issue-save.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"issue-save.js","sourceRoot":"","sources":["../../../src/app/workflows/issue-save.ts"],"names":[],"mappings":"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"}
|