vinta-ai-workflows 0.7.0-alpha1 → 0.7.0-alpha11

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 CHANGED
@@ -7,10 +7,115 @@ 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-alpha11 on 2026-09-15 -->
11
+ <!-- pre-release: 0.7.0-alpha10 on 2026-09-15 -->
12
+ <!-- pre-release: 0.7.0-alpha9 on 2026-09-14 -->
13
+ <!-- pre-release: 0.7.0-alpha8 on 2026-09-14 -->
14
+ <!-- pre-release: 0.7.0-alpha7 on 2026-09-14 -->
15
+ <!-- pre-release: 0.7.0-alpha6 on 2026-09-13 -->
16
+ <!-- pre-release: 0.7.0-alpha5 on 2026-09-13 -->
17
+ <!-- pre-release: 0.7.0-alpha4 on 2026-09-13 -->
18
+ <!-- pre-release: 0.7.0-alpha3 on 2026-09-12 -->
19
+ <!-- pre-release: 0.7.0-alpha2 on 2026-09-12 -->
10
20
  <!-- pre-release: 0.7.0-alpha1 on 2026-09-11 -->
11
21
 
12
22
  ### Added
13
23
 
24
+ - **A project declares what makes a lane runnable.** `project.env_files` copies
25
+ ignored configuration into each worktree, `project.setup_cmd` runs an
26
+ idempotent setup hook on provisioning and recycle, and the fixed
27
+ `project.commands` vocabulary tells implementers, reviewers and fixers the
28
+ project's real lint, typecheck, test and migration entry points.
29
+
30
+ - **Compose-backed lanes are isolated past `COMPOSE_PROJECT_NAME`.** The daemon
31
+ now generates an out-of-tree Compose override per lane, strips fixed host
32
+ ports, re-pins fixed and external volumes, and carries the complete lane
33
+ environment into agent and takeover processes as well as gates. Explicitly
34
+ published services receive probed per-lane ports recorded in the teardown
35
+ summary.
36
+
37
+ - **Shared services get one namespace per lane.** `project.services` models a
38
+ fixed index or lane-derived name inside one Redis, RabbitMQ, object-storage,
39
+ or similar server, with project-owned create/reset commands and capacity
40
+ validation before provisioning creates templates or worktrees.
41
+
42
+ - **Agents can take the semaphore a workflow declares.**
43
+ `vinta-ai-maestro with <resource> -- <cmd>` waits on the run's existing
44
+ `ResourcePools`, runs the heavy inner-loop command, and releases on exit.
45
+ Renewable leases expire when their client disappears, are visible in the
46
+ live journal, and are surfaced in implementer, reviewer and fixer prompts.
47
+
48
+ - **A failed phase retries itself, and then asks.** `--on-failure` defaults to
49
+ `retry`: one cold re-attempt (`--retries <n>`, 0–5), and then the node parks
50
+ on a question — retry, retry with another member of the crew, or stop. The
51
+ failures this system produces are overwhelmingly environmental and are gone by
52
+ the second attempt; the ones that survive it are the ones worth a person.
53
+ `ask` skips the automatic attempt. `stop` is the old behaviour and remains the
54
+ right choice for CI, because everything else eventually **waits**.
55
+
56
+ A failure that survives its retries says so: the reason carries
57
+ `(after N attempts)`, which no event otherwise records.
58
+
59
+ - **A failed phase can be retried by hand.** A **Retry phase** button, and
60
+ `POST /api/runs/:runId/nodes/:nodeId/retry`. It returns the node to the ready
61
+ set and unblocks the subtree its failure blocked — a phase that died of
62
+ something environmental holds up work that was never broken. It needs the run
63
+ to still be in flight, which with the new default it usually is; a run that
64
+ has genuinely ended is re-run, not retried, and says so rather than doing
65
+ nothing.
66
+
67
+ - **The monitor can go and look.** It was given a digest and nothing else, and
68
+ read exactly as thin as that sounds — it could say a phase had failed and
69
+ never say what the phase had written. It is briefed as a technical project
70
+ manager now, with the map: the plan document, each phase's brief, each lane's
71
+ worktree path, each branch and its base, and where the journal and transcripts
72
+ live. It has a shell and its working directory is the repository, so it can
73
+ read the diff before drawing a conclusion. The digest stays as the index —
74
+ bounded, cheap, enough to know which phase is worth a closer look.
75
+
76
+ - **The monitor conversation is kept.** It lived in component state, so a reload
77
+ threw away every question and every answer — a worse record than the run it
78
+ describes. It is written to the transcript store under a reserved node id and
79
+ read back on load, so it outlives the tab, the daemon and the run.
80
+
81
+ - **Panels expand.** Anything holding content that is only nominally
82
+ summarisable — an agent's transcript, a gate log, the monitor conversation,
83
+ the steering box — takes the full container width and about a screen of
84
+ height on demand. The height travels as a CSS variable, because the element
85
+ that has to grow is a scroller several components below the panel.
86
+
87
+ - **A run has a spokesperson you can ask.** `serve` grows a **Monitor** panel on
88
+ the run view: one agent that reads the journal and answers in prose — what is
89
+ blocked, why a phase failed, what it would take to move on. It runs on the
90
+ dearest tier on the crew, because it is read by a person deciding what to do
91
+ about a failing run, and it is asked a handful of times rather than hundreds.
92
+
93
+ Three things it deliberately is not.
94
+
95
+ **It is not in the permission path.** The obvious thought, once an agent has
96
+ been refused something, is to put a smarter agent in front of the refusals —
97
+ but a phase makes hundreds of tool calls (one node in one real run made 179)
98
+ and a model turn before each is latency and cost spent on questions like "may
99
+ I run ruff", which should never have been questions. Permissions are settled
100
+ structurally, once per spawn, for free.
101
+
102
+ **It does not read the transcripts.** Piping every agent's output into a
103
+ second agent would make the monitor the most expensive thing in the run and
104
+ tie its cost to the work rather than to the questions. It reads a bounded
105
+ digest — statuses, failure reasons, pending questions, the last few refusals
106
+ with their sentences, one trimmed last word per phase — so a one-hour run and
107
+ a one-day run cost about the same to ask about.
108
+
109
+ **It has no authority.** It can explain a §9.1 pause; it cannot answer one.
110
+ An agent that could quietly approve its colleagues' work would turn a
111
+ checkpoint into a formality, and the checkpoint is the point.
112
+
113
+ It answers about **finished runs too**, which is when "why did this fail" is
114
+ usually asked, and it needs no lane: it writes nothing.
115
+
116
+ - **`serve` reads `--permission`.** The flag has been accepted and ignored since
117
+ it existed. It decides how the monitor is spawned, so it is read now.
118
+
14
119
  - **A plan staffs a team, instead of picking a model per phase.** Choosing a tier
