Repository navigation
fix: make the state machines of the workflows engine explicit - #91
Merged
Vzlentin merged 6 commits intoNov 2, 2026
Merged
Conversation
…n memory Several bugs in the workflow scripts gave wrong results: an empty fix patch, a failed judge that counted as shipped, a stale rundown patch, a blind reset, an empty branch and duplicate notes. - thermoloop.js gives implement the HEAD before the round as `start`. A fix round that changes nothing is now blocked, and the next review never gets an empty fix patch. - The lib helper `writePatch(path, from)` in workflows/lib/git.js writes the diff from `from` to HEAD. It replaces the local `patch` of thermoloop.js. campaign.js also uses it. script.d.ts and the tool guidelines declare it. - campaign.js `settle`: an item whose judge failed is a failed item, with the status `shipped with a failed judge: <error>` and no fix round. An unknown fix status is a failed item with the outcome `the fix round ended with the unknown status <s>`. It no longer throws a TypeError. - The rundown gets `item-<n>/item.patch`, the change from the item's start to HEAD (also the fix commit), written before the reset. It replaces `round-<rounds>.patch`, which was missing or stale for some statuses. - Before the reset of a failed item, the campaign checks that the item started from the last shipped commit and that HEAD contains it. If not, it fails with a clear message and does not reset. - At the start of each item and at the end, the campaign checks that the worktree is on its branch. On a detached HEAD or another branch whose HEAD contains the last shipped commit, it moves the branch to HEAD with `checkout -B`. Else it fails with a clear message. So a result always has a branch. - src/run/child.ts fails an aborted or orphaned child run with `child run <id> stopped: <reason>`. fix.js rethrows that error, so a resume of the caller runs the fix round again. It keeps only real failures as `failed`. - workflows/lib/memory.js fails with a clear error when memo asks for a compression without `nap <a>-<b>`, and after 64 compressions in a row. - The plain store gives each note the key `<run-id>/<n>` in `notes.keys`. The note number is the line of its key. So an add that runs again at resume adds no line, and adds that run at the same time get different numbers. - docs/spec.md describes the new behaviour. `--rounds 0` and `--rounds 1.5` were already refused by the meta check (`number` is a positive integer), for argv and for run() args, so thermoloop.js does not change for them. A test now covers it. New tests cover each fix with the faux provider. TASK.version does not change.
…amilies unions Each command read the task state in its own way. So a waiting run counted as a past completed run, a faulted task read differently in different places, and stop and resume could pick different tasks and task keys. - src/run/past.ts holds `runStatus`, the one function that computes a run's status from the task state, the outcome, the waiting result and the task version. It returns the union `RunStatus`. `hasEnded` and `hasResult` read that union. `taskOf` moves to src/run/past.ts next to it, so run/ and cli/ can both import it. - `pastRuns`, `taskOf`, `list`, `show`, `stop`, `prune`, `resume`, `follow` and the child runs call `runStatus`. `pastRuns` returns only `done` runs and leaves out waiting runs. A faulted task is `failed` in every reader. - `prunable` decides by `taskOf`, not by the last task. - The key of a new task is the highest `#n` in harness's document of keys, plus 1 (`keysOf` in src/cli/saved.ts). `stop` and `resume` reach the task of `taskOf` by its own key. - When a concurrent resume took the key with another input, the second resume fails with a clear message that tells to use `workflows wait`. - `OutcomeStatus` comes from `OUTCOME` in src/tool.ts. The JSON outcome lines and `endOf` use it. The list marks switch on `RunStatus`. - A script status that `meta.exit` does not declare exits 1, not 0. The built-in scripts declare their success statuses. Callers of implement and rpi treat any status other than `implemented` as not implemented. - docs/spec.md and AGENTS.md describe the new behaviour. TASK.version does not change: the saved input keeps its shape.
… task end A crash or a closed connection at some points of a run left a run directory that no command showed, reported a stop as a host stop, mixed up call states in `show`, or faulted a task whose script had ended. - `start` writes the `project` file before it prepares the run. `list` shows a run directory with no task as `unstarted`, and `prune` reports it and removes it with `--yes` after 1 hour (`unstartedRuns` in src/cli/saved.ts). - When the `start` request fails after it was sent, the command sends the same `start` again on a new connection. The host creates the task of a key once, so the command follows the run. `findTask` is removed. - `abortRun` gives an aborted end when the host closes the connection after it replied to the abort, so the command reports `stopped`, not `host-stopped`. - A call record saves the task that started it, and the time when the end of the script cancelled it. `show --calls` gives each call a state: done, failed, cancelled, waiting, running or cut. - A `run` call saves the run ID of its child run before it makes the run directory. A rerun reuses the ID and cleans the directory. - A failure to end the sessions does not fault the task: the host reports it, and the run keeps the outcome of its script. - The end of an aborted run is saved with `stopped`. Its steps show `·` in the terminal, `show` and Pi. - `resume --answer` fails with a clear message when the script did not reach the answered `input` call. A resume without `--answer` drops the earlier answer. - docs/spec.md describes the new output lines and states. TASK.version does not change: the saved input keeps its shape.
…shes and stale state A crash or a stale state at some points left an orphaned Herdr workspace, a leaked branch, a stale PR branch under review, or a background run that the Pi extension stopped following. - `ensureWorktree` fails with `branch <branch> is checked out at <path>` before Herdr or Git makes anything. When Herdr opens a workspace but leaves no repository, the run closes that workspace (`closeWorkspace` in src/herdr.ts) before Git makes the worktree. - `prune --yes` writes the marker `runs/<run-id>/pruning` before it removes a run. The next prune finishes a marked run with no `gh` call. A removal that fails, such as of a dirty worktree, is reported on stderr, and prune goes on with the other runs and exits 1. - `changeOf` always fetches `pull/<n>/head`, moves a missing or behind branch to it, and fails when the branch diverged, or when it is behind and checked out in a worktree. - optimize removes the branch of the result, else the branch that the worktree is on. A removal that fails gives a `kept` report line. - A command on a gone run (no run directory or no task) exits 4 (`GONE_EXIT` in src/tool.ts). - The Pi watcher waits again with a backoff from 1 s to 60 s after a wait with no outcome to post. It gives a gone run one end entry with the reason. A foreground resume of a followed run gives the outcome once. - `outcomeOf` keeps the run ID in its error when the command printed `run <id>`. - The run view catches a read that rejects, and shows the error of a later failed read under the tree. - docs/spec.md describes the new lines, exit code and marker. The `entries.json` write does not change: `setEntry` of harness already writes only when the entry changes. The `worktree` report line of `inCheckout` does not change: a checkout branch keeps its name, so the line names its worktree.
Small state bugs hid errors, gave unclear messages, or let a run go on in a wrong state. - campaign fails with a clear message when `args.start` is not set. - `squash` makes no commit and logs `nothing to commit since <short>` when nothing changed since `start`. A repeat stays safe. - follow prints one warning for each series of failed progress reads, and the next good read catches up. A comment says why `seen` can index the progress events. - `connectTo` gives the project to harness's `connect`, and does not set `HARNESS_PROJECT` around an `await`. - `reading` runs its reads in one read transaction of the read-only store (`snapshot` in src/records/store.ts). - A second `input` call that would wait fails the run, also when the script catches its error. - `restartChild` names the kind and version of a child task that another version saved. - `renameBranch` gives a `Renamed` union: on a branch, detached, refused or raced. The run reports each case with its own line. - `removedByHerdr` gives false only when herdr is not installed. Other herdr errors fail the removal of the run. - The Pi tool finds progress lines by their parsed JSON, and keeps only the last 100 other stderr lines. - `show --agents --json` gives `ended`, and the run view stops reading after a read shows that the run ended. - docs/spec.md describes the new lines and fields.
Vzlentin
deleted the
fix/make-the-state-machines-of-the-workflows-engine
branch
November 2, 2026 07:17
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The engine, the bundled scripts and the Pi extension tracked run and call lifecycles with loose flags, counts and separate writes. This makes the run status one union computed in one place, and closes the windows where a crash, a race or a stale branch lost or repeated work.
Run status
runStatusis the only place that turns a task into a status (running,waiting,done,failed,aborted,orphaned,earlier-version,later-version).taskOf,stop,prune,pastRuns,listandshowall go through itfaultedreads asfailedeverywhere#n+ 1, so a gap or a task of another version no longer sendsresumeorstopto the wrong taskmeta.exit, an undeclared one exits 1Scripts
implementthe head of the round, so a fix round that changes nothing returnsblockedbranch: "")fix.jslets a stop through, the run memory fails clearly on a bad nap format and no longer duplicates a replayed noteCrash windows
listasunstarted, andpruneremoves it after an hourshow--answerfails the run, a child run dir is reused after a crash, a throw while closing sessions keeps the script result, and an abort shows as stoppedWorktrees, prune, Pi
changeOffetches the PR branch again when a local one existsworkflows still reads the
harness.tasksdocument by its kind, until harness exports it (Vzlentin/harness#44) and the harness range moves to the release with protocol 12.