vinta-ai-workflows 0.7.0-alpha3 → 0.7.0-alpha30
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 +2443 -3
- package/README.md +33 -19
- package/package.json +11 -9
- package/schemas/README.md +11 -3
- package/schemas/ai-models.v1.schema.json +5 -0
- package/schemas/interview-answers.v1.schema.json +51 -0
- package/schemas/interview-round.v1.schema.json +165 -0
- package/schemas/plan-review.v1.schema.json +342 -0
- package/schemas/postmortem.v1.schema.json +603 -3
- package/schemas/prs-context-comments.v1.schema.json +1 -1
- package/schemas/prs-context-frontmatter.v1.schema.json +18 -7
- package/schemas/vinta-ai-workflows-config.v1.schema.json +390 -18
- package/schemas/workflow.v1.schema.json +489 -42
- package/skills/vinta-analyze-codebase/SKILL.md +5 -0
- package/skills/vinta-bootstrap-ai-tools/SKILL.md +121 -31
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/README.md +1 -1
- package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/README.md +85 -0
- package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/lib-template.sh +160 -0
- package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/prepare-template.sh +250 -0
- package/skills/vinta-bootstrap-ai-tools/resources/worktree-command/teardown-template.sh +96 -0
- package/skills/vinta-derive-skills/SKILL.md +23 -15
- package/skills/vinta-derive-skills/resources/dependency-license-block.md +8 -9
- package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/SKILL.md +1 -1
- package/skills/vinta-derive-skills/resources/foundation-skills/create-qa-use-cases/SKILL.md +3 -3
- package/skills/vinta-derive-skills/resources/foundation-skills/create-spec/SKILL.md +21 -2
- package/skills/vinta-derive-skills/resources/foundation-skills/handoff/SKILL.md +3 -3
- package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/SKILL.md +117 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/examples/docs/D4-concurrency.md +28 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/examples/round-01.json +60 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/index.html +523 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/interview-ui/scripts/interview-ui.mjs +416 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/SKILL.md +1 -1
- package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +34 -7
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +321 -127
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +19 -13
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +39 -21
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/claude-worktree-write-guard.py +0 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-claude-sandbox-settings.sh +0 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-compose-worktree-override.sh +0 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/sandbox-run.sh +0 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md +8 -6
- package/skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-review-loop/SKILL.md +181 -0
- package/skills/vinta-derive-skills/resources/plan-execution/README.md +27 -3
- package/skills/vinta-derive-skills/resources/plan-execution/partials/agent-models.md +14 -35
- package/skills/vinta-derive-skills/resources/plan-execution/partials/commit-strategy/stacked.md +18 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/dispatched-agent.md +31 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/implementer-prompt.md +38 -4
- package/skills/vinta-derive-skills/resources/plan-execution/partials/model-pick.md +4 -2
- package/skills/vinta-derive-skills/resources/plan-execution/partials/parallel-lanes.md +33 -31
- package/skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md +4 -2
- package/skills/vinta-derive-skills/resources/plan-execution/partials/relay-questions.md +13 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/review-loop.md +72 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/worktree-seam.md +86 -17
- package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +26 -15
- package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-phase-template.md +12 -1
- package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md +33 -25
- package/skills/vinta-derive-skills/resources/plan-execution/shell/integrate-phase-template.md +2 -1
- package/skills/vinta-derive-skills/resources/plan-execution/shell/review-phase-template.md +19 -27
- package/skills/vinta-derive-skills/resources/prs-context-template.md +5 -4
- package/skills/vinta-derive-skills/resources/systematic-debugging-mcp-tools.md +1 -1
- package/skills/vinta-derive-skills/resources/systematic-debugging-template.md +8 -8
- package/skills/vinta-derive-subagents/SKILL.md +25 -9
- package/skills/vinta-install-ai-tools-setup/SKILL.md +29 -2
- package/skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs +0 -0
- package/skills/vinta-migrate-plans-specs/SKILL.md +7 -7
- package/skills/vinta-sync-ai-tools/SKILL.md +19 -2
- package/skills/vinta-update-project-skills/SKILL.md +1 -0
- package/skills/vinta-write-agents-md/SKILL.md +10 -6
- package/skills/vinta-write-agents-md/resources/asking-the-human.md +56 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/review-layers.md +0 -101
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,1304 @@ 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-alpha30 on 2026-10-09 -->
|
|
11
|
+
<!-- pre-release: 0.7.0-alpha20 on 2026-09-24 -->
|
|
12
|
+
<!-- pre-release: 0.7.0-alpha19 on 2026-09-19 -->
|
|
13
|
+
<!-- pre-release: 0.7.0-alpha18 on 2026-09-17 -->
|
|
14
|
+
<!-- pre-release: 0.7.0-alpha17 on 2026-09-17 -->
|
|
15
|
+
<!-- pre-release: 0.7.0-alpha16 on 2026-09-16 -->
|
|
16
|
+
<!-- pre-release: 0.7.0-alpha15 on 2026-09-16 -->
|
|
17
|
+
<!-- pre-release: 0.7.0-alpha14 on 2026-09-16 -->
|
|
18
|
+
<!-- pre-release: 0.7.0-alpha13 on 2026-09-16 -->
|
|
19
|
+
<!-- pre-release: 0.7.0-alpha12 on 2026-09-15 -->
|
|
20
|
+
<!-- pre-release: 0.7.0-alpha11 on 2026-09-15 -->
|
|
21
|
+
<!-- pre-release: 0.7.0-alpha10 on 2026-09-15 -->
|
|
22
|
+
<!-- pre-release: 0.7.0-alpha9 on 2026-09-14 -->
|
|
23
|
+
<!-- pre-release: 0.7.0-alpha8 on 2026-09-14 -->
|
|
24
|
+
<!-- pre-release: 0.7.0-alpha7 on 2026-09-14 -->
|
|
25
|
+
<!-- pre-release: 0.7.0-alpha6 on 2026-09-13 -->
|
|
26
|
+
<!-- pre-release: 0.7.0-alpha5 on 2026-09-13 -->
|
|
27
|
+
<!-- pre-release: 0.7.0-alpha4 on 2026-09-13 -->
|
|
10
28
|
<!-- pre-release: 0.7.0-alpha3 on 2026-09-12 -->
|
|
11
29
|
<!-- pre-release: 0.7.0-alpha2 on 2026-09-12 -->
|
|
12
30
|
<!-- pre-release: 0.7.0-alpha1 on 2026-09-11 -->
|
|
13
31
|
|
|
14
32
|
### Added
|
|
15
33
|
|
|
34
|
+
- **`vinta-ai-maestro`: `run` now shows what a slow start is doing.** Until
|
|
35
|
+
now `run` printed "starting in the background…" and nothing else until the
|
|
36
|
+
run was up, which can take minutes (`prepare_cmd`, preflight, database
|
|
37
|
+
templates, lane provisioning). The job now prints a timestamped line per
|
|
38
|
+
step — plan branch, `prepare_cmd`, environment checks, each database
|
|
39
|
+
template, each lane ready (`3/5`) — and the launching `run` streams the
|
|
40
|
+
job's log to the terminal as it is written, with a "still starting…" line
|
|
41
|
+
after 15 s of quiet. Lines carry names and counts only, never command
|
|
42
|
+
output. A refused start is no longer printed twice.
|
|
43
|
+
|
|
44
|
+
- **`vinta-ai-maestro`: `createdb_cmd` / `dropdb_cmd` on a Postgres
|
|
45
|
+
`external` database** (`workflow.v1.schema.json`, optional, defaults
|
|
46
|
+
`createdb` / `dropdb`). They replace only the binary name — in the template
|
|
47
|
+
setup, the per-lane clone and the reset — so a project whose Postgres is a
|
|
48
|
+
container with a password the host cannot supply can set
|
|
49
|
+
`docker compose exec -T db createdb`. `-h/-p/-U` from `server_url` still
|
|
50
|
+
follow. Existing workflows are unchanged.
|
|
51
|
+
|
|
52
|
+
- **`vinta-ai-maestro`: a database password prompt no longer hangs `run`.**
|
|
53
|
+
Template, clone and reset commands now run with stdin closed, no controlling
|
|
54
|
+
terminal and a two-minute timeout that ends the whole process tree; a prompt
|
|
55
|
+
becomes a failed run carrying the command's stderr instead of a job stuck at
|
|
56
|
+
`starting`. `doctor` now runs the effective `dropdb_cmd --if-exists` against
|
|
57
|
+
a name that cannot exist and `FAIL`s, naming the missing password, when it
|
|
58
|
+
cannot authenticate — a server that merely answers used to pass.
|
|
59
|
+
|
|
60
|
+
- **New optional foundation skill: `interview-ui`** — run decision-heavy
|
|
61
|
+
interviews as a full-screen browser form instead of a chat exchange. One
|
|
62
|
+
decision per screen with the context the agent already knows, Markdown +
|
|
63
|
+
Mermaid (or HTML) explainers, option cards that state each option's
|
|
64
|
+
consequences, the recommended default marked, and an in-page question panel
|
|
65
|
+
for asking the agent (questions, not decisions — the agent replies in the
|
|
66
|
+
next round). Round-based and file-based, so it works the same under Claude
|
|
67
|
+
Code, Codex, Cursor, and Copilot: the agent writes
|
|
68
|
+
`.vinta-ai-workflows/interviews/<slug>/rounds/round-NN.json`, blocks on
|
|
69
|
+
`interview-ui.mjs wait`, and resumes when the browser submits
|
|
70
|
+
`answers/round-NN.json`. Source:
|
|
71
|
+
[skills/vinta-derive-skills/resources/foundation-skills/interview-ui/](skills/vinta-derive-skills/resources/foundation-skills/interview-ui/)
|
|
72
|
+
— `SKILL.md`, the zero-dependency Node ≥ 18 localhost server + CLI
|
|
73
|
+
[scripts/interview-ui.mjs](skills/vinta-derive-skills/resources/foundation-skills/interview-ui/scripts/interview-ui.mjs)
|
|
74
|
+
(`start` / `wait` / `stop` / `status` / `serve`; binds `127.0.0.1` only,
|
|
75
|
+
serves files only from the interview's `docs/`), the shell
|
|
76
|
+
[resources/index.html](skills/vinta-derive-skills/resources/foundation-skills/interview-ui/resources/index.html),
|
|
77
|
+
and a sample round under `resources/examples/`. Copied verbatim as a whole
|
|
78
|
+
directory (bucket A), opt-in via the bootstrap's **Optional foundation
|
|
79
|
+
skills** question 7.
|
|
80
|
+
- **Two new JSON Schemas**:
|
|
81
|
+
[`schemas/interview-round.v1.schema.json`](schemas/interview-round.v1.schema.json)
|
|
82
|
+
(agent → browser: groups, decisions, options with `consequences` /
|
|
83
|
+
`recommended` / `tags`, inline or file explainers, `replies[]`, `readback`
|
|
84
|
+
rounds) and
|
|
85
|
+
[`schemas/interview-answers.v1.schema.json`](schemas/interview-answers.v1.schema.json)
|
|
86
|
+
(browser → agent: per-decision `answered` / `waived` / `skipped` with
|
|
87
|
+
values, free text and notes, plus `questions[]` for the agent). Inventory
|
|
88
|
+
rows added to `schemas/README.md`.
|
|
89
|
+
- **Config schema field `foundation_skills.interview-ui`** (`enabled` /
|
|
90
|
+
`disabled`). Additive — no schema major bump. Emitted by the bootstrap
|
|
91
|
+
Step 0.5 YAML; existing projects can add it by hand or on the next sync.
|
|
92
|
+
- **Plan review: a page for a plan before it runs, and a chat with the agent
|
|
93
|
+
that wrote it.** Maestro's UI gains a **Plans** section. `vinta-ai-maestro
|
|
94
|
+
review open <workflow.json>` serves it and prints a link straight to the
|
|
95
|
+
plan. The page shows:
|
|
96
|
+
- **Graph.** The phase DAG, coloured by review state. Below it, the selected
|
|
97
|
+
phase's brief, its staffing and pipeline, the implementer and fixer
|
|
98
|
+
prompts, and its chores' prompts (the review loop's among them). These are
|
|
99
|
+
the real cold prompts a run would send, composed by the scheduler's own
|
|
100
|
+
function.
|
|
101
|
+
- **Plan.** The markdown, section by section, with an outline.
|
|
102
|
+
- **Gates.** A phase × gate matrix of the commands a run would execute.
|
|
103
|
+
- **Schedule.** A projected timeline with the critical path marked.
|
|
104
|
+
- **Issues.** What `validate` reports.
|
|
105
|
+
|
|
106
|
+
The reviewer can comment on the plan, a section, a phase, a prompt or a
|
|
107
|
+
gate, or select text and comment on the quote. Comments stay drafts until
|
|
108
|
+
sent to the agent as one batch. The sidebar chat shows whether the agent is
|
|
109
|
+
listening, working or away. The agent answers from its own session through
|
|
110
|
+
two new commands:
|
|
111
|
+
- `review wait` blocks, then prints what the person sent as JSON.
|
|
112
|
+
- `review reply` answers in the chat or on a thread, with `--resolve`.
|
|
113
|
+
|
|
114
|
+
Edits the agent makes to the plan show on the page within seconds.
|
|
115
|
+
**Approve plan** ends the loop. The review is committed beside the plan as
|
|
116
|
+
`ai-plans/<id>.review.json`. A new generated schema,
|
|
117
|
+
`schemas/plan-review.v1.schema.json`, validates it. Maestro's SPEC gains
|
|
118
|
+
§19 to describe it.
|
|
119
|
+
- **`vinta-ai-maestro validate <workflow.json> [--json]`.** It checks a
|
|
120
|
+
workflow the way a run loads it, layered over `.vinta-ai-workflows.yaml`.
|
|
121
|
+
It also checks two things only a run used to find: the filename matches
|
|
122
|
+
the id, and every `plan_ref`, `prompt_ref` and `plan_context_refs` anchor
|
|
123
|
+
names a heading the plan has. Exit `0` means valid and `1` means not.
|
|
124
|
+
`--json` gives an agent one object with each issue's source, path and
|
|
125
|
+
message.
|
|
126
|
+
- **`plan-feature` validates its workflow and runs the review.** When
|
|
127
|
+
`vinta-ai-maestro` is installed (on `PATH`, in `node_modules/.bin`, or
|
|
128
|
+
already cached for `npx --no-install`), the skill runs `validate --json` on
|
|
129
|
+
every workflow it writes. It fixes each issue and runs the check again, and
|
|
130
|
+
runs it again after every edit made during review. It never installs maestro
|
|
131
|
+
itself.
|
|
132
|
+
|
|
133
|
+
Once the files are written, the skill asks how to review: on the review
|
|
134
|
+
page, in chat, or later. On the page it serves the review, posts an
|
|
135
|
+
opening message, and loops on `review wait`. It edits the plan and then the
|
|
136
|
+
workflow for each comment, and answers every thread. The loop ends when the
|
|
137
|
+
plan is approved.
|
|
138
|
+
|
|
139
|
+
- **Model fallbacks for models that run out of quota.** A frontier model sold
|
|
140
|
+
as a small credit allowance used to stall a whole run when its credits ran
|
|
141
|
+
out: the `quota` refusal parked the harness, and every model on it waited
|
|
142
|
+
for a reset that might never come. Now a model can name a fallback, and the
|
|
143
|
+
run switches to it instead of waiting.
|
|
144
|
+
- **`ai-models.yaml` entries take `fallback:`.** The shipped table sets
|
|
145
|
+
`claude-fable-5-1 → claude-opus-5-5` and `gpt-6-astra → gpt-5.6-sol`
|
|
146
|
+
(tier 4). `ai-models.v1.schema.json` gains the optional field, and the
|
|
147
|
+
nightly `check-ai-models` job checks fallback ids for staleness too.
|
|
148
|
+
- **Maestro: `defaults.model_fallbacks`** (model id → fallback id), in a
|
|
149
|
+
plan's workflow or in `.vinta-ai-workflows.yaml` under
|
|
150
|
+
`maestro.defaults`. A `quota` refusal on a listed model retries the
|
|
151
|
+
spawn on its fallback right away and leaves the harness unparked. Later
|
|
152
|
+
spawns of that model on the same harness go straight to the fallback,
|
|
153
|
+
until the reset time the vendor gave, or for the rest of the run if it
|
|
154
|
+
gave none. This also covers a turn cut off mid-way and the monitor and
|
|
155
|
+
conflict-fixer spawns. Chains are followed. If the fallback is refused
|
|
156
|
+
for quota too, the whole account is out: the harness parks as it did
|
|
157
|
+
before. Each substituted spawn is journaled as `node_model_fallback`.
|
|
158
|
+
- **`plan-feature` writes the map** into each plan's workflow from the
|
|
159
|
+
crew's `fallback:` entries. The plan-execution skills (`implement-phase`,
|
|
160
|
+
`review-phase`) follow the same rule when maestro isn't running them.
|
|
161
|
+
- **`thermo-nuclear-review-loop` foundation skill.** A review-and-fix loop
|
|
162
|
+
over a change: the agent running it spawns one reviewer sub-agent, checks
|
|
163
|
+
each finding against the code, fixes the justified ones and answers the
|
|
164
|
+
rest, until the reviewer explicitly approves under a strict code-quality
|
|
165
|
+
standard. Questions only the human can settle (unreachable scenarios,
|
|
166
|
+
requirement ambiguity, destructive operations) go to them instead of being
|
|
167
|
+
assumed. Always shipped and copied verbatim (new
|
|
168
|
+
`foundation_skills.thermo-nuclear-review-loop`), because `review-phase`
|
|
169
|
+
runs it and every maestro workflow `plan-feature` writes names it in its
|
|
170
|
+
`review` chore. Also
|
|
171
|
+
invokable on its own. **Consumers**: re-sync to pick it up.
|
|
172
|
+
- **Project defaults for maestro in `.vinta-ai-workflows.yaml`.** A new
|
|
173
|
+
`maestro:` section holds what every plan's `.workflow.json` used to repeat:
|
|
174
|
+
per-type gate defaults, resource pools, chores, run defaults and the
|
|
175
|
+
`project` block. Maestro layers each plan over it. The project's
|
|
176
|
+
`commands.*` come first, then `maestro.*`, then the plan, then the run's own
|
|
177
|
+
amendments. A plan now states only what is different, and editing the
|
|
178
|
+
project file reaches every plan that did not override it.
|
|
179
|
+
- **Gate types.** A gate with `"type": "test" | "lint" | "typecheck" |
|
|
180
|
+
"e2e"` takes its command from `maestro.gates.<type>`, else from
|
|
181
|
+
`commands.test_unit` / `lint` / `build` / `e2e`. A `maestro.gates.<type>.cmd`
|
|
182
|
+
gives maestro its own line, and `implement-plan` keeps running
|
|
183
|
+
`commands.*`. Every type the project has a command for is available under
|
|
184
|
+
the type's name, so a node can list `["typecheck", "test"]` with no gate
|
|
185
|
+
table.
|
|
186
|
+
- **`defaults.gates`**, the gates every phase runs unless it names its own.
|
|
187
|
+
`defaults.harness` and `defaults.pipeline` now default to `claude-code`
|
|
188
|
+
and `standard-phase`, and `base_branch` defaults to
|
|
189
|
+
`project.default_branch`.
|
|
190
|
+
- **The bootstrap asks for these** (interview item C.11) and writes the
|
|
191
|
+
section. `plan-feature` reads it and emits only plan-specific values.
|
|
192
|
+
- **Scoped and full gates.** A gate can carry a `scoped_cmd` with
|
|
193
|
+
`{changed_files}` / `{touches}`. Phase gates run it, and the full `cmd` runs
|
|
194
|
+
once on each wave's merged tree. A failure there is a regression between
|
|
195
|
+
phases, and it fails the merge (`defaults.gate_scope: scoped`, the default;
|
|
196
|
+
`full` runs `cmd` everywhere). `commands.test_unit_scoped` and the new
|
|
197
|
+
`commands.lint_scoped` are inherited as scoped commands when they contain a
|
|
198
|
+
placeholder. `implement-plan`'s scoped suite fills the same placeholders.
|
|
199
|
+
- **Every maestro run works on a plan branch, `plan/<workflow-id>/base`.**
|
|
200
|
+
It is cut from `base_branch` at start, and phases and waves build on it.
|
|
201
|
+
Pull requests still target `base_branch`. Commit a change to
|
|
202
|
+
`.vinta-ai-workflows.yaml` or the plan's `.workflow.json` on that branch,
|
|
203
|
+
and the running plan picks it up as a `config` amendment that names the
|
|
204
|
+
commit. A config change never undoes a change an operator or the monitor
|
|
205
|
+
made in the run, never rewrites a phase that has started, and ships with
|
|
206
|
+
the plan's PR. `run` warns when the checkout's config file differs from
|
|
207
|
+
what is committed on the plan branch.
|
|
208
|
+
- **The gate guard.** On claude-code, every maestro agent gets a
|
|
209
|
+
`PreToolUse` hook, in every permission mode, through the per-lane settings
|
|
210
|
+
file only. It refuses two things: a gate's full command typed by hand
|
|
211
|
+
(answer: `vinta-ai-maestro gate <id>`), and a command matching a pool's new
|
|
212
|
+
`match` patterns when run without the lease (answer:
|
|
213
|
+
`vinta-ai-maestro with <pool> --`). Refusals are journalled as
|
|
214
|
+
`bare_gate_blocked`. The hook fails open and never auto-approves anything.
|
|
215
|
+
On codex and opencode a matching command is recorded after the fact as
|
|
216
|
+
`bare_gate_detected`. `doctor` reports which you get as
|
|
217
|
+
`gate-guard:<harness>`.
|
|
218
|
+
|
|
219
|
+
- **Provision worktrees with your own script instead of the `prepare-worktree`
|
|
220
|
+
skill.** Set `commands.worktree_prepare` in `.vinta-ai-workflows.yaml` (and
|
|
221
|
+
optionally `commands.worktree_teardown`), and `implement-plan` runs that
|
|
222
|
+
command for the shared worktree, every lane and the integration worktree.
|
|
223
|
+
No LLM step and no `agent_models.worktree_prep` delegate are involved.
|
|
224
|
+
Worktrees, and therefore parallel phases, are now available when the skill
|
|
225
|
+
is enabled **or** the command is set. When both are set the command wins,
|
|
226
|
+
and the skill is offered as a fallback if the command fails.
|
|
227
|
+
- **The contract.** The command runs from the repo root with
|
|
228
|
+
`VINTA_WORKTREE_NAME`, `_PATH`, `_BRANCH`, `_BASE_REF`, `_KIND`
|
|
229
|
+
(`single` | `lane` | `integration`), `VINTA_MAIN_CHECKOUT`,
|
|
230
|
+
`VINTA_PLAN_PATH` and `VINTA_WORKTREE_SUMMARY` set. It must create a
|
|
231
|
+
runnable worktree at that path, on that new branch, and exit non-zero on
|
|
232
|
+
failure. The conductor then checks the result with git rather than
|
|
233
|
+
trusting the exit code: the right branch, the worktree registered, and
|
|
234
|
+
the main checkout's status unchanged. Writing the summary YAML is
|
|
235
|
+
optional. A summary carrying `reset_cmd`s lets a lane be reused across a
|
|
236
|
+
migration boundary. Without one the lane is re-provisioned (teardown,
|
|
237
|
+
then prepare again) instead. Lanes are provisioned one at a time, because
|
|
238
|
+
the command runs `git worktree add` itself.
|
|
239
|
+
- **Sandbox tier.** The conductor probes the tier itself. With the skill
|
|
240
|
+
disabled, its `sandbox-run.sh` is not installed, so the tier is `none` and
|
|
241
|
+
the review-phase stray-write check is the guard.
|
|
242
|
+
- **Bootstrap.** The `prepare-worktree` question gains a
|
|
243
|
+
`Yes — we provision with our own script/command` option.
|
|
244
|
+
`vinta-analyze-codebase` now records existing worktree scripts in
|
|
245
|
+
`commands.worktree_candidates`, and the interview offers them as the
|
|
246
|
+
command. Existing configs need no change.
|
|
247
|
+
- **The bootstrap can write the script for you.** A fourth answer,
|
|
248
|
+
`Yes — generate a provisioning script for this project`, adds an optional
|
|
249
|
+
step after `vinta-derive-skills`. It writes `prepare.sh`, `teardown.sh`
|
|
250
|
+
and a shared `lib.sh` (default `scripts/worktree/`) from bundled
|
|
251
|
+
templates, and points `commands.worktree_prepare` / `_teardown` at them.
|
|
252
|
+
The templates carry the contract: `--dry-run`, sanity checks, rollback of
|
|
253
|
+
a half-made worktree, generic compose isolation, the full summary YAML,
|
|
254
|
+
and a teardown that refuses a dirty worktree, drops only `*_wt_<name>`
|
|
255
|
+
databases and never runs `down -v`. They also refuse to fork or drop a
|
|
256
|
+
database on a non-local host. The bootstrap fills only the project's
|
|
257
|
+
dependency, env, database and service steps from the inventory, then
|
|
258
|
+
verifies with `bash -n`, a dry run, and an optional real smoke test. The
|
|
259
|
+
scripts are the project's to commit and edit; humans can run
|
|
260
|
+
`prepare.sh <name>` / `teardown.sh <name>` directly too.
|
|
261
|
+
- **Agents stop for input with clickable questions instead of prose.** When a
|
|
262
|
+
skill or a sub-agent needs a decision from you, it now asks through your
|
|
263
|
+
harness's structured question tool, so you get the options as buttons plus
|
|
264
|
+
a free-text field, rather than a question buried at the end of a long
|
|
265
|
+
report. The tools are Claude Code's `AskUserQuestion`, OpenCode's
|
|
266
|
+
`question`, Codex's `request_user_input`, Cursor's `AskQuestion`, VS Code
|
|
267
|
+
Copilot's `askQuestions` and Gemini CLI's `ask_user`.
|
|
268
|
+
- **`AGENTS.md` gains an "Asking the human" section.** Its canonical text is
|
|
269
|
+
in [`vinta-write-agents-md/resources/asking-the-human.md`](skills/vinta-write-agents-md/resources/asking-the-human.md),
|
|
270
|
+
which `vinta-write-agents-md` copies verbatim between
|
|
271
|
+
`asking-the-human:start/end` markers. The section maps the tool name per
|
|
272
|
+
harness and sets the question shape. Every question has 2–4 concrete
|
|
273
|
+
options with descriptions, the recommended one first and marked
|
|
274
|
+
` (Recommended)`, and no "Other" option. Questions are batched up to 4 per
|
|
275
|
+
call (3 on Codex). Open-ended questions still offer the candidates the
|
|
276
|
+
agent found. Confirmation gates are questions, never "reply go". It also
|
|
277
|
+
gives the fallback when no tool is available.
|
|
278
|
+
- **Sub-agents return `status: NEEDS_INPUT`.** Claude Code and Codex do not
|
|
279
|
+
let a sub-agent call the question tool. A phase agent that hits a decision
|
|
280
|
+
it should not make alone now stops at a clean point and returns a
|
|
281
|
+
`questions:` block in the tool's own shape. The new
|
|
282
|
+
`plan-execution/partials/relay-questions.md` is included by
|
|
283
|
+
`implement-phase`, `review-phase` and `amend-plan`. It turns that block
|
|
284
|
+
into an `AskUserQuestion` call, records the answer in tracking, and
|
|
285
|
+
continues the same agent. `implementer-prompt.md#NEEDS_INPUT` carries the
|
|
286
|
+
contract into every composed implementer prompt, and
|
|
287
|
+
`vinta-derive-subagents` puts it in every read-write agent body. The
|
|
288
|
+
dependency-license block, which used to tell a sub-agent to "ask via
|
|
289
|
+
`AskUserQuestion`", now returns `NEEDS_INPUT`.
|
|
290
|
+
- **Prose stop points became option questions** across the plan-execution
|
|
291
|
+
unit, the foundation skills and the bootstrap skills. Examples:
|
|
292
|
+
- choosing the plan file;
|
|
293
|
+
- the start-run gate, which replaces "wait for go";
|
|
294
|
+
- the Tier-4 failure: amend the plan, retry with guidance, skip the
|
|
295
|
+
phase, or stop;
|
|
296
|
+
- resuming a run;
|
|
297
|
+
- a missing worktree;
|
|
298
|
+
- graph and crew validation;
|
|
299
|
+
- wave-merge conflicts;
|
|
300
|
+
- rebase failures, co-authored branches and merged phases in `amend-plan`;
|
|
301
|
+
- review findings that need a human decision;
|
|
302
|
+
- migration failures in `prepare-worktree`;
|
|
303
|
+
- mismatches when `handoff` resumes;
|
|
304
|
+
- the three-failed-attempts stop and MCP preflight failures in
|
|
305
|
+
`systematic-debugging`;
|
|
306
|
+
- the unclear-file, date and sidecar questions in
|
|
307
|
+
`vinta-migrate-plans-specs`.
|
|
308
|
+
- **Consumers:** `vinta-sync-ai-tools` adds a one-time migration. It
|
|
309
|
+
appends the section to an existing `AGENTS.md`, adds the needs-input
|
|
310
|
+
contract to read-write agent YAMLs, and re-renders the affected skills
|
|
311
|
+
together.
|
|
312
|
+
- **`vinta-ai-maestro` shows an agent's questions as a card you click.**
|
|
313
|
+
- **Detection.** The scheduler reads every agent turn as it drains
|
|
314
|
+
(`src/questions`). It takes a `NEEDS_INPUT` block in the agent's last
|
|
315
|
+
message, or a question tool call (`AskUserQuestion` / `question` /
|
|
316
|
+
`request_user_input`) that was its last act. Either one parks the node on
|
|
317
|
+
a new `kind: 'agent'` question.
|
|
318
|
+
- **The card.** It shows each option as a button with its description, and
|
|
319
|
+
marks the recommended one. The last choice is always **Other**, which
|
|
320
|
+
holds a free-text field. On a single-choice question it is exclusive:
|
|
321
|
+
typing in it deselects the picked option, and picking an option deselects
|
|
322
|
+
it. The daemon applies the same rule to an API answer. A single single-choice question
|
|
323
|
+
answers on the click. Several questions run as a wizard: one step per
|
|
324
|
+
question, with picking an option moving on and number keys to pick, then
|
|
325
|
+
a review step and Send.
|
|
326
|
+
- **The answer resumes the agent's own session.** The turn's effect returns
|
|
327
|
+
only once the agent stops asking. Under `--retry-after`, an unanswered
|
|
328
|
+
question takes the recommended options and is journalled as `unattended`.
|
|
329
|
+
- **§11 still holds.** The questions are written to the transcript as an
|
|
330
|
+
`agent_question` entry. The journal row carries a fixed sentence and the
|
|
331
|
+
effect id, and the answer is journalled as option indices plus the
|
|
332
|
+
operator's words.
|
|
333
|
+
- **API.** `POST …/answer` accepts `answers` (one
|
|
334
|
+
`{ selected, text? }` per question) as well as the scalar `answer`.
|
|
335
|
+
`NodeDetail.question` gains `ask`. The implementer prompt now teaches the
|
|
336
|
+
`NEEDS_INPUT` block. `MockAdapter` takes `scripts`, one per session.
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
- **The run view's token, cost and cache figures explain themselves.** Each
|
|
340
|
+
row in *Sessions and cost* now has an info icon. Hover over it for a quick
|
|
341
|
+
look, or click to keep it open. It says where the figure comes from and
|
|
342
|
+
shows the exact counts it was summed from. Token totals are summed from what
|
|
343
|
+
each harness reports when a session ends. Fresh input is shown apart from
|
|
344
|
+
cached input. Cost is the harness's own estimate at API list prices, not
|
|
345
|
+
your bill on a subscription plan. Codex sessions report no cost. Sessions
|
|
346
|
+
still running are not counted yet. `vinta-design-system` gains a `popover`
|
|
347
|
+
component.
|
|
348
|
+
- **PR Review Canvas integration (`integrations.pr-review-canvas`).** The
|
|
349
|
+
bootstrap now offers [PR Review Canvas](https://github.com/vintasoftware/pr-review-canvas)
|
|
350
|
+
as its first *integration*: an external tool that installs its own skill.
|
|
351
|
+
Nothing from it is bundled here. A new optional top-level `integrations`
|
|
352
|
+
object in `vinta-ai-workflows-config.v1.schema.json` records the opt-in
|
|
353
|
+
(`enabled` / `disabled`). It is additive, so no schema major bump. The
|
|
354
|
+
question is interview group D.2. It is asked only when `code_host` is
|
|
355
|
+
GitHub or GitLab and `pr_creation` is `agents-create`.
|
|
356
|
+
When it is enabled:
|
|
357
|
+
- `vinta-install-ai-tools-setup` step 7b runs
|
|
358
|
+
`pr-review install-skill --claude-dir ai-tools/skills --codex-dir ai-tools/skills`.
|
|
359
|
+
It writes one tool-owned copy that every vendor symlink reaches, then
|
|
360
|
+
runs `pr-review doctor`. The CLI is never auto-installed; the step prints
|
|
361
|
+
`npm install -g @vintasoftware/pr-review-canvas` instead.
|
|
362
|
+
- The `integrate-phase` PR step runs `/pr-review-canvas <n>` after
|
|
363
|
+
`open-pr.sh` publishes. This is a runtime gate in `partials/pr-context.md`,
|
|
364
|
+
so enabling the integration later needs no re-derive.
|
|
365
|
+
- `plan-feature` adds an `after_pr` `review-canvas` chore to the maestro
|
|
366
|
+
workflow it writes.
|
|
367
|
+
|
|
368
|
+
`vinta-analyze-codebase`, `vinta-derive-skills` and
|
|
369
|
+
`vinta-update-project-skills` treat an integration-owned skill (marker
|
|
370
|
+
`.pr-review-install`) as the tool's and never diff or edit it. Refresh it
|
|
371
|
+
with `pr-review upgrade`. `vinta-sync-ai-tools` offers the integration to
|
|
372
|
+
existing projects as an `opt-in-offer`.
|
|
373
|
+
- **Maestro: System One classifiers (SPEC §17).** A run can now consult a
|
|
374
|
+
fast classifier: one that answers yes/no or scores a fixed set of labels.
|
|
375
|
+
Three things use it.
|
|
376
|
+
- **Configuration.** `run`, `serve` and `doctor` take
|
|
377
|
+
`--system-one <config.json>`. The file lives on the operator's machine and
|
|
378
|
+
is never part of the plan. It names the classifier adapter: `http` (POST,
|
|
379
|
+
bearer key read from the env var you name) or `command` (a local process,
|
|
380
|
+
JSON on stdin and stdout). Out-of-tree adapters register through
|
|
381
|
+
`registerSystemOneAdapter`. The file also turns on the built-in judges.
|
|
382
|
+
This is the only API key the package uses; LLM harnesses still run on your
|
|
383
|
+
logged-in CLIs. Before a run starts, the preflight asks the classifier one
|
|
384
|
+
synthetic question, so a missing or rejected key, or an unreachable URL,
|
|
385
|
+
fails at minute zero rather than at the first gate. `"probe": false`
|
|
386
|
+
skips it. `doctor` warns about an `http` adapter with no `api_key_env`.
|
|
387
|
+
- **Judge gates.** A workflow gate can be
|
|
388
|
+
`{ "judge": { "question" | "question_ref", "labels", "fail_on",
|
|
389
|
+
"threshold", "on_unavailable" } }` instead of `{ "cmd": … }`. It asks the
|
|
390
|
+
plan's question about the lane's diff and reports exit 0 or 1, so
|
|
391
|
+
pipelines, the fix loop and the fixer prompt are unchanged. A judge gate
|
|
392
|
+
can only fail a phase, never pass one that a command gate failed. Agents
|
|
393
|
+
are not told to run judge gates. Without `--system-one`, a judge gate
|
|
394
|
+
falls back to its `on_unavailable` setting (`pass` by default).
|
|
395
|
+
`schemas/workflow.v1.schema.json` is regenerated (`gates` values are now
|
|
396
|
+
`anyOf` command | judge). The change is additive: existing gates are
|
|
397
|
+
unchanged.
|
|
398
|
+
- **Gate triage** (`judges.gate_triage`). When a command gate fails, the
|
|
399
|
+
classifier reads the end of its log. If it calls the failure flaky or
|
|
400
|
+
environmental, the gate is rerun once before a fixer is spent. The result
|
|
401
|
+
is exposed as `gate.triage`.
|
|
402
|
+
- **`--permission judged`** (claude-code only). Agents run without vendor
|
|
403
|
+
prompts. A `PreToolUse` hook asks the classifier about each judged tool
|
|
404
|
+
call (default `Bash`) through a new `POST /api/runs/:runId/permission`
|
|
405
|
+
endpoint. It fails closed at every step. It needs `judges.permission` in
|
|
406
|
+
the config. `doctor` refuses it for codex or opencode phases. This is a
|
|
407
|
+
speed trade-off, not a sandbox.
|
|
408
|
+
|
|
409
|
+
Every judgement is journalled as `system_one_judged`, with labels and
|
|
410
|
+
scores only, never the diff, log or command. With a hosted classifier,
|
|
411
|
+
diffs, gate logs and shell commands leave the machine. Use the `command`
|
|
412
|
+
adapter with a local model where that is not acceptable.
|
|
413
|
+
- **Maestro: chores that run after the PR opens (`when: "after_pr"`).** A
|
|
414
|
+
chore can now declare `when`. The default, `before_gate`, keeps today's
|
|
415
|
+
`polish` timing. `after_pr` runs the chore at the end of `standard-phase`'s
|
|
416
|
+
`integrate` state, after `open_pr`, for work about the PR rather than the
|
|
417
|
+
diff, such as a review canvas. `open_pr` now states `pr.opened` / `pr.url` /
|
|
418
|
+
`pr.number` as facts, so a guard can read them, and the `pr` root joins the
|
|
419
|
+
guard context. An `after_pr` chore's prompt names the PR and forbids
|
|
420
|
+
editing, committing or pushing. It is skipped when no PR opened. The
|
|
421
|
+
validator refuses `after_pr` with `on_failure: "fail"`, because the phase is
|
|
422
|
+
already merged. `run_chore` takes a `when` param. Custom pipelines that
|
|
423
|
+
call it without one keep running the `before_gate` chores.
|
|
424
|
+
`schemas/workflow.v1.schema.json` is regenerated, and the change is
|
|
425
|
+
additive.
|
|
426
|
+
- **Maestro: the node view links the phase's PR.** `GET
|
|
427
|
+
/api/runs/:runId/nodes/:nodeId` now carries `pullRequest` (`opened`, `url`,
|
|
428
|
+
`number`, `reason`), read from the latest `node_pr` event. The **Changes**
|
|
429
|
+
card shows `#<n>` as a link, or says why no PR opened (`gh` missing or
|
|
430
|
+
failed). Only an `https://` URL becomes a link. The link carries
|
|
431
|
+
`rel="noreferrer noopener"` because the page URL holds the daemon token.
|
|
432
|
+
- **The node view shows what a phase changed, and the whole diff on request.**
|
|
433
|
+
The *Diff* panel used to print `git diff base...branch` for the operator to
|
|
434
|
+
paste into a terminal. It is now a *Changes* card: how many files changed
|
|
435
|
+
and how many lines were added and removed, then each file — up to ten, the
|
|
436
|
+
rest as a count — with its status mark (`A`, `M`, `D`, `R`, `?` for a file
|
|
437
|
+
not yet added to git), its own `+N −M`, and a five-block bar that compares
|
|
438
|
+
the files to each other at a glance. *View full diff* opens a new route,
|
|
439
|
+
`#/runs/<run>/nodes/<node>/changes`, that renders the unified diff with
|
|
440
|
+
syntax highlighting (shiki, GitHub light and dark following the theme),
|
|
441
|
+
old and new line numbers, hunk headers with their function context, a
|
|
442
|
+
sticky file list that scrolls to each file, and a fold on any file past 400
|
|
443
|
+
changed lines. Within a removed line and the added line it pairs with, the
|
|
444
|
+
words that differ are marked, so a flipped operator or a renamed variable
|
|
445
|
+
reads at a glance; a line rewritten rather than edited keeps the row colour
|
|
446
|
+
alone. *Unified* or *Split* — the old file beside the new — is a toggle in
|
|
447
|
+
the header, remembered per browser. The daemon serves it from a new endpoint,
|
|
448
|
+
`GET /api/runs/:runId/nodes/:nodeId/changes`, answered by the git unit: from
|
|
449
|
+
the lane's **working tree** while the lane still holds the phase's branch —
|
|
450
|
+
so an agent's uncommitted edits and untracked files are visible while it is
|
|
451
|
+
working — and from the branch itself once the lane has moved on. The card
|
|
452
|
+
polls the counts on the node view's cadence and never the patch; the diff
|
|
453
|
+
view reads the patch once, and again on a button or when the run moves. A
|
|
454
|
+
patch over 2 MiB is cut at a file boundary and says so; the per-file counts
|
|
455
|
+
are complete whatever its size. A daemon older than this browser gets the
|
|
456
|
+
reference — branch, base, lane — as before. (`vinta-ai-maestro`)
|
|
457
|
+
|
|
458
|
+
- **The transcript reads like the harnesses do.** An agent's prose is
|
|
459
|
+
rendered as the markdown it is — headings, lists, inline code and fenced
|
|
460
|
+
blocks, the blocks highlighted like the diff — instead of as its asterisks;
|
|
461
|
+
raw HTML in it stays text. Tool calls are a verb and a target on one line —
|
|
462
|
+
`Read` and a path, `Shell` and a command, `Edit` and a path with `+N −M`
|
|
463
|
+
beside it — whatever the harness named the tool (`Bash`, `bash`,
|
|
464
|
+
`command_execution` and an MCP server's `read_file` all read as what they
|
|
465
|
+
are), with the result's verdict as a dot at the end of the row because the
|
|
466
|
+
result now sits under the call it answers. Open, an edit is a diff of its
|
|
467
|
+
two sides, a write is the file, a shell call is the command and what it
|
|
468
|
+
printed, a failed one with a red edge. A stretch of consecutive reads,
|
|
469
|
+
searches and listings is one row — *Explored · 3 reads, 2 searches* — that
|
|
470
|
+
opens into the calls. The operator's own message is set apart in a tinted
|
|
471
|
+
block. The transcript panel also takes the height the window leaves it
|
|
472
|
+
rather than a fixed 480px. (`vinta-ai-maestro`)
|
|
473
|
+
|
|
474
|
+
- **A plan's PRs now reach `main`.** Phase PRs alone did not: a phase with
|
|
475
|
+
several dependencies targets its `integ-<id>` branch, and nothing ever
|
|
476
|
+
targeted that branch, so every PR stacked above it was stuck. Conflict
|
|
477
|
+
resolutions from wave merges were on no phase branch at all. maestro and the
|
|
478
|
+
stacked-branches `implement-plan` now open two more kinds of PR:
|
|
479
|
+
- an **integration PR** per `integ-<id>` branch, into `base_branch`, opened
|
|
480
|
+
by that phase just before its own PR;
|
|
481
|
+
- a **plan PR** from the final wave branch into `base_branch`, opened once
|
|
482
|
+
the last wave merges. Its body lists every phase and integration PR in an
|
|
483
|
+
order that merges, and the two ways to land the plan: merge it alone, or
|
|
484
|
+
merge the listed PRs in order and this one last.
|
|
485
|
+
|
|
486
|
+
maestro also pushes every wave branch, journals the new PRs (`node_pr` gains
|
|
487
|
+
`kind`, and a new `run_pr` event), and treats `gh`'s "a pull request already
|
|
488
|
+
exists" as opened, with that PR's URL, so a retry no longer reports a failure.
|
|
489
|
+
`prs-context-frontmatter.v1` gains an optional `kind`
|
|
490
|
+
(`phase` | `integration` | `plan`); `phase_id` and `phase_title` are no
|
|
491
|
+
longer required for `kind: plan`.
|
|
492
|
+
- **The schemas are checked.** `npm run validate-schemas` (and `npm test`)
|
|
493
|
+
compiles every schema under `schemas/` with Ajv in strict Draft 2020-12 mode
|
|
494
|
+
and checks the fixtures in `tests/schema-fixtures/`: `valid/` must pass,
|
|
495
|
+
`invalid/` must fail. Source-side only — `ajv` and `ajv-formats` are root
|
|
496
|
+
devDependencies, and nothing new ships.
|
|
497
|
+
- **The plan graph is readable, and says where you are in it.** The canvas
|
|
498
|
+
`vinta-ai-maestro`'s run view and workflow editor share (`vinta-dag-editor`)
|
|
499
|
+
was a strip of six-pixel cards: a five-wave plan was framed into a 380px box
|
|
500
|
+
at half scale, every dependency label was a bordered box truncated to "the
|
|
501
|
+
BookmarkFolder…" piled on the line it labelled, edges had no direction, and
|
|
502
|
+
nothing changed when a node was picked. Redrawn: edges carry arrowheads and
|
|
503
|
+
the ones touching the selected or hovered node are pulled forward while the
|
|
504
|
+
rest recede; cards show a status dot and label (running pulses; failed and
|
|
505
|
+
awaiting-human tint the whole card) and clamp a long name to two lines;
|
|
506
|
+
labels are quiet pills in the gap between waves and spread apart when two
|
|
507
|
+
share one; the zoom and fit controls are icons with tooltips; an empty canvas
|
|
508
|
+
says so. **Long text is reachable three ways:** an instant tooltip on hover
|
|
509
|
+
or keyboard focus for a clamped name or truncated artifact, a label that
|
|
510
|
+
grows to its full text when hovered or selected, and a details strip under
|
|
511
|
+
the canvas — in the run view too — printing the selected node's name and
|
|
512
|
+
status or the selected dependency as a sentence. Keyboard: `+`/`−`/`0` zoom
|
|
513
|
+
and fit, Enter on the selected node opens it. Double-clicking a node in the
|
|
514
|
+
run view opens its node view (`vinta-dag-node-activate`).
|
|
515
|
+
- **Dependencies are drawn by dragging.** Drag from a node's handle onto
|
|
516
|
+
another node; a dashed preview follows the pointer, the source is ringed, and
|
|
517
|
+
every card says whether it would take the drop — a cycle, a duplicate, or the
|
|
518
|
+
source itself is dimmed and refused before the gesture is spent. Click-then-
|
|
519
|
+
pick and the `e` key still work. A refused dependency now says which rule it
|
|
520
|
+
broke (`vinta-dag-refuse`), and the editor shows that where the gesture
|
|
521
|
+
happened instead of nothing.
|
|
522
|
+
|
|
523
|
+
### Changed
|
|
524
|
+
|
|
525
|
+
- **`vinta-ai-maestro`: lane database names stay within Postgres's 63-character
|
|
526
|
+
identifier limit.** A forked lane database or template whose
|
|
527
|
+
`<name>_wt_<lane>` form exceeded 63 characters was silently truncated by
|
|
528
|
+
Postgres, so lanes could collide. Such names are now cut and end in a short
|
|
529
|
+
hash of the full name (stable and unique per lane); names that already fit
|
|
530
|
+
are unchanged.
|
|
531
|
+
|
|
532
|
+
- **`create-spec` offers a browser surface for its Step 0 interview.** When
|
|
533
|
+
`ai-tools/skills/interview-ui/` exists, the skill asks once whether to make
|
|
534
|
+
decisions in the browser or in chat. In browser mode the question bank,
|
|
535
|
+
clarity loop, read-back, and exit conditions are unchanged; each question
|
|
536
|
+
becomes a decision with `why`, quoted-back `context`, options with
|
|
537
|
+
consequences, and explainer docs, written one round at a time (A–C first,
|
|
538
|
+
later groups once the journeys are known). Projects without `interview-ui`
|
|
539
|
+
see no change.
|
|
540
|
+
- **Bootstrap `Optional foundation skills` group grows to eight questions**
|
|
541
|
+
(`interview-ui` is question 7; `handoff-to-client` moves to 8).
|
|
542
|
+
- **`review-phase` runs the thermo-nuclear review loop.** On the skills path
|
|
543
|
+
(`implement-plan`, `amend-plan`, `systematic-debugging`), each phase is now
|
|
544
|
+
reviewed by `thermo-nuclear-review-loop`, the loop maestro's `review` chore
|
|
545
|
+
runs. The conductor hosts it, because a sub-agent cannot spawn sub-agents of
|
|
546
|
+
its own. It spawns one reviewer sub-agent one tier above the tier the
|
|
547
|
+
phase's implementer ran at (a Tier 4 phase is reviewed at Tier 4), and hands
|
|
548
|
+
each round of findings to the phase's own implementer. The implementer
|
|
549
|
+
verifies each finding, fixes the justified ones, rejects the rest with
|
|
550
|
+
counter-evidence, and commits the round. Questions only a person can settle
|
|
551
|
+
come to you as structured questions. The phase passes only when the
|
|
552
|
+
reviewer explicitly approves. After 20 passes that still return blockers,
|
|
553
|
+
the conductor asks whether to continue for 20 more, stop the phase
|
|
554
|
+
unapproved, or amend the plan. **Consumers**: re-sync to re-render the
|
|
555
|
+
plan-execution skills.
|
|
556
|
+
- **Maestro: a phase's review is now the `review` chore.** Once the phase's
|
|
557
|
+
gates are green, the implementer's own session runs
|
|
558
|
+
`thermo-nuclear-review-loop`: it spawns one reviewer sub-agent and fixes or
|
|
559
|
+
answers its findings until the reviewer approves. Then `deslop` runs, and
|
|
560
|
+
the gates run once more on the final tree. A red gate still goes to the
|
|
561
|
+
fixer. A review turn that ends unapproved asks the operator to continue or
|
|
562
|
+
stop. `plan-feature` now emits the `review`
|
|
563
|
+
chore and `defaults.chores: ["review", "deslop"]`, and transcribes the
|
|
564
|
+
Crew table's implementers into `crew`.
|
|
565
|
+
- **Maestro: chore `when` is now `review`, `after_review` (the default) or
|
|
566
|
+
`after_pr`.** `review` chores must end with `VERDICT: pass` or
|
|
567
|
+
`VERDICT: fail`, and the phase continues only on pass. `after_review`
|
|
568
|
+
replaces `before_gate`.
|
|
569
|
+
- **Maestro: `max_fix_rounds` has no default, and unset means no limit.** The
|
|
570
|
+
fixer works on a red gate until it is green, and the review loop runs until
|
|
571
|
+
the reviewer approves, without asking. Set `max_fix_rounds` on a node to get
|
|
572
|
+
the old behaviour back: after that many fix rounds or unsuccessful review
|
|
573
|
+
passes, the phase asks whether to continue or stop. **Migration:** a plan
|
|
574
|
+
that relied on the default of 4 now runs unbounded; add
|
|
575
|
+
`"max_fix_rounds": 4` to keep it.
|
|
576
|
+
- **Maestro: every `workflow_amended` row carries `targets`.** Operator and
|
|
577
|
+
config amendments get them too, computed from the snapshot and the
|
|
578
|
+
proposal. Before, only monitor amendments had them.
|
|
579
|
+
- **Maestro: a gate command can be amended after a phase that uses it is
|
|
580
|
+
done.** Changing a gate table no longer counts as rewriting a finished
|
|
581
|
+
phase. Before, such an amendment was refused as `body_changed_after_done`.
|
|
582
|
+
- **Maestro: the editor saves only what you changed.** It shows the workflow
|
|
583
|
+
resolved over the project config, and on save it writes back only the
|
|
584
|
+
edited values. A first save no longer copies every default, or the
|
|
585
|
+
project's values, into the plan file.
|
|
586
|
+
|
|
587
|
+
- **Maestro: the node view's right-hand column stays short.** Gate logs open
|
|
588
|
+
in a dialog instead of an inline accordion. The dialog opens at the end of
|
|
589
|
+
the log, keeps up with a running gate's output, numbers the lines, and has
|
|
590
|
+
← / → to switch gates and a copy button. The question card's gate reference
|
|
591
|
+
opens the same dialog. Agent sessions are now a timeline with the newest
|
|
592
|
+
turn first, showing the latest five. The turn that is currently running
|
|
593
|
+
pulses and counts up. The full history opens in a dialog. The Changes card
|
|
594
|
+
lists five files instead of ten.
|
|
595
|
+
|
|
596
|
+
- **`vinta-ai-maestro` node view: steering is a chat input, and every control
|
|
597
|
+
says what it does.** The separate Steering card is gone. Its message box now
|
|
598
|
+
sits at the bottom of the Transcript panel and follows it to full page. Add
|
|
599
|
+
context and Redirect are a toggle over one Send button, which also sends on
|
|
600
|
+
Ctrl/⌘+Enter. The sentence under the box says what will happen for this
|
|
601
|
+
node's status and harness before you press it. Pause, Take over, Abort and
|
|
602
|
+
Retry phase moved to the page header beside the node's status, and only the
|
|
603
|
+
ones that apply are shown. Abort now asks for confirmation. A new **How
|
|
604
|
+
steering works** panel documents each control: when it is available, what
|
|
605
|
+
happens in practice, how to undo it, and what the node's harness supports.
|
|
606
|
+
|
|
607
|
+
- **Worktrees now get their own copy of the dependency dirs, never a
|
|
608
|
+
symlink.** A symlinked `node_modules` / `vendor/` / `venv/` sent writes
|
|
609
|
+
back into the main checkout, because tools resolve real paths: bundler and
|
|
610
|
+
test-runner caches, `postinstall` output and installs all landed there.
|
|
611
|
+
Parallel worktrees then raced on that one tree, and the filesystem sandbox
|
|
612
|
+
blocked those writes in the middle of a run.
|
|
613
|
+
- **`prepare-worktree`** copies every dep dir with a copy-on-write clone
|
|
614
|
+
(`cp -ac` on macOS, `cp -a --reflink=auto` on Linux). On APFS, btrfs and
|
|
615
|
+
XFS that is close to free on disk. It reinstalls virtualenvs and yarn PnP
|
|
616
|
+
instead, because their trees store absolute paths into the main checkout.
|
|
617
|
+
When the plan adds deps, it runs an install on top of the copy. In a
|
|
618
|
+
workspace it copies every member's `node_modules/`, not only the root's.
|
|
619
|
+
The summary's `state.deps.strategy` is now `copy | reinstall`.
|
|
620
|
+
- **Config:** `skills.prepare-worktree.deps_strategy` now defaults to
|
|
621
|
+
`copy`, where it used to default to `symlink`. `symlink` stays in the
|
|
622
|
+
schema enum so existing configs still validate, but it is deprecated and
|
|
623
|
+
read as `copy`. The bootstrap interview no longer offers it.
|
|
624
|
+
- **Maestro:** `LanePool` copies (clones) the main checkout's
|
|
625
|
+
`node_modules` into each lane instead of linking it. The lane summary
|
|
626
|
+
records `deps.strategy: copy`. A lane keeps its own tree across recycles,
|
|
627
|
+
so a project whose phases change deps should run its install in
|
|
628
|
+
`setup_cmd`. If the main checkout's `node_modules` is itself a link, the
|
|
629
|
+
lane gets the same link (a junction on Windows). When a resumed lane still
|
|
630
|
+
holds a link from an older version, that link is replaced with a copy.
|
|
631
|
+
- **Consumers:** re-sync to pick up the new `prepare-worktree` body. To
|
|
632
|
+
stop the interview default from being re-emitted, change
|
|
633
|
+
`deps_strategy: symlink` to `copy` (or `reinstall`) in
|
|
634
|
+
`.vinta-ai-workflows.yaml`. Worktrees that already exist keep their links
|
|
635
|
+
until they are re-provisioned.
|
|
636
|
+
- **`plan-feature` AI model tiers pick up the late-September releases.**
|
|
637
|
+
`plan-feature/resources/ai-models.yaml` now cites `claude-sonnet-5-5`
|
|
638
|
+
(tier 2, was `claude-sonnet-5`) and `gpt-6.1-sol` (tier 3, was
|
|
639
|
+
`gpt-6-sol`). The retired `gemini-3-pro` (tiers 3–4) is replaced by
|
|
640
|
+
`gemini-3.1-pro-preview`, the only Gemini Pro id currently listed. Tier
|
|
641
|
+
*placement* is unchanged. **Consumers**: re-sync to pick up the refreshed
|
|
642
|
+
model suggestions.
|
|
643
|
+
|
|
644
|
+
- **`vinta-ai-maestro run` starts the run as a background job and returns.**
|
|
645
|
+
The run no longer needs the terminal that started it, or any terminal: close
|
|
646
|
+
it and the run carries on. `run` prints the run id and the commands that
|
|
647
|
+
reach it, and if the preflight refuses it prints the report and says the run
|
|
648
|
+
did not start. **Migration:** a script or CI job that relied on `run`
|
|
649
|
+
blocking until the run ended, and on its exit code, adds `--foreground`,
|
|
650
|
+
which keeps the old behaviour exactly. The run's job serves no UI and prints
|
|
651
|
+
no URL any more — see `ui` below. (`vinta-ai-maestro`)
|
|
652
|
+
- **New commands to reach a background run:** `status [run-id]` (every run, or
|
|
653
|
+
one run phase by phase; `--json` for scripts), `logs <run-id> [-f]` (the
|
|
654
|
+
run's job log — what `run` used to print — followed until the job exits),
|
|
655
|
+
`pause <run-id>` (nothing new starts, running phases finish their current
|
|
656
|
+
step, then the job exits with the run `paused`; `run --resume` continues it)
|
|
657
|
+
and `stop <run-id>` (live agent turns and gates are killed and the run ends
|
|
658
|
+
`cancelled`, which `--resume` refuses). Both also accept `--wait`. Two new
|
|
659
|
+
run statuses come with them, `paused` and `cancelled`; the post-mortem still
|
|
660
|
+
reports `done` or `failed`, and a paused run writes none until it ends.
|
|
661
|
+
(`vinta-ai-maestro`)
|
|
662
|
+
- **`vinta-ai-maestro ui` serves the browser UI, and hosts no runs.** `serve`
|
|
663
|
+
is the same command under its old name. It reads every run from the journal
|
|
664
|
+
and forwards a live run's requests and socket to the job hosting it, so
|
|
665
|
+
watching, steering, answering and taking over work as before — and closing
|
|
666
|
+
it no longer interrupts anything. A run started or resumed from the UI is
|
|
667
|
+
launched as a background job with `ui`'s run settings. The job's own token is
|
|
668
|
+
kept in `.vinta-ai-maestro/runs/<run-id>/job.json` (mode `0600`, removed when
|
|
669
|
+
the job ends) and is never printed; treat that file like the URL.
|
|
670
|
+
(`vinta-ai-maestro`)
|
|
671
|
+
- **`purge` keeps a run whose job is still running**, and says so.
|
|
672
|
+
(`vinta-ai-maestro`)
|
|
673
|
+
- **The run view has Pause and Stop buttons**, beside Replay, while the run is
|
|
674
|
+
running. Stop asks for confirmation first, because it cannot be undone; both
|
|
675
|
+
say what is happening until the run's job has ended it. (`vinta-ai-maestro`)
|
|
676
|
+
|
|
677
|
+
- **The canvas no longer swallows the page's scroll.** A bare wheel over the
|
|
678
|
+
graph used to zoom it and block the page from scrolling; it now scrolls the
|
|
679
|
+
page, and zoom is ⌘ or Ctrl + scroll (which is also how a trackpad pinch
|
|
680
|
+
arrives), the buttons, or the keys. Dragging the canvas shows a grab cursor
|
|
681
|
+
and never selects text. The host box follows the window's height instead of
|
|
682
|
+
being fixed at 380px.
|
|
683
|
+
|
|
684
|
+
### Deprecated
|
|
685
|
+
|
|
686
|
+
- **`agent_models.reviewer` is ignored.** The review runs one tier above the
|
|
687
|
+
phase's implementer, so no configured tier is read. The key stays in the v1
|
|
688
|
+
config schema so existing configs still validate; drop it at your next
|
|
689
|
+
edit. Bootstrap no longer asks for it. `agent_models.fixer` now sets only
|
|
690
|
+
the merge-conflict fixer's tier, since the phase's implementer fixes review
|
|
691
|
+
findings at its own tier.
|
|
692
|
+
|
|
693
|
+
### Removed
|
|
694
|
+
|
|
695
|
+
- **Maestro: the monitor's intervention documents.** The watchdog no longer
|
|
696
|
+
asks the monitor for a JSON proposal of four verbs; the coordinator acts
|
|
697
|
+
through the run's API under the same "how, never what" rule, enforced on
|
|
698
|
+
every amendment it makes. `schemas/intervention.v1.schema.json`, the
|
|
699
|
+
`intervention:schema:*` scripts and `runs/<id>/interventions.jsonl` are
|
|
700
|
+
gone; the post-mortem's `interventions` now point at the coordinator's
|
|
701
|
+
conversation.
|
|
702
|
+
- **The three-layer phase review.** `review-phase` no longer runs the
|
|
703
|
+
mechanical checks (including the review-time secret scan, dependency-license
|
|
704
|
+
check and co-author trailer check), the plan-compliance walkthrough
|
|
705
|
+
(including the comment-hygiene check), the crew reviewer, or a separate
|
|
706
|
+
fixer per finding. The implementer's own install-time license check and
|
|
707
|
+
co-author rules are unchanged.
|
|
708
|
+
- **Reviewers on the plan.** `plan-feature`'s Crew table loses its `Role`
|
|
709
|
+
column and its reviewer rows, and phases lose the optional
|
|
710
|
+
`**Review models**:` line. **Migration:** plans written with reviewer rows,
|
|
711
|
+
a `Role` column or `**Review models**:` lines still run; `implement-plan`
|
|
712
|
+
ignores them.
|
|
713
|
+
- **Maestro: the reviewer role and its review loop.** Crew members are
|
|
714
|
+
implementers only, and the crew `role` field is gone. Also gone:
|
|
715
|
+
`defaults.reviewer_model` (in the workflow file and in the
|
|
716
|
+
`.vinta-ai-workflows.yaml` `maestro.defaults`), the consult step and the
|
|
717
|
+
review ledger. **Migration:** a `.workflow.json` with a crew `role`, a
|
|
718
|
+
reviewer crew member, a `before_gate` chore or `reviewer_model` no longer
|
|
719
|
+
parses. Re-emit it with `plan-feature`, or edit it by hand: drop the
|
|
720
|
+
reviewer members and every `role`, rename `before_gate` to `after_review`,
|
|
721
|
+
delete `reviewer_model`, and add the `review` chore.
|
|
722
|
+
|
|
723
|
+
### Added
|
|
724
|
+
|
|
725
|
+
- **Maestro: `land` and `propagate`.** `land <run-id>` says how a finished
|
|
726
|
+
run lands — the plan PR, with a merge commit, and nothing else — lists every
|
|
727
|
+
phase and integration PR with its state, and marks the ones whose work is
|
|
728
|
+
already in the base (every commit they add is a merge of commits the base
|
|
729
|
+
has, such as a GitHub "Update branch"); `--close` closes those with a
|
|
730
|
+
comment. `propagate <run-id> <phase>` carries a fix made on a phase branch
|
|
731
|
+
after the run into every `integ-*` and `wave-*` branch that contains it,
|
|
732
|
+
keeping the wave chain, and stops at a conflict with the branch untouched.
|
|
733
|
+
- **Maestro: `defaults.substitution`** (`same_tier` | `up_one_tier` | `any`,
|
|
734
|
+
additive, default `up_one_tier`). A busy member's phase may go at most to
|
|
735
|
+
the next tier the roster has, and a warm session no longer moves a phase up
|
|
736
|
+
a tier unless `any`. **Behavior change:** runs that relied on warm-session
|
|
737
|
+
promotion to a higher tier now stay at the planned tier; set `any` to keep
|
|
738
|
+
it. Substitutions above plan are shown in `status`, the run view and the
|
|
739
|
+
post-mortem.
|
|
740
|
+
- **Maestro: post-mortem findings** `failure_causes`, `crew_substitutions`,
|
|
741
|
+
`operations` and `cross_phase_failures`, and `wave_conflicts[].where`
|
|
742
|
+
(additive). `plan-feature` reads them.
|
|
743
|
+
- **Maestro: the run coordinator.** The run's monitor becomes its
|
|
744
|
+
coordinator: one agent per run that is woken when a phase fails, an attempt
|
|
745
|
+
errors, maestro logs an error about itself, a phase waits 20 minutes for
|
|
746
|
+
the integration worktree, or a phase or gate crosses its cost threshold, and
|
|
747
|
+
that can act: steer, redirect, pause, abort or retry a phase, answer a
|
|
748
|
+
question a `--retry-after` timer could answer, amend the live run, and run
|
|
749
|
+
commands in a lane or the integration worktree. It reaches the run with a
|
|
750
|
+
token of its own, everything it does is journalled as its own, and the run
|
|
751
|
+
refuses what it may not do: change what the plan builds, weaken a check
|
|
752
|
+
(drop a gate or chore, rewrite a gate command beyond its
|
|
753
|
+
`tuning.allowed_flags`, edit a judge gate, change `hooks`, lower
|
|
754
|
+
`wave_gates`), answer a question the operator holds, or halt the run. Woken
|
|
755
|
+
at most 24 times per run; `--no-coordinator` turns the wakes off
|
|
756
|
+
(`--no-intervene` still works).
|
|
757
|
+
- **Maestro: `node`, `workflow`, `amend` and `exec` commands.** Steer one
|
|
758
|
+
phase (`context`, `redirect`, `pause`, `abort`, `retry`, `answer`), print or
|
|
759
|
+
amend a live run's workflow, and run a command in a lane or the integration
|
|
760
|
+
worktree with that worktree's environment — its database, compose project
|
|
761
|
+
and ports — from a terminal. `exec … integration` holds the integration
|
|
762
|
+
worktree while the command runs, so no merge or unattended retry checks a
|
|
763
|
+
branch out underneath a manual repair.
|
|
764
|
+
- **Maestro: `defaults.wave_gates`** (`off` | `final` | `every`, additive,
|
|
765
|
+
default `final`). The phases' gates now run in full on the merged tree:
|
|
766
|
+
every gate of the plan on the last wave before the plan PR opens, and a
|
|
767
|
+
wave's gates on any wave whose merge needed a conflict resolution; `every`
|
|
768
|
+
checks every wave. Merged waves were never tested under `gate_scope: full`,
|
|
769
|
+
and two parallel phases that each passed their gates disagreed on a signal's
|
|
770
|
+
keyword arguments, failing 22 tests that only showed up after the plan PR.
|
|
771
|
+
`plan-feature` now puts a contract test for any interface two parallel
|
|
772
|
+
phases share into the foundation phase.
|
|
773
|
+
- **Maestro: pausing a phase parked on a question holds it.** Under
|
|
774
|
+
`--retry-after` the unattended timer no longer answers a question the
|
|
775
|
+
operator paused; a person's answer releases it, and `status` and the node
|
|
776
|
+
view say "paused by operator". The pause used to be recorded as `ignored`
|
|
777
|
+
while the timer fired anyway. Node operations now answer with the delivery
|
|
778
|
+
they recorded, so `POST …/pause` on a node with nothing to pause reports
|
|
779
|
+
`"delivery": "ignored"` instead of a bare `ok`.
|
|
780
|
+
- **Maestro: `nodes[].deferred`** (additive). A phase carrying it — the
|
|
781
|
+
soak-gated flag removal `plan-feature` now emits it on — is not dispatched
|
|
782
|
+
when its dependencies merge; it parks on a question naming the reason, and
|
|
783
|
+
the operator answers `start` or `stop`. The scheduler used to run such a
|
|
784
|
+
phase immediately.
|
|
785
|
+
- **Maestro: `node_wait` and the "waiting for the integration worktree"
|
|
786
|
+
state.** A phase queued behind the integration worktree journals who holds
|
|
787
|
+
it; `status` and the node view show it. It used to read `running` with no
|
|
788
|
+
agent process, which looked like a frozen run.
|
|
789
|
+
- **Maestro: `interrupted` run status.** A signal to the job now kills every
|
|
790
|
+
live agent turn and gate, the fix round in progress and any merge standing
|
|
791
|
+
in the integration worktree, and ends the run `interrupted` (resumable)
|
|
792
|
+
instead of `failed` with orphaned agents still editing the worktree. The
|
|
793
|
+
daemon's close is bounded so an open browser socket cannot hang shutdown.
|
|
794
|
+
- **Maestro: `doctor` warns about `docker compose run`/`exec` without `-T`**
|
|
795
|
+
in gates and the `project` block, and `plan-feature` says so.
|
|
796
|
+
|
|
797
|
+
### Fixed
|
|
798
|
+
|
|
799
|
+
- **Maestro: PRs say how the plan lands.** Every phase and integration PR is
|
|
800
|
+
marked as a review unit that lands through the plan PR, and the plan PR says
|
|
801
|
+
to merge it alone; it used to offer two landing paths, and a 36-PR landing
|
|
802
|
+
resolved the same conflicts several times by hand. A phase PR also names the
|
|
803
|
+
sibling phases its base leaves out, so a CI failure in their area is not
|
|
804
|
+
read as its own.
|
|
805
|
+
- **Maestro: the post-mortem reports what happened.** Conflicts are read from
|
|
806
|
+
the journal, base merges included, so a run that restarted no longer
|
|
807
|
+
reports none; amendments and operator operations, failure causes, crew
|
|
808
|
+
substitutions and cross-phase failures on merged waves are recorded.
|
|
809
|
+
- **Maestro: a finished run stops its compose stacks** (volumes kept) and
|
|
810
|
+
prints how to remove its lanes and volumes; `status` and `doctor` list what
|
|
811
|
+
runs that are over left running or on disk. One run left twenty containers,
|
|
812
|
+
five worktrees and a job process up a day and a half after it was done.
|
|
813
|
+
- **Maestro: wave branches are built across a restart.** Wave completion was
|
|
814
|
+
counted in memory, so a run that restarted with some phases of a wave merged
|
|
815
|
+
before and the rest after never built that wave, and the next wave failed on
|
|
816
|
+
`git checkout` (exit 128) on every retry. A phase the journal calls `done`
|
|
817
|
+
now counts as arrived; a wave the run never built is built before the wave
|
|
818
|
+
on top of it, and a wave that completes before the one under it waits for
|
|
819
|
+
it (`wave_deferred`) instead of failing. A missing wave is named
|
|
820
|
+
("wave 5 was never built") rather than left to git. New `wave_built` event.
|
|
821
|
+
- **Maestro: a phase that moved lanes can check out its branch.** The branch
|
|
822
|
+
is released from the sibling worktree that still had it (detached at its
|
|
823
|
+
commit, tree untouched) before the new lane takes it. Git's "already used
|
|
824
|
+
by worktree" refusal failed one phase 39 attempts in a row.
|
|
825
|
+
- **Maestro: a conflict-fixer round has a time limit** (20 minutes by
|
|
826
|
+
default) and is told to verify only the files it touched; one round spent
|
|
827
|
+
hours re-running a suite that could not reach its database while three
|
|
828
|
+
phases queued behind it.
|
|
829
|
+
- **Maestro: recreated lanes read the amended `env_files`.** The pool takes
|
|
830
|
+
the amended `project` block, so a single-use lane re-provisioned mid-run no
|
|
831
|
+
longer loses files declared after the run started. A compose-delivered
|
|
832
|
+
database whose service is in `compose.publish` also gets
|
|
833
|
+
`<connection_url_var>_HOST`, the same URL on the published host port.
|
|
834
|
+
- **Maestro: a merge commit git refuses no longer leaves the integration
|
|
835
|
+
worktree mid-merge.** A pre-commit hook failing in the integration worktree
|
|
836
|
+
left `MERGE_HEAD` behind, and every phase whose base was built there then
|
|
837
|
+
failed in its own setup on `git checkout exited 1` — four phases, up to
|
|
838
|
+
twenty-three attempts each, in one run. The integrator now keeps the
|
|
839
|
+
resolution as a commit under `refs/vinta-ai-maestro/resolutions/<node>`,
|
|
840
|
+
abandons the merge, and asks the phase whose merge it is: `commit`,
|
|
841
|
+
`commit --no-verify`, or `abort merge` (the unattended answer under
|
|
842
|
+
`--retry-after`). A retry reuses the kept resolution. Before any checkout,
|
|
843
|
+
the integrator refuses a worktree that is mid-merge, mid-rebase or
|
|
844
|
+
mid-cherry-pick *by name* (`integration worktree is mid-merge: a merge of
|
|
845
|
+
plan/x/phase-b into plan/x/integ-c …`). A conflict fixer that could not run
|
|
846
|
+
abandons the merge it was handed.
|
|
847
|
+
- **Maestro: `GitCommandError` carries git's stderr.** The redacted, bounded
|
|
848
|
+
tail of what git said is in the error, in `node.attempt_failed`, in the
|
|
849
|
+
journal's `node_error` and so in the UI — `git commit exited 1: …No module
|
|
850
|
+
named 'settings.local'` instead of `git commit exited 1`. The integrator's
|
|
851
|
+
own errors (`UnresolvedConflictError` among them) keep their messages too
|
|
852
|
+
instead of being reported as their class name.
|
|
853
|
+
- **Maestro: a setup failure that repeats is not retried unattended.** A phase
|
|
854
|
+
that fails before any agent ran, twice in a row on the same line, is parked
|
|
855
|
+
on its question without `--retry-after`'s self-answer, and the question
|
|
856
|
+
says why.
|
|
857
|
+
- **Maestro: `project.env_files` reaches adopted lanes.** A resume gives each
|
|
858
|
+
lane the declared files it lacks and never overwrites one it has, so
|
|
859
|
+
amending the list reaches lanes that already exist. `doctor` fails a
|
|
860
|
+
declared file missing from the checkout and warns, by name, when a
|
|
861
|
+
gitignored file has a tracked example (`.env.example`,
|
|
862
|
+
`settings/local.py.example`) and nothing declares it.
|
|
863
|
+
- **Maestro: a phase with no gates is no longer a silent pass.** `validate`
|
|
864
|
+
refuses a node that resolves to an empty `gates` list and `doctor` warns
|
|
865
|
+
naming the phases, unless the plan sets `defaults.allow_ungated_phases:
|
|
866
|
+
true` (new, additive). Twelve phases had finished without a check because
|
|
867
|
+
every node carried `gates: []` where its author meant "the project's".
|
|
868
|
+
- **Maestro: a refused conflict-fixer spawn is backpressure.** A plan limit or
|
|
869
|
+
quota refusal while spawning the fixer parks the phase on the harness's
|
|
870
|
+
wait (`waiting_on_capacity`) instead of failing it and retrying on a timer.
|
|
871
|
+
- **Maestro: `createdb` / `dropdb` reach the server `server_url` names.** The
|
|
872
|
+
template and lane commands pass `-h`, `-p` and `-U` from the URL; they used
|
|
873
|
+
the local socket, which is not a Docker-published server. The password stays
|
|
874
|
+
off the command line (`PGPASSWORD` or `~/.pgpass`).
|
|
875
|
+
- **Maestro: `vinta-ai-maestro` resolves from inside every lane.** The daemon
|
|
876
|
+
writes a launcher for itself into `.vinta-ai-maestro/bin/` and puts it first
|
|
877
|
+
on the agents' `PATH`, so `with` and `gate` work when maestro is a project
|
|
878
|
+
devDependency reached through `node_modules/.bin` or `npx`.
|
|
879
|
+
- **Maestro: agents are told a compose-delivered database is unreachable from
|
|
880
|
+
the host.** The implementer prompt names the connection variables that
|
|
881
|
+
resolve only inside the lane's compose network and says to run the suite
|
|
882
|
+
through the project's commands or the gate verb.
|
|
883
|
+
- **Maestro: an editor save writes a patch even when the stored file does not
|
|
884
|
+
validate.** A cross-reference error in the committed file (a chore the
|
|
885
|
+
project no longer declares) made the save write the whole resolved document
|
|
886
|
+
— a hundred-line diff for a one-field edit. The shape is now enough to diff
|
|
887
|
+
against.
|
|
888
|
+
- **Maestro: `purge <run-id>` forgets the run in `flow.db` too.** The UI went
|
|
889
|
+
on listing, and offering to resume, a run whose directory was gone.
|
|
890
|
+
- **Maestro: the monitor thinks with the model the run declares now.** It was
|
|
891
|
+
built once at job start; an amendment to the crew's models reached it only
|
|
892
|
+
after a pause and resume.
|
|
893
|
+
- **Maestro: `run` says where the run's state lives.** The store is the
|
|
894
|
+
checkout's, so a `ui` started in another checkout of the same repository
|
|
895
|
+
(the main one, while the run is in a worktree) lists other runs; the output
|
|
896
|
+
now names the store and the `--repo` to pass.
|
|
897
|
+
- **plan-feature: `project.env_files` and the gate/chore inheritance rules.**
|
|
898
|
+
The skill told authors env-file copying did not belong in the `project`
|
|
899
|
+
block, which left every lane without `.env`; it now tells them to find the
|
|
900
|
+
gitignored files the project's hooks, gates and settings need (`*.example`
|
|
901
|
+
siblings, compose `env_file:` lines, the settings module) and to declare
|
|
902
|
+
them — once, in `.vinta-ai-workflows.yaml` under `maestro.project.env_files`,
|
|
903
|
+
or in the workflow. It also states plainly that `gates: []` and
|
|
904
|
+
`defaults.chores: []` mean *none* rather than "the project's", that an
|
|
905
|
+
override of a project gate must carry `type`, and that `vinta-ai-maestro
|
|
906
|
+
validate` must exit 0 before the plan is handed over. `vinta-bootstrap-ai-tools`
|
|
907
|
+
asks about `env_files` when it writes the `maestro:` section.
|
|
908
|
+
- **Maestro: wave merges again use the membership the executor counted.**
|
|
909
|
+
`RecordingIntegrator.mergeWave` dropped it, so in production a merge fell
|
|
910
|
+
back to the integrator's own reading of the wave. That reading could differ
|
|
911
|
+
from the count if an amendment landed in between.
|
|
912
|
+
- **Maestro: the gate run on a conflict resolution uses the amended gate
|
|
913
|
+
commands.** It used the workflow from the start of the run.
|
|
914
|
+
- **Maestro: the API serves a run's definition as amended.** It cached the
|
|
915
|
+
snapshot forever, so a monitor retune made in the run's own process never
|
|
916
|
+
reached the UI.
|
|
917
|
+
- **Maestro: an editor save from `ui` now reaches the run's job.** `ui`
|
|
918
|
+
is a separate process from the job hosting a run. A save to a running
|
|
919
|
+
plan's workflow used to replace the run's frozen snapshot from `ui` with no
|
|
920
|
+
runner: nothing adopted the change, and an amendment that needed a rebase
|
|
921
|
+
was refused. The job kept running the old definition until a resume. The
|
|
922
|
+
save is now sent to the job (`POST /api/runs/<id>/amend`), which applies it
|
|
923
|
+
under the usual amend rules, and `ui` writes the plan file only once the
|
|
924
|
+
job has accepted it. If the job does not answer, the save is refused
|
|
925
|
+
(`run_host_unreachable`) and nothing is written.
|
|
926
|
+
- **Maestro: saving in the editor during a run no longer reports a failure.**
|
|
927
|
+
The amendment response failed the client's schema check even though the
|
|
928
|
+
save had gone through.
|
|
929
|
+
|
|
930
|
+
- **`implement-plan`'s final report no longer suggests `docker compose down -v`
|
|
931
|
+
to tear down a lane.** `down -v` also deletes the compose volumes a worktree
|
|
932
|
+
deliberately shares with the main checkout (`shared_volumes`, such as a
|
|
933
|
+
dependency cache). The report now prints the same sequence the
|
|
934
|
+
`prepare-worktree` skill documents: `docker compose -p <project> down`, then
|
|
935
|
+
`docker volume rm` for each volume the worktree forked, then removal of the
|
|
936
|
+
generated compose override.
|
|
937
|
+
- **`open-pr.sh` posts inline comments on macOS and reports why a comment
|
|
938
|
+
failed.** Four bugs in the `open-pr-from-context` script, all seen while
|
|
939
|
+
publishing a stacked plan from a Mac:
|
|
940
|
+
- Trimming trailing blank lines from `# Title`, `# Description` and
|
|
941
|
+
`# Comments` used a GNU-only `sed` idiom. BSD `sed` fails on it with
|
|
942
|
+
"unused label" and trims nothing. A portable `awk` helper does it now.
|
|
943
|
+
- A comment with lowercase `side: right` got "422 Validation Failed" from
|
|
944
|
+
GitHub, so failures looked random across files. The script now converts
|
|
945
|
+
`side` to upper case and falls back to `RIGHT` for any other value. The
|
|
946
|
+
`prs-context-comments` schema still only allows `LEFT` / `RIGHT`.
|
|
947
|
+
- The `gh api` call threw away its stderr, so a run of failed comments
|
|
948
|
+
gave no reason. The CLI's error now prints on one line above each
|
|
949
|
+
`FAILED` line, and the comment still counts toward exit `1`. The
|
|
950
|
+
`glab api` call for GitLab had the same problem and gets the same fix.
|
|
951
|
+
- Adding entries to an existing `## Publish log` section passed several
|
|
952
|
+
lines through `awk -v`, which BSD `awk` rejects ("newline in string").
|
|
953
|
+
The entries go through the environment now.
|
|
954
|
+
|
|
955
|
+
Projects that patched their own copy can re-sync with `vinta-sync-ai-tools`.
|
|
956
|
+
|
|
957
|
+
- **`check-ai-models` no longer mistakes sibling models for snapshots of a
|
|
958
|
+
cited id.** The id matcher used substring matching, so `claude-sonnet-5`
|
|
959
|
+
matched `claude-sonnet-5-5` (hiding the newer Sonnet and Opus releases).
|
|
960
|
+
It also matched `gemini-3-pro` to `gemini-3-pro-image`, so a retired model
|
|
961
|
+
still looked available. It now accepts only exact ids, dated or `-vN`
|
|
962
|
+
snapshot suffixes, and provider prefixes.
|
|
963
|
+
|
|
964
|
+
- **The editor's canvas inspector no longer offers fields it then discards.**
|
|
965
|
+
Its Status and Wave fields were folded back into the workflow and silently
|
|
966
|
+
dropped — status is run state and wave is computed from dependencies — and
|
|
967
|
+
Name duplicated the field in the panel beside the graph. They are hidden in
|
|
968
|
+
the editor through the parts the component now exposes, leaving the
|
|
969
|
+
inspector the one thing only it can do: a dependency's artifact and the
|
|
970
|
+
delete actions.
|
|
971
|
+
- **Picking a dependency after a node no longer loses it on the next frame.**
|
|
972
|
+
The host reported an edge selection as "no node", pushed that back into the
|
|
973
|
+
canvas, and cleared the edge the moment it was picked; the edge's inspector
|
|
974
|
+
flashed and vanished. A cleared node selection now leaves an edge selection
|
|
975
|
+
alone.
|
|
976
|
+
- **Pressing on the canvas's toolbar or inspector no longer pans the graph.**
|
|
977
|
+
- **A timed-out gate says whether it hung or was slow.** A `gate_result` with
|
|
978
|
+
`status: timed_out` used to carry only the status, so a run with nine
|
|
979
|
+
timeouts could not say whether any of them was a stuck suite or a host too
|
|
980
|
+
busy to finish one. It now carries `timeout`: `quiet_ms` (how long the gate
|
|
981
|
+
had gone without writing anything when it was killed), `output_bytes`, and
|
|
982
|
+
the host's `load_1m` beside `cpus`. `quiet_ms` near the timeout is a gate
|
|
983
|
+
waiting on something; a small one with `load_1m` well above `cpus` is
|
|
984
|
+
contention. Numbers only, as before: nothing the gate printed reaches the
|
|
985
|
+
row. The monitor's brief points at the new field. Older rows have no
|
|
986
|
+
`timeout`.
|
|
987
|
+
|
|
988
|
+
- **Gate durations are now kept, not just displayed.** Every `gate_result` has
|
|
989
|
+
carried the runner's own `duration_ms` since gates were journalled, and
|
|
990
|
+
nothing read it back: the figure went to a live panel and was afterwards
|
|
991
|
+
reachable only by hand-parsing event payloads, so the one measurement that
|
|
992
|
+
answers "is the suite why this run took an hour" could not be asked of a
|
|
993
|
+
finished run. `analyzeRun` now rolls it up per gate — runs, cache hits,
|
|
994
|
+
summed runtime, the slowest single run, verdicts by kind — and each phase's
|
|
995
|
+
time split carries `gateRunMs` beside `gatePoolQueueMs`, so a run says how
|
|
996
|
+
much of its working time was the gate queue and how much was the gate. The
|
|
997
|
+
post-mortem carries the same as a `gate_costs` finding, which is what makes
|
|
998
|
+
it durable: `plan-feature` reads these artifacts when sizing the next plan,
|
|
999
|
+
and a gate pool's capacity and `max_parallel_lanes` are one decision made
|
|
1000
|
+
with half the numbers. Cache hits are counted and their time kept in its own
|
|
1001
|
+
`cache_saved_ms` column — a hit's duration is what the gate cost when it last
|
|
1002
|
+
ran, and adding it to the runtime would report time the run specifically did
|
|
1003
|
+
not spend. A verdict written before the runner measured itself is counted and
|
|
1004
|
+
its milliseconds are not: both reports emit a `gate_durations_unrecorded` gap
|
|
1005
|
+
naming the gates, because a gate reported as free is the number a planner
|
|
1006
|
+
would act on hardest.
|
|
1007
|
+
|
|
1008
|
+
- **Phases can run chores: declared agent turns that change the diff rather than
|
|
1009
|
+
judge it.** A gate is a shell command that says pass or fail; a chore is an
|
|
1010
|
+
agent asked to do something to the work — rewrite the comments this phase
|
|
1011
|
+
wrote, add the changelog entry, extract the strings that need translating. It
|
|
1012
|
+
runs on the implementer's own session, so the agent that wrote the diff is the
|
|
1013
|
+
one asked to act on it. Declare them in `chores.<id>` (a `prompt` or a
|
|
1014
|
+
`prompt_ref` into the plan, plus an optional `skill` the prompt names) and
|
|
1015
|
+
pick them with `defaults.chores` for the run or `nodes[].chores` for one
|
|
1016
|
+
phase — a node's list replaces the default rather than adding to it, so `[]`
|
|
1017
|
+
opts a phase out. In `standard-phase` they run in a new `polish` state,
|
|
1018
|
+
between a passing review and the gate: a chore edits the tree, so running it
|
|
1019
|
+
later would merge a diff the gates never ran against, and running it earlier
|
|
1020
|
+
would have the fixer rewrite what it just did. A chore that fails, or that the
|
|
1021
|
+
harness had no capacity for, is journalled and the phase continues to its
|
|
1022
|
+
gates — `on_failure: "fail"` is for one the phase is not correct without.
|
|
1023
|
+
|
|
1024
|
+
- **The post-mortem now reports what the schedule cost.** Two findings, both
|
|
1025
|
+
derived from events the journal already carried. `critical_path` is the
|
|
1026
|
+
dependency chain that decided the wall clock — each phase's span, the total,
|
|
1027
|
+
and its share of elapsed — because every phase *off* that chain could be made
|
|
1028
|
+
instant without moving the run, and nothing said which phases those were.
|
|
1029
|
+
`idle_capacity` reports the width the graph actually reached against the lanes
|
|
1030
|
+
it asked for: a plan declaring three lanes that never runs more than two
|
|
1031
|
+
phases at once pays for a third worktree, a third forked database and a third
|
|
1032
|
+
desk, and gets none of them back. Run against a real fourteen-hour build, the
|
|
1033
|
+
two say its critical path was **99% of elapsed time** — six phases, effectively
|
|
1034
|
+
serial — with peak concurrency 2 against 3 lanes and **55% of lane time idle**.
|
|
1035
|
+
`plan-feature` already reads these artifacts before drawing the next set of
|
|
1036
|
+
dependency lines; until now neither depth nor width was in them. A
|
|
1037
|
+
`blocking_cause_unrecorded` gap ships alongside, because the chain is what the
|
|
1038
|
+
*graph* forced and a phase can also wait on a busy lane or an unanswered
|
|
1039
|
+
question — which the journal cannot currently tell apart.
|
|
1040
|
+
|
|
1041
|
+
- **`--retry-after <15m>`: an unanswered failure question eventually answers
|
|
1042
|
+
itself.** An observed fourteen-hour run spent 4h07m — 29% of its wall clock —
|
|
1043
|
+
parked on "This phase failed. Try it again?" with nobody at the keyboard; the
|
|
1044
|
+
answer, when it came, was `retry`, and it worked. Unset by default, so a run
|
|
1045
|
+
behaves exactly as it did. Accepts minutes bare or a unit (`15`, `15m`, `90s`,
|
|
1046
|
+
`2h`) on both `run` and `serve`. It is deliberately unbounded and fires again
|
|
1047
|
+
on each new question: a version capped by `--retries` would stall at the cap
|
|
1048
|
+
and idle for the rest of the night, which is the failure it exists for. Every
|
|
1049
|
+
firing is a full phase attempt, so the interval is the throttle. It reaches
|
|
1050
|
+
the failure question and nothing else — a plan's own `await_human` gate asked
|
|
1051
|
+
for a person, and both park through the same code, so answering those would be
|
|
1052
|
+
the orchestrator overruling the plan. An unattended answer is journalled with
|
|
1053
|
+
`unattended: true`, because "a human said try again" and "nobody was here" are
|
|
1054
|
+
the same answer with very different meanings.
|
|
1055
|
+
|
|
1056
|
+
- **The daemon now has a log of its own, and a Logs view to read it in.**
|
|
1057
|
+
Transcripts said what the agents did; nothing said what the *daemon* did, so
|
|
1058
|
+
"the run stopped and I don't know why" had no evidence behind it at all.
|
|
1059
|
+
`.vinta-ai-maestro/logs/daemon.ndjson` now records what the process bound,
|
|
1060
|
+
every HTTP request and socket upgrade it refused and why, every start request
|
|
1061
|
+
that produced no run, every scheduler dispatch and node transition, and every
|
|
1062
|
+
deadlock — on one clock, so a node failure can be read beside the refusal
|
|
1063
|
+
thirty seconds before it. `GET /api/logs` serves it as a tail or a follow,
|
|
1064
|
+
with filters for level, run, node and a substring of any event or field; the
|
|
1065
|
+
UI's new **Logs** section follows the tail until you scroll up. The file
|
|
1066
|
+
rotates at 8 MiB and keeps five rotations, so it is bounded whatever the
|
|
1067
|
+
daemon's uptime, and `purge` leaves it alone for the same reason it leaves
|
|
1068
|
+
`flow.db` alone — it holds no repository contents, and it is the record of the
|
|
1069
|
+
failure somebody is about to ask about.
|
|
1070
|
+
|
|
1071
|
+
- **A crash is no longer silent.** An exception escaping a node's own handling
|
|
1072
|
+
used to become an unhandled rejection on a promise the scheduler does not
|
|
1073
|
+
hold, and an unhandled rejection ends the process: hours of agent turns and a
|
|
1074
|
+
real amount of money, gone with no journal row, no transcript entry and
|
|
1075
|
+
nothing on disk saying why — because the thing that would have said it was the
|
|
1076
|
+
thing that died. Three changes. A node that throws is now **contained** by the
|
|
1077
|
+
rule §6 already states for failures: it fails, its transitive dependents
|
|
1078
|
+
block, and everything independent of it keeps going. A crash that is still
|
|
1079
|
+
fatal is **recorded first** — `daemon.uncaught_exception` /
|
|
1080
|
+
`daemon.unhandled_rejection`, with the error's kind, its stack frames and the
|
|
1081
|
+
ids of every run in flight. And the in-flight runs are **journalled as ended**
|
|
1082
|
+
before the process goes, so `run --resume <run-id>` picks them up instead of
|
|
1083
|
+
finding a row that says `running` for ever. `serve` and `run` both take
|
|
1084
|
+
`--log-level`, `--log-stderr` and `--log-detail`.
|
|
1085
|
+
|
|
1086
|
+
- **The log is identifiers only, enforced rather than asked for — plus the one
|
|
1087
|
+
field that is deliberately prose.** A log is called from anywhere, and "please
|
|
1088
|
+
do not log repository contents" is a hope rather than a control. So a field
|
|
1089
|
+
may only hold a string, a number, a boolean or null — an object is dropped,
|
|
1090
|
+
never stringified, which is the way a diff or a file read becomes a log line —
|
|
1091
|
+
identifier values are capped at 200 characters, field names that are secrets
|
|
1092
|
+
by their name are redacted, and the daemon's token is registered at boot so
|
|
1093
|
+
any value containing it is written `<redacted>`.
|
|
1094
|
+
|
|
1095
|
+
The exception is an error's **`message`**, which every failure record carries
|
|
1096
|
+
beside its kind, capped at 2000 characters and redacted like anything else. It
|
|
1097
|
+
is the default because `.vinta-ai-maestro/runs/` already holds every agent
|
|
1098
|
+
transcript and gate log *verbatim*, in the same gitignored store and under the
|
|
1099
|
+
same `purge` — an error message is a rounding error against a directory that
|
|
1100
|
+
is already a copy of the repository, and excluding it cost the one string that
|
|
1101
|
+
most often explains a failure. `--log-detail kind` narrows to the kind and the
|
|
1102
|
+
stack frames for a checkout under a stricter obligation than this store's own.
|
|
1103
|
+
- **A run that is dragging can now tune itself, within bounds somebody wrote
|
|
1104
|
+
down.** A run can be mis-*configured* rather than broken — every phase doing
|
|
1105
|
+
what it was asked, every gate reporting honestly, and the whole thing paying
|
|
1106
|
+
for a test database it rebuilds on every gate run in every lane. Nobody needs
|
|
1107
|
+
waking for that, and the post-mortem found it out an afternoon too late to
|
|
1108
|
+
help. Now a watchdog wakes the run's monitor when a phase passes an hour or a
|
|
1109
|
+
gate's uncached cost passes thirty minutes, the monitor reads the gate logs
|
|
1110
|
+
and the durations, and it answers with a **proposal** that the daemon
|
|
1111
|
+
validates and applies through the existing amend path. `--no-intervene` turns
|
|
1112
|
+
it off.
|
|
1113
|
+
|
|
1114
|
+
It proposes; it never writes. Its authority is four verbs — a gate's command,
|
|
1115
|
+
a gate's timeout, a phase's fix budget, a phase's model — and the line they
|
|
1116
|
+
draw is that it may change **how** the run executes and never **what** it
|
|
1117
|
+
builds. There is no way to express a change to a dependency, a phase brief, a
|
|
1118
|
+
touch list, the base branch, or the set of phases: not refused at runtime,
|
|
1119
|
+
unrepresentable.
|
|
1120
|
+
|
|
1121
|
+
The one field that can do real damage is a gate's command, because
|
|
1122
|
+
`--reuse-db` and `-k not_slow` are the same edit to the same string, and a
|
|
1123
|
+
gate narrowed wrongly goes green and looks like success in every log there
|
|
1124
|
+
is. So a command may only be changed when the gate itself declares
|
|
1125
|
+
`tuning.allowed_flags` — a list a person wrote in the committed plan, under
|
|
1126
|
+
review, before the run started — and the proposed command must be the current
|
|
1127
|
+
one's argv tokens, in order, plus additions from that list. A gate that
|
|
1128
|
+
declares no `tuning` block is not tunable, which is the default.
|
|
1129
|
+
|
|
1130
|
+
A run gets three of these in its life, at most one per gate and per phase.
|
|
1131
|
+
What it changed is journalled as identifiers; why it changed it is the
|
|
1132
|
+
monitor's prose and goes in `runs/<run-id>/interventions.jsonl` beside the
|
|
1133
|
+
run, including every attempt that was refused — an agent trying to do
|
|
1134
|
+
something it may not do, with nobody in the room, is the record most worth
|
|
1135
|
+
keeping. See `packages/vinta-ai-maestro/docs/monitor-intervention.md`.
|
|
1136
|
+
|
|
1137
|
+
- **`plan-feature` is told what the newer post-mortem findings mean.** The
|
|
1138
|
+
skill documents each finding and what to *do* about it, and three had been
|
|
1139
|
+
added to the artifact without reaching it: `gate_costs`, `critical_path` and
|
|
1140
|
+
`idle_capacity`. A finding a planning agent cannot interpret is a finding
|
|
1141
|
+
that changes no plan, which is the whole return on emitting it — and the
|
|
1142
|
+
three are among the most actionable there are. `gate_costs` sizes the gate
|
|
1143
|
+
pool capacities a plan declares, and separates a slow suite from a fix loop
|
|
1144
|
+
re-paying for the same one. `critical_path` is the chain to re-draw the graph
|
|
1145
|
+
against: shortening a phase that is not on it changes nothing, so a plan that
|
|
1146
|
+
parallelises harder without touching that chain buys nothing at all.
|
|
1147
|
+
`idle_capacity` says the graph never got as wide as the lanes it asked for,
|
|
1148
|
+
which is a fact about the plan rather than about the machine. Each now has a
|
|
1149
|
+
row beside the findings that already had one.
|
|
1150
|
+
|
|
1151
|
+
- **The post-mortem says what a run changed about itself, and whether it
|
|
1152
|
+
helped.** `postmortem.v1` gains an `interventions[]` finding: one entry per
|
|
1153
|
+
amendment the run made to itself, with the effect on the thing it changed. A
|
|
1154
|
+
gate is scored against its own uncached durations either side of the
|
|
1155
|
+
amendment — mean before, mean after, `cheaper` / `dearer` / `unchanged`
|
|
1156
|
+
outside a ten-per-cent band. Cached hits are excluded, because a hit reports
|
|
1157
|
+
the duration of the run that filled the cache and would drag the mean toward
|
|
1158
|
+
whichever side of the boundary that run fell on.
|
|
1159
|
+
|
|
1160
|
+
It is a comparison and not a claim of cause, and the schema says so: a gate
|
|
1161
|
+
that got cheaper did so while phases were finishing and caches warming. It is
|
|
1162
|
+
worth carrying anyway, because without it an autonomous editor is one nobody
|
|
1163
|
+
can tell is making runs worse — and because `plan-feature` now reads it, so a
|
|
1164
|
+
project whose `unit` gate wants `--reuse-db` pays to find that out once
|
|
1165
|
+
instead of every run.
|
|
1166
|
+
|
|
1167
|
+
A phase-level change (a fix budget, a model) is reported `unmeasured` rather
|
|
1168
|
+
than estimated: a phase runs once, so there is no before to compare an after
|
|
1169
|
+
against, and the only candidate baseline is a different phase doing different
|
|
1170
|
+
work. An operator's amendment is not scored at all — a person deciding
|
|
1171
|
+
something is not the run choosing it.
|
|
1172
|
+
|
|
1173
|
+
- **`schemas/intervention.v1.schema.json`** — the document the monitor answers
|
|
1174
|
+
with, generated from `src/intervention/intervention.ts` the way the workflow
|
|
1175
|
+
and post-mortem schemas are generated from theirs. It is the odd one in
|
|
1176
|
+
`schemas/`: it validates something a *model* writes rather than a skill or a
|
|
1177
|
+
person, and it is the boundary deciding what an unattended run may change
|
|
1178
|
+
about itself.
|
|
1179
|
+
|
|
1180
|
+
- **A run no longer dies with the terminal that started it.** `vinta-ai-maestro
|
|
1181
|
+
serve` accepts `POST /api/runs`, so a run can be submitted to a daemon that
|
|
1182
|
+
was already listening and belongs to that daemon rather than to the shell that
|
|
1183
|
+
asked for it; the request answers with the run id as soon as the run is
|
|
1184
|
+
registered, not when it finishes. Previously `run` was the only way to start a
|
|
1185
|
+
run and it hosted its own daemon, so closing the window killed the scheduler
|
|
1186
|
+
and every agent under it — and left the journal claiming `running` for ever,
|
|
1187
|
+
with nothing able to tell that row apart from a run still in flight.
|
|
1188
|
+
|
|
1189
|
+
- **`vinta-ai-maestro run --resume <run-id>` picks an interrupted run back up.**
|
|
1190
|
+
Phases already `done` stay done and are not dispatched again, their lane
|
|
1191
|
+
worktrees are adopted rather than re-provisioned — so whatever an agent had
|
|
1192
|
+
written and not committed is still there — and a phase that was mid-turn when
|
|
1193
|
+
the process died runs again from the top of its pipeline, because the turn it
|
|
1194
|
+
was in belonged to a process that is gone. The plan comes from the run's
|
|
1195
|
+
frozen snapshot, never from the document it was written from, so editing that
|
|
1196
|
+
file in the meantime cannot switch plans under a run already under way. A new
|
|
1197
|
+
`run_resumed` event records each hand-over; it is deliberately not a second
|
|
1198
|
+
`run_started`, which would have moved the run's `started_at` to whenever it
|
|
1199
|
+
was last picked up and made every duration short by the length of the outage.
|
|
1200
|
+
|
|
1201
|
+
- **`vinta-ai-maestro gate <gate-id>` runs a declared gate from inside an agent
|
|
1202
|
+
turn.** A phase used to run its full gate suite five or six times a round —
|
|
1203
|
+
implementer, reviewer, fixer, reviewer again, then the authoritative `gate`
|
|
1204
|
+
node — and only the last of those consulted the gate cache or the resource
|
|
1205
|
+
pool. Agents now ask the daemon for a gate by id: the result is cached on the
|
|
1206
|
+
same `(gate id, lane tree hash)` key the `gate` node reads, the gate's
|
|
1207
|
+
resources are held by the daemon rather than by whichever agent remembered to
|
|
1208
|
+
wrap the command, and the id resolves to the plan's own command so a scoped
|
|
1209
|
+
approximation cannot be reported as the gate. Each run is journalled as a
|
|
1210
|
+
`gate_result` event and a `gate_run` transcript entry under the GATE band,
|
|
1211
|
+
whoever asked for it.
|
|
1212
|
+
|
|
1213
|
+
- **A project declares what makes a lane runnable.** `project.env_files` copies
|
|
1214
|
+
ignored configuration into each worktree, `project.setup_cmd` runs an
|
|
1215
|
+
idempotent setup hook on provisioning and recycle, and the fixed
|
|
1216
|
+
`project.commands` vocabulary tells implementers, reviewers and fixers the
|
|
1217
|
+
project's real lint, typecheck, test and migration entry points.
|
|
1218
|
+
|
|
1219
|
+
- **Compose-backed lanes are isolated past `COMPOSE_PROJECT_NAME`.** The daemon
|
|
1220
|
+
now generates an out-of-tree Compose override per lane, strips fixed host
|
|
1221
|
+
ports, re-pins fixed and external volumes, and carries the complete lane
|
|
1222
|
+
environment into agent and takeover processes as well as gates. Explicitly
|
|
1223
|
+
published services receive probed per-lane ports recorded in the teardown
|
|
1224
|
+
summary.
|
|
1225
|
+
|
|
1226
|
+
- **Shared services get one namespace per lane.** `project.services` models a
|
|
1227
|
+
fixed index or lane-derived name inside one Redis, RabbitMQ, object-storage,
|
|
1228
|
+
or similar server, with project-owned create/reset commands and capacity
|
|
1229
|
+
validation before provisioning creates templates or worktrees.
|
|
1230
|
+
|
|
1231
|
+
- **Agents can take the semaphore a workflow declares.**
|
|
1232
|
+
`vinta-ai-maestro with <resource> -- <cmd>` waits on the run's existing
|
|
1233
|
+
`ResourcePools`, runs the heavy inner-loop command, and releases on exit.
|
|
1234
|
+
Renewable leases expire when their client disappears, are visible in the
|
|
1235
|
+
live journal, and are surfaced in implementer, reviewer and fixer prompts.
|
|
1236
|
+
|
|
1237
|
+
- **A failed phase retries itself, and then asks.** `--on-failure` defaults to
|
|
1238
|
+
`retry`: one cold re-attempt (`--retries <n>`, 0–5), and then the node parks
|
|
1239
|
+
on a question — retry, retry with another member of the crew, or stop. The
|
|
1240
|
+
failures this system produces are overwhelmingly environmental and are gone by
|
|
1241
|
+
the second attempt; the ones that survive it are the ones worth a person.
|
|
1242
|
+
`ask` skips the automatic attempt. `stop` is the old behaviour and remains the
|
|
1243
|
+
right choice for CI, because everything else eventually **waits**.
|
|
1244
|
+
|
|
1245
|
+
A failure that survives its retries says so: the reason carries
|
|
1246
|
+
`(after N attempts)`, which no event otherwise records.
|
|
1247
|
+
|
|
1248
|
+
- **A failed phase can be retried by hand.** A **Retry phase** button, and
|
|
1249
|
+
`POST /api/runs/:runId/nodes/:nodeId/retry`. It returns the node to the ready
|
|
1250
|
+
set and unblocks the subtree its failure blocked — a phase that died of
|
|
1251
|
+
something environmental holds up work that was never broken. It needs the run
|
|
1252
|
+
to still be in flight, which with the new default it usually is; a run that
|
|
1253
|
+
has genuinely ended is re-run, not retried, and says so rather than doing
|
|
1254
|
+
nothing.
|
|
1255
|
+
|
|
1256
|
+
- **The monitor can go and look.** It was given a digest and nothing else, and
|
|
1257
|
+
read exactly as thin as that sounds — it could say a phase had failed and
|
|
1258
|
+
never say what the phase had written. It is briefed as a technical project
|
|
1259
|
+
manager now, with the map: the plan document, each phase's brief, each lane's
|
|
1260
|
+
worktree path, each branch and its base, and where the journal and transcripts
|
|
1261
|
+
live. It has a shell and its working directory is the repository, so it can
|
|
1262
|
+
read the diff before drawing a conclusion. The digest stays as the index —
|
|
1263
|
+
bounded, cheap, enough to know which phase is worth a closer look.
|
|
1264
|
+
|
|
1265
|
+
- **The monitor conversation is kept.** It lived in component state, so a reload
|
|
1266
|
+
threw away every question and every answer — a worse record than the run it
|
|
1267
|
+
describes. It is written to the transcript store under a reserved node id and
|
|
1268
|
+
read back on load, so it outlives the tab, the daemon and the run.
|
|
1269
|
+
|
|
1270
|
+
- **Panels expand.** Anything holding content that is only nominally
|
|
1271
|
+
summarisable — an agent's transcript, a gate log, the monitor conversation,
|
|
1272
|
+
the steering box — takes the full container width and about a screen of
|
|
1273
|
+
height on demand. The height travels as a CSS variable, because the element
|
|
1274
|
+
that has to grow is a scroller several components below the panel.
|
|
1275
|
+
|
|
1276
|
+
- **A run has a spokesperson you can ask.** `serve` grows a **Monitor** panel on
|
|
1277
|
+
the run view: one agent that reads the journal and answers in prose — what is
|
|
1278
|
+
blocked, why a phase failed, what it would take to move on. It runs on the
|
|
1279
|
+
dearest tier on the crew, because it is read by a person deciding what to do
|
|
1280
|
+
about a failing run, and it is asked a handful of times rather than hundreds.
|
|
1281
|
+
|
|
1282
|
+
Three things it deliberately is not.
|
|
1283
|
+
|
|
1284
|
+
**It is not in the permission path.** The obvious thought, once an agent has
|
|
1285
|
+
been refused something, is to put a smarter agent in front of the refusals —
|
|
1286
|
+
but a phase makes hundreds of tool calls (one node in one real run made 179)
|
|
1287
|
+
and a model turn before each is latency and cost spent on questions like "may
|
|
1288
|
+
I run ruff", which should never have been questions. Permissions are settled
|
|
1289
|
+
structurally, once per spawn, for free.
|
|
1290
|
+
|
|
1291
|
+
**It does not read the transcripts.** Piping every agent's output into a
|
|
1292
|
+
second agent would make the monitor the most expensive thing in the run and
|
|
1293
|
+
tie its cost to the work rather than to the questions. It reads a bounded
|
|
1294
|
+
digest — statuses, failure reasons, pending questions, the last few refusals
|
|
1295
|
+
with their sentences, one trimmed last word per phase — so a one-hour run and
|
|
1296
|
+
a one-day run cost about the same to ask about.
|
|
1297
|
+
|
|
1298
|
+
**It has no authority.** It can explain a §9.1 pause; it cannot answer one.
|
|
1299
|
+
An agent that could quietly approve its colleagues' work would turn a
|
|
1300
|
+
checkpoint into a formality, and the checkpoint is the point.
|
|
1301
|
+
|
|
1302
|
+
It answers about **finished runs too**, which is when "why did this fail" is
|
|
1303
|
+
usually asked, and it needs no lane: it writes nothing.
|
|
1304
|
+
|
|
1305
|
+
- **`serve` reads `--permission`.** The flag has been accepted and ignored since
|
|
1306
|
+
it existed. It decides how the monitor is spawned, so it is read now.
|
|
1307
|
+
|
|
16
1308
|
- **A plan staffs a team, instead of picking a model per phase.** Choosing a tier
|
|
17
1309
|
phase by phase answers "what runs this one" ten times and never adds it up, so the
|
|
18
1310
|
two questions that decide what a feature costs go unasked: how many agents does
|
|
@@ -80,10 +1372,33 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
80
1372
|
claimed per review turn rather than per phase — it has one session ledger and
|
|
81
1373
|
two concurrent reviews would collide over it — so one reviewer on a three-lane
|
|
82
1374
|
plan is a queue at the review step and not a serialised run.
|
|
1375
|
+
- **"Cheapest qualified" has one exception, and it is about sessions.** A
|
|
1376
|
+
member who already holds a session this phase would genuinely resume takes
|
|
1377
|
+
it ahead of a cheaper member who would have to start cold — even when they
|
|
1378
|
+
are more senior than the phase needs, and even when the member the plan
|
|
1379
|
+
named is free. What it buys is the context a cold session spends its first
|
|
1380
|
+
turn rebuilding; what it costs is the dearer model for that phase, which is
|
|
1381
|
+
the smaller of the two bills. **The tier floor is untouched**: warmth
|
|
1382
|
+
reorders the members who already qualify and never widens them, so a warm
|
|
1383
|
+
junior still cannot take a Tier 3 phase.
|
|
1384
|
+
- **Only when a session is genuinely already up.** "Warm" means the scheduler
|
|
1385
|
+
asked its own session-reuse rules and got a yes — same pinned lane, same
|
|
1386
|
+
harness, under the turn ceiling, not poisoned by a failed phase. A member
|
|
1387
|
+
who has merely run before is not warm, because a promotion bought on a
|
|
1388
|
+
resume that is then refused pays the senior's rate *and* cold-starts
|
|
1389
|
+
anyway. With nobody warm, staffing is exactly what it was: a session is
|
|
1390
|
+
being opened either way, and the plan's own level wins.
|
|
1391
|
+
- An operator who picks the member by hand on a retry gets that member. A
|
|
1392
|
+
saving nobody asked for does not overrule an explicit answer.
|
|
83
1393
|
- The run view reports who actually worked against who the plan said would.
|
|
84
1394
|
`substituted` is the figure to read beside a cost that overran: every
|
|
85
1395
|
substitution ran at or above the budgeted tier, so a run can be entirely green
|
|
86
|
-
and still have been staffed dearer than planned.
|
|
1396
|
+
and still have been staffed dearer than planned. The phases promoted to reuse
|
|
1397
|
+
a warm session are counted **within** that figure and reported apart from it —
|
|
1398
|
+
a peer covering for a busy member costs what the plan budgeted, while a warm
|
|
1399
|
+
promotion is the scheduler deliberately trading model rate against cold
|
|
1400
|
+
starts, and folded together a run that promoted everything to the top tier
|
|
1401
|
+
would read as an ordinary busy wave.
|
|
87
1402
|
|
|
88
1403
|
- **`implement-plan` runs independent phases in parallel.** The plan now carries a
|
|
89
1404
|
dependency graph and the conductor schedules against it: a phase starts as soon as
|
|
@@ -178,8 +1493,258 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
178
1493
|
**generated** from a zod source rather than hand-written — see
|
|
179
1494
|
[`schemas/README.md`](schemas/README.md).
|
|
180
1495
|
|
|
1496
|
+
- **The monitor keeps thinking when you look away, and thinks out loud.**
|
|
1497
|
+
Asking used to be one long HTTP request that produced the whole answer inside
|
|
1498
|
+
it, which made the turn's lifetime the browser's connection: switching view,
|
|
1499
|
+
reloading or letting a laptop sleep killed the monitor mid-thought and left a
|
|
1500
|
+
question with no answer and no record that one had been attempted. The daemon
|
|
1501
|
+
owns the turn now — `POST` answers `202`, and the monitor journals each
|
|
1502
|
+
thought and each sentence as it produces them, so the conversation is
|
|
1503
|
+
readable while it is being written and by any tab that opens it. The panel
|
|
1504
|
+
renders through the same rows a phase's transcript uses, which is what the
|
|
1505
|
+
API always claimed and the UI never did.
|
|
1506
|
+
|
|
1507
|
+
- **A phase's transcript says which agent wrote each line, and holds the gates.**
|
|
1508
|
+
Every spawn on a node appends to one file whatever its role, so a phase that
|
|
1509
|
+
took two fix rounds held five agents' output in one undifferentiated stream —
|
|
1510
|
+
the role was in scope at the append and simply not written down. Each line now
|
|
1511
|
+
carries the role and the session slot that produced it, the view bands the
|
|
1512
|
+
list where one agent stops and the next starts, and gate runs are entries in
|
|
1513
|
+
it rather than only in a log beside it. The operator's own steering stays the
|
|
1514
|
+
operator's (§7), even though the adapter echoes it back on the agent's event
|
|
1515
|
+
stream. Transcripts written before this render exactly as they did, with no
|
|
1516
|
+
bands. The band stays pinned to the top of the scroller while its own run of
|
|
1517
|
+
rows goes past, so the agent you are reading is named without scrolling back
|
|
1518
|
+
to where it started — in the phase transcript and in the monitor conversation
|
|
1519
|
+
alike.
|
|
1520
|
+
|
|
181
1521
|
### Changed
|
|
182
1522
|
|
|
1523
|
+
- **maestro's review/fix loop is now the thermo-nuclear review loop.** The
|
|
1524
|
+
shipped `standard-phase` changes for every run that uses it:
|
|
1525
|
+
- **The reviewer** keeps its session across rounds. It is held to a shipped
|
|
1526
|
+
Review Standard (evidence bar, priority order, approval bar), or to the
|
|
1527
|
+
project's `REVIEW.md`, which replaces that standard when the lane has one.
|
|
1528
|
+
`VERDICT: pass` is now an explicit approval. A re-review gets the fixer's
|
|
1529
|
+
report, every finding already rejected or settled, and the pass-two rules.
|
|
1530
|
+
- **The orchestrator checks that a review turn changed nothing.** If `HEAD`
|
|
1531
|
+
or a tracked file moved, the node stops and asks whether to keep the edits
|
|
1532
|
+
or stop the phase.
|
|
1533
|
+
- **The fixer** (the implementer continuing its session) verifies each
|
|
1534
|
+
finding before acting on it and rejects unsupported ones with
|
|
1535
|
+
counter-evidence. It sends scope decisions to the operator instead of
|
|
1536
|
+
making them: unreachable scenarios, defensive checks, requirements
|
|
1537
|
+
ambiguities, destructive operations. The new `consult` state asks about the
|
|
1538
|
+
whole batch at once and does not count as a fix round.
|
|
1539
|
+
- **The review ledger.** Rejections and the operator's answers are kept in
|
|
1540
|
+
`runs/<run-id>/nodes/<node-id>/review-ledger.jsonl` and shown to every
|
|
1541
|
+
later review and fix.
|
|
1542
|
+
- **The budget.** `max_fix_rounds` now defaults to **4** (was 2). Running out
|
|
1543
|
+
asks `continue` / `stop` instead of failing the phase.
|
|
1544
|
+
- **Unattended runs.** Under `--retry-after`, a scope question nobody answers
|
|
1545
|
+
takes each item's default, and an exhausted budget nobody answers takes
|
|
1546
|
+
`stop`, which fails into `--on-failure` as before. Without `--retry-after`
|
|
1547
|
+
both questions wait for a person.
|
|
1548
|
+
- **The reviewer's model.** An unstaffed review runs on the new optional
|
|
1549
|
+
`defaults.reviewer_model`, then the harness's top tier (`opus` on
|
|
1550
|
+
claude-code), then the phase's model.
|
|
1551
|
+
- **New pieces.** `await_human` gains an optional `unattended_answer`, and
|
|
1552
|
+
there are two new effects, `record_decision` and `grant_fix_rounds`; the
|
|
1553
|
+
schema stays at `workflow.v1`.
|
|
1554
|
+
- **Migration.** A workflow that pins `max_fix_rounds` keeps its number but
|
|
1555
|
+
now gets asked at the end of it. One that ships its own `standard-phase`
|
|
1556
|
+
shadows the built-in and is unaffected. The skills-path review partials
|
|
1557
|
+
are not updated yet (SPEC §16.6). See
|
|
1558
|
+
[SPEC §16](packages/vinta-ai-maestro/SPEC.md#16-the-review-loop).
|
|
1559
|
+
|
|
1560
|
+
- **maestro starts one session at a time per harness, and remembers the
|
|
1561
|
+
account's limit.** Sessions used to boot simultaneously up to the harness
|
|
1562
|
+
ceiling, and N Claude Code CLIs starting together contend for the same local
|
|
1563
|
+
config and OAuth token: a probe against 2.1.274 measured time to `init`
|
|
1564
|
+
rising from 1.2 s with two simultaneous starts to 4.8 s with sixteen, against
|
|
1565
|
+
a flat 0.7 s when each waited for the previous one. A spawn now waits until
|
|
1566
|
+
the one ahead of it has a session or a refusal, so a refusal also parks every
|
|
1567
|
+
queued spawn before it spends one. The ceiling AIMD discovers after a
|
|
1568
|
+
`concurrency` or `rate_limit` refusal is kept per harness in `flow.db` and a
|
|
1569
|
+
run started within six hours opens at it instead of re-learning the limit by
|
|
1570
|
+
being refused; clean spawns still probe back up to the configured value, and
|
|
1571
|
+
the hint is dropped once the ceiling recovers or goes stale. When a harness
|
|
1572
|
+
is throttled, the freed slot goes to the node with the longest chain of work
|
|
1573
|
+
still in front of it rather than to whichever queued first.
|
|
1574
|
+
|
|
1575
|
+
- **Updated the OpenAI and Anthropic models to their latest version.**
|
|
1576
|
+
|
|
1577
|
+
- **Updated the generated skills to have `disable-model-invocation: true` set.**
|
|
1578
|
+
|
|
1579
|
+
- **An expanded panel reads in a centred column.** Full page used to mean
|
|
1580
|
+
full width too, so on a large monitor a transcript or gate log ran in lines
|
|
1581
|
+
too long to follow. The overlay still covers the window; its header and
|
|
1582
|
+
content now sit in a centred column capped at `max-w-6xl`.
|
|
1583
|
+
|
|
1584
|
+
- **The node view reads like a chat.** The transcript now sits above the
|
|
1585
|
+
steering box instead of below it, so the conversation comes first and the
|
|
1586
|
+
box you answer it in is under its newest turn. A terminal opened by *Take
|
|
1587
|
+
over* still appears under the steering box.
|
|
1588
|
+
|
|
1589
|
+
- **`--retry-after` backs off when attempts fail faster than it.** An
|
|
1590
|
+
unanswered failure question used to answer itself on a flat interval
|
|
1591
|
+
forever, so a phase whose attempts died within seconds was retried every
|
|
1592
|
+
fifteen minutes for hours, against the same failure, at the price of a cold
|
|
1593
|
+
lane each time. Each attempt that fails in less time than the interval now
|
|
1594
|
+
doubles the next wait, up to 8× (15m becomes 2h); one attempt that runs at
|
|
1595
|
+
least the interval long puts it back. It still never stops on its own: a
|
|
1596
|
+
run whose cause goes away overnight is moving again before morning. The
|
|
1597
|
+
`node.unattended_retry` log line reports the actual wait and the streak.
|
|
1598
|
+
|
|
1599
|
+
- **Notifications have an inbox, and are heard from every view.** The UI's
|
|
1600
|
+
notifications used to be a row of banners in the top bar, shown only when
|
|
1601
|
+
browser notifications were refused. They piled up there and could be read
|
|
1602
|
+
only one at a time, by dismissing whichever was in front. Every notification
|
|
1603
|
+
now lands in an inbox behind a bell with an unread count: newest first,
|
|
1604
|
+
filterable by unread and by still-waiting pauses, marked read one at a time
|
|
1605
|
+
or all at once, dismissed singly or cleared. A pause that gets answered stays
|
|
1606
|
+
listed and says *Answered*. The inbox is kept in `localStorage` and shared by
|
|
1607
|
+
every tab, so a reload no longer loses it. It stores what a notification
|
|
1608
|
+
body already carries — a run id, a node id, a reason and the journal's
|
|
1609
|
+
timestamp — and no question text, output or diff. Browser notifications also
|
|
1610
|
+
reach the operator more often now. They used to fire only for the run open
|
|
1611
|
+
on screen; the UI now follows every running run in the background, and a run
|
|
1612
|
+
view takes over that run's stream instead of opening a second one. They also
|
|
1613
|
+
depended on a permission prompt raised by the first pause, which Firefox and
|
|
1614
|
+
Safari ignore when no click raised it, so on those browsers they could never
|
|
1615
|
+
turn on. An *Enable notifications* button now sits in the top bar until the
|
|
1616
|
+
browser has been asked (and in the inbox too), and confirms with one test
|
|
1617
|
+
notification. It also says why the channel is off
|
|
1618
|
+
when it is: blocked in the browser's site settings, or served over plain
|
|
1619
|
+
HTTP on a remote `--host`, which browsers do not allow notifications from.
|
|
1620
|
+
The reminder interval moved into the inbox.
|
|
1621
|
+
|
|
1622
|
+
- **The run view's Nodes table names a lane by what distinguishes it.**
|
|
1623
|
+
Every lane in a run is named `${runId}-...`, so the column spelled them in
|
|
1624
|
+
full as `2026-09-11-graphql-aggregations-mubou9ar-crew-3-tier4` on every
|
|
1625
|
+
row, phase after phase — the same forty characters of prefix repeated down
|
|
1626
|
+
the table, wide enough to push the whole table into a horizontal scroll for
|
|
1627
|
+
the sake of a string that was identical everywhere it appeared. The cell now
|
|
1628
|
+
shows the tail that actually differs (`crew-3-tier4`, `lane-2`, `integ`) and
|
|
1629
|
+
keeps the full name in its `title`, because that is what the worktree
|
|
1630
|
+
directory and the branch are called. The phase description beside the node
|
|
1631
|
+
id is bounded and wraps for the same reason — table cells are
|
|
1632
|
+
`whitespace-nowrap`, so one long phase name was setting the width of the
|
|
1633
|
+
whole table.
|
|
1634
|
+
|
|
1635
|
+
- **A dispatched phase agent does the work itself, instead of spawning an
|
|
1636
|
+
implementer under it.** claude-code sessions run by the orchestrator were
|
|
1637
|
+
handing their phase to a sub-agent and reporting its summary back, and the
|
|
1638
|
+
pull was not laziness — `implement-phase` ships into the same repositories,
|
|
1639
|
+
its description matches "you are implementing P3 of plan X", and its content
|
|
1640
|
+
is "spawn exactly one implementer subagent". So a session was loading a
|
|
1641
|
+
conductor skill on its own and correctly following it, one level below the
|
|
1642
|
+
conductor that had already spawned it. The cost is the whole point of session
|
|
1643
|
+
reuse: a sub-agent's reading of the codebase ends with the sub-agent, so the
|
|
1644
|
+
warm session that every cross-phase turn greets with "everything you learned
|
|
1645
|
+
still holds" was left holding a paragraph, and each phase paid a cold agent's
|
|
1646
|
+
first turn again. Both halves are now closed. Every `vinta-ai-maestro` role
|
|
1647
|
+
prompt (implementer, reviewer, fixer, chore — cold and continued) carries a
|
|
1648
|
+
no-delegation section that names those skills and says what delegating costs,
|
|
1649
|
+
and the plan-execution unit grows a `dispatched-agent` partial: a guard at the
|
|
1650
|
+
top of all four conductors refusing entry to an agent that was itself handed
|
|
1651
|
+
one phase, plus the same rule inside every composed implementer prompt.
|
|
1652
|
+
|
|
1653
|
+
- **The node view's gate panel is a list of verdicts you can open, not a stack
|
|
1654
|
+
of scrolling boxes.** Every gate used to render its whole log at once, each
|
|
1655
|
+
in its own fixed-height box with its own scrollbar — so three gates in a
|
|
1656
|
+
third of a grid row was a column of letterbox slots, and a wheel gesture
|
|
1657
|
+
over one of them was swallowed by that box instead of scrolling the page.
|
|
1658
|
+
Gates are now an accordion: one row per gate, only one open at a time, and
|
|
1659
|
+
the open log has no scroller of its own (it is tail-truncated at 64 KiB
|
|
1660
|
+
already, and long lines wrap the way the diff panel's do). The panel opens
|
|
1661
|
+
on the gate a human question points at, or on the first that is not passing.
|
|
1662
|
+
|
|
1663
|
+
- **Each gate row says what happened and what it cost.** The verdict —
|
|
1664
|
+
`running`, `passed`, `failed`, `timed out` — is on the wire now, so a phase
|
|
1665
|
+
whose lint gate passed and whose unit gate failed reads as that instead of
|
|
1666
|
+
as two identical rows; previously the UI could only name the single gate
|
|
1667
|
+
§9.1's question happened to point at, because `NodeDetail` carried an id and
|
|
1668
|
+
a log and nothing else. A running gate shows a clock counting from the
|
|
1669
|
+
daemon's own start time, so a tab opened halfway through a slow suite shows
|
|
1670
|
+
the true elapsed figure rather than starting from zero; a finished one shows
|
|
1671
|
+
the runner's measurement of its latest attempt, with `cached` where the
|
|
1672
|
+
cache served the verdict and a `×N` count where the gate has run more than
|
|
1673
|
+
once.
|
|
1674
|
+
|
|
1675
|
+
- **Crew members are named after their tier, not after a seniority.** A roster
|
|
1676
|
+
reads `tier1`, `tier2-1`, `tier2-2`, `tier4` and `reviewer` where it used to
|
|
1677
|
+
read `junior`, `mid-1`, `mid-2` and `senior`. Nothing about the data model
|
|
1678
|
+
moved — `tier` was already the 1-4 number the orchestrator staffs on, and it
|
|
1679
|
+
is still the only thing compared when one member covers for another. What
|
|
1680
|
+
changed is that the label beside it now says the same thing, instead of
|
|
1681
|
+
offering a second vocabulary that could drift from it and that reads as a
|
|
1682
|
+
ranking of people rather than of difficulty. Workflows using the old ids keep
|
|
1683
|
+
running: the ids are free-form and nothing resolves them by name.
|
|
1684
|
+
|
|
1685
|
+
- **`plan-feature` emits its workflow with the plan's date prefix.** The
|
|
1686
|
+
executable sibling is now
|
|
1687
|
+
`ai-plans/YYYY-MM-DD-<feature-kebab>.workflow.json` — previously it carried
|
|
1688
|
+
no date, which sorted it away from the `YYYY-MM-DD-FEATURE_NAME_PLAN.md` and
|
|
1689
|
+
`_SPEC.md` it belongs to, so the one file you want with the plan open was the
|
|
1690
|
+
one you had to go looking for. A feature's three files now land adjacent. The
|
|
1691
|
+
case difference between them is not an oversight: the markdown convention is
|
|
1692
|
+
`UPPERCASE_WITH_UNDERSCORES` and a workflow's filename stem *is* its `id`,
|
|
1693
|
+
which the schema requires to be lowercase kebab-case, so the date prefix is
|
|
1694
|
+
the only part the three can share. Existing dateless workflows are unaffected
|
|
1695
|
+
— this is the emitter's convention, not a schema rule.
|
|
1696
|
+
- **A gate's command can be retuned while the phase that runs it is still
|
|
1697
|
+
running.** §9's amend refused any change reaching a node in flight, which is
|
|
1698
|
+
exactly the node anyone is looking at when a run is dragging: a `unit` gate
|
|
1699
|
+
missing `--reuse-db` could not be fixed until the run it was costing hours
|
|
1700
|
+
had finished. The refusal now reads a narrower set. A node's harness, model,
|
|
1701
|
+
pipeline, body and base are each read at a moment that has already passed, so
|
|
1702
|
+
changing them is refused as before; a gate's *command* is resolved per gate
|
|
1703
|
+
run by every reader of it, so a running phase simply runs the new command at
|
|
1704
|
+
its next gate. Nothing downstream is blocked either, because a command moves
|
|
1705
|
+
no branch's base. Changing **which** gates a node declares is still refused
|
|
1706
|
+
mid-flight — a phase that gained a gate would finish without ever running it —
|
|
1707
|
+
so that now counts as part of the phase body.
|
|
1708
|
+
|
|
1709
|
+
- **A gate result is cached against the command that produced it.** The key was
|
|
1710
|
+
`(gate id, tree hash)`, on the assumption that a gate id names a command.
|
|
1711
|
+
Amending a live run makes that false, and an unchanged tree would have been
|
|
1712
|
+
served the old command's verdict. The command is now part of the key, hashed
|
|
1713
|
+
rather than stored — a command line is repository content. A cache database
|
|
1714
|
+
written before this is dropped when it is opened: its rows cannot say which
|
|
1715
|
+
command produced them, and one re-run per stale entry is the price a lost row
|
|
1716
|
+
already costs.
|
|
1717
|
+
|
|
1718
|
+
- **A session cannot inherit a machine's decision to stop compacting.** All
|
|
1719
|
+
three harnesses compact automatically when their context fills — there was
|
|
1720
|
+
never a flag to turn on — but each honours an environment variable that turns
|
|
1721
|
+
it off, and an agent spawned on a machine carrying one would run a long phase
|
|
1722
|
+
straight into a wall it was supposed to be able to survive. Those variables
|
|
1723
|
+
are now stripped from every child the daemon spawns, and claude-code's own
|
|
1724
|
+
setting is reasserted in the per-lane file beside its permission rules. The
|
|
1725
|
+
window itself is left alone: an operator who narrowed theirs compacts earlier,
|
|
1726
|
+
which is not the failure this guards against. A harness that compacts declares
|
|
1727
|
+
it, and a compaction the harness reports is a transcript row like any other.
|
|
1728
|
+
|
|
1729
|
+
One consequence worth knowing: `max_session_turns` used to be a proxy for "this
|
|
1730
|
+
session is nearing a context window it will die on". That death no longer
|
|
1731
|
+
happens, so the ceiling now guards a different thing — a session whose memory
|
|
1732
|
+
was replaced by a summary of itself, and which answers just as confidently.
|
|
1733
|
+
|
|
1734
|
+
- **Agent prompts name gates by id rather than printing their commands.** The
|
|
1735
|
+
implementer's step 4 was descriptive — "Outer gate… These run against your
|
|
1736
|
+
lane:" followed by the gate commands — and implementers were observed reading
|
|
1737
|
+
it as a note about what would run later, finishing the scoped inner loop and
|
|
1738
|
+
never running a gate at all. It is imperative now, and every role that runs a
|
|
1739
|
+
gate (implementer, reviewer, fixer, and the continuation of each) is pointed
|
|
1740
|
+
at `vinta-ai-maestro gate <id>`. The fixer prompts no longer say to re-run the
|
|
1741
|
+
gates "until they are green", which contradicted the commit protocol directly
|
|
1742
|
+
below them and left fixers running out the turn with the work uncommitted;
|
|
1743
|
+
they now say to commit and report FAILURE when a gate cannot be turned green.
|
|
1744
|
+
Implementers and fixers report which gates they ran and what each returned —
|
|
1745
|
+
a record for the reviewer to check against, explicitly not a licence for the
|
|
1746
|
+
reviewer to skip running them itself.
|
|
1747
|
+
|
|
183
1748
|
- **Phase branches base on their dependencies, not on the previous phase.** A phase
|
|
184
1749
|
with no dependencies cuts from the default branch; one with a single dependency
|
|
185
1750
|
cuts from that phase's branch; one with several cuts from a
|
|
@@ -216,8 +1781,872 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
216
1781
|
`plan/{plan-id}/lane-{phase-id}` branches, merged `--no-ff` into the single plan
|
|
217
1782
|
branch at each wave boundary, so the unit commits survive.
|
|
218
1783
|
|
|
1784
|
+
- **`project.prepare_cmd`, for the shared servers a run depends on.** A project
|
|
1785
|
+
whose databases are `delivery: external` and whose services point at one Redis
|
|
1786
|
+
has no long-lived containers of its own — the shape these fields recommend —
|
|
1787
|
+
and that makes some *other* stack a hard dependency of every run. Nothing in
|
|
1788
|
+
the package could bring it up. `setup_cmd` looks like the place and is not: it
|
|
1789
|
+
runs per lane, long after the template database was created on a server that
|
|
1790
|
+
had to be up for `createdb` to work at all.
|
|
1791
|
+
|
|
1792
|
+
It runs in the repository root **before anything else a run does** — before
|
|
1793
|
+
the preflight, before the templates, before the first worktree — and again
|
|
1794
|
+
before every lane recycle, so a server that dies mid-run is restored at the
|
|
1795
|
+
next hand-off rather than failing every phase after it. Failure aborts with
|
|
1796
|
+
the exit code and a bounded stderr tail, and says what the failure means
|
|
1797
|
+
rather than only that a command exited. It must be idempotent, and `--wait`
|
|
1798
|
+
is only as good as the `healthcheck` a service declares: without one, compose
|
|
1799
|
+
returns as soon as the container starts and `createdb` races the server.
|
|
1800
|
+
|
|
1801
|
+
- **A merge conflict goes to the most senior member who wrote one of its
|
|
1802
|
+
sides.** The conflict fixer was staffed from `defaults` and ignored the roster
|
|
1803
|
+
entirely, so on a tiered crew a collision between two Tier 4 phases was handed
|
|
1804
|
+
to a default-tier agent that had read neither of them — the hardest merge in
|
|
1805
|
+
the run going to the least contextualised agent available, and resolved by
|
|
1806
|
+
guessing which side looked more finished.
|
|
1807
|
+
|
|
1808
|
+
`Integrator` already knows which nodes own the contested paths, and `node_crew`
|
|
1809
|
+
already records who took each one; this is the join. Highest tier among them
|
|
1810
|
+
wins, on that member's own `model` and `harness`, and a tier tie breaks on
|
|
1811
|
+
member id the way roster substitution breaks its own. An unstaffed workflow
|
|
1812
|
+
still resolves through `defaults`, unchanged, and so does a conflict whose
|
|
1813
|
+
nodes cannot be resolved to a member.
|
|
1814
|
+
|
|
1815
|
+
What is carried is the member's **capability, not their session**. A phase's
|
|
1816
|
+
session ran in that phase's lane; the fixer runs in the integration worktree,
|
|
1817
|
+
where the files in dispute are half-merged and unlike anything that session
|
|
1818
|
+
saw — resuming there would give the agent confident, false memories of exactly
|
|
1819
|
+
the files it is merging. The prompt tells the fixer which side is its own, and
|
|
1820
|
+
in the same breath that it is a fresh session and must read rather than
|
|
1821
|
+
recall.
|
|
1822
|
+
|
|
219
1823
|
### Fixed
|
|
220
1824
|
|
|
1825
|
+
- **maestro asks you to log in instead of failing on a logged-out harness.**
|
|
1826
|
+
A CLI that was not logged in (or whose token was rejected) was `fatal`: the
|
|
1827
|
+
node failed and its whole subtree blocked. It now parks in `awaiting_human`
|
|
1828
|
+
on a "not logged in" question answered with `logged in` or `stop`, holding
|
|
1829
|
+
its lane. Nothing answers it for you — no automatic or unattended retry,
|
|
1830
|
+
regardless of `onFailure` — and one `logged in` resumes every node that
|
|
1831
|
+
harness refused. Adapters report it as the new `unauthenticated` refusal
|
|
1832
|
+
kind (Claude Code, Codex and opencode).
|
|
1833
|
+
- **maestro no longer fails a node on an organization spend cap.** Claude
|
|
1834
|
+
Code's `billing_error` "spend limit reached (daily; resets …)", the
|
|
1835
|
+
`org_spend_cap_reached` overage reason and "usage credit limit reached"
|
|
1836
|
+
matched no refusal pattern and were classified `fatal`, failing the node and
|
|
1837
|
+
blocking its subtree. They are now `quota` waits, and the stated
|
|
1838
|
+
`resets YYYY-MM-DD HH:MM UTC` time is honored as the wake time.
|
|
1839
|
+
|
|
1840
|
+
- **A failed Claude Code turn no longer reads as `claude-code result:
|
|
1841
|
+
success`.** When the CLI ends a turn with `is_error: true` beside
|
|
1842
|
+
`subtype: "success"` (how a plan limit arrives, for one), the recorded
|
|
1843
|
+
error printed the subtype and said "success" — 189 times in one observed
|
|
1844
|
+
run. It now reads `claude-code result: is_error`. Other subtypes
|
|
1845
|
+
(`error_max_turns`, …) are unchanged.
|
|
1846
|
+
|
|
1847
|
+
- **The monitor's conversation was mostly unformatted JSON.** Not its answers —
|
|
1848
|
+
its *watchdog's*. An intervention turn must reply with one document matching
|
|
1849
|
+
`intervention.v1.schema.json`, which is what makes the monitor's authority
|
|
1850
|
+
over a live run a closed set of verbs rather than an editor over
|
|
1851
|
+
`workflow.json`; and that turn is journalled into the conversation a person
|
|
1852
|
+
reads, because a run that retuned itself should say so where somebody is
|
|
1853
|
+
already looking. Both of those are right. Rendering the document verbatim was
|
|
1854
|
+
not, and the summary paragraph the operator wanted arrived a thousand
|
|
1855
|
+
characters into one unwrapped line of braces. A proposal now renders as what
|
|
1856
|
+
it says: the summary, then one line per proposed change with its evidence
|
|
1857
|
+
under it, and a tone dot only when something was actually proposed — a turn
|
|
1858
|
+
that looked and changed nothing is the expected outcome and should not shout.
|
|
1859
|
+
The verb table is keyed on the daemon's own union, so a fifth verb fails the
|
|
1860
|
+
UI build rather than shipping as a bare identifier. The journal still holds
|
|
1861
|
+
the bytes the model wrote — only the row changed, so conversations already on
|
|
1862
|
+
disk read as well as new ones. Any other whole-JSON answer is at least
|
|
1863
|
+
indented.
|
|
1864
|
+
|
|
1865
|
+
- **The monitor's conversation opened at its oldest entry and never scrolled.**
|
|
1866
|
+
It followed the newest row *only while a turn was running*, on the reasoning
|
|
1867
|
+
that the list is short and the operator is watching the answer they just
|
|
1868
|
+
asked for. Neither half held: the conversation is the journal's, so it holds
|
|
1869
|
+
every question ever asked about the run plus every proposal the run's own
|
|
1870
|
+
watchdog made with nobody asking anything — a hundred entries is ordinary.
|
|
1871
|
+
Opening the panel therefore put the reader at the oldest of them. It now
|
|
1872
|
+
follows the way a phase's transcript does: stuck to the newest row, letting
|
|
1873
|
+
go the moment the reader scrolls up to read, with a "Jump to latest" as the
|
|
1874
|
+
way back. That rule is one module now rather than a well-tested copy in the
|
|
1875
|
+
transcript and none in the monitor, which is how the panel came to be missing
|
|
1876
|
+
three of its parts — including the one that survives a panel being expanded
|
|
1877
|
+
to full page, where the portal hands the view a brand new list scrolled to
|
|
1878
|
+
the top.
|
|
1879
|
+
|
|
1880
|
+
- **Hitting the plan's session or weekly limit failed the node instead of
|
|
1881
|
+
waiting for the window to end.** `claude-code`'s refusal table recognised
|
|
1882
|
+
"usage limit reached" and nothing else, and the classifier's default is
|
|
1883
|
+
`fatal` — correctly, since an unrecognised failure must not become an
|
|
1884
|
+
unbounded wait. But the CLI announces a plan window in its own words:
|
|
1885
|
+
"You've hit your session limit", "Session limit reached · resets 3am",
|
|
1886
|
+
"5-hour limit reached", "You've reached your weekly limit". None of those
|
|
1887
|
+
contain "usage limit", so every one of them fell past the table to `fatal`,
|
|
1888
|
+
which fails the node and blocks its whole dependent subtree — for a window
|
|
1889
|
+
that ends by itself in a few hours. The machinery to do the right thing was
|
|
1890
|
+
already there and simply never reached: a `quota` refusal parks the harness,
|
|
1891
|
+
not the node, so every later node joins one wait instead of discovering the
|
|
1892
|
+
limit for itself; the wake time is journalled and survives a daemon restart;
|
|
1893
|
+
and the AIMD ceiling is left alone, because a plan window says nothing about
|
|
1894
|
+
how many agents may run at once. Those wordings are now `quota`. The new row
|
|
1895
|
+
sits *below* `concurrency` and `rate_limit` so that "concurrent session limit
|
|
1896
|
+
reached" keeps the kind that halves the ceiling, and "approaching session
|
|
1897
|
+
limit" — a warning from a harness that is still working — matches nothing.
|
|
1898
|
+
|
|
1899
|
+
- **A window that closed *under* a running turn was an ordinary turn failure.**
|
|
1900
|
+
§6.1 is written about a spawn, because that is where a vendor usually says
|
|
1901
|
+
"not right now" — and everything that follows from it, parking the harness so
|
|
1902
|
+
every later node joins one wait, journalling the wake time so a daemon
|
|
1903
|
+
restart does not forget it, leaving the AIMD ceiling alone for a window that
|
|
1904
|
+
says nothing about concurrency, hung off `admit`. So the other arrival of the
|
|
1905
|
+
same fact had none of it: a plan window that closes while an agent is twenty
|
|
1906
|
+
minutes into a phase ends the turn, and the node then burnt its retries
|
|
1907
|
+
against a closed window and failed, blocking its whole dependent subtree —
|
|
1908
|
+
the exact outcome §6.1 exists to prevent, reached by the one path that did not
|
|
1909
|
+
go through admission control. A session now reports a `TurnRefusal`, and the
|
|
1910
|
+
caller answers it with the `CapacityRetry` a refused spawn raises: the lane
|
|
1911
|
+
goes back, the harness parks until the stated reset, and the node re-drives
|
|
1912
|
+
when the window opens.
|
|
1913
|
+
|
|
1914
|
+
Each adapter reads it where its own failure prose is, which is not where its
|
|
1915
|
+
events are: `claude-code`'s terminal `result` frame carries the sentence in
|
|
1916
|
+
the `result` string that `mapResult` deliberately drops (it is agent output,
|
|
1917
|
+
§11), `codex` states a failure as prose on three different frames, and
|
|
1918
|
+
`opencode`'s `session.error` carries it under `data`, which `#errorName`
|
|
1919
|
+
refuses to put in an event. All three read that prose, match it against the
|
|
1920
|
+
table they already had, and drop it — what reaches the caller is a kind, a
|
|
1921
|
+
fixed reason token and a reset time, and what reaches the transcript is one
|
|
1922
|
+
token-only line so the record says why the turn stopped. The stderr-and-exit
|
|
1923
|
+
shape of the same event is read too, from the diagnostics buffer that is the
|
|
1924
|
+
only place that sentence exists.
|
|
1925
|
+
|
|
1926
|
+
The re-drive is a full one, and that is deliberate: the session the turn was
|
|
1927
|
+
using is gone, and resuming from a memory of files a recycle has removed is
|
|
1928
|
+
worse than redoing the work. Nothing is destroyed by it — `recycle` sets
|
|
1929
|
+
aside whatever the turn left uncommitted as a WIP ref first. What it costs is
|
|
1930
|
+
the turn. What it used to cost was the node.
|
|
1931
|
+
|
|
1932
|
+
- **A stated reset time was only read when it was an hour on the same day.**
|
|
1933
|
+
`parseRetryAfter` prefers a reset the vendor reported over a guessed backoff,
|
|
1934
|
+
and its wall-clock pattern wanted `am`/`pm` immediately after "reset", so
|
|
1935
|
+
"resets at 15:00", "resets tomorrow at 2am" and the weekly window's "resets
|
|
1936
|
+
on Monday at 12am" all reported nothing — falling back to the five-minute
|
|
1937
|
+
re-probe interval, which across a week-long window is two thousand spawns to
|
|
1938
|
+
discover what the first sentence said. It now reads 24-hour times, `today`,
|
|
1939
|
+
`tomorrow` and a named weekday (the next occurrence of it, which is today
|
|
1940
|
+
only while the hour is still ahead). A reset named by date, "resets Nov 3 at
|
|
1941
|
+
9am", still reports nothing on purpose: a month with no year is a guess, and
|
|
1942
|
+
the re-probe is a better answer than a wait that is wrong by a year. So are
|
|
1943
|
+
"reset 3", "resets at 25:00" and "resets at 13pm" — an hour with neither
|
|
1944
|
+
minutes nor a meridiem is as likely to be a version or a retry count, and
|
|
1945
|
+
everything this function returns becomes a wait.
|
|
1946
|
+
|
|
1947
|
+
- **`run --resume` could not provision a pool at all on a project that
|
|
1948
|
+
disables hooks.** `#configureHooks` enables `extensions.worktreeConfig`
|
|
1949
|
+
before it can set `core.hooksPath --worktree`, and that first key is
|
|
1950
|
+
repository-wide: the write lands in the shared `.git/config`, which git takes
|
|
1951
|
+
`.git/config.lock` to edit. Called once per lane out of the provisioning
|
|
1952
|
+
`Promise.all`, the lanes collided on that lock — `could not lock config file
|
|
1953
|
+
…: File exists`. A fresh provision never showed it, because `worktree add`
|
|
1954
|
+
goes through the pool's serialized git turn and staggered the lanes apart by
|
|
1955
|
+
the time they reached the shared write; `adopt` skips `worktree add`, so every
|
|
1956
|
+
lane arrived in the same tick and **every** resume of a `hooks: false` project
|
|
1957
|
+
failed, reproducibly, before a single phase was scheduled. The extension now
|
|
1958
|
+
goes through the same serialized turn. The `--worktree` write stays parallel:
|
|
1959
|
+
it lands in `.git/worktrees/<name>/config.worktree`, one file per lane.
|
|
1960
|
+
|
|
1961
|
+
- **A provision failure that was not one of three enumerated kinds said nothing
|
|
1962
|
+
about itself.** `refusal` detailed `DiskProbeError`, `LaneSetupError` and
|
|
1963
|
+
`LaneEnvFileError`; everything else — including the lock collision above and
|
|
1964
|
+
`LaneAdoptError`, the one error a resume is most likely to hit — collapsed to
|
|
1965
|
+
"could not provision the lane pool under …", and the daemon log recorded that
|
|
1966
|
+
same sentence as `run.provision_failed`'s `reason` with no `error` field and
|
|
1967
|
+
no stack. So the run that could not start left behind no record of why, in
|
|
1968
|
+
either place an operator looks. The exclusion was a §11 reading
|
|
1969
|
+
`LaneSetupError` had already revised for this exact reason: §11 keeps
|
|
1970
|
+
repository *contents* out of the record — diffs, file bodies, gate output —
|
|
1971
|
+
and an error's own complaint about a lock file or a ref is not that. An
|
|
1972
|
+
unenumerated error's message is now carried, capped at 500 characters and run
|
|
1973
|
+
through the log's redaction set; `LaneAdoptError` speaks for itself like the
|
|
1974
|
+
other lane errors; and the failure is logged with its kind and stack frames.
|
|
1975
|
+
|
|
1976
|
+
- **`doctor` reported the resumed run's own lanes as leftovers blocking it, and
|
|
1977
|
+
offered to destroy them.** The held-branch check fails a run whose phase
|
|
1978
|
+
branches are checked out in another worktree, which is right for a fresh run
|
|
1979
|
+
and exactly wrong for a resume: the integrator puts a lane on
|
|
1980
|
+
`plan/<id>/phase-<n>` for the duration of a phase, so a run killed mid-phase
|
|
1981
|
+
— the kill a resume is *for* — leaves its own lanes holding precisely those
|
|
1982
|
+
branches. Every `run --resume` therefore refused preflight with one failure
|
|
1983
|
+
per phase in flight, each remedied by `git worktree remove --force <lane>`,
|
|
1984
|
+
which deletes the uncommitted work the resume existed to adopt. The check now
|
|
1985
|
+
takes the run being resumed and excludes lanes belonging to it, identified by
|
|
1986
|
+
reading the lane root and asking each worktree which branch it holds — never
|
|
1987
|
+
by comparing git's printed paths with this process's, the platform trap
|
|
1988
|
+
`reap` and `LanePool` both document. `doctor` takes `--resume <run-id>` to ask
|
|
1989
|
+
the same question before a resume, and a lane of *another* run still fails.
|
|
1990
|
+
|
|
1991
|
+
- **`vinta-ai-maestro gate` gave up on the slowest gate after five minutes and
|
|
1992
|
+
called it a daemon it could not reach.** The command awaited one `fetch`, on
|
|
1993
|
+
the stated reasoning that a gate is slow for reasons the client cannot shorten
|
|
1994
|
+
and so the client should simply wait — while its own comment claimed it
|
|
1995
|
+
imposed no deadline of its own. It imposed 300 seconds, because that is where
|
|
1996
|
+
Node's `fetch` abandons a response whose headers have not arrived, and a gate
|
|
1997
|
+
that queues for a capacity-1 semaphore and then runs a test suite is routinely
|
|
1998
|
+
slower. In a measured fourteen-hour run this fired **33 times, every one of
|
|
1999
|
+
them on the `unit` gate and none on any other** — a perfect correlation with
|
|
2000
|
+
duration, not a flaky daemon. The gates were running fine and cached green
|
|
2001
|
+
results minutes later; only the answer was lost. Told a transport lie about
|
|
2002
|
+
work in progress, agents wrote polling loops around a command documented as
|
|
2003
|
+
needing none: in one phase, **24 of 108 shell turns were pure waiting, six of
|
|
2004
|
+
them burning a full ten-minute ceiling — 60 minutes, 31% of that phase.** The
|
|
2005
|
+
wait is a `202` hop loop now, which is what `with` already does, having found
|
|
2006
|
+
the same bug the same way and fixed it first.
|
|
2007
|
+
|
|
2008
|
+
- **A gate command cut short by the harness started the suite over.** An agent
|
|
2009
|
+
harness caps how long one command may run, and a gate may exceed any such cap
|
|
2010
|
+
— a plan declaring `timeout_s: 2400` is asking for forty minutes against a
|
|
2011
|
+
ten-minute budget. So being killed partway is the *ordinary* ending, and what
|
|
2012
|
+
an agent does next is run the command again. That used to start a second full
|
|
2013
|
+
suite beside the first, queued behind the very semaphore its own predecessor
|
|
2014
|
+
was still holding. The daemon now keys a running gate on `(phase, gate id)`
|
|
2015
|
+
and the re-invocation attaches to it, which makes re-running the command the
|
|
2016
|
+
correct move rather than a wasteful one — worth having, because it is the move
|
|
2017
|
+
an agent makes anyway. There is no token to carry, deliberately: a client that
|
|
2018
|
+
was killed has nothing in hand, and needing something would defeat the point.
|
|
2019
|
+
|
|
2020
|
+
- **A pipeline that could not progress was journalled as the word "Error".** The
|
|
2021
|
+
interpreter composes an id-safe reason for exactly this case — which state it
|
|
2022
|
+
could not leave, and which trigger failed to match — and the scheduler threw
|
|
2023
|
+
it as a bare `Error`. `failureReason` reports an unrecognised error by its
|
|
2024
|
+
name, so the sentence was discarded at the one moment it was worth keeping.
|
|
2025
|
+
It is a `PipelineStuckError` now, carrying the state, and the reason survives
|
|
2026
|
+
into the journal. The same shape as the non-zero git exit, fixed the same way.
|
|
2027
|
+
|
|
2028
|
+
- **A pull request said nothing but the plan anchor.** The daemon opened PRs
|
|
2029
|
+
with `title: node.name` and `body: node.prompt_ref` — one line, a link to a
|
|
2030
|
+
heading. It had no idea the `prs-context` mechanism existed: there was not a
|
|
2031
|
+
single reference to it in the package, so the rich path the skills use
|
|
2032
|
+
(`open-pr-from-context`, the project's own PR template, inline review
|
|
2033
|
+
comments) was never reached from a run. `open_pr` now reads the phase's
|
|
2034
|
+
`.vinta-ai-workflows/prs-context/<plan>/phase-<node>.md` when the agent wrote
|
|
2035
|
+
one, and the implementer is asked to write it — the 5–15 line summary it
|
|
2036
|
+
already produces, put where a human will read it instead of only the
|
|
2037
|
+
transcript. With no context file the body is composed from the run's own
|
|
2038
|
+
record: the brief, what the phase is based on and why that is not the default
|
|
2039
|
+
branch, its declared surface, its gates, how many attempts it took, and any
|
|
2040
|
+
merge conflicts an agent resolved on the way in — which nobody reviews.
|
|
2041
|
+
|
|
2042
|
+
- **A phase with two dependencies could never open a pull request.** Its base is
|
|
2043
|
+
an `integ-<id>` branch and `git_push` pushes only the node's own branch, so
|
|
2044
|
+
`gh` was asked to open against a ref the forge had never seen. It refused, the
|
|
2045
|
+
refusal was discarded, and the phase completed with no PR and nothing saying
|
|
2046
|
+
so — in one observed run, the single node with two dependencies was the single
|
|
2047
|
+
node with no PR. The integration base is pushed before the PR is opened, and
|
|
2048
|
+
every outcome is journalled as `node_pr`, so a PR that did not open is now as
|
|
2049
|
+
visible as one that did.
|
|
2050
|
+
|
|
2051
|
+
- **A conflict resolution was never gated.** `IntegratorOptions.verify` has
|
|
2052
|
+
existed as long as the conflict loop, documented as "a resolution that does
|
|
2053
|
+
not build is not a resolution", and nothing ever supplied it. Six merges in
|
|
2054
|
+
one run were resolved by an agent and went straight in — ungated — to become
|
|
2055
|
+
the base the next phase built on, while the phases either side were gated to
|
|
2056
|
+
the hilt. The union of the conflicting nodes' declared gates now runs in the
|
|
2057
|
+
integration worktree, with that worktree's environment, and a red gate spends
|
|
2058
|
+
a fix round instead of shipping.
|
|
2059
|
+
|
|
2060
|
+
- **A conflict fixer that committed its own resolution failed the phase.** An
|
|
2061
|
+
agent told to resolve a merge conflict reaches for the sequence a person
|
|
2062
|
+
would — abort, merge again, resolve, commit — and the orchestrator then
|
|
2063
|
+
committed unconditionally on top of it. Against an already-committed
|
|
2064
|
+
resolution `git add --all` is a no-op that exits 0 and `git commit --no-edit`
|
|
2065
|
+
exits 1 with "nothing to commit", which is a throw. A successful resolution
|
|
2066
|
+
was reported as a failed phase, deterministically, on **every wave after a
|
|
2067
|
+
parallel one**: a multi-dependency node is the only thing that merges here,
|
|
2068
|
+
and two sibling phases both creating one file is the common case rather than
|
|
2069
|
+
the rare one. The phase died before its branch was cut, so it left no
|
|
2070
|
+
transcript, no gate log and no branch — only a resolved merge commit sitting
|
|
2071
|
+
in the integration worktree. The commit is now conditional on there being
|
|
2072
|
+
something to commit, and a fixer that discarded the merge instead of resolving
|
|
2073
|
+
it is refused rather than recorded as integrated.
|
|
2074
|
+
|
|
2075
|
+
- **The conflict fixer's turn is in the transcript now.** Its event stream was
|
|
2076
|
+
drained and every event dropped — an unread stream never ends — so the one
|
|
2077
|
+
agent turn in a run nobody could watch live was also the one nobody could read
|
|
2078
|
+
afterwards: a resolved merge, a row saying it took two rounds, and no record of
|
|
2079
|
+
what was decided. It lands in the incoming phase's own transcript, beside the
|
|
2080
|
+
implementer and reviewer turns that produced the branches being merged, under
|
|
2081
|
+
a `conflict-fixer` role that keeps it distinguishable from the review fixer —
|
|
2082
|
+
a different job, in a different worktree, on a different thing. The UI has had
|
|
2083
|
+
a band for that role since transcripts learned to name their authors; it had
|
|
2084
|
+
simply never been given one to draw.
|
|
2085
|
+
|
|
2086
|
+
- **`PlanDefectError` is now `UnresolvedConflictError`.** The old name made a
|
|
2087
|
+
judgement the orchestrator is not entitled to make. Two same-wave phases
|
|
2088
|
+
touching one file is not a broken plan — the plan's file-overlap analysis is a
|
|
2089
|
+
guess made before a line was written, and the fixer exists because of it. What
|
|
2090
|
+
reaching that error means is narrower: *this* conflict outlasted its rounds and
|
|
2091
|
+
needs a person. The message says so, and now leads with finishing the merge by
|
|
2092
|
+
hand in the integration worktree — which, since a prepared base is reused,
|
|
2093
|
+
survives into the retry — rather than with re-cutting the plan.
|
|
2094
|
+
|
|
2095
|
+
- **A conflict was resolved again on every retry, and recorded nowhere.**
|
|
2096
|
+
Conflicts are an ordinary outcome — the plan's file-overlap analysis is a
|
|
2097
|
+
guess and two sibling phases legitimately edit one file — but `prepareBase`
|
|
2098
|
+
reset and re-merged its `integ-<id>` branch unconditionally, so each retry
|
|
2099
|
+
spawned a fresh fixer agent to redo work already done, and discarded any
|
|
2100
|
+
resolution a person had finished by hand in that worktree. One observed run
|
|
2101
|
+
paid for the same resolution three times. A base that already integrates every
|
|
2102
|
+
dependency's current tip is now kept; one built before a dependency moved is
|
|
2103
|
+
still rebuilt, because reuse is about reachability of the tips rather than
|
|
2104
|
+
about the branch existing. Every settled conflict is also journalled as a
|
|
2105
|
+
`node_conflict` event naming both participants, the paths and the rounds it
|
|
2106
|
+
took — previously a wave conflict reached only the post-mortem and a *base*
|
|
2107
|
+
conflict was discarded entirely, so a phase could sit in `running` for minutes
|
|
2108
|
+
while an agent merged in a worktree nobody could see.
|
|
2109
|
+
|
|
2110
|
+
- **A failed attempt now says why, wherever it goes next.** The reason was
|
|
2111
|
+
computed at the failure site and handed to the path that gives up — which the
|
|
2112
|
+
default `onFailure: retry` does not take: it retries automatically, then parks
|
|
2113
|
+
on "This phase failed. Try it again?". Both paths dropped the reason, and
|
|
2114
|
+
`#offerRetry` carried a comment asserting the opposite. A phase that failed
|
|
2115
|
+
three times before any agent ran was undiagnosable after the fact, and the
|
|
2116
|
+
only offered action was to repeat it. Every attempt now writes a `node_error`
|
|
2117
|
+
event carrying its reason and attempt number, and a `node.attempt_failed`
|
|
2118
|
+
record to the daemon log.
|
|
2119
|
+
|
|
2120
|
+
- **A git command that exited non-zero was persisted as the word "Error".**
|
|
2121
|
+
`failureReason` reports an unrecognised error by its name plus a *string*
|
|
2122
|
+
`code`, and an `execFile` rejection carries `name: 'Error'` with a *numeric*
|
|
2123
|
+
one — so the most common real failure in this package said nothing at all.
|
|
2124
|
+
Git failures now carry their subcommand and exit status (`git commit exited
|
|
2125
|
+
1`), which is a command name and an exit code rather than anything §11 keeps
|
|
2126
|
+
out of the journal.
|
|
2127
|
+
|
|
2128
|
+
- **The conflict fixer ran without its worktree's environment.** It was the one
|
|
2129
|
+
agent spawn not given `AgentTask.env`, whose own docstring records this exact
|
|
2130
|
+
bug class. With the daemon's bare environment its `docker compose` resolved to
|
|
2131
|
+
a project named after the directory rather than the isolated one, ignored the
|
|
2132
|
+
`compose.publish: []` override that rides in `COMPOSE_FILE`, and published the
|
|
2133
|
+
project's fixed ports on the host — colliding with the developer's own stack
|
|
2134
|
+
and outliving the run.
|
|
2135
|
+
|
|
2136
|
+
- **"Retry with <member>" offered the member that had just failed.** The menu
|
|
2137
|
+
excluded the member the *plan* named, but a substitution means the member that
|
|
2138
|
+
actually ran is someone else — so a phase declared for one tier and covered by
|
|
2139
|
+
a higher one offered that higher one as its escalation, and a third of the
|
|
2140
|
+
menu did nothing distinguishable from plain retry.
|
|
2141
|
+
- **An amended workflow now reaches the integrator too, and a wave merge is
|
|
2142
|
+
pinned to the membership it was decided with.** Fixing the executor without
|
|
2143
|
+
fixing the integrator is what made this reachable: `#lastOfWave` decides a
|
|
2144
|
+
wave is complete by counting the executor's node set and `mergeWave` decided
|
|
2145
|
+
what to merge by reading the integrator's, so while both were stale they
|
|
2146
|
+
agreed with each other and nothing showed. One fresh and one stale is a wave
|
|
2147
|
+
whose merge silently omits the branch of a phase that ran.
|
|
2148
|
+
|
|
2149
|
+
The case that gets there is a phase nobody depends on. An amendment is
|
|
2150
|
+
refused while a node it *blocks* is in flight, and such a phase blocks
|
|
2151
|
+
nothing, so it lands freely beside the wave-mates it will be merged with.
|
|
2152
|
+
|
|
2153
|
+
A wave's membership is now computed once, synchronously, by the code that
|
|
2154
|
+
decides the wave is complete, and handed to the merge — a wave merge runs
|
|
2155
|
+
behind the integration worktree's queue and an amendment can land in between,
|
|
2156
|
+
so a merge that re-derived its own membership would merge a phase that never
|
|
2157
|
+
ran. §9's rebase goes through that same queue now, since "the nodes this
|
|
2158
|
+
amendment blocks are idle" says nothing about whether another wave's merge is
|
|
2159
|
+
writing in that directory.
|
|
2160
|
+
|
|
2161
|
+
- **An amended workflow now reaches the code that runs the gate.** `adopt`
|
|
2162
|
+
replaced the scheduler's copy of the run's definition and nothing else, while
|
|
2163
|
+
the effect executor and the agent-gate broker each held the snapshot they were
|
|
2164
|
+
constructed with — and the command a gate actually runs is read from the
|
|
2165
|
+
executor's. So an operator who edited a gate command mid-run moved the
|
|
2166
|
+
snapshot, the journal and the pool reservations, watched the amendment be
|
|
2167
|
+
accepted, and went on getting the old command. All three take the amendment
|
|
2168
|
+
now, fanned out from one place, host side before the scheduler so that no
|
|
2169
|
+
phase is dispatched against a definition half the system has not seen yet.
|
|
2170
|
+
|
|
2171
|
+
- **Closing a terminal on a run now records the interruption.** `run` and
|
|
2172
|
+
`serve` handle SIGHUP as well as SIGINT and SIGTERM — SIGHUP is what a
|
|
2173
|
+
terminal sends when its window closes, and with no handler Node's default was
|
|
2174
|
+
to die on the spot, running no teardown and writing no `run_ended`. Both
|
|
2175
|
+
commands now mark their live runs interrupted before exiting and print the
|
|
2176
|
+
`--resume` line for each, so a closed window leaves something recoverable
|
|
2177
|
+
instead of a row nothing will ever move again.
|
|
2178
|
+
|
|
2179
|
+
- **A member's reviews were counted as phases they took.** `node_crew` is
|
|
2180
|
+
written for both seats — the reviewer's claim carries `role: 'reviewer'`, the
|
|
2181
|
+
implementer's omits it — and the staffing rollup read neither, so every phase
|
|
2182
|
+
a member reviewed was folded into the count of phases they implemented. On any
|
|
2183
|
+
run with reviewers that inflated the number the rollup exists to put beside
|
|
2184
|
+
the plan's estimate, and inflated `asPlanned`, which the run view divides by.
|
|
2185
|
+
The two seats are counted apart now rather than one of them dropped: a review
|
|
2186
|
+
is real work by a real member, and a member who only reviewed is neither a
|
|
2187
|
+
phase-taker nor idle.
|
|
2188
|
+
|
|
2189
|
+
- **An agent taking a lease left the resource panel showing the one before it.**
|
|
2190
|
+
The scheduler's own gate-pool transitions are journalled as `gate_pool`
|
|
2191
|
+
events, so the browser re-reads the snapshot when one lands. An agent's lease,
|
|
2192
|
+
taken through `vinta-ai-maestro with`, was only a row in the `leases` table —
|
|
2193
|
+
correct in the daemon's projection and announced to nobody. A projection
|
|
2194
|
+
nobody is told changed is a projection nobody sees change, so the panel showed
|
|
2195
|
+
whatever holders it had last time some unrelated event happened to arrive, and
|
|
2196
|
+
the waits it was stalest about were the agent ones, which are the long ones.
|
|
2197
|
+
The broker now journals `agent_lease` on both edges. A renewal writes nothing:
|
|
2198
|
+
it is a heartbeat on a lease already reported, and journalling it would bury
|
|
2199
|
+
the two rows that are transitions under a liveness log.
|
|
2200
|
+
|
|
2201
|
+
- **An agent waiting for a semaphore could be overtaken without bound.** A
|
|
2202
|
+
`vinta-ai-maestro with` wait is served in fifteen-second hops so no client
|
|
2203
|
+
timeout has an opinion about it, and each hop used to abort the underlying
|
|
2204
|
+
`pools.acquire`. Aborting leaves the pool queue, and the next POST re-entered
|
|
2205
|
+
it — at the tail, behind every waiter that had arrived in the meantime. Since
|
|
2206
|
+
a gate run holds a capacity-1 semaphore for minutes, an agent lost that race
|
|
2207
|
+
on hop after hop while the scheduler's own in-process waiters, which never
|
|
2208
|
+
leave the queue, did not. The pool's aging rule could not help: aging reserves
|
|
2209
|
+
against waiters *behind* the aged one, and the waiters that overtook it are in
|
|
2210
|
+
front of it. A `202` now carries a `waitToken` naming the wait, the client
|
|
2211
|
+
sends it back, and the daemon keeps the one queue entry — one arrival time,
|
|
2212
|
+
one place in line — across the whole wait. A parked wait whose client stops
|
|
2213
|
+
hopping is reaped after a minute, releasing the slot if one was granted to it
|
|
2214
|
+
while nobody was watching.
|
|
2215
|
+
|
|
2216
|
+
- **The monitor's conversation could never be written on Windows.** It is kept
|
|
2217
|
+
under a reserved node id that becomes a directory, and that id was
|
|
2218
|
+
`monitor:conversation` — a colon, chosen because no phase id may contain one,
|
|
2219
|
+
and the drive separator on Windows. `mkdir` failed with `ENOENT`, so every
|
|
2220
|
+
question threw on its first append and the endpoint reported the monitor
|
|
2221
|
+
unavailable. A colon is legal in a macOS or Linux filename, which is why no
|
|
2222
|
+
machine the feature was built on ever saw it. The id is now
|
|
2223
|
+
`_monitor-conversation`: still unrepresentable as a phase id, and legal
|
|
2224
|
+
everywhere. Conversations recorded under the old id are not migrated.
|
|
2225
|
+
|
|
2226
|
+
- **An implementer was told both to commit and never to commit.** The prompt
|
|
2227
|
+
carried "never commit while a gate is red" beside "the turn is not complete
|
|
2228
|
+
until `git status --porcelain` is empty of your work" — a contradiction
|
|
2229
|
+
whenever the gate could not be turned green inside the turn, which is most of
|
|
2230
|
+
why fix rounds exist. Agents obeyed the `never`, and the reviewer one node
|
|
2231
|
+
later raised the BLOCKER it is told to raise for uncommitted work, spending a
|
|
2232
|
+
fix round re-implementing code that was already on disk. A red gate now
|
|
2233
|
+
changes the report and never the decision to commit: commit, and report
|
|
2234
|
+
FAILURE saying what is still red.
|
|
2235
|
+
|
|
2236
|
+
The reviewer's side was a false BLOCKER independently of that. Any unclean
|
|
2237
|
+
tree was a finding, and a lane's tree is never clean — the pool copies
|
|
2238
|
+
configuration in, links dependency trees, and the gates leave build output
|
|
2239
|
+
behind, none of which the implementer is allowed to stage. It is scoped to
|
|
2240
|
+
the failure it was written for now: work that is in the tree and missing from
|
|
2241
|
+
the diff.
|
|
2242
|
+
- **A live transcript stops following at entry sixty.** The effect that kept the
|
|
2243
|
+
box on the newest row was keyed on how many rows were *shown*, which is
|
|
2244
|
+
`min(entries, window)` — so it stopped changing the moment a transcript
|
|
2245
|
+
outgrew one window, and the following died silently and permanently, including
|
|
2246
|
+
after the operator scrolled back to the bottom to ask for it. Which is roughly
|
|
2247
|
+
minute two of any real phase. It follows on the entry count now, growing the
|
|
2248
|
+
window keeps the reader on the row they were reading instead of teleporting
|
|
2249
|
+
them, and a **Jump to latest** button says so out loud rather than leaving a
|
|
2250
|
+
60px band at the bottom of a scroller as the only way back in.
|
|
2251
|
+
|
|
2252
|
+
- **The transcript spends its space on the noisiest rows.** An agent's answer,
|
|
2253
|
+
its private reasoning and four hundred characters of tool-call JSON all
|
|
2254
|
+
rendered as the same two lines at the same weight. Thinking is now small and
|
|
2255
|
+
quiet, and a streamed thought is one row rather than the dozen events it
|
|
2256
|
+
arrived as; tool calls and their results fold to the argument that says what
|
|
2257
|
+
they did — a command, a path — and open individually or a whole kind at a time
|
|
2258
|
+
from the panel header. Prose is untouched and never folds.
|
|
2259
|
+
|
|
2260
|
+
- **A panel's expand control did nothing on the node view.** It expanded by
|
|
2261
|
+
taking `col-span-full`, which on the run view bought a strip a third wider and
|
|
2262
|
+
on the node view matched nothing at all — those panels sit in a flex column,
|
|
2263
|
+
not a grid, so the only thing the button changed was the height of the
|
|
2264
|
+
scroller inside it. It now opens the panel over the whole page through a
|
|
2265
|
+
portal, so no scrolling ancestor can clip it, with Escape to leave and the
|
|
2266
|
+
page behind it held still. A transcript that was following stays on its newest
|
|
2267
|
+
row across the change.
|
|
2268
|
+
|
|
2269
|
+
- **`doctor` checks that the shared servers answer.** It verified that binaries
|
|
2270
|
+
existed and disk fitted, and never looked at a `server_url` — so a project
|
|
2271
|
+
whose Postgres lives in another checkout's compose stack passed the preflight
|
|
2272
|
+
with that stack down and died on the first `createdb`, before a worktree
|
|
2273
|
+
existed. Each external database's server and each service URL is now probed
|
|
2274
|
+
with a TCP connect, and a failure names the address that did not answer. A
|
|
2275
|
+
compose-delivered database is deliberately not probed: the lane starts it.
|
|
2276
|
+
|
|
2277
|
+
The order matters as much as the check. `prepare_cmd` runs *before* the
|
|
2278
|
+
preflight, so the check reports the world that hook just made rather than the
|
|
2279
|
+
one in front of it.
|
|
2280
|
+
|
|
2281
|
+
- **A service's `create_cmd` runs with the lane's environment.** `reset_cmd`
|
|
2282
|
+
always had it and `create_cmd` did not, so a `create_cmd` that reached for
|
|
2283
|
+
`docker compose` ran against the lane's own compose file with no project name
|
|
2284
|
+
and no override — booting a stack on the project's fixed host ports, which is
|
|
2285
|
+
the collision the override exists to prevent, caused by the step that sets a
|
|
2286
|
+
lane up.
|
|
2287
|
+
|
|
2288
|
+
- **`vinta-ai-maestro with` waits for the lock instead of giving up on it.** Its
|
|
2289
|
+
usage said it blocked until the resource was granted, and it did — for about
|
|
2290
|
+
five minutes, which is where Node's own `fetch` stops waiting for a response.
|
|
2291
|
+
A test suite behind a capacity-1 semaphore beats that regularly. What the
|
|
2292
|
+
agent saw was "could not reach the lease daemon", and what agents did with
|
|
2293
|
+
that, repeatedly, was run the command without a lease: exactly the stampede
|
|
2294
|
+
the pool exists to prevent, arrived at by an agent behaving reasonably.
|
|
2295
|
+
|
|
2296
|
+
The wait is the client's now. The daemon answers `202 still queued` within
|
|
2297
|
+
seconds and **leaves its own queue behind it**, so nothing is granted to a
|
|
2298
|
+
request that has gone away; the client loops until it is granted, saying so
|
|
2299
|
+
once and then occasionally, so a transcript shows a wait rather than a
|
|
2300
|
+
silence. Only answers that waiting cannot change — a resource this run does
|
|
2301
|
+
not declare, a run that is no longer live — end it.
|
|
2302
|
+
|
|
2303
|
+
And when it does end that way, the refusal says what not to do about it.
|
|
2304
|
+
"The resource lease was not granted" is an error with no alternative in it;
|
|
2305
|
+
the rule against working around it now travels with the message rather than
|
|
2306
|
+
sitting only in a prompt the agent read twenty minutes earlier. The prompt
|
|
2307
|
+
says it too, including that waiting is the expected outcome and not a failure.
|
|
2308
|
+
|
|
2309
|
+
- **A retry gets a fresh fix budget.** `fix_rounds` was set to 0 when a node was
|
|
2310
|
+
created and never again, so a phase that spent its whole budget on attempt 1
|
|
2311
|
+
began attempt 2 already exhausted — one review, one fix, and the exhaustion
|
|
2312
|
+
transition fired straight back. Seen with `max_fix_rounds: 4`: the retry got a
|
|
2313
|
+
single round before the operator was asked again, forty seconds of work
|
|
2314
|
+
standing in for four rounds of it. A retry that inherits the reason the last
|
|
2315
|
+
attempt ran out is not a retry. It resets wherever a node is re-driven from
|
|
2316
|
+
its pipeline's initial state — a capacity refusal, an automatic retry, the
|
|
2317
|
+
operator's answer, the `retry` verb — which is one place rather than four.
|
|
2318
|
+
|
|
2319
|
+
- **The last fix is reviewed rather than failed unread.** `standard-phase`
|
|
2320
|
+
checked the budget on the way *out* of `fix`, so the final fixer's work went
|
|
2321
|
+
straight to `failed` with nothing looking at it. Two phases in one run ended
|
|
2322
|
+
on a fixer reporting "all gates green, committed, tree clean" and were failed
|
|
2323
|
+
anyway. The check now sits in front of the fixer, on both doors into it: every
|
|
2324
|
+
fixer that runs is reviewed, and the review after the last one can still pass
|
|
2325
|
+
the phase. Guarding only the review side left a red gate under a passing
|
|
2326
|
+
reviewer looping forever, which is the other half of the same change.
|
|
2327
|
+
|
|
2328
|
+
`max_fix_rounds` still means the number of fixers a phase may spend. `0` now
|
|
2329
|
+
means none, where before it let one run and failed the phase regardless.
|
|
2330
|
+
|
|
2331
|
+
- **Agents are told not to background their work.** An implementer started five
|
|
2332
|
+
commands with `run_in_background: true` and closed its turn with "I'll wait for
|
|
2333
|
+
the test result notification before continuing to the outer gate". There is no
|
|
2334
|
+
notification: a headless session ends when the turn ends and whatever it
|
|
2335
|
+
backgrounded is killed with it. Three sessions ended with no report and no
|
|
2336
|
+
commit, and the reviewer failed each for uncommitted work — true, and not the
|
|
2337
|
+
cause. Nothing had said so, and believing a tool that offers backgrounding will
|
|
2338
|
+
still be there afterwards is not unreasonable.
|
|
2339
|
+
|
|
2340
|
+
- **The reviewer runs the gates instead of taking their word for it.** The prompt
|
|
2341
|
+
asked it to confirm the outer gate was green, which an agent can do by reading
|
|
2342
|
+
the implementer's report — one reused reviewer session reached a verdict in
|
|
2343
|
+
under four minutes without running the project's tests at all. It is now asked
|
|
2344
|
+
to run them, to say what they returned, and to treat being unable to run them
|
|
2345
|
+
as a finding. Re-review rounds are asked again, because a session that
|
|
2346
|
+
remembers running them last round is remembering a different tree.
|
|
2347
|
+
|
|
2348
|
+
- **A retried phase keeps the commits the attempt before it made.** A failed
|
|
2349
|
+
phase re-enters its pipeline at the initial state, which runs `git_branch`
|
|
2350
|
+
again — and `checkout -B` moves an existing branch unconditionally, so attempt
|
|
2351
|
+
2 began by resetting attempt 1's work to base. The files left the worktree,
|
|
2352
|
+
the next reviewer correctly reported "phase not implemented at all", and the
|
|
2353
|
+
fix budget burned re-implementing from nothing until the operator was asked. A
|
|
2354
|
+
phase that produced real commits was guaranteed to lose them.
|
|
2355
|
+
|
|
2356
|
+
"Cold" was always meant to describe the agent's *session*, not its branch —
|
|
2357
|
+
and the default policy targets environmental failures, which is exactly when
|
|
2358
|
+
the previous attempt's code is worth keeping. A resumed branch is checked out,
|
|
2359
|
+
never rebased onto a base that moved: a conflict there would fail the retry
|
|
2360
|
+
during its setup, before the agent that might resolve it has run. A branch
|
|
2361
|
+
that committed nothing is still moved up. Whether this is a second attempt is
|
|
2362
|
+
read from the journal rather than from the ref, because a phase branch's name
|
|
2363
|
+
carries the plan id and not the run id — an identically named branch left by
|
|
2364
|
+
an earlier run must still be cut fresh. `node_assigned` now records
|
|
2365
|
+
`previous_head`, so a reset that does happen is visible in the run rather than
|
|
2366
|
+
only in a reflog.
|
|
2367
|
+
|
|
2368
|
+
- **A phase is judged on commits, and the prompts finally say so.** An
|
|
2369
|
+
implementer ran four sessions, reported `SUCCESS` with a green inner loop, and
|
|
2370
|
+
never ran `git commit`: its deliverables were untracked files. The reviewer
|
|
2371
|
+
reads the committed diff, so it saw an empty one and reported "not implemented
|
|
2372
|
+
at all"; the fixer re-implemented, also without committing; the rounds ran out
|
|
2373
|
+
and the lane was recycled over the files. Every instruction that agent had was
|
|
2374
|
+
*about* committing — "never commit while a gate is red" — and none of them
|
|
2375
|
+
said it had to.
|
|
2376
|
+
|
|
2377
|
+
Implementers and fixers, cold and continued, are now told that the phase is
|
|
2378
|
+
reviewed and merged from commits on its branch, that an uncommitted file does
|
|
2379
|
+
not exist as far as the run is concerned, and that the turn is not over until
|
|
2380
|
+
`git status --porcelain` is empty of their work — staged by explicit path,
|
|
2381
|
+
never `git add -A`, because a lane holds local files that are not theirs to
|
|
2382
|
+
commit. The reviewer reads the working tree as well as the diff: a full tree
|
|
2383
|
+
with an empty diff is a real failure and is *not* "not implemented", and the
|
|
2384
|
+
difference decides whether the fixer writes the code again or simply commits
|
|
2385
|
+
it.
|
|
2386
|
+
|
|
2387
|
+
- **A recycled lane no longer deletes work nobody committed.** Both recycle
|
|
2388
|
+
paths throw the working tree away. The tree is now committed first — with
|
|
2389
|
+
plumbing, and **off the branch**, under `refs/vinta-ai-maestro/wip/`. On the
|
|
2390
|
+
branch was the first implementation, and it was wrong: a lane's dirty tree
|
|
2391
|
+
holds gate artifacts as often as deliverables, and a phase branch is the base
|
|
2392
|
+
of its dependents, so one gate's scratch file reached the next phase and
|
|
2393
|
+
failed a gate for a reason nothing in that phase caused. Nothing merges,
|
|
2394
|
+
nothing reaches a dependent, and one `git restore --source <ref>` gets a file
|
|
2395
|
+
back. Where the rescue itself fails, the recycle refuses and names the paths.
|
|
2396
|
+
|
|
2397
|
+
The retry question says how much is at stake: commits are kept, and *N*
|
|
2398
|
+
uncommitted files in the lane will be set aside.
|
|
2399
|
+
|
|
2400
|
+
- **`project.hooks: skip`.** Committing is not optional any more, and a
|
|
2401
|
+
`language: system` pre-commit chain reads a never-committed-in worktree as a
|
|
2402
|
+
fresh machine — one project's hooks built a 510 MB virtualenv before allowing
|
|
2403
|
+
a first commit, per lane, after four attempts and a two-minute timeout. Set
|
|
2404
|
+
per worktree, so the operator's own checkout keeps its hooks, and opt-in,
|
|
2405
|
+
because hooks are usually there for a reason.
|
|
2406
|
+
|
|
2407
|
+
- **`doctor` knows when a project needs docker compose.** It assembled its
|
|
2408
|
+
options without a `project`, so `needsCompose` was handed `undefined` on every
|
|
2409
|
+
invocation and reported "not required by this project" for a
|
|
2410
|
+
compose-delivered database — and `run`'s own preflight did the same. A check
|
|
2411
|
+
nothing can reach is worse than an absent one: it reads as a pass.
|
|
2412
|
+
|
|
2413
|
+
- **A failing `setup_cmd` says why.** It carried a lane name and nothing else,
|
|
2414
|
+
so the operator saw "could not provision the lane pool under …" and had to
|
|
2415
|
+
replay the command by hand with the lane environment rebuilt to find a missing
|
|
2416
|
+
settings module. It carries the exit code and a bounded stderr tail now — the
|
|
2417
|
+
same class of thing as a harness refusal's own explanation, which this package
|
|
2418
|
+
already decided to carry after an afternoon lost to the same silence.
|
|
2419
|
+
|
|
2420
|
+
- **A lane the executor was not given is refused, not guessed.** It fell back to
|
|
2421
|
+
the right directory with an *empty environment* — a gate running against the
|
|
2422
|
+
wrong compose project and no forked connection string, with nothing anywhere
|
|
2423
|
+
saying a lane was missing.
|
|
2424
|
+
|
|
2425
|
+
- **The monitor reads, and does not run.** Its shell has no lane environment, so
|
|
2426
|
+
the project's own commands run from it contend with the lanes instead of
|
|
2427
|
+
observing them. One reported "the final gate fails on a port conflict" when no
|
|
2428
|
+
gate had run at all, and the operator believed it. Gate results come from the
|
|
2429
|
+
journal and the gate logs.
|
|
2430
|
+
|
|
2431
|
+
- **`serve --host 0.0.0.0` prints a URL another machine can open.** Binding
|
|
2432
|
+
every interface made the server report the wildcard back, so the line printed
|
|
2433
|
+
for the operator to open — and share — was `http://0.0.0.0:<port>`, which
|
|
2434
|
+
resolves for nobody. The bind is unchanged; the printed host is now this
|
|
2435
|
+
machine's LAN address. The daemon still warns, because the token in that URL
|
|
2436
|
+
is the only thing between the run and anyone who can reach the port.
|
|
2437
|
+
|
|
2438
|
+
- **An agent can run a shell command.** `auto` mapped to the vendor's
|
|
2439
|
+
`acceptEdits`, which accepts file *edits* and nothing else — a shell command
|
|
2440
|
+
still goes to a permission prompt, and in `-p` there is nobody to answer one.
|
|
2441
|
+
A run of eight phases died of it: the agents wrote their code and were then
|
|
2442
|
+
refused `ruff`, `pytest`, `git add` and `docker compose`, **69 denials across
|
|
2443
|
+
two lanes**, every gate failing on work that was never allowed to be checked.
|
|
2444
|
+
`auto` now allows `Bash` in the policy file it already writes.
|
|
2445
|
+
|
|
2446
|
+
What that costs is worth stating plainly: **`Bash` was never confined to the
|
|
2447
|
+
lane.** The deny list covers the file-editing tools and a shell redirection
|
|
2448
|
+
walks through it, so allowing the shell allows commands on the machine.
|
|
2449
|
+
`auto` has always promised exactly this in words — "the agent works unattended
|
|
2450
|
+
inside its lane" — and this is the first version where the words are true.
|
|
2451
|
+
`ask` still does not allow it: that mode exists for a human at the browser,
|
|
2452
|
+
and the prompt is its purpose.
|
|
2453
|
+
|
|
2454
|
+
- **A refusal says why, in the words the harness used.** `permission_denied`
|
|
2455
|
+
carried a decision token and deliberately dropped the sentence beside it, on
|
|
2456
|
+
§11 grounds. Then a run failed with forty-five denials reading
|
|
2457
|
+
`reason: "other"`, and finding out what that meant cost an afternoon and a
|
|
2458
|
+
rebuilt reproduction — the sentence said "this Bash command contains multiple
|
|
2459
|
+
operations; the following parts require approval". §11 keeps repository
|
|
2460
|
+
*contents* out of the record; a refusal's own explanation is not contents, and
|
|
2461
|
+
the `tool_use` row above it already carries the path and the command verbatim.
|
|
2462
|
+
Withholding it protected nothing and hid the one fact worth having.
|
|
2463
|
+
|
|
2464
|
+
- **`serve` opens a run that has already finished.** The API resolved a run from
|
|
2465
|
+
the in-memory registry *and* the journal and required both, so a daemon
|
|
2466
|
+
started with nothing running listed the operator's history — the list reads
|
|
2467
|
+
the journal — and then answered 404 for every run in it, under a message
|
|
2468
|
+
saying the daemon was not running. Reads now need only the journal, which is
|
|
2469
|
+
where a run's whole record lives and which outlives the daemon by design
|
|
2470
|
+
(§5.3). Steering a finished run is still refused, with `409` rather than
|
|
2471
|
+
`404`: the run exists, and saying it does not sends the operator hunting for a
|
|
2472
|
+
typo instead of reading the status in front of them.
|
|
2473
|
+
|
|
2474
|
+
- **Four places the UI asked the operator to work around it.** The transcript
|
|
2475
|
+
opened at the *oldest* row it held, so a live agent's newest line — the one
|
|
2476
|
+
the panel was opened to read — sat below the fold and every arriving entry
|
|
2477
|
+
pushed it further down; it now opens at the newest and stays there, but only
|
|
2478
|
+
while the operator is already at the bottom, because scrolling up is reading.
|
|
2479
|
+
The `git diff` command overflowed its panel, uncopyable without selecting
|
|
2480
|
+
blind, and now wraps. The shell's bounded column went from 1280px to 1600px:
|
|
2481
|
+
1280 is a reading measure, and the Nodes table's columns are ids, lane names
|
|
2482
|
+
and branch names, none of which can be abbreviated without losing what
|
|
2483
|
+
identifies them. And the notification interval said "Remind every 5 min"
|
|
2484
|
+
without ever saying what it would remind anyone about — it repeats the
|
|
2485
|
+
notification for a phase that stopped to ask the operator something, until
|
|
2486
|
+
they answer, which is what the options say now.
|
|
2487
|
+
|
|
2488
|
+
- **A killed terminal releases its directory on Windows.** Two PTY tests failed
|
|
2489
|
+
teardown with `EBUSY: rmdir`: killing a process there is not synchronous with
|
|
2490
|
+
releasing what it held, so the conpty and its background child are gone as far
|
|
2491
|
+
as the test is concerned while the OS still has the working directory open.
|
|
2492
|
+
|
|
2493
|
+
- **A failed phase can be retried instead of ending the run.** `run` takes
|
|
2494
|
+
`--on-failure <stop|ask>`. `stop` is the default and is what every run has
|
|
2495
|
+
always done — the phase fails, its dependents block, the run finishes. `ask`
|
|
2496
|
+
parks the node on a question instead: retry, retry with another member of the
|
|
2497
|
+
crew, or stop.
|
|
2498
|
+
|
|
2499
|
+
It exists because the failures worth retrying are overwhelmingly
|
|
2500
|
+
environmental — a permission wall, a grant nobody made, a gate whose service
|
|
2501
|
+
was not up — and the only recovery was to start the whole plan again, running
|
|
2502
|
+
every phase that had already succeeded a second time. Only pass `ask` when
|
|
2503
|
+
somebody is watching: the run waits, which is exactly why it is not the
|
|
2504
|
+
default.
|
|
2505
|
+
|
|
2506
|
+
The alternatives offered are **members of the crew, not models**. A staffed
|
|
2507
|
+
run has no free-floating models in it — a phase is taken by a member, and the
|
|
2508
|
+
tier floor that decides who may take it is the assigned member's own — so a
|
|
2509
|
+
model is something the plan cannot express and nobody could be found to hold.
|
|
2510
|
+
A member below the phase's tier is never offered: an operator answering a
|
|
2511
|
+
question is not a reason to hand a phase to somebody the plan judged too
|
|
2512
|
+
junior for it.
|
|
2513
|
+
|
|
2514
|
+
The retry starts **cold**, and everything the node held goes back before it
|
|
2515
|
+
waits (§6.1) — an operator at lunch must not be holding a lane another phase
|
|
2516
|
+
could use.
|
|
2517
|
+
|
|
2518
|
+
- **A refusal nobody was asked about is now said out loud.** The adapter knew
|
|
2519
|
+
about `permission_request`, a question waiting for an answer, and nothing
|
|
2520
|
+
else. The refusal an operator actually hits is not a question: a read outside
|
|
2521
|
+
the working directory is decided by the CLI and announced as
|
|
2522
|
+
`permission_denied`, with no request to reply to. That frame was dropped at
|
|
2523
|
+
the first `switch` — no event, no journal row, no transcript line. The new
|
|
2524
|
+
`permission_denied` event carries the decision token (`workingDir`, `mode`)
|
|
2525
|
+
and never the vendor's prose, which names the file being read (§11).
|
|
2526
|
+
|
|
2527
|
+
- **A turn that ended blocked is no longer a turn that succeeded.** The CLI
|
|
2528
|
+
says `status_category: "blocked"` one frame before reporting
|
|
2529
|
+
`is_error: false`, and both are true from its side: it was asked for
|
|
2530
|
+
something it could not do and said so. Taking the second at its word is how a
|
|
2531
|
+
phase passed having written no code and then failed two steps later under
|
|
2532
|
+
another name. A session that reported an error no longer ends `ok` — which
|
|
2533
|
+
also makes a reviewer fail closed, since its verdict is read back out of a
|
|
2534
|
+
transcript whose session did not end cleanly.
|
|
2535
|
+
|
|
2536
|
+
- **An agent still could not write in its own lane.** The entry below passed
|
|
2537
|
+
claude-code `--permission-mode auto`, which matched our own vocabulary for
|
|
2538
|
+
"works unattended" and is not what the vendor means by the word: `auto` still
|
|
2539
|
+
routes a write to a permission prompt, and in `-p` there is nobody to answer
|
|
2540
|
+
one. Run against the CLI, a `Write` to the agent's *own working directory*
|
|
2541
|
+
comes back denied with no reason attached — the same dead end, wearing the
|
|
2542
|
+
word that made it look closed. `acceptEdits` is the mode under which that
|
|
2543
|
+
write succeeds, and is what `auto` now maps to. The test covering this
|
|
2544
|
+
asserted the word rather than the behaviour, so it pinned the bug in place.
|
|
2545
|
+
|
|
2546
|
+
- **A phase can read the operator's checkout.** A lane is a worktree cut from a
|
|
2547
|
+
branch, so anything uncommitted — a plan written this morning, a spec that
|
|
2548
|
+
never leaves the operator's machine — is present where they are and absent
|
|
2549
|
+
from every lane. Reaching for it was refused before the model saw a byte, with
|
|
2550
|
+
a message that reads like a prompt awaiting an answer ("Claude requested
|
|
2551
|
+
permissions to read from …, but you haven't granted it yet") when in fact
|
|
2552
|
+
nothing is asking: the CLI denies it outright, emits no permission request,
|
|
2553
|
+
and the session then ends *successfully*, having written nothing.
|
|
2554
|
+
|
|
2555
|
+
The repository root is now granted to every claude-code agent for **reading**.
|
|
2556
|
+
Granting a directory also grants writing in it, and the lane sits inside the
|
|
2557
|
+
directory being granted, so the grant is paired with a deny list that keeps
|
|
2558
|
+
the lane the only writable place under it — the operator's source and every
|
|
2559
|
+
sibling lane stay refused. That list names the *siblings* at each level down
|
|
2560
|
+
to the lane rather than the root itself, because the vendor resolves deny
|
|
2561
|
+
before allow with no carve-out: `deny: <repo>/**` plus `allow: <lane>/**`
|
|
2562
|
+
refuses the lane too. Each of those facts was established by running the CLI.
|
|
2563
|
+
|
|
2564
|
+
**This covers the file-editing tools and not `Bash`.** A shell redirection was
|
|
2565
|
+
never confined to the working directory and is not confined now; that boundary
|
|
2566
|
+
is the OS's, and the vendor's sandbox that enforces it also switches off the
|
|
2567
|
+
network a phase needs to install anything.
|
|
2568
|
+
|
|
2569
|
+
codex needed nothing: under `--approve-for-me` it already reads anywhere on
|
|
2570
|
+
disk, which was confirmed the same way.
|
|
2571
|
+
|
|
2572
|
+
- **An agent can write in its own lane.** Every adapter declared
|
|
2573
|
+
`permissionControl: true` and passed no policy at all: claude-code was spawned
|
|
2574
|
+
as `-p --output-format stream-json …` with no `--permission-mode`, and codex as
|
|
2575
|
+
`exec --json` with no sandbox and no approval flag, under a comment saying it
|
|
2576
|
+
took both on the command line. So a headless run asked before its first write,
|
|
2577
|
+
emitted a `permission_request`, rendered it in the transcript — and nothing
|
|
2578
|
+
anywhere answered. The phase failed reporting a permission system it could not
|
|
2579
|
+
see, having written nothing.
|
|
2580
|
+
|
|
2581
|
+
`run` and `serve` now take `--permission <ask|auto|full>`, defaulting to
|
|
2582
|
+
`auto`. **The operator sets it, never the workflow document**: the document is
|
|
2583
|
+
committed and shared, and a file in a repository should not be able to tell
|
|
2584
|
+
someone else's machine to run agents without approvals.
|
|
2585
|
+
|
|
2586
|
+
Both CLIs refuse combinations that looked reasonable, and both were found by
|
|
2587
|
+
running them rather than reading them: codex rejects `--sandbox` alongside
|
|
2588
|
+
`--approve-for-me`, and `codex exec resume` accepts neither — passing the
|
|
2589
|
+
fresh-spawn policy there turned a `stale_session` refusal, which the scheduler
|
|
2590
|
+
retries cold, into a `fatal` one that fails the node.
|
|
2591
|
+
|
|
2592
|
+
This reverses a documented decision. The README said the daemon passed no
|
|
2593
|
+
permission flags and that a committed `.claude/settings.json` was what made a
|
|
2594
|
+
run able to write; that is now the narrowing layer on top of a mode, not the
|
|
2595
|
+
only thing standing between an agent and a blocked lane.
|
|
2596
|
+
- **`purge --lanes --branches` clears what a failed run leaves behind.** A run's
|
|
2597
|
+
lane worktrees are not under `runs/`, so no purge ever reached them: three
|
|
2598
|
+
failed attempts at one plan left twelve worktrees, twelve summaries, and a set
|
|
2599
|
+
of `plan/<id>/phase-*` branches still checked out — which is what makes the
|
|
2600
|
+
*next* run of that plan impossible, since git refuses to check out a branch a
|
|
2601
|
+
second worktree holds.
|
|
2602
|
+
|
|
2603
|
+
One rule decides both: **never delete the only copy of work.** A phase can
|
|
2604
|
+
leave work committed on its branch or uncommitted in its worktree, and both
|
|
2605
|
+
count — so a branch carrying commits no other branch has is kept, a lane with
|
|
2606
|
+
a dirty tree is kept, and each is listed with the command to remove it by
|
|
2607
|
+
hand. Untracked files count as work: a phase that wrote three new modules and
|
|
2608
|
+
never committed them is exactly the case worth protecting.
|
|
2609
|
+
|
|
2610
|
+
Emptiness is measured as "carries no commit another branch does not already
|
|
2611
|
+
have", not as "merged into HEAD". The latter is relative to wherever the
|
|
2612
|
+
operator is standing, so on a feature branch — the checkout this is most often
|
|
2613
|
+
run from — every empty phase branch looked unmerged and nothing was ever
|
|
2614
|
+
cleaned up.
|
|
2615
|
+
|
|
2616
|
+
`--dry-run` and `--yes` work as they already did, and a worktree that will not
|
|
2617
|
+
go is named rather than counted.
|
|
2618
|
+
|
|
2619
|
+
- **`doctor` now catches, before a lane exists, the failures that used to take a
|
|
2620
|
+
run each.** Four consecutive runs of one plan failed four different ways, and
|
|
2621
|
+
every one of them was knowable at minute zero from the workflow and the repo.
|
|
2622
|
+
- **Every `prompt_ref` and `plan_context_ref` is resolved against
|
|
2623
|
+
`base_branch`**, with `git cat-file -e`, which reads the branch's tree and
|
|
2624
|
+
so cannot be satisfied by a working-tree copy — the working tree being
|
|
2625
|
+
exactly what a lane will not have. It distinguishes the three cases, because
|
|
2626
|
+
they have three different fixes: never committed, **staged but never
|
|
2627
|
+
committed** (`git add` alone does not put a file in a branch), and committed
|
|
2628
|
+
on a different branch. The last offers both routes out — merge it, or point
|
|
2629
|
+
`base_branch` at the branch that has it — since only the operator knows
|
|
2630
|
+
which they want.
|
|
2631
|
+
- **Phase branches held by another worktree** are reported with the directory
|
|
2632
|
+
to remove. Lane directories are named per run and phase branches per
|
|
2633
|
+
workflow, so a failed run leaves worktrees holding the branch names the next
|
|
2634
|
+
run will try to cut, and git refuses to check out a branch twice. Nothing
|
|
2635
|
+
cleans those up: `purge` deletes run state under `.vinta-ai-maestro/runs/`,
|
|
2636
|
+
and lane worktrees are not there. `doctor` does not delete them either — a
|
|
2637
|
+
failed lane's worktree is the only place its state survives — but it now
|
|
2638
|
+
names each one and the exact command.
|
|
2639
|
+
|
|
2640
|
+
- **A failed node records why.** `node_status` carried `{"status":"failed"}` and
|
|
2641
|
+
nothing else, so two runs failing for two unrelated reasons produced identical
|
|
2642
|
+
journal rows and the cause survived only in the operator's terminal. The
|
|
2643
|
+
reason is now persisted — **sanitized**, not raw: errors the package builds
|
|
2644
|
+
itself are identifiers by construction and are kept verbatim, and anything
|
|
2645
|
+
else is reduced to its kind, the rule `recycleStage` already followed. The
|
|
2646
|
+
journal is durable and API-served, and an exception message from a dependency
|
|
2647
|
+
is exactly how repository content gets into one. Blocked dependents record
|
|
2648
|
+
which node blocked them.
|
|
2649
|
+
|
|
221
2650
|
- **Two diagnostics that named the symptom and hid the cause.** Both came out of
|
|
222
2651
|
one real run, and neither was a wrong answer — just an unusable one.
|
|
223
2652
|
- **A phase whose plan is not committed** failed with `prompt_ref "…" names no
|
|
@@ -295,6 +2724,19 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
295
2724
|
nothing produces a merge commit full of markers that passes into the wave
|
|
296
2725
|
branch unnoticed.
|
|
297
2726
|
|
|
2727
|
+
### Security
|
|
2728
|
+
|
|
2729
|
+
- **Maestro: lane agents hold a token of their own.** A run's agents used to
|
|
2730
|
+
reach its API with the operator's token (`VINTA_AI_MAESTRO_TOKEN`), so an
|
|
2731
|
+
agent could steer, abort, amend, `exec` in, pause or stop its own run, or
|
|
2732
|
+
save a workflow from the editor, as the operator. They now get an agent
|
|
2733
|
+
token that reaches only their own leases (`with`), `gate`, and the
|
|
2734
|
+
permission-judge and gate-guard hooks; every other route answers
|
|
2735
|
+
`403 agent_forbidden`. It does not open the WebSocket (and so no PTY
|
|
2736
|
+
takeover), `ui` never forwards it to a run's job, and it is redacted from
|
|
2737
|
+
the logs like the other tokens. Nothing to migrate: lanes are handed the new
|
|
2738
|
+
token when a run starts or resumes.
|
|
2739
|
+
|
|
298
2740
|
> The repository also gained `packages/vinta-ai-maestro/`, the workspace package
|
|
299
2741
|
> that executes these workflows. It is published as its own npm package,
|
|
300
2742
|
> `vinta-ai-maestro`, carrying the same version as `vinta-ai-workflows`, and is
|
|
@@ -676,7 +3118,6 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
676
3118
|
**Consumers**: re-sync to pick up the opt-in questions. E2E-disabled projects
|
|
677
3119
|
are unaffected (the e2e regions strip out as before).
|
|
678
3120
|
|
|
679
|
-
|
|
680
3121
|
<!-- pre-release: 0.2.0-alpha6 on 2026-07-13 -->
|
|
681
3122
|
|
|
682
3123
|
### Changed
|
|
@@ -713,7 +3154,6 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
713
3154
|
|
|
714
3155
|
### Added
|
|
715
3156
|
|
|
716
|
-
|
|
717
3157
|
- **`handoff` foundation skill — session-continuation handoff docs between
|
|
718
3158
|
agents.** Write mode captures the current task (goal, verified-vs-unverified
|
|
719
3159
|
state, decisions + rejected alternatives, landmines, single concrete next
|