15
120
  phase by phase answers "what runs this one" ten times and never adds it up, so the
16
121
  two questions that decide what a feature costs go unasked: how many agents does
@@ -216,6 +321,423 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
216
321
 
217
322
  ### Fixed
218
323
 
324
+ - **`vinta-ai-maestro with` waits for the lock instead of giving up on it.** Its
325
+ usage said it blocked until the resource was granted, and it did — for about
326
+ five minutes, which is where Node's own `fetch` stops waiting for a response.
327
+ A test suite behind a capacity-1 semaphore beats that regularly. What the
328
+ agent saw was "could not reach the lease daemon", and what agents did with
329
+ that, repeatedly, was run the command without a lease: exactly the stampede
330
+ the pool exists to prevent, arrived at by an agent behaving reasonably.
331
+
332
+ The wait is the client's now. The daemon answers `202 still queued` within
333
+ seconds and **leaves its own queue behind it**, so nothing is granted to a
334
+ request that has gone away; the client loops until it is granted, saying so
335
+ once and then occasionally, so a transcript shows a wait rather than a
336
+ silence. Only answers that waiting cannot change — a resource this run does
337
+ not declare, a run that is no longer live — end it.
338
+
339
+ And when it does end that way, the refusal says what not to do about it.
340
+ "The resource lease was not granted" is an error with no alternative in it;
341
+ the rule against working around it now travels with the message rather than
342
+ sitting only in a prompt the agent read twenty minutes earlier. The prompt
343
+ says it too, including that waiting is the expected outcome and not a failure.
344
+
345
+ - **A retry gets a fresh fix budget.** `fix_rounds` was set to 0 when a node was
346
+ created and never again, so a phase that spent its whole budget on attempt 1
347
+ began attempt 2 already exhausted — one review, one fix, and the exhaustion
348
+ transition fired straight back. Seen with `max_fix_rounds: 4`: the retry got a
349
+ single round before the operator was asked again, forty seconds of work
350
+ standing in for four rounds of it. A retry that inherits the reason the last
351
+ attempt ran out is not a retry. It resets wherever a node is re-driven from
352
+ its pipeline's initial state — a capacity refusal, an automatic retry, the
353
+ operator's answer, the `retry` verb — which is one place rather than four.
354
+
355
+ - **The last fix is reviewed rather than failed unread.** `standard-phase`
356
+ checked the budget on the way *out* of `fix`, so the final fixer's work went
357
+ straight to `failed` with nothing looking at it. Two phases in one run ended
358
+ on a fixer reporting "all gates green, committed, tree clean" and were failed
359
+ anyway. The check now sits in front of the fixer, on both doors into it: every
360
+ fixer that runs is reviewed, and the review after the last one can still pass
361
+ the phase. Guarding only the review side left a red gate under a passing
362
+ reviewer looping forever, which is the other half of the same change.
363
+
364
+ `max_fix_rounds` still means the number of fixers a phase may spend. `0` now
365
+ means none, where before it let one run and failed the phase regardless.
366
+
367
+ - **Agents are told not to background their work.** An implementer started five
368
+ commands with `run_in_background: true` and closed its turn with "I'll wait for
369
+ the test result notification before continuing to the outer gate". There is no
370
+ notification: a headless session ends when the turn ends and whatever it
371
+ backgrounded is killed with it. Three sessions ended with no report and no
372
+ commit, and the reviewer failed each for uncommitted work — true, and not the
373
+ cause. Nothing had said so, and believing a tool that offers backgrounding will
374
+ still be there afterwards is not unreasonable.
375
+
376
+ - **The reviewer runs the gates instead of taking their word for it.** The prompt
377
+ asked it to confirm the outer gate was green, which an agent can do by reading
378
+ the implementer's report — one reused reviewer session reached a verdict in
379
+ under four minutes without running the project's tests at all. It is now asked
380
+ to run them, to say what they returned, and to treat being unable to run them
381
+ as a finding. Re-review rounds are asked again, because a session that
382
+ remembers running them last round is remembering a different tree.
383
+
384
+ - **A retried phase keeps the commits the attempt before it made.** A failed
385
+ phase re-enters its pipeline at the initial state, which runs `git_branch`
386
+ again — and `checkout -B` moves an existing branch unconditionally, so attempt
387
+ 2 began by resetting attempt 1's work to base. The files left the worktree,
388
+ the next reviewer correctly reported "phase not implemented at all", and the
389
+ fix budget burned re-implementing from nothing until the operator was asked. A
390
+ phase that produced real commits was guaranteed to lose them.
391
+
392
+ "Cold" was always meant to describe the agent's *session*, not its branch —
393
+ and the default policy targets environmental failures, which is exactly when
394
+ the previous attempt's code is worth keeping. A resumed branch is checked out,
395
+ never rebased onto a base that moved: a conflict there would fail the retry
396
+ during its setup, before the agent that might resolve it has run. A branch
397
+ that committed nothing is still moved up. Whether this is a second attempt is
398
+ read from the journal rather than from the ref, because a phase branch's name
399
+ carries the plan id and not the run id — an identically named branch left by
400
+ an earlier run must still be cut fresh. `node_assigned` now records
401
+ `previous_head`, so a reset that does happen is visible in the run rather than
402
+ only in a reflog.
403
+
404
+ - **A phase is judged on commits, and the prompts finally say so.** An
405
+ implementer ran four sessions, reported `SUCCESS` with a green inner loop, and
406
+ never ran `git commit`: its deliverables were untracked files. The reviewer
407
+ reads the committed diff, so it saw an empty one and reported "not implemented
408
+ at all"; the fixer re-implemented, also without committing; the rounds ran out
409
+ and the lane was recycled over the files. Every instruction that agent had was
410
+ *about* committing — "never commit while a gate is red" — and none of them
411
+ said it had to.
412
+
413
+ Implementers and fixers, cold and continued, are now told that the phase is
414
+ reviewed and merged from commits on its branch, that an uncommitted file does
415
+ not exist as far as the run is concerned, and that the turn is not over until
416
+ `git status --porcelain` is empty of their work — staged by explicit path,
417
+ never `git add -A`, because a lane holds local files that are not theirs to
418
+ commit. The reviewer reads the working tree as well as the diff: a full tree
419
+ with an empty diff is a real failure and is *not* "not implemented", and the
420
+ difference decides whether the fixer writes the code again or simply commits
421
+ it.
422
+
423
+ - **A recycled lane no longer deletes work nobody committed.** Both recycle
424
+ paths throw the working tree away. The tree is now committed first — with
425
+ plumbing, and **off the branch**, under `refs/vinta-ai-maestro/wip/`. On the
426
+ branch was the first implementation, and it was wrong: a lane's dirty tree
427
+ holds gate artifacts as often as deliverables, and a phase branch is the base
428
+ of its dependents, so one gate's scratch file reached the next phase and
429
+ failed a gate for a reason nothing in that phase caused. Nothing merges,
430
+ nothing reaches a dependent, and one `git restore --source <ref>` gets a file
431
+ back. Where the rescue itself fails, the recycle refuses and names the paths.
432
+
433
+ The retry question says how much is at stake: commits are kept, and *N*
434
+ uncommitted files in the lane will be set aside.
435
+
436
+ - **`project.hooks: skip`.** Committing is not optional any more, and a
437
+ `language: system` pre-commit chain reads a never-committed-in worktree as a
438
+ fresh machine — one project's hooks built a 510 MB virtualenv before allowing
439
+ a first commit, per lane, after four attempts and a two-minute timeout. Set
440
+ per worktree, so the operator's own checkout keeps its hooks, and opt-in,
441
+ because hooks are usually there for a reason.
442
+
443
+ - **`doctor` knows when a project needs docker compose.** It assembled its
444
+ options without a `project`, so `needsCompose` was handed `undefined` on every
445
+ invocation and reported "not required by this project" for a
446
+ compose-delivered database — and `run`'s own preflight did the same. A check
447
+ nothing can reach is worse than an absent one: it reads as a pass.
448
+
449
+ - **A failing `setup_cmd` says why.** It carried a lane name and nothing else,
450
+ so the operator saw "could not provision the lane pool under …" and had to
451
+ replay the command by hand with the lane environment rebuilt to find a missing
452
+ settings module. It carries the exit code and a bounded stderr tail now — the
453
+ same class of thing as a harness refusal's own explanation, which this package
454
+ already decided to carry after an afternoon lost to the same silence.
455
+
456
+ - **A lane the executor was not given is refused, not guessed.** It fell back to
457
+ the right directory with an *empty environment* — a gate running against the
458
+ wrong compose project and no forked connection string, with nothing anywhere
459
+ saying a lane was missing.
460
+
461
+ - **The monitor reads, and does not run.** Its shell has no lane environment, so
462
+ the project's own commands run from it contend with the lanes instead of
463
+ observing them. One reported "the final gate fails on a port conflict" when no
464
+ gate had run at all, and the operator believed it. Gate results come from the
465
+ journal and the gate logs.
466
+
467
+ - **`serve --host 0.0.0.0` prints a URL another machine can open.** Binding
468
+ every interface made the server report the wildcard back, so the line printed
469
+ for the operator to open — and share — was `http://0.0.0.0:<port>`, which
470
+ resolves for nobody. The bind is unchanged; the printed host is now this
471
+ machine's LAN address. The daemon still warns, because the token in that URL
472
+ is the only thing between the run and anyone who can reach the port.
473
+
474
+ - **An agent can run a shell command.** `auto` mapped to the vendor's
475
+ `acceptEdits`, which accepts file *edits* and nothing else — a shell command
476
+ still goes to a permission prompt, and in `-p` there is nobody to answer one.
477
+ A run of eight phases died of it: the agents wrote their code and were then
478
+ refused `ruff`, `pytest`, `git add` and `docker compose`, **69 denials across
479
+ two lanes**, every gate failing on work that was never allowed to be checked.
480
+ `auto` now allows `Bash` in the policy file it already writes.
481
+
482
+ What that costs is worth stating plainly: **`Bash` was never confined to the
483
+ lane.** The deny list covers the file-editing tools and a shell redirection
484
+ walks through it, so allowing the shell allows commands on the machine.
485
+ `auto` has always promised exactly this in words — "the agent works unattended
486
+ inside its lane" — and this is the first version where the words are true.
487
+ `ask` still does not allow it: that mode exists for a human at the browser,
488
+ and the prompt is its purpose.
489
+
490
+ - **A refusal says why, in the words the harness used.** `permission_denied`
491
+ carried a decision token and deliberately dropped the sentence beside it, on
492
+ §11 grounds. Then a run failed with forty-five denials reading
493
+ `reason: "other"`, and finding out what that meant cost an afternoon and a
494
+ rebuilt reproduction — the sentence said "this Bash command contains multiple
495
+ operations; the following parts require approval". §11 keeps repository
496
+ *contents* out of the record; a refusal's own explanation is not contents, and
497
+ the `tool_use` row above it already carries the path and the command verbatim.
498
+ Withholding it protected nothing and hid the one fact worth having.
499
+
500
+ - **`serve` opens a run that has already finished.** The API resolved a run from
501
+ the in-memory registry *and* the journal and required both, so a daemon
502
+ started with nothing running listed the operator's history — the list reads
503
+ the journal — and then answered 404 for every run in it, under a message
504
+ saying the daemon was not running. Reads now need only the journal, which is
505
+ where a run's whole record lives and which outlives the daemon by design
506
+ (§5.3). Steering a finished run is still refused, with `409` rather than
507
+ `404`: the run exists, and saying it does not sends the operator hunting for a
508
+ typo instead of reading the status in front of them.
509
+
510
+ - **Four places the UI asked the operator to work around it.** The transcript
511
+ opened at the *oldest* row it held, so a live agent's newest line — the one
512
+ the panel was opened to read — sat below the fold and every arriving entry
513
+ pushed it further down; it now opens at the newest and stays there, but only
514
+ while the operator is already at the bottom, because scrolling up is reading.
515
+ The `git diff` command overflowed its panel, uncopyable without selecting
516
+ blind, and now wraps. The shell's bounded column went from 1280px to 1600px:
517
+ 1280 is a reading measure, and the Nodes table's columns are ids, lane names
518
+ and branch names, none of which can be abbreviated without losing what
519
+ identifies them. And the notification interval said "Remind every 5 min"
520
+ without ever saying what it would remind anyone about — it repeats the
521
+ notification for a phase that stopped to ask the operator something, until
522
+ they answer, which is what the options say now.
523
+
524
+ - **A killed terminal releases its directory on Windows.** Two PTY tests failed
525
+ teardown with `EBUSY: rmdir`: killing a process there is not synchronous with
526
+ releasing what it held, so the conpty and its background child are gone as far
527
+ as the test is concerned while the OS still has the working directory open.
528
+
529
+ - **A failed phase can be retried instead of ending the run.** `run` takes
530
+ `--on-failure <stop|ask>`. `stop` is the default and is what every run has
531
+ always done — the phase fails, its dependents block, the run finishes. `ask`
532
+ parks the node on a question instead: retry, retry with another member of the
533
+ crew, or stop.
534
+
535
+ It exists because the failures worth retrying are overwhelmingly
536
+ environmental — a permission wall, a grant nobody made, a gate whose service
537
+ was not up — and the only recovery was to start the whole plan again, running
538
+ every phase that had already succeeded a second time. Only pass `ask` when
539
+ somebody is watching: the run waits, which is exactly why it is not the
540
+ default.
541
+
542
+ The alternatives offered are **members of the crew, not models**. A staffed
543
+ run has no free-floating models in it — a phase is taken by a member, and the
544
+ tier floor that decides who may take it is the assigned member's own — so a
545
+ model is something the plan cannot express and nobody could be found to hold.
546
+ A member below the phase's tier is never offered: an operator answering a
547
+ question is not a reason to hand a phase to somebody the plan judged too
548
+ junior for it.
549
+
550
+ The retry starts **cold**, and everything the node held goes back before it
551
+ waits (§6.1) — an operator at lunch must not be holding a lane another phase
552
+ could use.
553
+
554
+ - **A refusal nobody was asked about is now said out loud.** The adapter knew
555
+ about `permission_request`, a question waiting for an answer, and nothing
556
+ else. The refusal an operator actually hits is not a question: a read outside
557
+ the working directory is decided by the CLI and announced as
558
+ `permission_denied`, with no request to reply to. That frame was dropped at
559
+ the first `switch` — no event, no journal row, no transcript line. The new
560
+ `permission_denied` event carries the decision token (`workingDir`, `mode`)
561
+ and never the vendor's prose, which names the file being read (§11).
562
+
563
+ - **A turn that ended blocked is no longer a turn that succeeded.** The CLI
564
+ says `status_category: "blocked"` one frame before reporting
565
+ `is_error: false`, and both are true from its side: it was asked for
566
+ something it could not do and said so. Taking the second at its word is how a
567
+ phase passed having written no code and then failed two steps later under
568
+ another name. A session that reported an error no longer ends `ok` — which
569
+ also makes a reviewer fail closed, since its verdict is read back out of a
570
+ transcript whose session did not end cleanly.
571
+
572
+ - **An agent still could not write in its own lane.** The entry below passed
573
+ claude-code `--permission-mode auto`, which matched our own vocabulary for
574
+ "works unattended" and is not what the vendor means by the word: `auto` still
575
+ routes a write to a permission prompt, and in `-p` there is nobody to answer
576
+ one. Run against the CLI, a `Write` to the agent's *own working directory*
577
+ comes back denied with no reason attached — the same dead end, wearing the
578
+ word that made it look closed. `acceptEdits` is the mode under which that
579
+ write succeeds, and is what `auto` now maps to. The test covering this
580
+ asserted the word rather than the behaviour, so it pinned the bug in place.
581
+
582
+ - **A phase can read the operator's checkout.** A lane is a worktree cut from a
583
+ branch, so anything uncommitted — a plan written this morning, a spec that
584
+ never leaves the operator's machine — is present where they are and absent
585
+ from every lane. Reaching for it was refused before the model saw a byte, with
586
+ a message that reads like a prompt awaiting an answer ("Claude requested
587
+ permissions to read from …, but you haven't granted it yet") when in fact
588
+ nothing is asking: the CLI denies it outright, emits no permission request,
589
+ and the session then ends *successfully*, having written nothing.
590
+
591
+ The repository root is now granted to every claude-code agent for **reading**.
592
+ Granting a directory also grants writing in it, and the lane sits inside the
593
+ directory being granted, so the grant is paired with a deny list that keeps
594
+ the lane the only writable place under it — the operator's source and every
595
+ sibling lane stay refused. That list names the *siblings* at each level down
596
+ to the lane rather than the root itself, because the vendor resolves deny
597
+ before allow with no carve-out: `deny: <repo>/**` plus `allow: <lane>/**`
598
+ refuses the lane too. Each of those facts was established by running the CLI.
599
+
600
+ **This covers the file-editing tools and not `Bash`.** A shell redirection was
601
+ never confined to the working directory and is not confined now; that boundary
602
+ is the OS's, and the vendor's sandbox that enforces it also switches off the
603
+ network a phase needs to install anything.
604
+
605
+ codex needed nothing: under `--approve-for-me` it already reads anywhere on
606
+ disk, which was confirmed the same way.
607
+
608
+ - **An agent can write in its own lane.** Every adapter declared
609
+ `permissionControl: true` and passed no policy at all: claude-code was spawned
610
+ as `-p --output-format stream-json …` with no `--permission-mode`, and codex as
611
+ `exec --json` with no sandbox and no approval flag, under a comment saying it
612
+ took both on the command line. So a headless run asked before its first write,
613
+ emitted a `permission_request`, rendered it in the transcript — and nothing
614
+ anywhere answered. The phase failed reporting a permission system it could not
615
+ see, having written nothing.
616
+
617
+ `run` and `serve` now take `--permission <ask|auto|full>`, defaulting to
618
+ `auto`. **The operator sets it, never the workflow document**: the document is
619
+ committed and shared, and a file in a repository should not be able to tell
620
+ someone else's machine to run agents without approvals.
621
+
622
+ Both CLIs refuse combinations that looked reasonable, and both were found by
623
+ running them rather than reading them: codex rejects `--sandbox` alongside
624
+ `--approve-for-me`, and `codex exec resume` accepts neither — passing the
625
+ fresh-spawn policy there turned a `stale_session` refusal, which the scheduler
626
+ retries cold, into a `fatal` one that fails the node.
627
+
628
+ This reverses a documented decision. The README said the daemon passed no
629
+ permission flags and that a committed `.claude/settings.json` was what made a
630
+ run able to write; that is now the narrowing layer on top of a mode, not the
631
+ only thing standing between an agent and a blocked lane.
632
+ - **`purge --lanes --branches` clears what a failed run leaves behind.** A run's
633
+ lane worktrees are not under `runs/`, so no purge ever reached them: three
634
+ failed attempts at one plan left twelve worktrees, twelve summaries, and a set
635
+ of `plan/<id>/phase-*` branches still checked out — which is what makes the
636
+ *next* run of that plan impossible, since git refuses to check out a branch a
637
+ second worktree holds.
638
+
639
+ One rule decides both: **never delete the only copy of work.** A phase can
640
+ leave work committed on its branch or uncommitted in its worktree, and both
641
+ count — so a branch carrying commits no other branch has is kept, a lane with
642
+ a dirty tree is kept, and each is listed with the command to remove it by
643
+ hand. Untracked files count as work: a phase that wrote three new modules and
644
+ never committed them is exactly the case worth protecting.
645
+
646
+ Emptiness is measured as "carries no commit another branch does not already
647
+ have", not as "merged into HEAD". The latter is relative to wherever the
648
+ operator is standing, so on a feature branch — the checkout this is most often
649
+ run from — every empty phase branch looked unmerged and nothing was ever
650
+ cleaned up.
651
+
652
+ `--dry-run` and `--yes` work as they already did, and a worktree that will not
653
+ go is named rather than counted.
654
+
655
+ - **`doctor` now catches, before a lane exists, the failures that used to take a
656
+ run each.** Four consecutive runs of one plan failed four different ways, and
657
+ every one of them was knowable at minute zero from the workflow and the repo.
658
+ - **Every `prompt_ref` and `plan_context_ref` is resolved against
659
+ `base_branch`**, with `git cat-file -e`, which reads the branch's tree and
660
+ so cannot be satisfied by a working-tree copy — the working tree being
661
+ exactly what a lane will not have. It distinguishes the three cases, because
662
+ they have three different fixes: never committed, **staged but never
663
+ committed** (`git add` alone does not put a file in a branch), and committed
664
+ on a different branch. The last offers both routes out — merge it, or point
665
+ `base_branch` at the branch that has it — since only the operator knows
666
+ which they want.
667
+ - **Phase branches held by another worktree** are reported with the directory
668
+ to remove. Lane directories are named per run and phase branches per
669
+ workflow, so a failed run leaves worktrees holding the branch names the next
670
+ run will try to cut, and git refuses to check out a branch twice. Nothing
671
+ cleans those up: `purge` deletes run state under `.vinta-ai-maestro/runs/`,
672
+ and lane worktrees are not there. `doctor` does not delete them either — a
673
+ failed lane's worktree is the only place its state survives — but it now
674
+ names each one and the exact command.
675
+
676
+ - **A failed node records why.** `node_status` carried `{"status":"failed"}` and
677
+ nothing else, so two runs failing for two unrelated reasons produced identical
678
+ journal rows and the cause survived only in the operator's terminal. The
679
+ reason is now persisted — **sanitized**, not raw: errors the package builds
680
+ itself are identifiers by construction and are kept verbatim, and anything
681
+ else is reduced to its kind, the rule `recycleStage` already followed. The
682
+ journal is durable and API-served, and an exception message from a dependency
683
+ is exactly how repository content gets into one. Blocked dependents record
684
+ which node blocked them.
685
+
686
+ - **Two diagnostics that named the symptom and hid the cause.** Both came out of
687
+ one real run, and neither was a wrong answer — just an unusable one.
688
+ - **A phase whose plan is not committed** failed with `prompt_ref "…" names no
689
+ readable file`, against a path that was spelled correctly. A lane is a fresh
690
+ worktree of the base branch, so a plan sitting untracked in the operator's
691
+ checkout is in the one place the run cannot look. The message now names the
692
+ directory it resolved against and says why a plan is often not in it.
693
+ - **A lane summary the daemon could not accept** reported `summary unreadable`
694
+ and advised deleting the lane — which takes its forked databases with it,
695
+ over what turned out to be two wrong fields. `doctor` now names the fields
696
+ and what was expected, and offers repair before deletion. A file that does
697
+ not parse at all still reports as unreadable, because there is no field to
698
+ name. Neither message carries the offending *value*: a summary holds
699
+ database names and connection variables, and the value that failed
700
+ validation is the likeliest thing in it to be one.
701
+
702
+ `prepare-worktree` gained the rule that would have prevented the summary in
703
+ the first place — its `|` alternatives are closed sets read by machine, `null`
704
+ is how you say "none", and a strategy outside the set should be the closest
705
+ one plus a `note:` rather than a fourth word.
706
+
707
+ - **`npx vinta-ai-maestro` now runs.** The published package pointed its `bin` at
708
+ `src/cli/bin.ts` and relied on Node stripping the types at startup — which Node
709
+ refuses to do anywhere under `node_modules`, unconditionally and with no flag to
710
+ override. So the CLI worked from a checkout and died on first run once installed,
711
+ with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
712
+
713
+ `packages/vinta-ai-maestro` now builds: `tsc` emits `dist/` with
714
+ `rewriteRelativeImportExtensions` (the source imports `.ts` specifiers, which
715
+ emitted JS cannot keep), vite writes the daemon's UI into `dist/ui`, and the
716
+ shebang is rewritten from the source's `--experimental-transform-types` form to
717
+ a plain `#!/usr/bin/env node` — a compiled entry point has no types left to
718
+ transform, and should not ask Node for a flag it is free to retire. `prepack`
719
+ runs the build, so a tarball can never be cut from a stale `dist/`.
720
+
721
+ Three packaging faults surfaced with it, each of which broke an install on its
722
+ own:
723
+ - **The UI was never in the tarball.** `serve` resolves its static root as
724
+ `dist/ui` and refuses to read outside it, so it would have booted and answered
725
+ every page with "build the UI first". The build now asserts `dist/ui/index.html`
726
+ exists rather than trusting a vite config that writes outside its own root.
727
+ - **`tailwindcss` and `tw-animate-css` were imported but never declared.** They
728
+ resolved only through pnpm's `.bin` shim setting `NODE_PATH`; building by any
729
+ other path failed to resolve them. Both are now devDependencies.
730
+ - **Everything the UI imports was a runtime dependency.** React, xterm,
731
+ lucide-react and the three workspace packages are bundled into `dist/ui` by
732
+ vite and cannot be loaded at runtime — yet a consumer installed all of them,
733
+ and two carried `workspace:*` specifiers that no registry understands. Moved to
734
+ devDependencies; the published package now declares six runtime dependencies
735
+ instead of fourteen.
736
+
737
+ CI packs the tarball, installs it into a throwaway project and runs the binary
738
+ from `node_modules`. Every previous check ran from a checkout, which is the one
739
+ place this class of bug cannot appear.
740
+
219
741
  - **Lane provisioning serializes `git worktree add`, not only the database
220
742
  template.** `prepare-worktree` previously named one serialization point; there
221
743
  are two. Git rewrites `.git/worktrees/` metadata on every add, and concurrent
@@ -238,22 +760,25 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
238
760
  nothing produces a merge commit full of markers that passes into the wave
239
761
  branch unnoticed.
240
762
 
241
- > The repository also gained `packages/vinta-ai-maestro/`, a **private workspace
242
- > package** that executes these workflows. It is not published and is not part of
243
- > the `vinta-ai-workflows` package — the root `files` whitelist excludes
244
- > `packages/`, so nothing in it is installed by `npx vinta-ai-workflows install`.
245
- > What ships from that work is the two generated schemas above and the skill
246
- > changes that produce and consume them.
763
+ > The repository also gained `packages/vinta-ai-maestro/`, the workspace package
764
+ > that executes these workflows. It is published as its own npm package,
765
+ > `vinta-ai-maestro`, carrying the same version as `vinta-ai-workflows`, and is
766
+ > run with `npx vinta-ai-maestro`. It is not part of the `vinta-ai-workflows`
767
+ > tarball — the root `files` whitelist excludes `packages/`, so nothing in it is
768
+ > installed by `npx vinta-ai-workflows install`. What ships from that work into
769
+ > the skills package is the two generated schemas above and the skill changes
770
+ > that produce and consume them.
247
771
  >
248
772
  > Its browser UI is now built on `packages/design-system/` (`vinta-design-system`),
249
- > a second private workspace package: oklch tokens in three layers mirroring
773
+ > a second workspace package: oklch tokens in three layers mirroring
250
774
  > `vinta-schedule-design-system`, six run-status **tones** (`idle`, `active`,
251
775
  > `wait`, `attention`, `ok`, `error` — waiting is amber and failure is red, never
252
776
  > the same hue), shadcn/ui components on Tailwind CSS v4, a small layout kit, and
253
777
  > a light/dark/system theme. The run graph and the pipeline editor are re-skinned
254
778
  > through their own custom properties, so the canvas and the badges beside it
255
- > share one palette. Equally unpublished: nothing in it reaches `npx
256
- > vinta-ai-workflows install`.
779
+ > share one palette. It and `vinta-dag-editor` publish on their own cadence
780
+ > (`0.1.0`) as dependencies of `vinta-ai-maestro`; nothing in either reaches
781
+ > `npx vinta-ai-workflows install`.
257
782
 
258
783
  ## [0.6.1] — 2026-08-17
259
784
 
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.7.0-alpha1",
3
+ "version": "0.7.0-alpha11",
4
4
  "description": "Bootstrap AI tooling (AGENTS.md, skills, sub-agents) across Claude Code, Codex, Cursor, and VS Code + GitHub Copilot.",
5
5
  "type": "module",
6
- "packageManager": "pnpm@10.33.0",
7
6
  "bin": {
8
7
  "vinta-ai-workflows": "./vinta-ai-workflows.mjs"
9
8
  },
10
- "scripts": {
11
- "check-ai-models": "node scripts/check-ai-models.mjs",
12
- "test": "node --test tests/*.test.mjs"
13
- },
14
9
  "devDependencies": {
15
10
  "yaml": "^2.4.0"
16
11
  },
@@ -26,10 +21,14 @@
26
21
  },
