vinta-ai-workflows 0.7.0-alpha35 → 0.7.0-alpha36
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/CHANGELOG.md +278 -0
- package/package.json +1 -1
- package/schemas/postmortem.v1.schema.json +213 -2
- package/schemas/vinta-ai-workflows-config.v1.schema.json +89 -2
- package/schemas/workflow.v1.schema.json +131 -1
- package/skills/vinta-bootstrap-ai-tools/SKILL.md +11 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/django/notes.md +14 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +24 -0
- package/skills/vinta-derive-skills/resources/plan-execution/README.md +9 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/maestro-live-run.md +40 -0
- package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +8 -1
- package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/django.md +4 -1
- package/skills/vinta-derive-skills/resources/write-unit-test-template.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,7 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [0.7.0] — YYYY-MM-DD
|
|
9
9
|
|
|
10
|
+
<!-- pre-release: 0.7.0-alpha36 on 2026-10-18 -->
|
|
10
11
|
<!-- pre-release: 0.7.0-alpha35 on 2026-10-16 -->
|
|
11
12
|
<!-- pre-release: 0.7.0-alpha34 on 2026-10-16 -->
|
|
12
13
|
<!-- pre-release: 0.7.0-alpha33 on 2026-10-16 -->
|
|
@@ -35,6 +36,188 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
35
36
|
|
|
36
37
|
### Added
|
|
37
38
|
|
|
39
|
+
- **`amend-plan` amends a live `vinta-ai-maestro` run instead of rewriting
|
|
40
|
+
its branches.** When a run of the plan is running, paused, interrupted or
|
|
41
|
+
failed, the skill asks whether to amend it, then edits the plan and the
|
|
42
|
+
run's own workflow copy (`vinta-ai-maestro workflow`, so earlier operator and
|
|
43
|
+
coordinator amendments survive), commits both on `plan/<workflow-id>/base`
|
|
44
|
+
and applies them with `vinta-ai-maestro amend`, which rebases the done
|
|
45
|
+
phases the change reaches. Before, the plan file changed and the run went on
|
|
46
|
+
building its snapshot. The skill also carries operator recipes for a live
|
|
47
|
+
run: switching harness or model, switching the account a harness runs as,
|
|
48
|
+
resuming with the same policy flags, and moving the base forward. New
|
|
49
|
+
partial `plan-execution/partials/maestro-live-run.md`; re-run
|
|
50
|
+
`vinta-derive-skills` to pick it up.
|
|
51
|
+
- **`vinta-ai-maestro`: a phase the review keeps rejecting moves up a crew
|
|
52
|
+
tier.** One run left a Tier 1 phase on its desk through 16 review rejections
|
|
53
|
+
over six hours until the coordinator moved it by hand. New
|
|
54
|
+
`defaults.escalate_after_rejections` (default `3`, `0` turns it off): when a
|
|
55
|
+
phase's review has failed to approve that many times on one desk, counted
|
|
56
|
+
across attempts, the scheduler amends the run. The phase moves to the
|
|
57
|
+
cheapest member of the next tier up and takes the new desk from its next
|
|
58
|
+
attempt. The amendment is journalled as `workflow_amended` with the new
|
|
59
|
+
author `scheduler`, a one-line `reason` and a new `rule` field
|
|
60
|
+
(`review_rejections`, the members, the tiers and the count). A phase already at the top tier stays
|
|
61
|
+
where it is. Amendments may now leave a member the run already had with no
|
|
62
|
+
phase, so moving a member's last phase is no longer refused.
|
|
63
|
+
- **`vinta-ai-maestro`: `purge` drops lane rescues whose work landed.** The
|
|
64
|
+
`refs/vinta-ai-maestro/wip/*` commits a recycled lane keeps of a phase's
|
|
65
|
+
uncommitted files piled up (~80 in one project). A purge now drops those of
|
|
66
|
+
the runs it purges whose phase is done and merged into a branch outside
|
|
67
|
+
`plan/` and `wt/`, and those of already-purged runs whose rescued files read
|
|
68
|
+
the same at `HEAD`. Every other rescue is kept, and `--dry-run` lists them.
|
|
69
|
+
- **`vinta-ai-maestro`: a `ui` left up across an upgrade says so.** It warns
|
|
70
|
+
once when the installed package's version no longer matches the one it
|
|
71
|
+
loaded, and `run` and `status` warn about a live `ui` serving another
|
|
72
|
+
release. A `ui` started days earlier used to keep serving pre-upgrade code
|
|
73
|
+
without a word.
|
|
74
|
+
- **`vinta-ai-maestro`: a resume remembers how the run was started.** Every
|
|
75
|
+
job records its `--permission`, `--system-one`, `--on-failure`, `--retries`,
|
|
76
|
+
`--retry-after`, `--no-coordinator`, `--host` and `--harness-profile` in
|
|
77
|
+
`runs/<id>/policy.json`, and `run --resume` (and the UI's Resume) reuses
|
|
78
|
+
them, saying which; a flag given on the command line still wins. Each resume
|
|
79
|
+
used to need all of them restated.
|
|
80
|
+
- **`vinta-ai-maestro`: harness profiles.** `--harness-profile <name>` on `run`
|
|
81
|
+
and `ui` picks a named binary and environment for one harness — a second
|
|
82
|
+
Claude account's `CLAUDE_CONFIG_DIR`, say — from the operator's
|
|
83
|
+
`~/.config/vinta-ai-maestro/profiles.yaml` (or `$VINTA_AI_MAESTRO_PROFILES`).
|
|
84
|
+
The run keeps the name, so a resume picks the same account.
|
|
85
|
+
- **`vinta-ai-maestro`: `propagate` works on a live run.** It refused one, so
|
|
86
|
+
a fix on a phase could not reach older `integ-*` branches until the run
|
|
87
|
+
ended, and their PRs stayed red. On a live run the run's job now makes the
|
|
88
|
+
merges, holding the integration worktree's queue, so none of the run's own
|
|
89
|
+
merges interleave with them.
|
|
90
|
+
- **`vinta-ai-maestro`: `sync-base <run>` brings a moved `base_branch` into a
|
|
91
|
+
running plan.** It merges the base (the remote's, when ahead) into
|
|
92
|
+
`plan/<id>/base`, then the plan branch into the lowest wave and each wave
|
|
93
|
+
into the one above. On a live run a wave's conflict goes to the run's
|
|
94
|
+
conflict fixer, gated as a phase merge is; the plan branch's own conflict,
|
|
95
|
+
and any conflict on a run that is not live, stops it with the branch as it
|
|
96
|
+
was. Both commands journal `branches_moved` on a live run.
|
|
97
|
+
- **`vinta-ai-maestro`: a list of what the run decided without you.** One
|
|
98
|
+
run answered 79 of its 125 questions unattended — review loops stopped,
|
|
99
|
+
agents' design questions given their first option, one of which added a
|
|
100
|
+
migration — and none of it was anywhere a person would look. Every question
|
|
101
|
+
a `--retry-after` timer or the run coordinator answered, and everything the
|
|
102
|
+
coordinator did on its own (steering, pauses, aborts, amendments, `exec`),
|
|
103
|
+
is now listed in:
|
|
104
|
+
- `status <run>` (*Decided without you*; `unattended` in `--json`), with an
|
|
105
|
+
agent question's header and the option it got;
|
|
106
|
+
- the run view's **Decided without you** panel (`GET /api/runs/:runId/unattended`);
|
|
107
|
+
- the plan PR's *Decided without a person* section;
|
|
108
|
+
- the post-mortem's new optional `unattended_decisions` finding
|
|
109
|
+
(`schemas/postmortem.v1.schema.json`, additive), which `plan-feature`
|
|
110
|
+
now reads: an agent question answered unattended is a decision the plan
|
|
111
|
+
left open.
|
|
112
|
+
- **`vinta-ai-maestro`: merge resolutions that undo a phase's deletion are
|
|
113
|
+
flagged.** One run's phase deleted an unused notification kind; the
|
|
114
|
+
conflict fixer kept "both sides" and every gate passed, so the deletion
|
|
115
|
+
never landed. After every merge in the integration worktree — a fixer's
|
|
116
|
+
resolution, or the `append_only` union driver, which re-adds lines one side
|
|
117
|
+
removed — the integrator counts lines one side deleted that the result
|
|
118
|
+
kept (codegen outputs excluded). Each is recorded as path, phase and line
|
|
119
|
+
count, never the lines: on the `node_conflict` journal row
|
|
120
|
+
(`reverted_deletions`), in the post-mortem's `wave_conflicts` and new
|
|
121
|
+
optional `unattended_decisions.reverted_deletions`
|
|
122
|
+
(`schemas/postmortem.v1.schema.json`, additive), and in the list of what
|
|
123
|
+
was decided without you (`status`, the run view, the plan PR body). It
|
|
124
|
+
flags; it does not block the merge. The conflict-fixer prompt now says a
|
|
125
|
+
phase's intent includes what it deleted.
|
|
126
|
+
- **`vinta-ai-maestro`: `--agent-answer-after` on `run` and `ui`.** How long an
|
|
127
|
+
agent's own question waits before it takes the recommended options. It
|
|
128
|
+
used to be `--retry-after`, so a one-minute retry interval answered design
|
|
129
|
+
questions in a minute. Unset, it is twice `--retry-after` and never under 30
|
|
130
|
+
minutes; set alone, agents' questions are answered while failures still
|
|
131
|
+
wait for a person.
|
|
132
|
+
- **`vinta-ai-maestro`: `doctor` checks the gates against CI.** It reads
|
|
133
|
+
`.github/workflows/*.yml` and warns about each `run:` command no gate runs,
|
|
134
|
+
or runs without a flag CI passes (`manage.py check` for CI's `check
|
|
135
|
+
--deploy`) — the reason every gate passed while CI failed on twenty review
|
|
136
|
+
PRs of one run. Matching is heuristic (tool and subcommand after `uv run`,
|
|
137
|
+
`npx`, `docker compose run`, …; `uses:` steps are not read) and it is always
|
|
138
|
+
a warning.
|
|
139
|
+
- **`vinta-ai-maestro`: project-wide custom gates.** `maestro.gates` in
|
|
140
|
+
`.vinta-ai-workflows.yaml` now takes ids of your own beside the four types —
|
|
141
|
+
`django-check: { cmd: uv run python manage.py check --fail-level WARNING }`,
|
|
142
|
+
`api-client-fresh: { cmd: make api_client && git diff --exit-code … }` —
|
|
143
|
+
with the fields a type takes (`cmd`, `scoped_cmd`, `requires`, `timeout_s`,
|
|
144
|
+
`tuning`). Every plan has each one under its id without declaring it, and
|
|
145
|
+
`maestro.defaults.gates` names it like a type, so a check CI runs is
|
|
146
|
+
declared once and keeps its own verdict instead of being chained onto
|
|
147
|
+
`typecheck` or repeated in every plan. A plan's gate of the same id replaces
|
|
148
|
+
it whole. `validate` refuses an id that is not lowercase kebab-case, the
|
|
149
|
+
reserved `codegen`, and a custom gate with no `cmd`. Previously any key but
|
|
150
|
+
a type failed with "Unrecognized keys". Additive in the config schema.
|
|
151
|
+
`plan-feature` and the Django stack notes now point CI-mirroring checks at
|
|
152
|
+
`maestro.gates.<id>`.
|
|
153
|
+
- **`vinta-ai-maestro`: codegen steps — generated files are regenerated, not
|
|
154
|
+
merged.** A workflow (or `maestro.codegen` in `.vinta-ai-workflows.yaml`)
|
|
155
|
+
declares `codegen.<id>`: a deterministic command, the files it writes
|
|
156
|
+
(`outputs`), the files they are a function of (`inputs`), and `after` for the
|
|
157
|
+
chain (schema, then client). The steps run in every lane before the gates
|
|
158
|
+
and their outputs are committed there; in the integration worktree a
|
|
159
|
+
conflict confined to their outputs is settled by taking either side and
|
|
160
|
+
running them, with no conflict fixer, and every merge is regenerated before
|
|
161
|
+
it is committed. A failing step is a red gate. `inputs` skip a step whose
|
|
162
|
+
inputs and outputs match a recorded run, a step that writes different
|
|
163
|
+
outputs from the same inputs is journalled `nondeterministic`, and a
|
|
164
|
+
`format: true` step formats only the files the phase changed. Agents are told
|
|
165
|
+
which files are generated and regenerate them mid-turn with
|
|
166
|
+
`vinta-ai-maestro codegen [<step>]`; `doctor --codegen` runs each step twice
|
|
167
|
+
to flag non-deterministic or stale output. Generated files caused most of one
|
|
168
|
+
run's 37 wave conflicts. `workflow.v1` and the config schema gain the
|
|
169
|
+
additive `codegen` field.
|
|
170
|
+
|
|
171
|
+
- **`vinta-ai-maestro`: append-only files merge as a union.** `append_only` (or
|
|
172
|
+
`maestro.append_only`) lists files phases only add entries to — DI
|
|
173
|
+
containers, URL routes, notification kinds, beat schedules — and merges in
|
|
174
|
+
the integration worktree use git's `union` driver for them, so two phases
|
|
175
|
+
each adding an entry no longer needs a fixer round. The merged tree is gated
|
|
176
|
+
as before. Additive in `workflow.v1` and the config schema.
|
|
177
|
+
|
|
178
|
+
- **`plan-feature`: emits codegen steps and append-only files** detected from the
|
|
179
|
+
project — the OpenAPI schema, generated API clients, route trees and
|
|
180
|
+
file-modifying pre-commit hooks as `codegen`; DI containers, URL routes and
|
|
181
|
+
beat schedules as `append_only` — preferring `maestro.codegen` /
|
|
182
|
+
`maestro.append_only` in the project file. `vinta-bootstrap-ai-tools` offers
|
|
183
|
+
to record them there once.
|
|
184
|
+
- **`vinta-ai-maestro`: take a phase over without failing it.** Moving a phase
|
|
185
|
+
to another agent took abort, amend and retry, and the retry restarted its
|
|
186
|
+
pipeline from the top — one phase went from review back to implementing.
|
|
187
|
+
`node interrupt <run> <phase>` stops the turn now and **holds** the phase,
|
|
188
|
+
lane, branch, commits and uncommitted work kept, until `resume`, `reassign`
|
|
189
|
+
or `step` releases it; the cut step runs again then. `node reassign <run>
|
|
190
|
+
<phase> --crew <member> | --harness <h> --model <m>` is interrupt, amend and
|
|
191
|
+
resume in one (`--after-turn` lets the turn finish): the phase carries on at
|
|
192
|
+
the same step, keeps its session when only the model changes, and otherwise
|
|
193
|
+
starts fresh with a handoff — the last report, the work in the lane, the red
|
|
194
|
+
gate. `node step <run> <phase> <state>` moves the pipeline by hand, never
|
|
195
|
+
into a final state. Journalled as `node_reassigned` and `node_operation`.
|
|
196
|
+
- **`vinta-ai-maestro`: `amend` accepts a held phase, or one waiting on a
|
|
197
|
+
question.** Its harness, model or crew member (`crew_changed`, a new
|
|
198
|
+
amendment kind) may change while it is parked, and it carries on at its
|
|
199
|
+
step. A brief, a gate list or a base still waits for the phase to stop.
|
|
200
|
+
- **`vinta-ai-maestro`: `switch-harness <run> --from --to --model` and
|
|
201
|
+
`defaults.harness_fallbacks`.** A live run moves off a harness — an account
|
|
202
|
+
out of credits — without a pause and resume: phases in flight are
|
|
203
|
+
reassigned at their step, later phases run on the new harness, and the
|
|
204
|
+
conflict fixer moves too. With `harness_fallbacks` the run does it by
|
|
205
|
+
itself when a harness parks on a quota refusal, the refused turn going
|
|
206
|
+
again at once on the fallback. Journalled `harness_switched`.
|
|
207
|
+
- **`vinta-ai-maestro`: `fixer interrupt|reassign <run>`.** End the conflict
|
|
208
|
+
fixer's running round as a round spent (what it left stays), or hand later
|
|
209
|
+
rounds to another harness and model. Journalled `fixer_operation`.
|
|
210
|
+
- **`vinta-ai-maestro`: one API for the operator and the coordinator, and every
|
|
211
|
+
act says why.** Every acting request takes a reason (`--reason` on every
|
|
212
|
+
verb, the `X-Maestro-Reason` header on the API), journalled with what it
|
|
213
|
+
explains; the coordinator is refused without one (`reason_required`). The
|
|
214
|
+
coordinator gains `interrupt`, `reassign`, `switch-harness`, `fixer`,
|
|
215
|
+
`resume` of its own holds, and `pause` of the run (`run_halt_requested`).
|
|
216
|
+
`run --coordinator-authority full` also lets it change the plan's shape —
|
|
217
|
+
phases, dependency edges, briefs, touch lists, deferrals — answer any
|
|
218
|
+
question you are not holding, `node step`, and stop the run. A check is
|
|
219
|
+
never weakened under either authority.
|
|
220
|
+
|
|
38
221
|
- **`vinta-ai-maestro`: integration is visible while it happens.** A phase in
|
|
39
222
|
`integrate` — a merge, a suite on the merged tree, a push, none with an
|
|
40
223
|
agent — showed `running` over a silent transcript, and one queued behind it
|
|
@@ -659,6 +842,67 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
659
842
|
|
|
660
843
|
### Changed
|
|
661
844
|
|
|
845
|
+
- **`plan-feature`: a rename of a shared symbol is an interface change.** A
|
|
846
|
+
phase that renames something a same-wave sibling also uses does it in the
|
|
847
|
+
foundation phase or keeps the old name as an alias until the wave lands.
|
|
848
|
+
One run had a phase call a helper its sibling renamed; each passed alone and
|
|
849
|
+
the merged wave failed the typecheck.
|
|
850
|
+
- **`plan-feature`: gates cover what CI runs.** The workflow rules now tell the
|
|
851
|
+
author to read the CI config and give every locally runnable CI job a gate,
|
|
852
|
+
naming the checks plans miss: a deploy check at warning level, a
|
|
853
|
+
migrations-complete check, a "generated client is up to date" check. The
|
|
854
|
+
Django stack notes suggest `manage.py check --deploy --fail-level WARNING`,
|
|
855
|
+
`makemigrations --check` and a regenerate-and-diff gate for the schema or
|
|
856
|
+
client. One run's regex-style routes passed every gate and failed CI.
|
|
857
|
+
- **`write-unit-test`: time-independent tests and deterministic generated
|
|
858
|
+
output.** Rule 8 now requires a frozen clock and an explicit timezone on any
|
|
859
|
+
test touching dates, and host-independent schema or client generation
|
|
860
|
+
(values from a locked package such as `tzdata`, sorted). The Django pack and
|
|
861
|
+
stack notes spell out `freezegun` / `time-machine` with
|
|
862
|
+
`override_settings(TIME_ZONE=...)` and `tzdata`-sourced timezone choices.
|
|
863
|
+
One run had a date test fail only near midnight UTC, and a timezone enum
|
|
864
|
+
differ by machine.
|
|
865
|
+
- **`vinta-bootstrap-ai-tools`: test-tool caches are ignored before lanes run.**
|
|
866
|
+
The maestro defaults step reads `.gitignore` for the selected runners'
|
|
867
|
+
caches and reports (`.testmondata*`, `junit/`, coverage outputs, …),
|
|
868
|
+
checks for the `check-added-large-files` pre-commit hook, which a committed
|
|
869
|
+
testmon database trips on every lane and merge commit, and offers to append
|
|
870
|
+
the missing entries.
|
|
871
|
+
- **`vinta-ai-maestro`: a review that stated no verdict is no longer reported
|
|
872
|
+
as a rejection.** One run recorded 22 attempts as `review verdict "fail"; 0
|
|
873
|
+
fix rounds spent`. None had rejected anything: each was a review turn that
|
|
874
|
+
ended before its reviewer answered (fixed in alpha33 and alpha34). The
|
|
875
|
+
pipeline still treats silence as `fail`, but the failure reason now says
|
|
876
|
+
`the review turn stated no verdict`, and the `review` fact carries
|
|
877
|
+
`stated: false`. The budget clause is renamed to `gate fix rounds spent`,
|
|
878
|
+
since a review's findings are fixed inside its own turn and never in a
|
|
879
|
+
separate fix round.
|
|
880
|
+
|
|
881
|
+
- **`vinta-ai-maestro`: two empty turns in a row park the phase instead of
|
|
882
|
+
burning its retries.** Right after one run moved its desks to codex, "codex
|
|
883
|
+
ended the turn without the agent doing anything" failed 20 attempts across 4
|
|
884
|
+
phases in about two minutes. When one harness ends two turns in a row
|
|
885
|
+
without the agent doing anything, across attempts, the phase now skips its
|
|
886
|
+
automatic retries and asks: the question says the harness is failing and
|
|
887
|
+
suggests `retry with` a member on another harness. `--retry-after` still
|
|
888
|
+
answers it, and backs off from the first question.
|
|
889
|
+
|
|
890
|
+
- **`vinta-ai-maestro`: a red gate on a merged base is re-run on retry.** One
|
|
891
|
+
run's base test gate failed once on a test that broke only near midnight.
|
|
892
|
+
Seven retries replayed the cached verdict, each reported as "failed during
|
|
893
|
+
setup, before any agent ran", and the unattended retry was withheld until an
|
|
894
|
+
operator ran `gate-cache invalidate`. The base gates now replay only a green
|
|
895
|
+
verdict and re-run a red one, and a red base gate no longer counts as a
|
|
896
|
+
setup failure, so `--retry-after` keeps retrying it with its usual backoff.
|
|
897
|
+
|
|
898
|
+
- **`vinta-ai-maestro`: a lane that will not recycle says why.** One run
|
|
899
|
+
failed 20 attempts with only "could not be recycled (worktree)" or
|
|
900
|
+
"(database)" in the journal and the daemon log. The failure now carries the
|
|
901
|
+
failing step's exit code and the tail of its stderr, the way a failed
|
|
902
|
+
`setup_cmd` already did, and a `reset_cmd`, `setup_cmd` or `prepare_cmd`
|
|
903
|
+
failing during a recycle reports that command's own error instead of
|
|
904
|
+
`LaneSetupError`. Command lines are still left out.
|
|
905
|
+
|
|
662
906
|
- **`vinta-ai-maestro`: the full suite runs before a PR, and on every merged
|
|
663
907
|
tree.** Under `gate_scope: scoped`, a phase's gates ran their scoped form
|
|
664
908
|
everywhere, so a PR could carry a suite only a scoped run had seen, and most
|
|
@@ -1031,6 +1275,40 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
1031
1275
|
|
|
1032
1276
|
### Fixed
|
|
1033
1277
|
|
|
1278
|
+
- **`vinta-ai-maestro`: the integrator no longer stages untracked files into
|
|
1279
|
+
a merge resolution.** It stages tracked paths and the merge's own paths
|
|
1280
|
+
instead of `git add --all`, so a gate's leftovers in the integration
|
|
1281
|
+
worktree (a `.testmondata-wal`, say) stay out of the merge commit. A kept
|
|
1282
|
+
resolution that changes paths outside the merge, or whose commit is refused
|
|
1283
|
+
again, is dropped instead of replayed on every retry. A conflict fixer now
|
|
1284
|
+
stages any new file it creates itself, and its prompt says so.
|
|
1285
|
+
- **`vinta-ai-maestro`: an amendment reaches the conflict fixer.** The fixer
|
|
1286
|
+
is rebuilt from the amended crew when the plan is amended, so a run whose
|
|
1287
|
+
desks moved to another harness stops failing on "conflict fixer spawn
|
|
1288
|
+
refused" for the harness it left.
|
|
1289
|
+
- **`vinta-ai-maestro`: a conflict fixer stopped by its time limit keeps its
|
|
1290
|
+
work.** What the round had is kept under `refs/vinta-ai-maestro/abandoned/`
|
|
1291
|
+
(an interrupted round's too), and the retry's fixer is pointed at it to
|
|
1292
|
+
continue instead of redoing the resolution from scratch.
|
|
1293
|
+
- **`vinta-ai-maestro`: an amended harness reaches every phase it moved.** An
|
|
1294
|
+
amendment changed the harness only on phases still `pending`; a failed or
|
|
1295
|
+
blocked one waiting on a retry kept naming the old harness in `status` and
|
|
1296
|
+
the run view, and once the job loaded only the new harness's adapter the
|
|
1297
|
+
run view answered 500 `unknown harness`. Phases past `pending` now take the
|
|
1298
|
+
new harness without losing their status, and the run view leaves out the
|
|
1299
|
+
capacity of a harness the live run loaded no adapter for.
|
|
1300
|
+
- **`vinta-ai-maestro`: `land` decides "already in the base" by content.** A
|
|
1301
|
+
review PR is landed when merging it into the base would leave the base's
|
|
1302
|
+
tree unchanged. The ancestry test it used alone kept an `integ-*` PR open
|
|
1303
|
+
after its plan PR merged: three bookkeeping merges were not in `main`, though
|
|
1304
|
+
every branch they merged was. A draft plan PR is now named as one (GitHub
|
|
1305
|
+
will not merge a draft), and `land <run> --ready` marks it ready for review.
|
|
1306
|
+
- **`vinta-ai-maestro`: a hook that rewrites files no longer refuses the
|
|
1307
|
+
commit outright.** When a commit fails having left modified files behind — a
|
|
1308
|
+
`pre-commit` formatter or schema generator — they are staged and the commit
|
|
1309
|
+
is tried once more before the operator is asked, in lanes and in the
|
|
1310
|
+
integration worktree. One run had seven merge commits refused this way.
|
|
1311
|
+
|
|
1034
1312
|
- **`vinta-ai-maestro`: a claude-code turn the CLI calls "blocked" no longer
|
|
1035
1313
|
fails unless a tool was refused in it.** The CLI's own turn summary marks a
|
|
1036
1314
|
turn `blocked` whenever its last message names a next step for a person —
|
package/package.json
CHANGED
|
@@ -199,6 +199,37 @@
|
|
|
199
199
|
"minimum": 0,
|
|
200
200
|
"maximum": 9007199254740991,
|
|
201
201
|
"description": "Conflict-fixer rounds the resolution took. High is a strong split signal."
|
|
202
|
+
},
|
|
203
|
+
"reverted_deletions": {
|
|
204
|
+
"description": "Paths where the resolution kept lines one side had deleted — a fixer keeping “both sides”, or the `append_only` union driver. Flagged, not refused: check each deletion reached the branch. Absent when there were none.",
|
|
205
|
+
"type": "array",
|
|
206
|
+
"items": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"properties": {
|
|
209
|
+
"path": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"maxLength": 500
|
|
212
|
+
},
|
|
213
|
+
"node": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"maxLength": 200,
|
|
216
|
+
"description": "The phase whose deletion was reverted, or the branch standing in when no phase on that side touched the path."
|
|
217
|
+
},
|
|
218
|
+
"lines": {
|
|
219
|
+
"type": "integer",
|
|
220
|
+
"minimum": 1,
|
|
221
|
+
"maximum": 9007199254740991,
|
|
222
|
+
"description": "Lines that side deleted and the resolution kept. A count, never the lines."
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": [
|
|
226
|
+
"path",
|
|
227
|
+
"node",
|
|
228
|
+
"lines"
|
|
229
|
+
],
|
|
230
|
+
"additionalProperties": false,
|
|
231
|
+
"description": "A deletion a merge resolution undid. Identifiers and a count only."
|
|
232
|
+
}
|
|
202
233
|
}
|
|
203
234
|
},
|
|
204
235
|
"required": [
|
|
@@ -651,7 +682,8 @@
|
|
|
651
682
|
"operator",
|
|
652
683
|
"monitor",
|
|
653
684
|
"coordinator",
|
|
654
|
-
"config"
|
|
685
|
+
"config",
|
|
686
|
+
"scheduler"
|
|
655
687
|
]
|
|
656
688
|
}
|
|
657
689
|
},
|
|
@@ -680,8 +712,13 @@
|
|
|
680
712
|
"add_context",
|
|
681
713
|
"redirect",
|
|
682
714
|
"pause",
|
|
715
|
+
"resume",
|
|
683
716
|
"abort",
|
|
684
|
-
"fresh"
|
|
717
|
+
"fresh",
|
|
718
|
+
"interrupt",
|
|
719
|
+
"reassign",
|
|
720
|
+
"step",
|
|
721
|
+
"retry"
|
|
685
722
|
]
|
|
686
723
|
},
|
|
687
724
|
"by": {
|
|
@@ -840,6 +877,180 @@
|
|
|
840
877
|
"additionalProperties": false,
|
|
841
878
|
"description": "A gate that every phase passed on its own branch and that failed on the merged wave: two of `members` (or one of them and an earlier wave) disagree. The evidence of a missing dependency or a missing contract test that `missing_dependencies`’ ordering cannot see."
|
|
842
879
|
}
|
|
880
|
+
},
|
|
881
|
+
"unattended_decisions": {
|
|
882
|
+
"type": "object",
|
|
883
|
+
"properties": {
|
|
884
|
+
"answers": {
|
|
885
|
+
"type": "array",
|
|
886
|
+
"items": {
|
|
887
|
+
"type": "object",
|
|
888
|
+
"properties": {
|
|
889
|
+
"node": {
|
|
890
|
+
"type": "string",
|
|
891
|
+
"minLength": 1,
|
|
892
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
893
|
+
},
|
|
894
|
+
"at_ms": {
|
|
895
|
+
"type": "integer",
|
|
896
|
+
"minimum": -9007199254740991,
|
|
897
|
+
"maximum": 9007199254740991
|
|
898
|
+
},
|
|
899
|
+
"category": {
|
|
900
|
+
"type": "string",
|
|
901
|
+
"enum": [
|
|
902
|
+
"retry",
|
|
903
|
+
"agent_question",
|
|
904
|
+
"plan_question",
|
|
905
|
+
"merge_commit",
|
|
906
|
+
"other"
|
|
907
|
+
],
|
|
908
|
+
"description": "`retry`: a failed attempt retried or stopped. `agent_question`: a design question the agent stopped to ask, given its recommended options. `plan_question`: a pipeline question such as an exhausted review budget, given the plan’s `unattended_answer`. `merge_commit`: a merge commit git refused."
|
|
909
|
+
},
|
|
910
|
+
"by": {
|
|
911
|
+
"type": "string",
|
|
912
|
+
"enum": [
|
|
913
|
+
"unattended",
|
|
914
|
+
"coordinator"
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
"answer": {
|
|
918
|
+
"anyOf": [
|
|
919
|
+
{
|
|
920
|
+
"type": "string",
|
|
921
|
+
"maxLength": 200
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
"type": "number"
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"type": "boolean"
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
"type": "null"
|
|
931
|
+
}
|
|
932
|
+
],
|
|
933
|
+
"description": "The answer’s value: a choice, or an agent question’s picked option indices as JSON."
|
|
934
|
+
},
|
|
935
|
+
"waited_ms": {
|
|
936
|
+
"description": "How long the question stood unanswered.",
|
|
937
|
+
"type": "integer",
|
|
938
|
+
"minimum": 0,
|
|
939
|
+
"maximum": 9007199254740991
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
"required": [
|
|
943
|
+
"node",
|
|
944
|
+
"at_ms",
|
|
945
|
+
"category",
|
|
946
|
+
"by",
|
|
947
|
+
"answer"
|
|
948
|
+
],
|
|
949
|
+
"additionalProperties": false
|
|
950
|
+
},
|
|
951
|
+
"description": "Questions a `--retry-after` timer or the run coordinator answered, oldest first."
|
|
952
|
+
},
|
|
953
|
+
"coordinator_actions": {
|
|
954
|
+
"type": "array",
|
|
955
|
+
"items": {
|
|
956
|
+
"type": "object",
|
|
957
|
+
"properties": {
|
|
958
|
+
"at_ms": {
|
|
959
|
+
"type": "integer",
|
|
960
|
+
"minimum": -9007199254740991,
|
|
961
|
+
"maximum": 9007199254740991
|
|
962
|
+
},
|
|
963
|
+
"node": {
|
|
964
|
+
"type": "string",
|
|
965
|
+
"minLength": 1,
|
|
966
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
967
|
+
},
|
|
968
|
+
"action": {
|
|
969
|
+
"type": "string",
|
|
970
|
+
"enum": [
|
|
971
|
+
"add_context",
|
|
972
|
+
"redirect",
|
|
973
|
+
"pause",
|
|
974
|
+
"abort",
|
|
975
|
+
"fresh",
|
|
976
|
+
"amend",
|
|
977
|
+
"exec",
|
|
978
|
+
"gate_cache_invalidate",
|
|
979
|
+
"interrupt",
|
|
980
|
+
"reassign",
|
|
981
|
+
"retry",
|
|
982
|
+
"step"
|
|
983
|
+
]
|
|
984
|
+
},
|
|
985
|
+
"target": {
|
|
986
|
+
"description": "An amendment’s targets, an `exec` target or a gate id.",
|
|
987
|
+
"type": "string",
|
|
988
|
+
"maxLength": 500
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
"required": [
|
|
992
|
+
"at_ms",
|
|
993
|
+
"action"
|
|
994
|
+
],
|
|
995
|
+
"additionalProperties": false
|
|
996
|
+
},
|
|
997
|
+
"description": "What the coordinator did on its own, oldest first. Its reasons are in its conversation and, for steering, in the phase’s decisions — not here."
|
|
998
|
+
},
|
|
999
|
+
"reverted_deletions": {
|
|
1000
|
+
"description": "Merge resolutions that kept lines a phase deliberately deleted, oldest first: flagged, not refused. Unused code breaks no gate, so check each deletion reached the branch. Absent when there were none, and on post-mortems written before it was checked.",
|
|
1001
|
+
"type": "array",
|
|
1002
|
+
"items": {
|
|
1003
|
+
"type": "object",
|
|
1004
|
+
"properties": {
|
|
1005
|
+
"at_ms": {
|
|
1006
|
+
"type": "integer",
|
|
1007
|
+
"minimum": -9007199254740991,
|
|
1008
|
+
"maximum": 9007199254740991
|
|
1009
|
+
},
|
|
1010
|
+
"node": {
|
|
1011
|
+
"type": "string",
|
|
1012
|
+
"maxLength": 200,
|
|
1013
|
+
"description": "The phase whose deletion was reverted, or the branch standing in when no phase on that side touched the path."
|
|
1014
|
+
},
|
|
1015
|
+
"path": {
|
|
1016
|
+
"type": "string",
|
|
1017
|
+
"maxLength": 500
|
|
1018
|
+
},
|
|
1019
|
+
"lines": {
|
|
1020
|
+
"type": "integer",
|
|
1021
|
+
"minimum": 1,
|
|
1022
|
+
"maximum": 9007199254740991,
|
|
1023
|
+
"description": "Lines that side deleted and the resolution kept. A count, never the lines."
|
|
1024
|
+
},
|
|
1025
|
+
"branch": {
|
|
1026
|
+
"type": "string",
|
|
1027
|
+
"description": "The branch the merge was made on."
|
|
1028
|
+
},
|
|
1029
|
+
"fix_rounds": {
|
|
1030
|
+
"type": "integer",
|
|
1031
|
+
"minimum": 0,
|
|
1032
|
+
"maximum": 9007199254740991,
|
|
1033
|
+
"description": "Conflict-fixer rounds the resolution took; 0 is git’s own merge, the `append_only` union driver."
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
"required": [
|
|
1037
|
+
"at_ms",
|
|
1038
|
+
"node",
|
|
1039
|
+
"path",
|
|
1040
|
+
"lines",
|
|
1041
|
+
"branch",
|
|
1042
|
+
"fix_rounds"
|
|
1043
|
+
],
|
|
1044
|
+
"additionalProperties": false
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
"required": [
|
|
1049
|
+
"answers",
|
|
1050
|
+
"coordinator_actions"
|
|
1051
|
+
],
|
|
1052
|
+
"additionalProperties": false,
|
|
1053
|
+
"description": "Every decision made with nobody watching. A plan whose run decided much on its own left those choices to chance: a design question answered unattended belonged in the plan."
|
|
843
1054
|
}
|
|
844
1055
|
},
|
|
845
1056
|
"required": [
|
|
@@ -334,7 +334,7 @@
|
|
|
334
334
|
"default": []
|
|
335
335
|
},
|
|
336
336
|
"maestro": {
|
|
337
|
-
"description": "Project-wide defaults for `vinta-ai-maestro` runs, layered UNDER every plan's `ai-plans/*.workflow.json`: a plan's own value wins, and a run's mid-run amendments win over both. Maps keyed by id (`resources`, `chores`, `project.databases`, `project.services`) merge per id with the plan's entry replacing the project's whole; a typed gate merges field by field; lists replace. Shapes are the workflow schema's (`workflow.v1.schema.json`) — maestro validates them strictly at load. A run follows its plan branch (`plan/<workflow-id>/base`): committing a change to this file on that branch applies it to the live run. Absent: every plan carries everything itself.",
|
|
337
|
+
"description": "Project-wide defaults for `vinta-ai-maestro` runs, layered UNDER every plan's `ai-plans/*.workflow.json`: a plan's own value wins, and a run's mid-run amendments win over both. Maps keyed by id (`resources`, `chores`, `codegen`, `project.databases`, `project.services`) merge per id with the plan's entry replacing the project's whole; a typed gate merges field by field; lists replace. Shapes are the workflow schema's (`workflow.v1.schema.json`) — maestro validates them strictly at load. A run follows its plan branch (`plan/<workflow-id>/base`): committing a change to this file on that branch applies it to the live run. Absent: every plan carries everything itself.",
|
|
338
338
|
"type": "object",
|
|
339
339
|
"additionalProperties": false,
|
|
340
340
|
"properties": {
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
"additionalProperties": false
|
|
447
447
|
},
|
|
448
448
|
"gates": {
|
|
449
|
-
"description": "
|
|
449
|
+
"description": "Keyed by gate type (`test`, `lint`, `typecheck`, `e2e`) or by a custom id. Each type with a command is also available to every plan under the type's own name (`gates: [typecheck, test]` on a node, with no gate table). Any other key is a custom project-wide gate — e.g. `django-check: { cmd: uv run python manage.py check --fail-level WARNING }` — that every plan has under that id and that `maestro.defaults.gates` names like a type. A custom id is lowercase kebab-case; `codegen` is reserved (it names a plan's codegen steps).",
|
|
450
450
|
"type": "object",
|
|
451
451
|
"additionalProperties": false,
|
|
452
452
|
"properties": {
|
|
@@ -638,6 +638,56 @@
|
|
|
638
638
|
}
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
|
+
},
|
|
642
|
+
"patternProperties": {
|
|
643
|
+
"^(?!(?:test|lint|typecheck|e2e|codegen)$)[a-z0-9][a-z0-9-]*$": {
|
|
644
|
+
"description": "A custom gate every plan has under this id, untyped: nothing supplies its command but `cmd` here. A plan's own gate of the same id replaces it whole. Same fields as a gate type's entry.",
|
|
645
|
+
"type": "object",
|
|
646
|
+
"additionalProperties": false,
|
|
647
|
+
"required": ["cmd"],
|
|
648
|
+
"properties": {
|
|
649
|
+
"cmd": {
|
|
650
|
+
"description": "The full check. Required: only the four gate types inherit a command from `commands.*`.",
|
|
651
|
+
"type": "string",
|
|
652
|
+
"minLength": 1
|
|
653
|
+
},
|
|
654
|
+
"scoped_cmd": {
|
|
655
|
+
"description": "The check narrowed to what a phase changed, with `{changed_files}` and/or `{touches}`. Run by phase gates under `gate_scope: scoped`; the full `cmd` runs once on each wave's merged tree.",
|
|
656
|
+
"type": "string",
|
|
657
|
+
"minLength": 1
|
|
658
|
+
},
|
|
659
|
+
"requires": {
|
|
660
|
+
"description": "Resource pools the gate holds while it runs, e.g. `[test-suite]`.",
|
|
661
|
+
"type": "array",
|
|
662
|
+
"items": {
|
|
663
|
+
"type": "string",
|
|
664
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
"timeout_s": {
|
|
668
|
+
"type": "integer",
|
|
669
|
+
"minimum": 1
|
|
670
|
+
},
|
|
671
|
+
"tuning": {
|
|
672
|
+
"description": "Flags the monitor may add to `cmd` when it retunes the gate mid-run. Same shape as a workflow gate's `tuning`.",
|
|
673
|
+
"type": "object",
|
|
674
|
+
"properties": {
|
|
675
|
+
"allowed_flags": {
|
|
676
|
+
"type": "array",
|
|
677
|
+
"items": {
|
|
678
|
+
"type": "string",
|
|
679
|
+
"minLength": 1
|
|
680
|
+
},
|
|
681
|
+
"minItems": 1
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
"required": [
|
|
685
|
+
"allowed_flags"
|
|
686
|
+
],
|
|
687
|
+
"additionalProperties": false
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
641
691
|
}
|
|
642
692
|
},
|
|
643
693
|
"resources": {
|
|
@@ -690,6 +740,43 @@
|
|
|
690
740
|
"type": "object"
|
|
691
741
|
}
|
|
692
742
|
},
|
|
743
|
+
"codegen": {
|
|
744
|
+
"description": "Codegen steps by id (`cmd`, `outputs`, `inputs`, `after`, `requires`, `timeout_s`, `format`) — the workflow schema's `codegen` entries. Deterministic commands that write declared files: an OpenAPI schema, the client generated from it, a route tree, or a formatter scoped to the files a phase changed (`format: true`, `{changed_files}` in `cmd`). Maestro runs them in every lane before its gates and commits what they write, and in the integration worktree around every merge, where a conflict confined to their outputs is settled by running them instead of by a fixer agent. A failing step is a red gate.",
|
|
745
|
+
"type": "object",
|
|
746
|
+
"propertyNames": {
|
|
747
|
+
"type": "string",
|
|
748
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
749
|
+
},
|
|
750
|
+
"additionalProperties": {
|
|
751
|
+
"type": "object",
|
|
752
|
+
"required": [
|
|
753
|
+
"cmd",
|
|
754
|
+
"outputs"
|
|
755
|
+
],
|
|
756
|
+
"properties": {
|
|
757
|
+
"cmd": {
|
|
758
|
+
"type": "string",
|
|
759
|
+
"minLength": 1
|
|
760
|
+
},
|
|
761
|
+
"outputs": {
|
|
762
|
+
"type": "array",
|
|
763
|
+
"minItems": 1,
|
|
764
|
+
"items": {
|
|
765
|
+
"type": "string",
|
|
766
|
+
"minLength": 1
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
"append_only": {
|
|
773
|
+
"description": "Files every phase only appends to — a DI container, a URL route list, a registry of notification kinds, a beat schedule — as gitattributes patterns. Maestro merges them in the integration worktree with git's `union` driver, so two phases each adding an entry do not conflict; the merged tree is then gated as any other. A plan's own `append_only` replaces this list.",
|
|
774
|
+
"type": "array",
|
|
775
|
+
"items": {
|
|
776
|
+
"type": "string",
|
|
777
|
+
"minLength": 1
|
|
778
|
+
}
|
|
779
|
+
},
|
|
693
780
|
"project": {
|
|
694
781
|
"description": "The workflow schema's `project` block (`migrate_cmd`, `databases`, `env_files`, `commands`, `hooks`, `services`, `compose`, `prepare_cmd`, `setup_cmd`), merged under a plan's own field by field. `project.commands` additionally inherits `lint`, `typecheck` (from `commands.build`) and `test` (from `commands.test_unit`).",
|
|
695
782
|
"type": "object"
|
|
@@ -437,6 +437,13 @@
|
|
|
437
437
|
"any"
|
|
438
438
|
]
|
|
439
439
|
},
|
|
440
|
+
"escalate_after_rejections": {
|
|
441
|
+
"default": 3,
|
|
442
|
+
"description": "Review rejections a phase may take on one desk before it moves up a crew tier. Every review that does not approve counts, across attempts; at the Nth the scheduler amends the run, moving the phase to the cheapest member of the next tier up, journalled as a `workflow_amended` row by `scheduler` with the reason and the count. The new desk takes the phase from its next attempt, and the count starts again there. Nothing happens at the top tier, or in a workflow with no roster. `0` turns it off. One observed run left a Tier 1 phase rejected 16 times over six hours before a person moved it.",
|
|
443
|
+
"type": "integer",
|
|
444
|
+
"minimum": 0,
|
|
445
|
+
"maximum": 9007199254740991
|
|
446
|
+
},
|
|
440
447
|
"pull_requests": {
|
|
441
448
|
"default": "phase",
|
|
442
449
|
"description": "Which pull requests a run opens. `phase` — the default — one per phase, one per integration branch, and the plan PR that lands them all: the phase PRs are review units stacked in merge order. `plan` opens the plan PR alone, from the last wave branch, which already holds every phase and every conflict resolution; review happens there. One observed ten-phase refactor opened fifteen PRs that nobody was going to merge. Under `plan` a phase has no PR of its own, so its `after_pr` chores are skipped.",
|
|
@@ -504,6 +511,43 @@
|
|
|
504
511
|
"minLength": 1
|
|
505
512
|
}
|
|
506
513
|
},
|
|
514
|
+
"harness_fallbacks": {
|
|
515
|
+
"default": {},
|
|
516
|
+
"description": "Harness id → where the run moves once that harness is out of quota and has no model fallback left (§6.1). Instead of parking every phase on it until the window resets, the run switches: the turn that was refused runs again at once on the fallback, at the same pipeline step, and every later turn that would have run on the harness runs there — phases in a turn move when it ends. A moved phase starts a fresh session with a handoff (the previous agent’s last report, its work in the lane, the red gate). One switch per harness; a fallback that runs out too parks as it would have. One observed run moved five phases off an account out of credits by aborting and retrying each by hand.",
|
|
517
|
+
"type": "object",
|
|
518
|
+
"propertyNames": {
|
|
519
|
+
"type": "string",
|
|
520
|
+
"enum": [
|
|
521
|
+
"claude-code",
|
|
522
|
+
"codex",
|
|
523
|
+
"opencode"
|
|
524
|
+
]
|
|
525
|
+
},
|
|
526
|
+
"additionalProperties": {
|
|
527
|
+
"type": "object",
|
|
528
|
+
"properties": {
|
|
529
|
+
"harness": {
|
|
530
|
+
"type": "string",
|
|
531
|
+
"enum": [
|
|
532
|
+
"claude-code",
|
|
533
|
+
"codex",
|
|
534
|
+
"opencode"
|
|
535
|
+
],
|
|
536
|
+
"description": "The harness the run moves to."
|
|
537
|
+
},
|
|
538
|
+
"model": {
|
|
539
|
+
"type": "string",
|
|
540
|
+
"minLength": 1,
|
|
541
|
+
"description": "The model every moved turn runs on there."
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
"required": [
|
|
545
|
+
"harness",
|
|
546
|
+
"model"
|
|
547
|
+
],
|
|
548
|
+
"additionalProperties": false
|
|
549
|
+
}
|
|
550
|
+
},
|
|
507
551
|
"max_session_turns": {
|
|
508
552
|
"default": 12,
|
|
509
553
|
"description": "Turns one reused session slot may take before the next spawn starts fresh (§15.5). A shared session only ever grows; without a ceiling a long node eventually dies on a context-window error that reads as a broken harness.",
|
|
@@ -607,7 +651,7 @@
|
|
|
607
651
|
},
|
|
608
652
|
"gates": {
|
|
609
653
|
"default": {},
|
|
610
|
-
"description": "Gates by id. A command gate with a `type` may omit `cmd` and inherit it from the project. Gates the project configures by type (`maestro.gates.<type>`, or a `commands.*` line) are also available under the type’s name without being declared here.",
|
|
654
|
+
"description": "Gates by id. A command gate with a `type` may omit `cmd` and inherit it from the project. Gates the project configures by type (`maestro.gates.<type>`, or a `commands.*` line) are also available under the type’s name without being declared here, and so is every custom gate the project declares (`maestro.gates.<id>`) under its id. An entry here of the same id replaces the project’s whole.",
|
|
611
655
|
"type": "object",
|
|
612
656
|
"propertyNames": {
|
|
613
657
|
"type": "string",
|
|
@@ -841,6 +885,92 @@
|
|
|
841
885
|
"additionalProperties": false
|
|
842
886
|
}
|
|
843
887
|
},
|
|
888
|
+
"codegen": {
|
|
889
|
+
"default": {},
|
|
890
|
+
"description": "Deterministic commands that write declared files, by id — see CodegenStepSchema. They run in every lane before its gates and in the integration worktree around every merge, in `after` order. A failing step is a red gate.",
|
|
891
|
+
"type": "object",
|
|
892
|
+
"propertyNames": {
|
|
893
|
+
"type": "string",
|
|
894
|
+
"minLength": 1,
|
|
895
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
896
|
+
},
|
|
897
|
+
"additionalProperties": {
|
|
898
|
+
"type": "object",
|
|
899
|
+
"properties": {
|
|
900
|
+
"cmd": {
|
|
901
|
+
"type": "string",
|
|
902
|
+
"minLength": 1,
|
|
903
|
+
"description": "Shell command, run in the worktree with its environment — the lane’s or the integration worktree’s. Must be deterministic: the same inputs produce byte-identical outputs (`doctor --codegen` checks). A `format` step’s command takes `{changed_files}`."
|
|
904
|
+
},
|
|
905
|
+
"outputs": {
|
|
906
|
+
"minItems": 1,
|
|
907
|
+
"type": "array",
|
|
908
|
+
"items": {
|
|
909
|
+
"type": "string",
|
|
910
|
+
"minLength": 1
|
|
911
|
+
},
|
|
912
|
+
"description": "The files the command writes, as git glob pathspecs relative to the repository root (`schema.yml`, `frontend/src/api-client/**`, `**/*.gen.ts`). What it changes among them is committed; anything else it changes is left alone. Agents are told these are not to be edited by hand, and a merge conflict confined to them is settled by running the step instead of by a fixer."
|
|
913
|
+
},
|
|
914
|
+
"inputs": {
|
|
915
|
+
"default": [],
|
|
916
|
+
"description": "The files the outputs are a function of, as git glob pathspecs. When the step last ran on exactly these contents and its outputs are still what it wrote, it is skipped, as a gate is on a tree the cache has seen. Empty — the default — runs it every time.",
|
|
917
|
+
"type": "array",
|
|
918
|
+
"items": {
|
|
919
|
+
"type": "string",
|
|
920
|
+
"minLength": 1
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
"after": {
|
|
924
|
+
"default": [],
|
|
925
|
+
"description": "Steps that run before this one: the schema before the client generated from it, the client before whatever reads it.",
|
|
926
|
+
"type": "array",
|
|
927
|
+
"items": {
|
|
928
|
+
"type": "string",
|
|
929
|
+
"minLength": 1,
|
|
930
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
"requires": {
|
|
934
|
+
"default": [],
|
|
935
|
+
"description": "Resource pool ids held while the step runs, as a gate’s are. Never `lane`.",
|
|
936
|
+
"type": "array",
|
|
937
|
+
"items": {
|
|
938
|
+
"type": "string",
|
|
939
|
+
"minLength": 1,
|
|
940
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
"timeout_s": {
|
|
944
|
+
"default": 600,
|
|
945
|
+
"type": "integer",
|
|
946
|
+
"minimum": 1,
|
|
947
|
+
"maximum": 9007199254740991
|
|
948
|
+
},
|
|
949
|
+
"format": {
|
|
950
|
+
"default": false,
|
|
951
|
+
"description": "A formatter rather than a generator. `{changed_files}` in `cmd` is the files the phase changed that match `outputs`, shell-quoted; with none, the step is skipped. Formatting the whole repository makes diffs in files the phase never touched. A format step’s outputs are ordinary source, so they are not listed as hand-edit-free and a conflict in them still goes to the fixer.",
|
|
952
|
+
"type": "boolean"
|
|
953
|
+
},
|
|
954
|
+
"description": {
|
|
955
|
+
"type": "string"
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
"required": [
|
|
959
|
+
"cmd",
|
|
960
|
+
"outputs"
|
|
961
|
+
],
|
|
962
|
+
"additionalProperties": false
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
"append_only": {
|
|
966
|
+
"default": [],
|
|
967
|
+
"description": "Files every phase only appends to — a DI container, a URL route list, a registry of notification kinds, a beat schedule — as gitattributes patterns. Merged in the integration worktree with git’s `union` driver, which keeps both sides’ lines, so two phases each adding an entry is not a conflict. The merged tree is gated as any other is; a union that reads wrong (a duplicate key, a missing comma) is a red gate there, not a silent pass.",
|
|
968
|
+
"type": "array",
|
|
969
|
+
"items": {
|
|
970
|
+
"type": "string",
|
|
971
|
+
"minLength": 1
|
|
972
|
+
}
|
|
973
|
+
},
|
|
844
974
|
"nodes": {
|
|
845
975
|
"minItems": 1,
|
|
846
976
|
"type": "array",
|
|
@@ -106,6 +106,9 @@ These bleed across sub-skills, so capture once now:
|
|
|
106
106
|
- **A test-suite pool.** `AskUserQuestion`: `One suite at a time (Recommended for DB-backed suites)`, `Two at a time`, `No limit`. A limit emits `maestro.resources.test-suite` (`kind: semaphore`, `capacity` 1 or 2) and `maestro.gates.test.requires: [test-suite]`. Then offer the runner's command patterns as the pool's `match` (`pytest*`, `uv run pytest*`, `pnpm test*`), which makes agents take the lease before running tests by hand.
|
|
107
107
|
- **Files git does not carry.** A maestro lane is a worktree and has only what git tracks. Read `.gitignore` for entries with a tracked `*.example` / `*.sample` / `*.template` sibling, the compose file's `env_file:` lines, and the gitignored settings module the framework imports (`DJANGO_SETTINGS_MODULE`'s local module, typically). Propose the list in a multi-select `AskUserQuestion` (header `Env files`), each path pre-selected; the user confirms or drops entries. → `maestro.project.env_files`. Every hook, gate and `manage.py` call in a lane depends on these, and a plan that omits them fails in ways that look like code problems. Nothing found → omit the key and say so.
|
|
108
108
|
|
|
109
|
+
- **Test-tool caches git must not carry.** Gates in a lane write cache and report files into the worktree, and an agent's `git add -A` there commits whatever is not ignored. Read `.gitignore` for the outputs of the runners and gates selected above: `.testmondata*` (pytest-testmon's SQLite database, plus its `-shm` / `-wal` siblings — tens of MB), `.pytest_cache/`, `junit/` or `junit*.xml`, `coverage/`, `.coverage`, `.coverage.*`, `htmlcov/`, `coverage.xml`, `.nyc_output/`, `.vitest/` and `.jest-cache/` where those runners are in use. Then read `.pre-commit-config.yaml` (or the project's hook runner) for `check-added-large-files`: that hook checks staged files, so a testmon database that is not ignored fails every lane commit and every maestro merge commit (`project.hooks: run`), and looks like a hook problem rather than a missing ignore rule. Propose the missing entries in a multi-select `AskUserQuestion` (header `Cache ignores`), each pre-selected, with `Add the selected entries (Recommended)` meaning append them to `.gitignore` under a `# test-tool caches` comment. When one is already tracked, say so: ignoring it does not untrack it, and the human runs `git rm --cached <path>` themselves. Nothing missing → say so and add nothing.
|
|
110
|
+
- **Generated files and append-only registries.** Read the project for generators — an API schema command (`spectacular`, an `openapi.json` dump), a generated client (`openapi-ts`, `orval`, `openapi-generator`, `graphql-codegen`; `*.gen.ts`), a route tree (`routeTree.gen.ts`) — and for file-modifying hooks in `.pre-commit-config.yaml` / `lefthook.yml` / `lint-staged` (a schema hook, `ruff format`, `prettier --write`). Propose each as a codegen step in a multi-select `AskUserQuestion` (header `Codegen`), pre-selected, showing its command, inputs and outputs; a formatter is `format: true` with `{changed_files}` in its command. → `maestro.codegen`. Then propose append-only registries — DI containers, URL route lists, notification-kind registries, beat schedules — in a second multi-select (header `Registries`). → `maestro.append_only`. Nothing found → omit both keys and say so.
|
|
111
|
+
|
|
109
112
|
Everything here is a default under every plan. A plan's own `.workflow.json` overrides any of it, and a commit to this file on a running plan's branch (`plan/<workflow-id>/base`) is applied to that run.
|
|
110
113
|
|
|
111
114
|
### D. Optional foundation skills
|
|
@@ -379,6 +382,14 @@ maestro:
|
|
|
379
382
|
capacity: <1 | 2>
|
|
380
383
|
kind: semaphore
|
|
381
384
|
match: <C.11 → runner command patterns, e.g. ['pytest*', 'uv run pytest*']>
|
|
385
|
+
codegen: # C.11 → generators and file-modifying hooks; omit when none
|
|
386
|
+
<step-id>:
|
|
387
|
+
cmd: <the generator command, e.g. uv run python manage.py spectacular --file schema.yml>
|
|
388
|
+
inputs: <globs it reads, e.g. ['**/serializers.py', '**/views.py']; omit to run every time>
|
|
389
|
+
outputs: <globs it writes, e.g. [schema.yml]>
|
|
390
|
+
after: <steps that run first, e.g. [schema]; omit when none>
|
|
391
|
+
format: <true for a formatter whose cmd takes {changed_files}; omit otherwise>
|
|
392
|
+
append_only: <C.11 → registries phases only append to, e.g. [app/containers.py, app/urls.py]; omit when none>
|
|
382
393
|
project:
|
|
383
394
|
env_files: <C.11 → gitignored files every lane needs its own copy of, e.g. [.env, .env.docker, app/settings/local.py]; omit when none>
|
|
384
395
|
|
|
@@ -45,6 +45,20 @@ If no multi-tenancy signals → single-tenant Django; the team's templates may s
|
|
|
45
45
|
- One-off script invocation surface (Jupyter notebook in `notebooks/<name>/`, Django management command, both) — drives the `run-one-off-script-django` skill's runner artefact + which `Runtime` adapter ships
|
|
46
46
|
- Notebook directory path (`notebooks/`, `<project>/notebooks/`) — only when Jupyter is selected as a runner surface
|
|
47
47
|
|
|
48
|
+
## Gates that should mirror CI
|
|
49
|
+
|
|
50
|
+
Ask which of these CI runs, and suggest each one it runs as a gate, so a phase cannot pass every gate and still fail CI. Declare each once, project-wide, as a custom gate under `maestro.gates.<id>` in `.vinta-ai-workflows.yaml` (e.g. `django-check: { cmd: ... }`, `api-client-fresh: { cmd: ... }`) and add its id to `maestro.defaults.gates` — every plan then runs it as its own gate, with its own verdict. Don't chain these onto the `typecheck` / `test` lines in `commands`: one verdict for several unrelated checks tells a fixer nothing about which one failed.
|
|
51
|
+
|
|
52
|
+
- **Deploy check at warning level** — `python manage.py check --deploy --fail-level WARNING` (with the CI settings module). Catches what `check` alone passes: URL patterns Django warns about (e.g. a `path()` route that looks like a regex), security settings, model warnings. One observed run added regex-style routes that passed every gate and failed this check in CI.
|
|
53
|
+
- **Migrations complete** — `python manage.py makemigrations --check --dry-run`.
|
|
54
|
+
- **Generated client / schema up to date** — when the project generates an OpenAPI/GraphQL schema or a typed frontend client from the Django API (drf-spectacular, Strawberry, django-ninja + an openapi codegen), a gate that regenerates it and fails on a diff (`<generate command> && git diff --exit-code <schema/client paths>`).
|
|
55
|
+
|
|
56
|
+
Placeholders to ask about: the settings module CI runs checks with, and the schema/client generate command and its output paths.
|
|
57
|
+
|
|
58
|
+
## Test determinism
|
|
59
|
+
|
|
60
|
+
Tests that touch dates freeze the clock and set the timezone explicitly (`freezegun` / `time-machine` + `override_settings(TIME_ZONE=...)`), and schema generation reads no host state — timezone choices come from a pinned `tzdata`, not the OS tz database. Ask whether the project already has `freezegun`/`time-machine` and `tzdata` in its dev/lock dependencies; the `write-unit-test` Django pack assumes them.
|
|
61
|
+
|
|
48
62
|
## When this stack doesn't apply cleanly
|
|
49
63
|
|
|
50
64
|
- Django REST framework but no DB (read-only API on top of an external service) → most of these skills don't apply.
|
|
@@ -262,6 +262,7 @@ Two habits pay for themselves:
|
|
|
262
262
|
|
|
263
263
|
- **Front-load shared scaffolding into a wave-1 foundation phase.** Types, the migration, the empty module, the fixture. Every use-case phase then depends only on that one phase, and they all run at once, instead of forming a chain.
|
|
264
264
|
- **Pin every interface two parallel phases share with a contract test in that foundation phase.** A signal payload one phase sends and another receives, a protocol or ABC one implements and another calls, a DI provider one registers and another injects, an event schema, a serializer shape. Write the test that states the contract (the keyword arguments a receiver requires, the methods an implementation must have) into the foundation phase, so each parallel phase's own gates run it on its own branch. Without it, two phases can each be green alone and disagree with each other: one observed run had a sender omit an `organization_id` its sibling's receiver required, and the merged wave failed 22 tests that no phase's gate could have seen. The executor also runs the plan's gates once on the merged tree before the plan PR (`defaults.wave_gates`), but that catches the disagreement at the end, when it costs a rework of both phases.
|
|
265
|
+
- **A rename of a shared symbol is an interface change too.** A phase that renames a function, class, module or field that a same-wave sibling also touches either does the rename in the foundation phase, or keeps the old name as an alias (a re-export, a thin wrapper, a deprecated property) until the whole wave has landed, and a later phase removes the alias. Otherwise the sibling adds a new call to the old name on its own branch: one observed run had a phase add a call to a private helper while its sibling renamed it, each passed every gate alone, and the merged wave failed the typecheck. Say which of the two the plan does in the renaming phase's body, and put the alias's removal in a phase that depends on every phase of that wave.
|
|
265
266
|
- **Split by seam, not by layer, when the seams are independent.** Four use-cases on the same entity are four independent phases if they only share the model. Four layers of one use-case (repository → service → serializer → view) are a chain no matter how you number them.
|
|
266
267
|
|
|
267
268
|
Don't contort the plan for concurrency, though. A genuinely sequential feature is a chain of waves of one, and that is a correct plan.
|
|
@@ -289,6 +290,7 @@ Each file carries `findings` and `gaps`. Use them like this:
|
|
|
289
290
|
- **`failure_causes`** — failed attempts grouped by reason, with `attempts` and the phases each one hit. An entry with `setup: true` failed before any agent ran — provisioning, a base merge, a checkout — and repeating it is the project configuration's fault, not the plan's: name it to the human (a missing `env_files` entry, a compose service that will not start) rather than reshaping the graph around it. A `setup: false` cause that hit many phases is a gate or a contract the plan should have settled first.
|
|
290
291
|
- **`crew_substitutions`** — phases run by somebody other than the member the plan named. Entries with `above_plan: true` ran dearer than budgeted; many of them on one member means the roster was too thin at that tier for the plan's width. Size the **Crew** table so each wave's phases are covered at their own tier, rather than relying on cover from above.
|
|
291
292
|
- **`operations`** — what people (and the coordinator) did to the run: amendments, steering, pauses, aborts, who answered the questions, commands run by hand. A plan whose runs needed many of these is telling you where it was underspecified: a phase steered or redirected repeatedly needed a sharper brief; the same question answered the same way every time belonged in the plan.
|
|
293
|
+
- **`unattended_decisions`** — what the run decided with nobody watching: `answers` (each question a `--retry-after` timer or the coordinator answered, by `category` — `agent_question` is a design choice an agent stopped to ask about and got its recommended option for, `plan_question` is usually a review budget that ran out and was stopped) and `coordinator_actions`. Every `agent_question` here is a decision this plan left open: settle that kind of choice in the next plan's briefs, before a phase can stop on it. Many `plan_question` stops on one phase mean its review never converged — split it or sharpen its acceptance criteria.
|
|
292
294
|
|
|
293
295
|
Rules for using them:
|
|
294
296
|
|
|
@@ -606,8 +608,11 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
|
|
|
606
608
|
| `gates.<id>` | The checks a phase must pass, as **shell commands run in the phase's lane** — the project's real typecheck / test / lint invocations, not an agent and not prose. **Declare only what the project does not already supply** (see "What the project already says"): a gate the project configures by type is available under the type's name without an entry here, and an entry with `"type"` overrides just the fields it sets. **An entry that overrides a project gate must carry `"type"`** — `{ "type": "test", "requires": ["test-suite"] }`. Without `type`, `{ "requires": [...] }` is a new gate with no `cmd`, and the schema rejects it. Give a slow plan-specific gate `requires` naming the pool it contends for, and a `timeout_s` that is generous rather than tight. |
|
|
607
609
|
| `chores.<id>` | Agent turns a phase runs once its gates are green. **Always emit the `review` and `deslop` chores below unless `maestro.chores.review` / `maestro.chores.deslop` already declare them** — `review` is the phase's code review (see "The review chore") and `deslop` the comment-hygiene pass, and without them a maestro run has neither. Add another only when the plan genuinely needs one (a changelog entry, a translation extraction); a chore is a model turn per phase, so each one has to earn it. |
|
|
608
610
|
| `chores.review-canvas` | **Emit only when `.vinta-ai-workflows.yaml` has `integrations.pr-review-canvas: enabled`.** An `after_pr` chore: maestro runs it once each phase's PR is open, and it posts a review canvas on that PR. Emit it exactly as shown under "The review-canvas chore" below. Never emit it when the integration is `disabled` or absent. The skill it names is installed only when the integration is enabled. |
|
|
611
|
+
| `codegen.<id>` | The project's generators — the commands that write files other files determine: the OpenAPI schema, the API client generated from it, a route tree, a GraphQL schema — plus a formatter a pre-commit hook would otherwise run. `{ "cmd": "<the command>", "inputs": [<globs it reads>], "outputs": [<globs it writes>], "after": [<steps first>] }`, and `"format": true` with `{changed_files}` in `cmd` for a formatter. **Omit the whole key when `maestro.codegen` already declares them**, and declare one here only when this plan adds a generator. See "Generated files and append-only registries". |
|
|
612
|
+
| `append_only` | Files every phase only appends entries to — DI containers, URL route lists, notification-kind registries, beat schedules — as gitattributes patterns. **Omit when `maestro.append_only` already lists them**; a list here *replaces* the project's. See "Generated files and append-only registries". |
|
|
609
613
|
| `defaults.chores` | `["review", "deslop"]`, plus `"review-canvas"` when that chore is emitted — or **omit the key** when `maestro.defaults.chores` already names the same list. The chores every phase runs unless it names its own. **Never write `"chores": []` to mean "the project's"**: an empty list *replaces* the project's list with nothing, and the run then runs no chore at all. |
|
|
610
614
|
| `defaults.substitution` | **Omit** — it defaults to `up_one_tier`: a busy member's phase may go to the next tier up the roster has, and a warm session never moves a phase up. Set `same_tier` when the top tier is a limited credit allowance that must not absorb other tiers' work. |
|
|
615
|
+
| `defaults.escalate_after_rejections` | **Omit** — it defaults to `3`: a phase whose review fails to approve it three times on one desk moves to the cheapest member of the next tier up. Set `0` only when the human wants every phase kept on the tier the plan gave it. |
|
|
611
616
|
| `defaults.wave_gates` | **Omit** — it defaults to `every`: each wave's gates run in full on the merged tree, so the next wave builds on a passing one, and every gate of the plan once more before the plan PR opens. Set `final` only for a plan whose suite is too slow to run once per wave. Never `off` unless the project's suite cannot run in the integration worktree. |
|
|
612
617
|
| `defaults.verify_gates` / `defaults.base_gates` | **Omit** — both default to `full`: a phase runs its gates' full commands once its review approves, and a multi-dependency phase's merged base is gated before it starts. Set `verify_gates: scoped` or `base_gates: off` only when the human says the suite is too slow for it, and say so in the plan. |
|
|
613
618
|
| `defaults.gates` | The gate ids every phase runs. **Omit the key** when `maestro.defaults.gates` is already the list this plan wants; set it when most phases share a list the project does not name. A node with no `gates` key takes it. **Never `[]`**: that is a run with no gates. |
|
|
@@ -633,6 +638,7 @@ Rules the mapping depends on:
|
|
|
633
638
|
- **Never invent a model id.** Pick the tier from the rubric under "Staff the plan", then read the id out of [resources/ai-models.yaml](resources/ai-models.yaml). Ids drift; tiers don't.
|
|
634
639
|
- **The roster is the same decision as the Crew table.** `crew` transcribes it: one entry per row, `tier` from the Tier column, `model` from that tier in `ai-models.yaml`. A member the table does not list, or a row with no `crew` entry, means the two were edited separately.
|
|
635
640
|
- **A gate is a command; a chore is an agent.** Both run per phase and that is where the resemblance stops. A gate is a shell line that says pass or fail and decides whether the phase merges. A chore is a turn that *changes* the diff — the review loop's fixes, the comment pass, a changelog entry — and it runs only once the phase's gates are green. Only the `review` chore stands between a phase and its merge; any other chore that fails is recorded and the phase carries on to its final gate run. Anything you can express as a command belongs in `gates`, where it is cached and queued and costs no model time.
|
|
641
|
+
- **Gates cover what CI runs.** A check CI runs and no gate runs is a check every phase passes and the plan PR fails. Read the CI config (`.github/workflows/`, `.gitlab-ci.yml`, `.circleci/`, …) next to the task runner, and give every CI job that can run locally a gate — or say in the plan why it cannot (it needs a secret, a deploy, a hosted service). The ones a plan most often misses are not the test suite: a framework's deploy or system check run at warning level, a migrations-are-complete check, a "generated client / schema / lockfile is up to date" check that regenerates and diffs, a docs build. One observed run had a phase add routes that passed every gate and failed CI's deploy check. These are project-wide checks: they belong in `.vinta-ai-workflows.yaml` as custom gates (`maestro.gates.<id>` with a `cmd`, the id listed in `maestro.defaults.gates`), which every plan then runs without declaring them. When the project's `maestro.gates` lacks such a check, declare it here for this plan and tell the human it belongs in the project file — and give the exact `maestro.gates.<id>` entry to add.
|
|
636
642
|
- **Gate commands must be commands the repo actually runs today.** Read them out of the project's task runner (`package.json` scripts, `Makefile`, `pyproject.toml`, CI config) rather than guessing a conventional one. A gate that doesn't exist fails every phase identically, and looks like a code problem. A command that goes through `docker compose run` or `exec` needs `-T`: a gate runs with no terminal, and without it compose fails with "the input device is not a TTY" before the command starts (`doctor` warns about it).
|
|
637
643
|
- **The graph must agree with the Execution graph table.** Same nodes, same edges, same waves — they are two renderings of one set of `**Depends on**:` lines, so derive both from the lines rather than transcribing one from the other. A disagreement means one was hand-edited, and the executor flags it.
|
|
638
644
|
- **`plan_context_refs` is anchors, never prose.** It names sections of the plan; it never restates them. A summary written into the JSON is a second copy that drifts the first time someone edits the plan, and the whole point of the field is that the implementer reads what the plan actually says.
|
|
@@ -644,6 +650,7 @@ Read `.vinta-ai-workflows.yaml` before writing the document. Its `commands` and
|
|
|
644
650
|
- **Gates by type.** A gate typed `test`, `lint`, `typecheck` or `e2e` takes its command from the project — `maestro.gates.<type>`, else `commands.test_unit` / `lint` / `build` / `e2e`. Each type the project has a command for is available under the type's own name, so a node can list `"gates": ["typecheck", "test"]` with no `gates` table at all. Write an entry only to change something for this plan: `{ "type": "test", "timeout_s": 3600 }` keeps the project's command and extends its timeout; `{ "cmd": "…" }` (no type) replaces it outright.
|
|
645
651
|
- **`scoped_cmd`** is the same check narrowed to what a phase changed, with `{changed_files}` / `{touches}` in it. The project normally supplies it (`commands.test_unit_scoped`, `maestro.gates.<type>.scoped_cmd`). A phase's gates run it until its review approves; the full command runs after that, on the phase branch, and on every merged tree — each wave and each multi-dependency base. Set one here only for a plan whose phases need a different narrowing.
|
|
646
652
|
- **Pools, chores and the `project` block** merge per id: an entry here replaces the project's entry of the same id whole. Declare `resources.lane` (it is this plan's — one desk per implementer), and a pool, chore or database only when the project has none or this plan needs a different one.
|
|
653
|
+
- **Codegen steps** merge per id like chores: `maestro.codegen.<id>` reaches every plan, and an entry here replaces the project's whole. **`append_only`** is a list: omit it to take `maestro.append_only`. Both are project facts — when the project file does not have them yet, write them here and tell the human they belong under `maestro:` in `.vinta-ai-workflows.yaml`.
|
|
647
654
|
- **`defaults`** merge field by field: omit what `maestro.defaults` already says.
|
|
648
655
|
- **Omitting a key inherits; writing `[]` replaces with nothing.** `nodes[].gates`, `defaults.gates`, `defaults.chores` and `nodes[].chores` are lists, and lists replace rather than merge. The only way to take the project's list is to leave the key out. `"gates": []` on a node is a phase with no gates, `"chores": []` in `defaults` is a run with no review and no comment pass — both silent at run time, and both exactly what the observed run did. When in doubt, omit the key and read what `validate --json` resolved.
|
|
649
656
|
- **`project.env_files` and `project.hooks`** are project facts, not plan facts: declare them once under `maestro.project` in `.vinta-ai-workflows.yaml` and every plan inherits them. Write them in the workflow only when the project file does not have them yet — and then tell the human they belong in the project file.
|
|
@@ -652,6 +659,21 @@ When the project has no `maestro:` section and no `commands`, everything stays i
|
|
|
652
659
|
|
|
653
660
|
Maestro runs the plan from its own branch, `plan/<workflow-id>/base`, cut from `base_branch`. A commit to that branch that edits this file or `.vinta-ai-workflows.yaml` is applied to the run while it is going — that is how a team retunes a gate mid-plan — and ships to `base_branch` with the plan's PR.
|
|
654
661
|
|
|
662
|
+
### Generated files and append-only registries
|
|
663
|
+
|
|
664
|
+
Most merge conflicts between parallel phases are in two kinds of file, and neither needs an agent to resolve it. Declaring them is what lets maestro settle them itself — one observed run had 37 wave conflicts, most of them in exactly these files.
|
|
665
|
+
|
|
666
|
+
**Generated files → `codegen`.** A file that is a function of other files is regenerated after every merge rather than merged: maestro runs the steps in every lane before the gates and commits what they write, and settles a conflict confined to their outputs by running them. Look for, and **read the command out of the project** rather than guessing it:
|
|
667
|
+
|
|
668
|
+
- **An API schema** the backend emits — `manage.py spectacular --file schema.yml`, `generateschema`, a FastAPI `openapi.json` dump script, `nest build` + a swagger export. Inputs are the views, serializers or routers it reads.
|
|
669
|
+
- **A generated API client** built from that schema — `openapi-ts`, `orval`, `openapi-generator`, `@hey-api/openapi-ts`, `graphql-codegen` (look for `*.gen.ts`, `sdk.gen.ts`, `types.gen.ts`, `api-client/`). Its input is the schema; it runs `after` the schema step.
|
|
670
|
+
- **A route tree** — TanStack Router's `routeTree.gen.ts`, Next's typed routes. Inputs are the route files.
|
|
671
|
+
- **A file-modifying pre-commit hook** — read `.pre-commit-config.yaml` (and `lefthook.yml`, `.husky/`, `lint-staged`): a hook that regenerates a file (the schema hook above) becomes a generator step with that file as its output; a hook that reformats (`ruff format`, `prettier --write`, `black`) becomes a `"format": true` step whose `cmd` takes `{changed_files}` and whose `outputs` are the globs it formats. A hook that only checks is a gate, not a step.
|
|
672
|
+
|
|
673
|
+
Each step must be **deterministic**: the same inputs give byte-identical outputs. A generator that stamps the date, sorts by a hash, or embeds the machine's timezone (one observed schema's timezone enum differed by machine) conflicts on every merge whatever maestro does; fix its configuration first, and run `vinta-ai-maestro doctor <workflow.json> --codegen` to check. Name every file a step reads in its `inputs`, or leave `inputs` empty to run it every time. The phase briefs need no instruction about these files: maestro tells every agent which paths are generated and how to regenerate them.
|
|
674
|
+
|
|
675
|
+
**Append-only registries → `append_only`.** A file where each phase adds its own entries and never edits another's — a dependency-injection container, a URL `urlpatterns` list, a registry of notification kinds or event types, a Celery beat schedule, a `periodic_tasks.py` — is merged with git's `union` driver, which keeps both sides' lines. Find them in the Touch Lists: a file two or more phases of different waves both touch, and only by adding lines, is one. The merged tree is still gated, so a union that reads wrong (two entries with one key, a missing comma between list items) is caught there; prefer registries whose entries are one line each, ending in a comma. Never list a file where phases edit existing lines — union keeps both versions of the line.
|
|
676
|
+
|
|
655
677
|
### Plan-level context
|
|
656
678
|
|
|
657
679
|
`prompt_ref` gives a phase its own body. It gives it nothing else — and a phase body alone is how an implementer ends up building something the plan explicitly ruled out, or re-deciding a question **Guiding Decisions** already closed. `plan_context_refs` is where the plan hands every phase the two sections that bound all of them:
|
|
@@ -1194,6 +1216,8 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
|
|
|
1194
1216
|
- [ ] Post-mortems from previous runs (`.vinta-ai-maestro/runs/*/postmortem.json`, plus any committed beside a plan) read **before** the graph was drawn; every finding either changed an edge, a wave or a split, or was consciously dismissed as not applying to this feature.
|
|
1195
1217
|
- [ ] Slow-moving / cross-repo work sits in wave 1, and no in-repo phase depends on a cross-repo phase when it only needs the contract.
|
|
1196
1218
|
- [ ] Every interface two same-wave phases share (a signal payload, a protocol, a DI provider, an event schema) has a contract test in the foundation phase they both depend on.
|
|
1219
|
+
- [ ] A phase that renames a symbol a same-wave sibling also uses either does it in the foundation phase or keeps the old name as an alias until the wave lands.
|
|
1220
|
+
- [ ] Every CI job that can run locally is a gate (the project's or this plan's), or the plan says why it is not.
|
|
1197
1221
|
- [ ] **Crew** table is the first thing under **Phased Rollout**: one row per implementer and no reviewer rows, every member taking at least one phase, every `Takes` cell agreeing with that phase's `**Assigned to**:` line.
|
|
1198
1222
|
- [ ] **Every member earns their place** — by concurrency (a wave genuinely needs that many hands *at or above* those phases' tiers) or by cheapness (they take work a dearer member would otherwise do). Check it wave by wave: sort the wave's phase tiers against the roster's and confirm the roster covers them one for one.
|
|
1199
1223
|
- [ ] Every phase is assigned to a member whose tier is **at or above** the rubric tier its work implies — no phase handed down a tier to save money.
|
|
@@ -65,6 +65,15 @@ A dispatched agent cannot reach the human: Claude Code and Codex refuse their qu
|
|
|
65
65
|
|
|
66
66
|
The shape is defined once, in the **Asking the human** section every project's `AGENTS.md` carries ([source](../../../vinta-write-agents-md/resources/asking-the-human.md)). [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) parses the same block from a dispatched agent's report and renders it as a question card, so a plan run under the daemon and one run under the skills unblock the same way.
|
|
67
67
|
|
|
68
|
+
## The live-run seam (`partials/maestro-live-run.md`)
|
|
69
|
+
|
|
70
|
+
A plan run by [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) is amended through the run, not by rewriting its branches, and its operator changes (harness, account, resume policy, base branch) go through the maestro CLI.
|
|
71
|
+
|
|
72
|
+
| Block | Consumed by | What it owns |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `AMEND_LIVE_RUN` | `amend-plan` Step 0.5 | detecting a live run of the plan; editing the run's own workflow copy, committing on the plan branch, applying it with `amend`, handling a refusal |
|
|
75
|
+
| `OPERATOR_RECIPES` | `amend-plan` | switching harness or model, switching the account a harness runs as, resuming with the same policy flags, moving the base forward |
|
|
76
|
+
|
|
68
77
|
## The parallel-lanes seam (`partials/parallel-lanes.md`)
|
|
69
78
|
|
|
70
79
|
The plan gives every phase a `**Depends on**:` line. The conductor turns those into a DAG and dispatches a phase the moment its dependencies are green and a lane is free. Six blocks:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Partial — a plan with a live vinta-ai-maestro run
|
|
2
|
+
|
|
3
|
+
Included by `amend-plan`. A plan run by [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) holds its own frozen copy of the workflow, owns the phase, `integ-` and `wave-` branches, and rebases them itself. An amendment that edits only the plan files leaves that run building the old plan; one that force-pushes its branches pulls them out from under it. Both blocks describe the CLI as `vinta-ai-maestro --help` shows it; when a command named here is missing or its options differ, follow `--help` and say so.
|
|
4
|
+
|
|
5
|
+
<!-- block-begin: AMEND_LIVE_RUN -->
|
|
6
|
+
### Is a maestro run live for this plan?
|
|
7
|
+
|
|
8
|
+
Run `npx --no-install vinta-ai-maestro status --json` from the main checkout. It lists every run in the project; the ones for this plan carry the workflow id of `{{PLAN_DIR}}/<plan>.workflow.json`. When maestro is not installed, or no run of this workflow is `running`, `paused`, `interrupted` or `failed`, there is no live run: skip this block and continue with Step 1. A run that is `done` or `cancelled` is over, and its branches are amended like any other, as below.
|
|
9
|
+
|
|
10
|
+
When a live run exists, ask via `AskUserQuestion` (header `Live run`), naming the run id and its state:
|
|
11
|
+
|
|
12
|
+
- `Amend the live run (Recommended)` — edit the plan and the workflow, then hand the change to the run. maestro rebases the done phases the change reaches, in dependency order; this skill rewrites no branch itself.
|
|
13
|
+
- `Edit the plan only` — the run keeps building its snapshot. Choose this only when the run will be stopped and the plan run again.
|
|
14
|
+
- `Stop` — nothing written.
|
|
15
|
+
|
|
16
|
+
On `Amend the live run`, Steps 2–5 below are the run's job, not this skill's. Do this instead:
|
|
17
|
+
|
|
18
|
+
1. **Edit the plan** as Step 1 says.
|
|
19
|
+
2. **Edit the workflow to match.** Start from what the run holds, not from the committed file: `vinta-ai-maestro workflow <run-id> > <scratch>/workflow.json`. That copy carries every amendment already applied — the operator's and the run coordinator's — and editing the committed file instead silently reverts them. Change only what the plan change implies: a new node (with `prompt_ref`, `depends_on` with `artifact`, `touches`, `crew`), a changed `depends_on`, a changed `touches` or gate list. Then `vinta-ai-maestro validate <scratch>/workflow.json`.
|
|
20
|
+
3. **Commit both files on the run's plan branch**, `plan/<workflow-id>/base` (and wherever the plan lives, when that is elsewhere). Phases cut from that branch afterwards read the amended body. The commit also reaches the run as a `config` amendment, but that path keeps every started phase's definition, so it is not enough on its own.
|
|
21
|
+
4. **Apply it:** `vinta-ai-maestro amend <run-id> <scratch>/workflow.json`. Not-started phases take the change at once; done phases whose dependency closure changed are rebased by the run.
|
|
22
|
+
5. **A refusal names the running phases the change would move.** Wait for them, or pause each with `vinta-ai-maestro node pause <run-id> <phase>` and run `amend` again. Never stop the run to get past a refusal: a stopped run cannot be resumed.
|
|
23
|
+
6. **Record it** under `## Amendments` in the plan, with the run id in place of the branch list.
|
|
24
|
+
|
|
25
|
+
A done phase whose PR is already open is updated by the run's rebase. Tell the user which PRs will move before step 4; Step 3's approved-PR question applies to them too.
|
|
26
|
+
<!-- block-end: AMEND_LIVE_RUN -->
|
|
27
|
+
|
|
28
|
+
<!-- block-begin: OPERATOR_RECIPES -->
|
|
29
|
+
## Operator recipes for a live maestro run
|
|
30
|
+
|
|
31
|
+
Changes to *how* a live run executes rather than *what* the plan builds. None rewrites the plan, so none needs the amendment log. Read `vinta-ai-maestro --help` first: when it lists a dedicated command for one of these (`switch-harness`, `node reassign`, `sync-base`), use that command instead of the generic recipe below.
|
|
32
|
+
|
|
33
|
+
**Switch the harness or model for the remaining phases.** Get the run's workflow (`vinta-ai-maestro workflow <run-id> > <scratch>/workflow.json`), change the crew members' `harness` / `model` (or `defaults.harness`), `validate` it, and `amend <run-id>` it. A phase already running keeps the harness it was spawned on; the change is refused for it until it settles, or pause that phase first with `node pause`. Run `vinta-ai-maestro doctor <workflow.json> --resume <run-id>` beforehand so a harness that is not installed or not logged in shows up before a phase is dispatched to it.
|
|
34
|
+
|
|
35
|
+
**Switch the account a harness runs as** (a quota or credit allowance ran out, or the login expired). The run's agents use the login of the environment its job was started from. `vinta-ai-maestro pause <run-id> --wait`, log the harness in as the other account in that shell (or point its config-directory variable at the other account's directory), check with `doctor <workflow.json> --resume <run-id>`, then `run --resume <run-id>` from the same shell, with the same flags as the original start (next recipe). A phase reported as logged out is waiting for exactly this; no timer retries it.
|
|
36
|
+
|
|
37
|
+
**Resume with the same policy.** Check `vinta-ai-maestro run --help`: unless it says a resume reuses the run's own policy, pass the policy flags again — `--on-failure`, `--retries`, `--retry-after`, `--permission`, `--system-one`, `--no-coordinator` — exactly as the run was started. A resume without them runs on the defaults, which can turn an unattended run into one that waits on every question. Write the start command into the plan's tracking notes when the run starts, so the resume can copy it.
|
|
38
|
+
|
|
39
|
+
**Move the base branch forward** (the base gained commits the remaining phases need). The run's `base_branch` is fixed for its life, and a config commit never moves it. Pause the run (`pause <run-id> --wait`), merge the base into the plan branch `plan/<workflow-id>/base` with a merge commit — never a rebase: lanes, `integ-` and `wave-` branches are built on its commits — push it, and `run --resume <run-id>`. Phases cut after that build on the new commits, and the wave merges carry them to the rest. When nothing still to run needs them, skip this and resolve against the base once, on the plan PR. Basing the plan on a different branch is a new run, not a move.
|
|
40
|
+
<!-- block-end: OPERATOR_RECIPES -->
|
|
@@ -26,7 +26,7 @@ The flow is destructive (force-push). Every modification is gated on user confir
|
|
|
26
26
|
- Default branch: `{{DEFAULT_BRANCH}}`.
|
|
27
27
|
- Branch naming convention (set by [implement-plan](../implement-plan/SKILL.md)): `plan/{plan-id-kebab}/phase-{phase.id}`.
|
|
28
28
|
- **`WORKROOT`.** Resolve once, same as the [implement-plan Resolve WORKROOT step](../implement-plan/SKILL.md#step-05--resolve-workroot): the main checkout by default, or the plan's worktree when `run_options.use_worktree = true` in `run.md`. When the run used a **lane pool**, amend in the **integration worktree** — lanes are sized for forward implementation and may still hold state from their last phase. Every `git` call below runs with `git -C <WORKROOT>`; when no worktree is in play, `WORKROOT` is the main checkout and the commands read exactly as in-place git.
|
|
29
|
-
- **Amend only when no implementation is in flight.** A rewrite force-pushes branches other lanes may be based on. If `run.md` shows any phase `running`, stop and tell the user to let the run finish (or stop it) first.
|
|
29
|
+
- **Amend only when no implementation is in flight.** A rewrite force-pushes branches other lanes may be based on. If `run.md` shows any phase `running`, stop and tell the user to let the run finish (or stop it) first. A plan run by `vinta-ai-maestro` is the exception: that run is amended through its own CLI, never by force-pushing its branches — see [Step 0.5](#step-05--a-live-maestro-run).
|
|
30
30
|
|
|
31
31
|
## When to use
|
|
32
32
|
|
|
@@ -121,6 +121,10 @@ The flow is destructive (force-push). Every modification is gated on user confir
|
|
|
121
121
|
|
|
122
122
|
Anything in `merged-to-default` state shown explicitly with a warning. **Do not** include those in the force-push list — user must convert those to `append-new` phases instead.
|
|
123
123
|
|
|
124
|
+
## Step 0.5 — A live maestro run
|
|
125
|
+
|
|
126
|
+
<!-- include: partials/maestro-live-run.md#AMEND_LIVE_RUN -->
|
|
127
|
+
|
|
124
128
|
## Step 1 — Edit the plan file
|
|
125
129
|
|
|
126
130
|
Always the first write. Plan file is durable; commits get rewritten next.
|
|
@@ -337,6 +341,8 @@ After every queue entry processes:
|
|
|
337
341
|
|
|
338
342
|
<!-- include: partials/relay-questions.md#RELAY -->
|
|
339
343
|
|
|
344
|
+
<!-- include: partials/maestro-live-run.md#OPERATOR_RECIPES -->
|
|
345
|
+
|
|
340
346
|
## Important rules
|
|
341
347
|
|
|
342
348
|
- **Never `--force`. Always `--force-with-lease`.** Protects against silent overwrites.
|
|
@@ -358,6 +364,7 @@ After every queue entry processes:
|
|
|
358
364
|
## Quick checklist (orchestrator, per amendment run)
|
|
359
365
|
|
|
360
366
|
- [ ] User-requested change captured verbatim; classification determined (`body-rewrite` / `insert-new` / `append-new` / `guiding-decisions-change`).
|
|
367
|
+
- [ ] Live maestro run checked: when one exists and the user chose to amend it, the plan and the workflow were edited, committed on `plan/<workflow-id>/base`, and applied with `vinta-ai-maestro amend`; Steps 2–5 skipped.
|
|
361
368
|
- [ ] Plan parsed; per-phase state map built (state, branch, base, pr_status, merged_to_default); `WORKROOT` resolved from tracking `run_options`.
|
|
362
369
|
- [ ] Blast-radius signals computed; `low` → straight to confirmation, `high-blast-radius` / `restart-recommended` → user offered `Restart` / `Amend in place` / `Stop` before the force-push plan is shown.
|
|
363
370
|
- [ ] On `Restart` choice: new plan drafted (or hand-off to [plan-feature](../plan-feature/SKILL.md)); old plan annotated `Superseded`; tracking marked; this skill exits.
|
|
@@ -25,7 +25,9 @@ Django-specific testing — DB isolation, ORM data, settings, the test client, e
|
|
|
25
25
|
- **`unittest.mock.patch`** where the name is *used* (`patch("myapp.services.stripe_client")`), not where defined.
|
|
26
26
|
- **Email:** assert on `django.core.mail.outbox` (the test runner's `locmem` backend), don't mock `send`.
|
|
27
27
|
- **HTTP:** `responses` / `respx` — never a real outbound call.
|
|
28
|
-
- **Settings/time:** `@override_settings(...)` / `self.settings(...)`; `freezegun` or an injected clock.
|
|
28
|
+
- **Settings/time:** `@override_settings(...)` / `self.settings(...)`; `freezegun` (or `time-machine`) or an injected clock.
|
|
29
|
+
- **Time-independent tests:** freeze the clock (`@freeze_time("2026-03-04T23:30:00Z")`) on every test that reads `timezone.now()`, `date.today()` or `timezone.localdate()`, and pin the zone with `@override_settings(TIME_ZONE="UTC")` (or the zone the behavior is about). `localdate()` and `now().date()` disagree for part of the day; an unfrozen test meets that only near midnight.
|
|
30
|
+
- **Deterministic schema generation:** a schema or generated-client test (drf-spectacular, Strawberry/Graphene `print_schema`, django-ninja's OpenAPI) must not read host state. A timezone choice list built from `zoneinfo.available_timezones()` reads the host's tz database unless `tzdata` is installed and preferred — pin `tzdata` in the lockfile and build the choices from it, sorted.
|
|
29
31
|
- **Test client:** `self.client` exercises the real URL/middleware/view stack — good. Mock only the external service the view calls, not the view.
|
|
30
32
|
|
|
31
33
|
## Pitfalls
|
|
@@ -34,3 +36,4 @@ Django-specific testing — DB isolation, ORM data, settings, the test client, e
|
|
|
34
36
|
- `on_commit` hooks never fire under `TestCase` (the transaction rolls back) — use `TestCase.captureOnCommitCallbacks(execute=True)` or `TransactionTestCase`.
|
|
35
37
|
- A lone `assertEqual(Model.objects.count(), 1)` — asserts a count, not that the right object with the right fields exists.
|
|
36
38
|
- Asserting on `obj` after `save()` without `refresh_from_db()`.
|
|
39
|
+
- `date.today()` vs `timezone.localdate()` — the first is the host's date, the second the active Django zone's. A test that mixes them fails only when the two dates differ.
|
|
@@ -27,6 +27,8 @@ A test should fail when the behavior breaks and pass when it's correct — nothi
|
|
|
27
27
|
6. **Decouple from internals** — assert observable state/output, not private methods or call counts. The test survives a behavior-preserving rewrite.
|
|
28
28
|
7. **One behavior per test**, named for what it pins (`returns_zero_for_empty_cart`).
|
|
29
29
|
8. **Kill nondeterminism** — control every entropy source (clock, randomness, timezone/locale, iteration/DB order, concurrency). Freeze/inject them; a test that passes only sometimes is worse than none.
|
|
30
|
+
- **Time-independent:** a test that touches a date, a "today", an expiry or a timezone freezes the clock to a fixed instant and sets the timezone explicitly. Never compute the expected value from the real "now", and never assume the host's timezone. A date test that is green all day can fail in the hour around midnight UTC, or on a CI runner in another zone; pick a frozen instant near a day boundary when the code does date arithmetic, so the edge is exercised on purpose.
|
|
31
|
+
- **Deterministic generated output:** a test (or gate) that compares generated output — an OpenAPI/GraphQL schema, a generated client, a snapshot — must get the same bytes on every machine. Values that come from the host (the OS timezone database, installed locales, the filesystem order, the Python/Node version) make it differ by machine. Source them from a locked package (e.g. `tzdata` pinned in the lockfile for a timezone enum) and sort anything whose order isn't defined.
|
|
30
32
|
9. **Cover the branches, not just the happy path** — aim high (95%+ branch coverage): each error path, guard, and edge (empty, boundary, null) gets a case. Coverage is the floor, correct assertions are the point.
|
|
31
33
|
|
|
32
34
|
## Regression tests
|
|
@@ -52,3 +54,4 @@ Apply these; `framework-default` defers to the pack. If the existing suite contr
|
|
|
52
54
|
- Giant snapshots as a substitute for asserting the fields that matter.
|
|
53
55
|
- `delete all rows` in setup instead of rollback — masks leaked state, races other tests.
|
|
54
56
|
- Testing the framework/ORM/language instead of your behavior on top of it.
|
|
57
|
+
- `date.today()` / `new Date()` in the expected value — the test passes until the clock crosses a day boundary between the call under test and the assertion.
|