27
22
  "repository": {
28
23
  "type": "git",
29
- "url": "git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git"
24
+ "url": "git+https://github.com/vintasoftware/vinta-ai-workflows.git"
30
25
  },
31
26
  "publishConfig": {
32
27
  "access": "public"
33
28
  },
34
- "license": "MIT"
35
- }
29
+ "license": "MIT",
30
+ "scripts": {
31
+ "check-ai-models": "node scripts/check-ai-models.mjs",
32
+ "test": "node --test tests/*.test.mjs"
33
+ }
34
+ }
@@ -191,6 +191,150 @@
191
191
  }
192
192
  },
193
193
  "additionalProperties": false
194
+ },
195
+ "env_files": {
196
+ "default": [],
197
+ "description": "Repo-relative ignored files every lane needs its own COPY of — `.env`, `.env.docker`, `.envrc`. Copied, never symlinked: lane provisioning appends to them (a connection string, a compose override path), and a symlink would write those lane-specific lines back into the main checkout. A declared file that is missing fails provisioning rather than producing a lane whose stack cannot boot.",
198
+ "type": "array",
199
+ "items": {
200
+ "type": "string",
201
+ "minLength": 1
202
+ }
203
+ },
204
+ "commands": {
205
+ "default": {},
206
+ "type": "object",
207
+ "properties": {
208
+ "lint": {
209
+ "type": "string",
210
+ "minLength": 1
211
+ },
212
+ "typecheck": {
213
+ "type": "string",
214
+ "minLength": 1
215
+ },
216
+ "test": {
217
+ "description": "The whole suite.",
218
+ "type": "string",
219
+ "minLength": 1
220
+ },
221
+ "test_one": {
222
+ "description": "A single test or a subtree. The agent appends the target, so end it accordingly.",
223
+ "type": "string",
224
+ "minLength": 1
225
+ },
226
+ "migrate": {
227
+ "type": "string",
228
+ "minLength": 1
229
+ }
230
+ },
231
+ "additionalProperties": false,
232
+ "description": "The project’s own command lines, handed to every agent verbatim. A fixed vocabulary rather than free-form, because the prompt has to be able to say which one step 3 of the working instructions means. Declaring these is how an agent stops guessing: a project whose suite only runs inside a container, or only against a lane-specific database, has no way of telling one otherwise, and `pytest` is a plausible guess that is wrong in exactly that project."
233
+ },
234
+ "hooks": {
235
+ "default": "run",
236
+ "description": "Whether the repository’s git hooks run on commits made inside a lane. `run` is the default and is what a developer’s own checkout does. `skip` points the lane at an empty `core.hooksPath`, for the case this exists for: a `language: system` pre-commit chain that, in a worktree that has never been committed in, builds a virtualenv per lane before it will let a commit through — observed as four failed commit attempts, one of them a two-minute timeout, and a 510 MB `.venv` per lane. The gates still run; this only stops each lane paying a whole-environment install to make a commit.",
237
+ "type": "string",
238
+ "enum": [
239
+ "run",
240
+ "skip"
241
+ ]
242
+ },
243
+ "services": {
244
+ "default": {},
245
+ "description": "Shared servers each lane gets its own namespace inside, by id. One postgres, one redis, one rabbit on the machine, with a database / db index / vhost per lane — which is the difference between six lanes being viable on a laptop and not. The alternative a project falls into without this is booting a server per lane, or sharing one with no isolation at all.",
246
+ "type": "object",
247
+ "propertyNames": {
248
+ "type": "string",
249
+ "minLength": 1,
250
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
251
+ },
252
+ "additionalProperties": {
253
+ "type": "object",
254
+ "properties": {
255
+ "namespace": {
256
+ "type": "string",
257
+ "enum": [
258
+ "index",
259
+ "name"
260
+ ],
261
+ "description": "How this lane’s namespace inside the shared server is derived. `index` is a small integer, for a server with a fixed number of slots — redis’s sixteen databases are the case it exists for. `name` is a token derived from the lane, for a server that names things freely: a vhost, a bucket prefix, a schema."
262
+ },
263
+ "url": {
264
+ "description": "The shared server, without the per-lane segment — `redis://localhost:6379`. Omit it where the namespace *is* the value: an object-storage prefix has no URL to hang off, and the variable then carries the bare token.",
265
+ "type": "string",
266
+ "minLength": 1
267
+ },
268
+ "url_var": {
269
+ "type": "string",
270
+ "minLength": 1,
271
+ "description": "Env var the lane reads this service’s address from."
272
+ },
273
+ "capacity": {
274
+ "default": 16,
275
+ "description": "How many distinct namespaces the server has. Only meaningful for `index`, where a pool larger than this is refused before anything is created — two lanes on one redis database is the bug this exists to prevent, and reaching it by arithmetic is no better than reaching it by neglect.",
276
+ "type": "integer",
277
+ "minimum": 1,
278
+ "maximum": 9007199254740991
279
+ },
280
+ "create_cmd": {
281
+ "description": "Run once per lane, in the lane. `{namespace}`, `{url}` and `{lane}` are substituted. Declaring a vhost is a `rabbitmqadmin` invocation this package has no business knowing; a project that needs none declares none.",
282
+ "type": "string",
283
+ "minLength": 1
284
+ },
285
+ "reset_cmd": {
286
+ "description": "Empties this lane’s namespace when the lane is handed to the next phase. Same substitutions. A service without one keeps whatever the last phase left in it, which for a cache is usually right and for a queue usually is not — declaring this is how a project says which of those it has.",
287
+ "type": "string",
288
+ "minLength": 1
289
+ }
290
+ },
291
+ "required": [
292
+ "namespace",
293
+ "url_var"
294
+ ],
295
+ "additionalProperties": false,
296
+ "description": "A shared server each lane gets its own namespace inside."
297
+ }
298
+ },
299
+ "compose": {
300
+ "default": {
301
+ "enabled": true,
302
+ "publish": [],
303
+ "shared_volumes": []
304
+ },
305
+ "type": "object",
306
+ "properties": {
307
+ "enabled": {
308
+ "default": true,
309
+ "description": "Whether lane provisioning generates a compose override. On by default, and it does nothing at all in a project with no compose file. Where there is one and docker cannot answer, provisioning fails rather than quietly producing lanes that share a data volume — set this to false to say that is genuinely what you want.",
310
+ "type": "boolean"
311
+ },
312
+ "publish": {
313
+ "default": [],
314
+ "description": "Services that must keep a reachable host port, republished on one this lane was granted instead of the one the project pinned. Empty is the default and is usually right outright: where the project’s own test command runs inside compose, services reach each other by container DNS on the lane’s own network and a published port buys nothing but a collision. The granted port reaches the lane as `LANE_PORT_<SERVICE>_<CONTAINER_PORT>`.",
315
+ "type": "array",
316
+ "items": {
317
+ "type": "string",
318
+ "minLength": 1
319
+ }
320
+ },
321
+ "shared_volumes": {
322
+ "default": [],
323
+ "description": "Volume keys that leak past the project name and are to be left leaking. The escape hatch for the one decision this cannot make for you: re-pinning a shared dependency volume is correct, and it costs every lane the install that sharing it was avoiding. A read-only cache with no dependency churn in the plan is safe here. A data volume never is — that is two servers on one data directory, which is the bug this exists to prevent.",
324
+ "type": "array",
325
+ "items": {
326
+ "type": "string",
327
+ "minLength": 1
328
+ }
329
+ }
330
+ },
331
+ "additionalProperties": false,
332
+ "description": "How a lane’s compose stack is isolated past `COMPOSE_PROJECT_NAME`."
333
+ },
334
+ "setup_cmd": {
335
+ "description": "The project’s own lane-setup command, run inside each freshly provisioned lane with that lane’s environment applied. The escape hatch for whatever a project needs that no field here describes. It MUST be idempotent: it runs again every time the lane is recycled for another phase.",
336
+ "type": "string",
337
+ "minLength": 1
194
338
  }
195
339
  },
196
340
  "required": [
@@ -455,6 +599,7 @@
455
599
  },
456
600
  "max_fix_rounds": {
457
601
  "default": 2,
602
+ "description": "Rounds the fixer gets to clear a review’s findings before the phase fails. Two is a budget, not a target, and it is measured in *rounds* rather than findings — a first review raising four legitimate blockers can exhaust it while every round is making progress. Raise it on a phase you expect to be argued over; it is the knob that decides how much a phase gets to be wrong before it is handed to `--on-failure`.",
458
603
  "type": "integer",
459
604
  "minimum": 0,
460
605
  "maximum": 9007199254740991
@@ -355,7 +355,7 @@ state:
355
355
  files: [.env, .envrc, ...]
356
356
  dev_db:
357
357
  engine: postgres | mysql | sqlite | mongo | redis
358
- strategy: fork | share | stub
358
+ strategy: fork | share | stub # closed set — see "The summary is read by machine"
359
359
  forked_name: <forked db name> # null when share / stub
360
360
  connection_url_var: DATABASE_URL
361
361
  reset_cmd: <shell command that returns this DB to the state of a fresh checkout>
@@ -365,7 +365,7 @@ state:
365
365
  # instead of reusing this worktree across a migration boundary.
366
366
  test_db:
367
367
  engine: ...
368
- strategy: fork | share
368
+ strategy: fork | share # closed set, as above
369
369
  forked_name: ...
370
370
  connection_url_var: TEST_DATABASE_URL
371
371
  reset_cmd: <same idea, for the test DB> # null when not resettable
@@ -467,6 +467,14 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
467
467
 
468
468
  - **Symlink for reads, copy for writes, fork for state.** This is the only mental model that scales. Default to fork when unsure — disk is cheap, corrupted main-checkout DBs are not.
469
469
  - **Never share a writable DB across worktrees by default.** The race conditions are subtle and the failure mode is silent data corruption.
470
+ ### The summary is read by machine
471
+
472
+ This file is not a note to the next human. [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) parses it to decide whether a lane can be handed to another phase, so **every `|` above is a closed set and every key is required** — `null` is how you say "none", and omitting the key is not the same thing.
473
+
474
+ A value outside the set makes the lane unusable by the daemon. It does not fail loudly at write time; it fails later, as `doctor` reporting the lane and the field it could not accept, long after the worktree and its forked databases exist. Seen in the wild: a `strategy: create-empty-and-migrate` invented to describe a database that had been created empty and migrated by hand, with `reset_cmd` left off entirely. Neither is in the spec, and the pair cost eleven lanes their readability.
475
+
476
+ If none of the listed strategies describes what you did, **the honest record is the closest one plus a `note:`** — the schema keeps `note` free-form for exactly this. A database created empty and migrated is a `fork` whose `reset_cmd` re-runs that same creation; a database you could not provision at all is a `stub`. What it must never be is a fourth word.
477
+
470
478
  - **`COMPOSE_PROJECT_NAME` does not isolate `external:`/fixed-`name:` volumes or fixed host ports — always run the [4a](#4a--neutralize-compose-isolation-leaks) generator.** For a compose-delivered DB the data volumes are forked *unconditionally* (independent of `schema_change` and `test_db_strategy`): the worktree runs its own server, and two servers on one volume is corruption. "Share the DB" only ever means share a connection to a single already-running server, never a second server on shared storage. The fix is a generated, out-of-tree override + a `COMPOSE_FILE` line in the copied `.env` — never an edit to any tracked compose file. Verify the volume-differs invariant before declaring the worktree runnable.
471
479
  - **Every fork decision lands in `.vinta-ai-workflows/worktrees/<name>.yaml`.** Teardown reads it; humans grep it; agents resuming a stalled plan read it. No decision lives only in conversation memory.
472
480
  - **Worktree root governed by runtime conventions.** claude-code uses `.claude/worktrees/`; other harnesses use sibling dirs. Don't fight the harness — match it.