Troubleshooting
This guide covers common issues you may encounter with LumenFlow and how to resolve them.
A claimed WU edit stays pending instead of releasing its code paths
Section titled “A claimed WU edit stays pending instead of releasing its code paths”On an active worktree claim, wu:edit records the requested WU spec in the
pending ledger. A strict reduction of the validated code_paths set is also
published to canonical main. Inherited duplicate entries are ignored for the
set comparison, and a successful publication writes the requested paths once
in first-occurrence order.
Removing duplicate entries alone, adding or remapping a path, an empty or
malformed scope, or a scope that no longer reduces the freshly fetched WU does
not release the canonical overlap reservation. The final edited=yes line
means the edit was recorded; it does not prove canonical publication. Look
for the bounded Published canonical code_paths message to confirm a remote
release. A Canonical code_paths not published message gives the pending
reason and says the local WU spec will materialize at wu:done. If publication
was refused or failed, retry only after resolving the reported claim, caller,
or remote condition; a successful release is never inferred from the pending
edit alone.
Prep reports a stale path from a pending WU edit
Section titled “Prep reports a stale path from a pending WU edit”For an in-progress WU, a governed wu:edit may publish a strict code_paths
reduction while the matching acceptance or test-path change remains pending
until wu:done. Both the wu:prep pre-step and the spec:linter gate validate
the materialized YAML plus the authorized pending field deltas. Ready and done
WU records continue to use their materialized YAML.
If validation reports a path that a pending edit was meant to remove, check the
pending spec update and its field list, then rerun wu:prep. Keep the acceptance
and declared paths consistent in the pending edit. If the pending state cannot
be read or projected, validation fails closed; resolve that state through the
normal lifecycle flow rather than editing the protected WU YAML or bypassing
the gate.
Recovery refuses an unsafe private namespace
Section titled “Recovery refuses an unsafe private namespace”Recovery worktrees are stored outside the checkout in a private per-user
namespace. A checkout may use
shared group-write permissions such as 0775; recovery does not create or
remove its temporary worktrees below .lumenflow/tmp. New namespace
directories are private to your user, while ordinary Git-created 0755
descendants and 0644 files remain supported inside them.
On POSIX systems, recovery refuses a home directory that grants group or
other users write access, and it preserves any non-private or unowned recovery
namespace for inspection. It does not change checkout, home, or legacy
.lumenflow/tmp permissions. If setup refuses, repair the permissions of the
home/private namespace and retry. Windows uses the native per-user isolation
primitive; Node mode bits alone do not establish ACL privacy.
Release reports an incoherent CLI artifact
Section titled “Release reports an incoherent CLI artifact”The release entry is required to use a dist artifact whose provenance sidecar matches the CLI source, dependency/lockfile, and build configuration digests, the current Node major/ABI, and the expected entry path. A source edit, dependency or build-config change, missing/corrupt sidecar, or Node ABI change therefore causes a locked rebuild; the old dist is never executed.
Follow the exact remediation printed by the launcher (normally:
pnpm install && pnpm build) and retry with Node 26. If the rebuild fails or
the sidecar remains mismatched, release stops before mutating versions, tags,
or registry state. Do not bypass this check with a timestamp assumption or a
force flag.
CLI build cache reports an unusable or unactivatable build
Section titled “CLI build cache reports an unusable or unactivatable build”Lifecycle commands that need a freshly built CLI resolve the build output by a
digest of its inputs and keep it in a per-machine cache outside every checkout
(in the per-user LumenFlow cache by default, moved with LUMENFLOW_CLI_DIST_CACHE
— see the config reference). Every success claim is
verified: the expected entry exists, the provenance sidecar is readable, every
declared output file is present, and provenance matches the current inputs.
FreshDistIncompleteArtifactError names which of those failed:
- A cached build could not be activated (a refused file removal, a full or read-only filesystem, a symlink inside the output directory). The command falls back to a real build on its own; no action is needed unless the real build then fails for the same environmental reason.
- A peer build did not leave a usable artifact. The command rebuilds instead of trusting the peer’s completion. If it repeats, another process is writing the same output directory outside the lifecycle.
- The build completed without replacing the output, so its provenance
cannot be republished. When the build is the default scoped Turbo rebuild the
command re-runs it once with the cache disabled on its own (for example after
a raw
turbo run buildleft the dist correct and the provenance stale), so a manual--forcebuild is no longer needed. The case that still needs you is a custom build command, or a forced run that again changes nothing: an incremental build cannot replace an output it already considers current, so it can never repair a missing or corrupt provenance sidecar and every retry fails identically. Re-run the configured build with its cache disabled so it re-executes — the error prints the exact command. Do not delete adistdirectory that other checkouts read through; that empties the artifact those checkouts depend on while your build runs.
The input digest covers the CLI and every workspace package it depends on. For
each of those packages it covers every source file, not only src/: top-level
files, tools/, tool-impl/ and similar directories count too. Test files
outside src/, READMEs, dist and node_modules do not. An edit to any of these sources
therefore triggers a rebuild. If you build the CLI by hand, build its
dependencies with it (pnpm --filter '@hellmai/lumenflow-cli...' build). A
CLI-only build leaves dependency packages running their previous code.
A poisoned or partially copied cache entry is rejected before activation rather than published over a working output, so a bad entry costs one rebuild, not a broken checkout. If you must discard the cache entirely, remove the cache root; it is rebuilt on demand and contains no state that is not reproducible from source.
Node version and CLI artifact coherence
Section titled “Node version and CLI artifact coherence”The CLI hashes source, dependencies, and the Node ABI together, so sessions
running different Node versions invalidate each other’s dist in a loop: pin
the engine version on every command. Treat ERR_MODULE_NOT_FOUND under dist
after a successful build as a stale build cache, not a code problem — remedy
it with a forced rebuild of the package, not a debugging session.
Why pnpm <command> starts fast, and when it does not
Section titled “Why pnpm <command> starts fast, and when it does not”Every pnpm lifecycle command goes through the LumenFlow CLI launcher. On an
unchanged checkout the coherence check is a stat fingerprint of the files the
digests read (path, size, mtime, ctime, inode), so it costs tens of
milliseconds and spawns nothing. Any added, removed, edited or restored input
changes the fingerprint and runs the full content hash. A file written within
the last two seconds is never trusted from the cache, so the first command
after an edit pays one full check. The Node major and ABI are checked on every
call.
After a landing moves main, the next command rebuilds what is stale
Section titled “After a landing moves main, the next command rebuilds what is stale”A landing fast-forwards your main checkout, which leaves the built packages
(dist) behind the new sources. You do not need to run anything. The next
pnpm <command> checks the dist of the CLI and of every workspace package it
loads (the kernel, the software-delivery pack, and so on), rebuilds the stale
ones once under the build lock, and carries on. Nothing is rebuilt when nothing
is stale; the extra check costs about 65 ms. wu:done does the same rebuild
before it opens the landing transaction, never inside it. If main moves again
while a landing is already inside the transaction, the landing stops at once
with a message naming the stale packages, releases the landing lock, and does
not block anyone behind it; re-run pnpm wu:done and it rebuilds first.
The launcher also sets --max-old-space-size in its own environment so every
process it spawns inherits it; it no longer starts a second Node process to do
so. Set your own NODE_OPTIONS=--max-old-space-size=... to override it.
Why a read-only command still takes seconds, and what is cached
Section titled “Why a read-only command still takes seconds, and what is cached”After the launcher, a command pays for its own module graph and reads. What is cached or deferred, and what remains:
- The built-in pack’s import-boundary scan and integrity hash run once per pack content change (a stat fingerprint in the per-user pack-verification cache); editing any pack file re-verifies in full. Delete that cache to force a full verification.
- Each ledger (
wu-events.jsonl) line is validated against the event schema once: the lines the schema accepted are remembered by digest in the per-user ledger-validation cache (keyed by a digest of the schema, so a schema change starts empty). A changed line is always validated again. An unchanged ledger is read, decoded and digested once per process, andworkspace.yamland the pack manifests are parsed once and cached as JSON in the per-user yaml-parse cache keyed by their text. Delete either cache to force full validation and parsing. --help, the licence gate and a read-onlymem:inboxno longer import the pack registry barrel, the kernel runtime, the confinement schema, the WU document schema,simple-gitor the ledger event schema. Lifecycle telemetry (heartbeat, duration tracking, branch-pr finaliser) loads only for a command that carries a session capability, opts into duration tracking, or is a lifecycle command.- Still paid by every verified command: the pack registry’s zod schemas (workspace spec, pack
manifest, config).
wu:statusadditionally waits on about a dozengitprocesses (twogit statusruns are the longest) and reads every ledger it needs; a very largewu-events.jsonlmakes it slower in proportion.
Where unread-signal previews appear
Section titled “Where unread-signal previews appear”Only wu:claim, wu:prep, wu:done and mem:checkpoint print a
[signals] preview, and only for unread signals addressed to your WU, lane,
initiative or session. Other commands print none. To see everything unread, run
pnpm mem:inbox. Printing a preview does not mark anything read, and the
wu:prep unread-directed-signal gate is unchanged.
Gate Failures
Section titled “Gate Failures”Format check fails
Section titled “Format check fails”Symptom:
Fix:
Lint check fails
Section titled “Lint check fails”Symptom:
Fix:
- Read the error message to understand the issue
- Fix the code (don’t disable the rule unless necessary)
- Retry:
pnpm wu:prep --id <wu-id>
Common lint fixes:
| Error | Fix |
|---|---|
no-explicit-any | Add proper TypeScript types |
no-unused-vars | Remove the variable or prefix with _ |
exhaustive-deps | Add missing useEffect dependencies |
no-restricted-imports | Use the correct import path |
For a fresh TypeScript project, initialize with the Node gate preset. Init
installs ESLint and the TypeScript ESLint toolchain and writes an ESLint flat
config. The configured lint gate uses pnpm exec, so it fails when the local
executable is missing rather than downloading one. If doctor reports a missing
configured gate tool, run the package manager’s install command, such as
pnpm install, then rerun pnpm lumenflow:doctor.
Type check fails
Section titled “Type check fails”Symptom:
Fix:
- Fix the first error (later errors often cascade from earlier ones)
- Save and check again
- Repeat until clean
Tests fail
Section titled “Tests fail”Symptom:
Fix:
- Run the failing test in isolation:
pnpm test src/__tests__/auth.test.ts - Debug: check test setup, mocks, and assertions
- Fix and retry
Common causes:
- Mocks not reset between tests (use
beforeEach) - Async tests not awaited
- Environment differences (CI vs local)
Pre-existing gate failures
Section titled “Pre-existing gate failures”If gates fail on code you didn’t change:
-
Verify the failure existed before your changes
-
Use a named gate skip only when the gate is explicitly skippable
-
Create a follow-up WU to fix the issue
WU State Issues
Section titled “WU State Issues”WU stuck in in_progress
Section titled “WU stuck in in_progress”Symptom: WU shows in_progress but worktree is gone or work is done elsewhere.
Fix:
Use restore first when the WU is active or recently active and its lane branch still exists. It
recreates the worktree without resetting WU status, branch, baseline/main metadata, or completion
evidence. Use reset for orphaned ready / in_progress state only after restore is impossible
or intentionally not wanted.
Current non-PR automatic wu:done publication builds and validates completion metadata in an
isolated temporary worktree, then publishes the exact commit without mutating checked-out main. If
remote acknowledgement is lost, it preserves that evidence; rerun the same command to prove and
retire an accepted publication or rebuild from the latest remote main. For historical residue from
older versions, run the read-only analyzer. If it proves that rollback completed but the index
retains only the stale completion tuple, use --action reconcile. Reconcile preserves every
worktree byte and refuses unrelated staged edits, unregistered worktrees, ambiguous projections, a
busy Git index, or missing transaction evidence. Never substitute a raw reset or manual index edit.
Two claims from one checkout left a WU in_progress with no branch
Section titled “Two claims from one checkout left a WU in_progress with no branch”Symptom: two wu:claim runs started together from the same checkout. One finished normally.
The other printed fatal: ambiguous argument 'FETCH_HEAD': unknown revision and then said it was
preserving the claim. The WU shows in_progress, but it has no lane branch, no claim ref and no
worktree, so wu:recover --action restore and --action resume both refuse.
Cause: older versions recorded the claim commit by re-reading the checkout’s FETCH_HEAD after
the push. Every fetch in a checkout rewrites that one file, so another claim’s fetch in progress
could empty it after the claim commit was already published.
Current behaviour: concurrent claims from one checkout are supported. A claim records the exact
commit it pushed and never reads FETCH_HEAD. If a claim fails after its canonical claim commit is
published but before its lane branch and claim ref are published, it rolls the canonical record
back to ready instead of preserving it. Rerun wu:claim afterwards.
Fix for a WU left behind by an older version:
INTERRUPTED_DONE: the lane says done but main says in_progress
Section titled “INTERRUPTED_DONE: the lane says done but main says in_progress”Symptom: wu:done stopped after it committed the done record on the lane branch and before it
landed. wu:status shows main in_progress and the branch done; wu:repair --check reports
INTERRUPTED_DONE. The lane holds unlanded work, so nothing auto-repairs or deletes it; never run
plain wu:repair or wu:recover --action reset on it.
Fix (from the main checkout):
Then run wu:prep from the worktree and the wu:done it prints. The release backs the lane tip up
to backup/<wu-id>-interrupted-done-<sha>, removes only the unlanded done commit (what wu:done
itself rolls back when a landing fails) and replays every later commit; the claim then reclaims the
retained work. Without --keep-branch the release refuses.
wu:rebase after a passing wu:prep refuses as dirty
Section titled “wu:rebase after a passing wu:prep refuses as dirty”Symptom: an older WU whose prep passed needs to catch up to main, and wu:rebase reports a dirty
working tree. A passing prep leaves its rows in .lumenflow/skip-gates-audit.ndjson uncommitted.
Fix: none needed. wu:rebase takes custody of that unstaged append the way wu:done does: it holds
the append out of the tree while Git runs and puts it back byte for byte, once, after success, a
content-conflict pause, a failure and --abort (--continue and --abort do the same). Do not commit
the audit rows to get a clean tree. A staged, rewritten or malformed audit change, a row for another
WU, or any other modified or untracked file still refuses with the dirty-tree message before anything is
fetched. A rebase invalidates the old prep evidence, so run wu:prep again on the rebased tip.
The same holds when you also ran mem:checkpoint: its row in the tracked wu-events.jsonl ledger is a
second, legitimate kind of dirt, and wu:rebase holds and restores it together with the audit append,
including on --continue and --abort. Only a compact checkpoint row exactly as the checkpoint command
wrote it, for your own WU and carrying your claim session, is taken. The session match filters accidents;
it is not authentication, and custody never commits a row. A row of another WU or session, a row with no
session, any other event type, a tracked proof checkpoint left uncommitted, or a rewritten, truncated or
staged ledger still refuses. When a refusal is unclear, the log carries the specific reason. The check never compares the row’s time with
your clock. A paused rebase whose replayed commit only staged the ledger has nothing to hold, so --continue
and --abort always work. If a run is killed between taking the rows out and putting them back, the next
ordinary wu:rebase re-appends them from the retained copy under the Git common directory
(lumenflow-generated-audits/<WU>/) and removes it, but only into the same claim and checkout; a copy from
another claim stays put with a warning, and a copy naming a ledger this checkout lacks is renamed
*.quarantined.
Taking over a stuck claim: no manual commit step
Section titled “Taking over a stuck claim: no manual commit step”Symptom: a claim is stuck and a second session takes it over, for example with
pnpm agent:session --wu <wu-id> --resume, a checkpoint or a recovery commit of its own, and then
wu:release --keep-branch. Earlier releases refused for about half an hour with Recent successor activity guard failed, counting the taking-over session’s own commit and events as someone else’s.
Fix: none is needed. When the same-user claim authority holds (your Git identity and the live
origin claim) the session you run wu:release from is the claim’s holder, and the activity guard
ignores what that session and the session named in the claim record recorded. A ledger event recorded
by any other session within the window still refuses, so the release stays safe against a live
successor. The sequence is wu:release --keep-branch, then the ordinary wu:claim, then
wu:brief; none of it asks you to commit a ledger line by hand.
wu:repair --claim --check likewise accepts the claim-auto checkpoint row that wu:claim records,
if it is ever left staged: it tolerates exactly that row (and the claim’s own claim rows) for this
WU, and refuses any other staged file or row.
A finished WU left its claim marker on origin (CLAIM_LANE_MISSING for a done WU)
Section titled “A finished WU left its claim marker on origin (CLAIM_LANE_MISSING for a done WU)”Symptom: lumenflow:upgrade or another command prints CLAIM_LANE_MISSING: <wu-id> is done on the integration branch and its lane ... is gone, so the claim marker refs/lumenflow/claims/<wu-id> is a leftover of a finished WU, not a live claim; or wu:done ends with an error line saying the marker
was NOT removed.
Fix: pnpm state:cleanup --claim-markers (add --dry-run to preview). It removes the marker of
every WU that origin main records as done, whose claim generation equals the marker’s and whose lane
branch is gone. Each delete leases the marker sha it observed, and an in-progress claim is never
touched. lumenflow:upgrade runs the same cleanup before its claim checks and reports the WU ids it
removed. wu:done retries a slow marker delete on longer bounds before it reports a failure. A
CLAIM_LANE_MISSING message that ends in a git push is a live claim: follow it instead.
remote-diverged: reclaiming a kept branch whose origin lane copy differs
Section titled “remote-diverged: reclaiming a kept branch whose origin lane copy differs”Symptom: after wu:release --keep-branch, wu:claim refuses Retained-work reclaim refused for <wu-id> (remote-diverged). The usual cause is a governed wu:rebase whose claim-source publication
failed (for example at a pre-push hook): origin kept the pre-rebase lane tip, the kept branch carries
the rebased commits, and the origin tip is no longer an ancestor.
Fix: none is needed when the kept branch is a rebase of the origin lane. Run the ordinary
pnpm wu:claim --id <wu-id> --lane "<lane>". The claim compares the two sides by patch id, accepts
origin-only commits that have a patch-identical twin on the kept branch (or that touch only lifecycle
bookkeeping and the WU record), and publishes the kept branch under a lease on the origin tip it
observed. No git reset and no force push is involved. The replaced origin tip stays reachable under
the local backup ref refs/lumenflow/lane-backups/<WU-ID>/<sha12>.
Genuinely divergent lane: when origin holds commits with different content, the refusal names both tips and prints the governed command:
It reclaims from the kept branch, publishes it under the same lease and keeps the origin tip under the backup ref, so its commits can be cherry-picked afterwards. Nothing is discarded silently.
”Trusted policy content changed since this claim pinned it” during wu:done
Section titled “”Trusted policy content changed since this claim pinned it” during wu:done”A trusted policy file (workspace.yaml, LUMENFLOW.md, LUMENFLOW.local.md) can change on the
integration target while your wu:done waits for gates or the merge lock. wu:done checks the claim’s
policy pin before it writes any done metadata, and again when it is about to publish. Both checks happen
before the push, so the refusal always means your run published nothing:
- Refused before the metadata commit: the lane, the index and the WU record are untouched.
- Refused after the metadata commit (the change landed during the merge lock wait or the auto-rebase):
wu:donerolls the lane back to its pre-done commit (“WORKTREE RESTORED, RERUN IS SAFE”). It no longer reports “publication outcome is unknown”.
Run pnpm wu:prep --id <wu-id> again: when main moved the policy and your branch changed no trusted
policy file, prep re-pins the claim itself (audited as a claim_policy_refresh event with the reason
main-moved), runs wu:brief, and re-runs. If your branch edits a trusted policy file, or prep still
refuses, run the steps the box prints, in order. The refresh re-pins the claim and invalidates earlier
prep evidence, so wu:prep runs again:
then the wu:done command prep prints.
Your own generated audit append and mem:checkpoint rows in the claim worktree need no action: the refresh
and wu:rebase hold them out of the tree and restore them byte-exactly once, and --check lists the paths it
would hold. If the refresh refuses with “changes that are not its own lifecycle appends”, it names the path;
nothing was published on a first run, so commit your own lane work with a hooked commit and re-run. When the
refusal says the successor generation is already published, resolve the named path and re-run the same refresh
to finish the sync. Running wu:rebase first is safe: it refuses naming the refresh and leaves those rows untouched.
If a refresh or rebase is killed while it holds your appends, the next wu:repair --refresh-policy or
wu:rebase puts the generated audit append and the checkpoint rows back exactly once, including when the kill
came after the claim marker moved to the successor generation. A retained copy that no longer matches its
recorded digest is set aside (quarantined) and the run refuses before restoring anything; inspect it under the
Git common directory (lumenflow-generated-audits/<WU>/) before re-running.
If the run instead refuses with “Retained lifecycle copies could not be inspected”, it could not list or read
the retained copies of one kind (ledger or audit): the message names the kind, the path and the
underlying error, and says that nothing was restored, quarantined or removed. Both kinds are inspected
together, so neither is restored while the other cannot be read. Restore read access to the named path
(a permission, a locked file, a failing disk or share) and re-run the same command; it then recovers every
copy exactly once. A read failure never quarantines a copy, so waiting cannot make the refusal permanent. The
same refusal covers a retention path of the wrong type (a file where the directory should be) and an audit copy
whose receipt exists but whose content file is missing (a copy with no receipt at all is an interrupted hold that
never held anything: it is left in place with a warning and does not block the run; it is not cleaned up automatically, the warning
cannot tell an interrupted retention from a stripped receipt, and the next hold of the same append rewrites it): the named path shows what to inspect, and only a copy whose bytes were
read and fail their recorded digest is ever quarantined.
A refresh that was published but never finished (the reported “Published claim source descriptor differs from the active complete binding”) is finished by a real wu:rebase (--dry-run only reports what it would finish) or wu:recover --action resume from the claim holder session, through the resume-only mode of the governed refresh: it never mints a new generation, whatever a marker or a record names. A refused finish changes nothing; if the lane sync committed and only the marker publication failed, re-running finishes it. A refresh interrupted after the lane sync (only the claim-source marker still names the previous generation) and a chain of several published refreshes are finished the same way. The finish is skipped while a governed content-conflict pause is active (use --continue or --abort). If the finish cannot complete, the refusal names the exact wu:repair --claim --integration-target --refresh-policy --id <wu-id> command that resumes it.
Run the refresh from the claim worktree or from the primary checkout (it finds the registered lane
worktree itself). It refuses before publishing when the worktree has uncommitted changes, so commit or
discard them first. If a sync still fails after the successor was published, the refusal says the
refreshed generation is already published and the lane and claim-source marker are unfinished: fix
that cause and re-run the same refresh command, which resumes. A lane left on the previous generation
is read through the integration target by wu:rebase, wu:recover --action resume and the heartbeat
only when it is a verified predecessor (same WU, same lane branch, a recorded refresh chain); anything
else still refuses.
If an older run already left a done commit on the lane that is not on the target (the lane says done,
the canonical record says in_progress, and wu:rebase or wu:done refuse with “Worktree claim binding
state conflict between canonical and worktree ledgers” or CLAIM_DOCUMENTS_DISAGREE), run the same
wu:repair --claim --id <wu-id> --integration-target (add --refresh-policy --reason when the policy also
changed). As the claim holder it fetches the target, proves the done commit is not on it, and removes
only that commit behind backup/<wu-id>-interrupted-done-<sha>, keeping every later commit (a
retained-branch reclaim’s commits included). It changes nothing when the done commit is on the target, when
the target cannot be fetched, or when you are not the holder. Only after that does it publish the claim
lane; if the lane ledger still records the WU as done ahead of the target and no done commit can be
located, it refuses with UNLANDED_DONE_UNPROVEN and publishes nothing.
A branch released by an older CLI can keep the done commit: an ordinary wu:claim of it unwinds that
commit itself (same backup branch), so the reclaimed lane and the canonical claim both say in_progress
and wu:rebase, wu:prep, wu:done and wu:recover proceed. A lane already reclaimed by an older CLI
whose worktree ledger still carries a complete older than the canonical claim generation is accepted by
wu:rebase; a complete newer than that claim is still refused.
A done WU’s only worktree registration points at a directory that no longer exists
Section titled “A done WU’s only worktree registration points at a directory that no longer exists”Symptom: a scratch checkout of a done WU was deleted (it may have lived outside the worktrees
directory), git worktree list shows the entry as prunable, and every wu:claim in the lane is
refused with “orphan done WU”.
Fix: pnpm wu:repair --id <wu-id> removes exactly that one registration, after confirming the
directory is absent, git reports the entry prunable, it is not locked, and the WU has its done stamp.
The WU’s lane branch is deleted locally and on origin, as with any orphan repair. Other worktrees, branches, logs and unrelated stale registrations are left alone, so the broad
wu:prune --execute is not needed. A locked registration is refused; unlock it first with
git worktree unlock if the lock is stale.
Orphan-done worktree repair does not clear a suffixed re-adoption worktree
Section titled “Orphan-done worktree repair does not clear a suffixed re-adoption worktree”Symptom: a done WU still has a leftover worktree, but the leftover directory
uses a suffixed name from re-adoption (<lane-kebab>-<wu-id>-r2, -p0, …) rather than the
canonical <lane-kebab>-<wu-id> name — for example a wu:done cleanup step that failed partway
through an ownership handoff. wu:repair --id <wu-id> reports Successfully repaired, but
wu:repair --id <wu-id> --check still reports ORPHAN_WORKTREE_DONE, and every wu:claim in the
same lane is refused by the lane’s orphan pre-flight.
Fix: none needed — wu:repair resolves every worktree registered under git worktree list
that matches the WU id (by directory basename or branch name), not only the canonical directory
name, so a suffixed re-adoption worktree is found and removed in the same pass as the canonical
one:
Before removing a matched worktree, the repair quarantine-copies its memory store (if
present) into the workspace’s quarantine area and records the copy
location in a cleanup log, so a shared-memory node written inside
the leftover worktree is never silently discarded. The repair reports success only once
git worktree list no longer matches the WU id at all; if a matched worktree cannot be removed (for
example it is git worktree locked), it reports a named failure or skip listing every path it examined
instead of a false success.
WU status mismatch
Section titled “WU status mismatch”Symptom: backlog.md says one status, WU YAML says another.
Fix:
wu:release reports WU YAML / state-store divergence
Section titled “wu:release reports WU YAML / state-store divergence”wu:release compares the WU YAML status with the event-sourced state store before any release
mutation. When they disagree, it stops without changing the YAML, event log, derived docs, branch,
or lane lock and names both statuses, for example:
Run the named repair, review its output, then rerun the intended release command. Do not use a release retry or a force flag to choose one state source over the other.
wu:rebase refuses a legacy claim with no claimed_branch
Section titled “wu:rebase refuses a legacy claim with no claimed_branch”Symptom: wu:rebase fails closed because the WU documents carry no claimed_branch, and
wu:repair --claim refuses first with:
Claims minted before the branch-binding mechanism have no claimed_branch in either the canonical
or the worktree document, and their local baseline_main_sha is often stale because the branch has
not been rebased since it was claimed. The repair needed the rebase, and the rebase needed the
repair.
Fix: run the claim repair as the recorded claim owner, with --base-dir naming the canonical
main checkout. It now reconciles the binding and the baseline together:
--check reports the absent binding and, when it applies, the divergent baseline as repairable.
It never advertises a repair the apply path would reject: both resolve the canonical baseline
through the same authority, so a well-formed but unresolvable object name fails --check closed
instead of being reported repairable. The apply path writes the live attached branch into both
documents and refreshes the local baseline_main_sha to the canonical value in the same commit,
so a half-repaired claim document is never published.
Scope of this repair. It heals a claimed_branch that is absent from a local record over
coherent canonical history. A canonical commit that was already published without
claimed_branch is immutable history, and no repair heals it.
If a governed rebase is paused, --continue refuses before any write and
wu:repair --claim refuses while the pause is active. Run
pnpm wu:rebase --id <wu-id> --abort, then pnpm wu:repair --id <wu-id> --claim, then rerun
pnpm wu:rebase --id <wu-id>. Unpaused, the repair and then the rebase complete.
Only a claim that predates integration-target binding is upgraded with
pnpm wu:repair --id <wu-id> --claim --integration-target --check, then without --check.
Other worktrees do not interrupt the repair. The repair proves this claim’s own worktree
registration (the records on its branch or at its path, with their branch and HEAD), not the whole
git worktree list. A sibling worktree committing, being added or removed, or an unrelated
micro-worktree no longer makes it refuse with authority changed. A moved own HEAD, a branch switch
at the same tip, a second registration of the lane branch, or a changed local or remote lane ref
still refuse before any write. A detached probe worktree that vanishes while the repair attests is
re-listed a bounded number of times; a stale record that persists keeps the
pnpm wu:prune --tmp-only remedy. Only transient git errors (lock contention) are retried; a
mismatch never is.
Lane already has a WU in progress
Section titled “Lane already has a WU in progress”Symptom:
Fix:
- Check if
<wu-id>is truly in progress:pnpm wu:status --id <wu-id> - If blocked, mark it:
pnpm wu:block --id <wu-id> --reason "Waiting for API" - If done, complete it:
pnpm wu:done --id <wu-id> - If orphaned, recover:
pnpm wu:recover --id <wu-id> --action reset
Lane lock not auto-clearing despite dead PID
Section titled “Lane lock not auto-clearing despite dead PID”Symptom:
The lock holder’s process is no longer running, but the lock is not auto-cleared.
Explanation: Lane locks persist after the wu:claim process exits. A dead PID alone does not trigger auto-clearing — the lock must also be older than 2 hours (stale). This prevents a second claim from stealing a legitimately held lane.
Fix:
- If the lock is recent (less than 2 hours old), it is likely still valid. Complete or release the existing WU first.
- If the lock is genuinely abandoned, unlock manually:
- Locks older than 2 hours with a dead PID will auto-clear on the next
wu:claimattempt.
Evidence-only state drift during wu:done
Section titled “Evidence-only state drift during wu:done”Symptom:
But git status shows only the WU event log (wu-events.jsonl) as modified (from a recent wu:brief run).
Cause: Running wu:brief records evidence to wu-events.jsonl. If other WUs merged to main and triggered auto-rebase, the rebase could fail because this workflow-generated file was dirty.
Fix: As of v3.19.0, wu:done auto-rebase automatically preserves and restores wu:brief evidence when wu-events.jsonl is the only dirty file. If you’re on an older version, commit the evidence file before retrying:
Ownership violation on wu:block or wu:release
Section titled “Ownership violation on wu:block or wu:release”Symptom:
or:
Cause: As of v3.19.0, state-mutating commands validate session ownership. You’re trying to modify a WU that belongs to another agent session.
Fix:
- Don’t modify another agent’s WU to free a lane. Wait for the owning WU to complete or block itself.
- If you have legitimate reason for cross-session recovery, use the override:
All overrides are audited.
Worktree Issues
Section titled “Worktree Issues”A claimed worktree cannot run wu:brief, wu:edit or wu:prep
Section titled “A claimed worktree cannot run wu:brief, wu:edit or wu:prep”Symptom:
What it means: wu:claim refused to report success for a worktree it could not prove is
usable. This is the automatic bootstrap contract: a claim either hands you a worktree that runs the
governed commands, or it does not complete.
wu:claim performs three steps, and only reports success after all three:
- Seed generated artifact roots from the main checkout, following the workspace dependency
graph in deterministic topological order, so a dependency’s artifacts land before a dependent’s.
An artifact is refused rather than copied when it resolves outside the main checkout or when its
provenance records a different Node major or ABI than the claim is running. Mutable
node_modulesstate is never copied. - Install worktree-local dependencies with the immutable install for your package manager
(
pnpm install --frozen-lockfile,npm ci, …). This resolves dependencies; it does not build or validate the artifacts those dependencies expose. - Verify by really running
wu:brief --help,wu:edit --helpandwu:prep --helpinside the worktree, through the checkout-local dependency graph. The first of these builds a coherent local CLI dist if one is owed.
Step 3 exists because step 2 can report success over an incoherent artifact graph. Executing the command is the only check that distinguishes artifact presence from artifact coherence.
In a project that installs @hellmai/lumenflow-cli as a dependency, each command runs as the
package’s own bin script under the current Node executable (node <package>/dist/wu-brief.js --help), never through the node_modules/.bin launcher, so the check behaves the same on Windows,
macOS and Linux. The message ends with the exact command it ran and why it failed: the spawn error
code and message when the process could not be started at all, or the exit code and the command’s
own output when it started and failed.
Fix (a project that installs @hellmai/lumenflow-cli):
- Confirm
@hellmai/lumenflow-cliis a declared dependency and reinstall dependencies in the main checkout with the project’s package manager. - Run the command the message printed, by hand, from the worktree to see the same failure directly.
- Claim again.
Fix (the LumenFlow framework repository itself, which builds its CLI from source):
The failed claim rolls back atomically, so no in_progress WU, lane lock, branch or worktree is
left behind and nothing needs cleaning up first.
Note on --skip-setup: it opts out of all three steps. It is read-only by design — it seeds
nothing, installs nothing, builds nothing and therefore certifies nothing — and a --skip-setup
worktree is not certified to run wu:brief, wu:edit or wu:prep. It still fails closed when the
main checkout it reuses has unsafe dependency links.
Note on --help: <command> --help skips the build when this checkout already has a CLI dist,
but it never answers from another checkout’s dist. What help is being asked in a fresh worktree is
whether this checkout can run the command; a file in the main checkout has never been evidence
about this one. With no local dist, help builds one.
Worktree not found
Section titled “Worktree not found”Symptom:
Fix:
Changes made in wrong directory
Section titled “Changes made in wrong directory”Symptom: You made changes in main instead of the worktree.
Fix:
Worktree has merge conflicts
Section titled “Worktree has merge conflicts”Symptom: wu:rebase pauses after reporting ordinary content conflicts.
Fix:
The governed continuation verifies the active WU, lane branch, worktree and claim, integration
target and SHA, replayed commit, paused HEAD, unresolved set, and staged path scope. It never
selects ours or theirs. If one additional reviewed path must be staged, authorize it explicitly
with repeatable --allow-staged <repo-relative-path> flags. To discard the attempt and restore the
pre-rebase branch state, run pnpm wu:rebase --id <wu-id> --abort.
A paused wu:rebase refuses --continue because the policy pin is stale
Section titled “A paused wu:rebase refuses --continue because the policy pin is stale”Symptom: a trusted policy file (workspace.yaml, LUMENFLOW.md, LUMENFLOW.local.md) changed on the
integration target while the rebase was paused, and wu:rebase --continue refuses with “Trusted policy
content changed since this claim pinned it”. wu:repair --claim --integration-target --refresh-policy
also refuses, because the lane is mid-rebase.
Fix: end the pause with the governed abort, refresh the pin, and rebase again. --abort only restores the
pre-rebase state and grants nothing, so a stale pin never blocks it; --continue creates lane commits, so it
keeps the pin check. The abort clears the pause record itself, so never run git rebase --abort or move the
record by hand.
The only conflict is pnpm-lock.yaml (parallel dependency changes)
Section titled “The only conflict is pnpm-lock.yaml (parallel dependency changes)”Symptom: another WU that changed dependencies landed first, and the only conflict in your
rebase is pnpm-lock.yaml.
Fix: none needed. wu:done’s auto-rebase and wu:rebase regenerate the lockfile from the
merged manifests, continue the rebase, and wu:done re-runs the gates on the result. You do not
release, re-claim or carry files.
How it works:
- It runs only when
pnpm-lock.yamlis the single unmerged path. It starts from the integration target’s lockfile and runspnpm install --lockfile-only --ignore-scripts --no-frozen-lockfilein your lane worktree, using the pnpm your repository pins inpackageManager. No script runs andnode_modulesis not touched. - It checks that the result has no conflict markers, parses, passes a
--frozen-lockfilecheck, and that no other file changed. Only then does it stage the lockfile and continue. - Each regeneration appends a
lockfile-regenerationrow to the git-ignored meta-rebase audit log.
If regeneration fails (registry unreachable, a manifest error, a timeout), wu:done aborts the
rebase, leaves the worktree clean, and prints pnpm-lock.yaml was the only content conflict, but regenerating it from the merged manifests failed: <reason>. Fix the cause, then run
pnpm wu:rebase --id <wu-id> from the worktree. If it records a pause, leave pnpm-lock.yaml
unresolved, resolve and stage any other named paths, and run:
--continue regenerates the lockfile and finishes the rebase. If it fails again it refuses and the
conflict stays intact. Any other conflict beside the lockfile, a lockfile deleted on one side, or a
package manager other than pnpm keeps the behaviour described above: wu:done gives manual-fix
guidance and wu:rebase pauses. Never resolve the lockfile by choosing a side.
A gate cannot find a dependency a landed WU added
Section titled “A gate cannot find a dependency a landed WU added”Symptom: after another WU landed a dependency change, a gate fails with a missing module or an unresolved workspace package that your code did not change.
Fix: none needed. wu:done’s auto-rebase and wu:rebase compare package metadata across the
rebase. When it changed, they run pnpm install --frozen-lockfile --offline in your worktree (online if
the offline run fails), once, before any gate. When it did not change, no install runs.
If the install fails or times out, the command refuses and prints the exact repair command, so no gate runs on stale links:
Run it, then retry pnpm wu:done --id <wu-id> (or pnpm wu:prep). The rebase is already complete, so a
retry does not reinstall by itself.
Continue a conflict pause after a handoff
Section titled “Continue a conflict pause after a handoff”A fresh session of the assigned user may continue when its own Git identity and
live origin claim ref verify. A different authenticated session of the assigned
Git user uses the original unchanged valid wu:brief receipt when origin proof
is unavailable, including a legacy claim without that ref. Rebase still requires
the caller’s Git identity to match the canonical assigned_to owner:
LUMENFLOW_HANDOFF_RECEIPT also supplies the receipt. Resolve and stage only the
reported conflict paths first; additional staged paths require repeated
--allow-staged flags. The CLI authenticates the pinned native pause before
checking the receipt against the original branch commit’s content identity,
then rechecks current canonical ownership, policy, protected scope, expiry,
addressee and journal evidence. Successful continuation spends rebase once;
a replay refuses. An explicit invalid receipt cannot fall back to same-user
claim authority.
If replay pauses before a later tracked pending scope update, verification reads the original effective protected WU fields from the authenticated original tip’s WU YAML and validated pending journal, with its legacy tracked event ledger as fallback. A newer current private pending protected edit still invalidates the receipt. Malformed or unreadable historical metadata refuses; an absent optional journal or ledger alone is allowed.
The current canonical protected declaration must match the original tracked WU
declaration before the historical pending snapshot applies. This conservative
guard can refuse even a canonical protected edit made before receipt issuance
when it differs from the original branch YAML. Metadata-only edits remain
allowed. Do not restore metadata or re-brief while paused to force a match.
Current wu:edit normally writes a private journal; intentionally committing
that journal is compatibility test scaffolding, not a normal recovery step.
The command syntax and help are unchanged.
Do not run wu:brief while paused, copy the claimant’s session ID, or alter the
receipt or pause record. A brief commit changes the paused HEAD. If the pinned
refs, WU/worktree identity or protected context changed, recovery refuses; inspect
the named reason and use supported --abort when its guards still hold.
Once continue or abort clears the pause, use normal briefing if needed.
Git Issues
Section titled “Git Issues”Git objects are empty after an unclean shutdown (GIT_OBJECT_STORE_CORRUPT)
Section titled “Git objects are empty after an unclean shutdown (GIT_OBJECT_STORE_CORRUPT)”Symptom: after a power loss or hard reboot, a lifecycle command (wu:claim, wu:prep,
wu:done, wu:recover, wu:rebase, wu:status) stops with [GIT_OBJECT_STORE_CORRUPT]. The
message names the damaged object ids and the quarantine directory. git’s own
error: object file ... is empty / fatal: bad object follows it.
Cause: git’s default core.fsync=committed,-loose-object does not flush loose objects to disk,
and git-config(1) warns that it “risks losing recent work in the event of an unclean system
shutdown”. LumenFlow’s ledger commits, claims and rebases are loose-object writes.
Fix: follow the steps the error prints, in order. Nothing is deleted:
- Move each empty object file it lists into
.git/lumenflow-object-quarantine/. - Run
git fetch --refetch origin(git 2.36 or later; older git:git fetch origin).- If it stops at
bad object refs/heads/<branch>, that branch’s newest commits were never pushed and are lost. - Point the branch at its pushed state with
git update-ref refs/heads/<branch> refs/remotes/origin/<branch>, then repeat this step.
- If it stops at
- Run
git fsck --connectivity-only. Everyinvalid reflog entry <id>ormissing <type> <id>it prints is a commit that was never pushed. Its files are still in your working tree: rungit statusand commit them again. - Run
git fsck --connectivity-only --no-reflogs. It exits 0 once the object store is sound.
pnpm lumenflow:doctor runs the same scan on demand (zero-byte loose objects and an unresolvable
HEAD) in its Git Object Durability section and prints the same steps.
Prevention: lumenflow init, pnpm lumenflow:upgrade and the pnpm lumenflow:enable repair
set two repository-local keys. They need git 2.37.0 or later.
core.fsync=committedhardens loose and packed objects.core.fsyncMethod=batchmakes one disk flush per operation instead of one per object.
One write covers every lane worktree. A stricter value you already set (for example
core.fsync=all or core.fsyncMethod=fsync, locally or globally) is kept. A weaker one is extended,
never replaced. The cost on a claim is small, inside run-to-run noise.
pnpm lumenflow:doctor reports one of five states:
hardenedowner-setopted-outmissinggit-too-old, which names the minimum version
To opt out:
A git command was terminated by a signal (GIT_TERMINATED_BY_SIGNAL)
Section titled “A git command was terminated by a signal (GIT_TERMINATED_BY_SIGNAL)”Symptom: a lifecycle command stops with git push was terminated by signal SIGPIPE before it reported an exit code (the subcommand and the signal vary). It often follows a long pause in a
push, for example a slow pre-push hook.
Cause: git did not exit on its own; a signal stopped it. The usual cause is a dropped remote connection. SSH or HTTPS closes while git is waiting, and git dies writing to the closed pipe. Git never reported whether the operation completed, so LumenFlow treats it as failed rather than trusting it.
Fix: check what reached the remote, then run the same command again. For a push, compare
git ls-remote origin <ref> with your local ref. Lifecycle commands are safe to rerun.
Profiling shows a 50 ms gap after quiet git commands
Section titled “Profiling shows a 50 ms gap after quiet git commands”Symptom: in a profile or spawn-timing trace of a lifecycle command, git commands that print
nothing (cat-file -e, check-ref-format, a clean status --porcelain, a quiet fetch) each
finish about 50 ms after the git process has exited. A command that prints output has no gap.
Cause: in earlier releases, the git library LumenFlow uses (simple-git) waited a fixed
50 ms after any git command that wrote nothing, even after the process and its output had closed. A
wu:claim runs about 40 such commands and a wu:done about 75.
Fix: upgrade. LumenFlow now reports each git command as soon as its process and output have closed. Exit codes, error messages, signal handling (see the entry above) and hook output behave exactly as before. If a hook leaves a background process holding git’s output open, the command still finishes when git exits, as before.
wu:done reported failure after main moved
Section titled “wu:done reported failure after main moved”Symptom: wu:done exits non-zero or prints RESULT ... status=failure, but the lane branch’s
commit is already on origin/main (check git log origin/main or the WU YAML’s status: done).
Cause: Once wu:done’s merge/push transaction has committed, every remaining step (worktree
and branch cleanup, auto state cleanup, external work-item closeout) is advisory: it reports its
own outcome on a POST-PUSH ADVISORY line and can never flip the command’s RESULT line or exit
code. A concurrent sibling landing racing the auto-cleanup identity check, a merge lock
held by another wu:done, or a worktree cleanup step can each fail independently after the
landing is already durable.
Fix: Do not retry wu:done blindly — retrying an already-landed WU can duplicate cleanup work
or trip an ownership guard. Read the printed POST-PUSH ADVISORY line(s) to see which step
failed, and address that step directly:
wu:done failed before publishing
Section titled “wu:done failed before publishing”Symptom: wu:done stops with a failure box before anything lands (a failing hook, a formatter
or docs:generate error, a staged-file check, a stale or unstamped dist inside the transaction),
and the output starts with WU:DONE FAILED.
What happens: wu:done records the worktree’s pre-done state before it writes anything. When
publication is proven absent, it restores HEAD, the original index bytes, pre-existing staged and
partially staged changes, user file bytes and modes, untracked files, and metadata. Completion-
introduced files are removed or restored. You do not unstage or restore anything by hand.
Fix: read the error in the box, fix its cause, and run the command the box prints:
The box says WORKTREE RESTORED, RERUN IS SAFE when the restore completed. If it says WORKTREE NOT FULLY RESTORED, it lists what could not be restored. Run git -C <worktree> status and
pnpm wu:recover --id <wu-id>, then rerun. Do not use a raw reset or a manual index edit.
wu:done failed after publication or with an unknown outcome
Section titled “wu:done failed after publication or with an unknown outcome”Symptom: wu:done exits with a failure after its completion commit, and the output says
Publication was verified or Publication outcome is unknown.
What happens: A verified publication is irreversible: the local completion commit is preserved
and the output names --already-merged for reconciliation. If Git cannot verify the target, the
outcome is unknown. This includes a negative ancestry check: it proves only that the captured lane
SHA is not an ancestor, since a replayed completion may have landed with a different SHA. The local
commit and worktree are preserved, and the command does not claim that nothing was published or
that a plain rerun is safe.
Fix: For a verified publication, reconcile the completed WU:
For an unknown outcome, first check the exact lane commit against the integration target:
If the ancestry check succeeds, use --already-merged. If it does not, inspect the target history
and completion files for a replayed commit, and confirm publication is absent before retrying
pnpm wu:done --id <wu-id>. Preserve the WU worktree and its completion files while the outcome is
unresolved; do not reset or blindly replay the merge.
wu:done refuses: Spawn brief attestation mismatch
Section titled “wu:done refuses: Spawn brief attestation mismatch”Symptom: wu:done for a delegated WU (wu:delegate, or wu:brief --parent-wu) refuses with
Spawn brief attestation mismatch, even though the pickup was recorded and prep was green.
Cause: The delegation records two different hashes. The wu:brief evidence row hashes the brief
before the lifecycle handoff authority is appended. The registry attestation promptHash hashes the
final bytes after it. Entries written before the fix carry only promptHash, so the two never matched.
What wu:done verifies now (read-only, nothing is written):
- Claim-time pickup is present, and the attestation exists. Both are still required.
- Entries that carry
contentHash(the pre-authority hash): accepted when it equals a recordedwu:briefevidence hash of the same WU. The evidence metadata usescontent=(written by the current resolver) as well as the legacyhash=; either matches.promptHashstays recorded as the final-brief attestation. - Entries without
contentHash:wu:donesearches the retained.logs/wu-delegate-<WU>.mdand.logs/wu-brief-<WU>.mdin the checkout it runs from and in the main checkout. It accepts only a candidate that hashes topromptHashwith the same length AND whose pre-authority bytes (the authority block removed) hash to a recorded evidence row of the same WU. Reads are bounded (regular files up to 4 MiB); a directory, oversized or unreadable candidate is skipped and the search continues. - Those filenames are reused by later legitimate briefs (your own first post-claim
wu:brief, an explicitwu:brief --parent-wu). A candidate that does not match is not the delegated brief and is ignored; it is never treated as a contradiction. If no candidate matches,wu:donerefuses.
Fix: No hand edit of the registry or journal and no --force is needed when the delegated brief is
retained. The existing --force is an audited override (it records one), but it asserts rather than
proves. There is no new repair writer; the existing delegation writer now records contentHash. Do not delete or move the retained .logs files, and do not re-run wu:delegate for a claimed
WU, because that overwrites the retained file. Entries recorded after the fix need no retained file.
Stale local main: if the refusal reads Local main is behind origin, the delegation evidence is on
origin/main but not in your checkout. Run pnpm git:sync-main and retry pnpm wu:done --id <wu-id>;
no --force is needed. wu:done finds this by reading the ledger from the origin/main it already
fetched. The Spawn brief attestation mismatch text appears only when origin does not hold the evidence
either.
Limit: .logs/ is untracked and per checkout. An entry without contentHash can only be proven from
the checkout (or its main checkout) where the delegation ran; a delegation run on another machine, or a
deleted or overwritten file, cannot be proven and wu:done stays refused.
wu:prune lists a worktree as unmanaged
Section titled “wu:prune lists a worktree as unmanaged”Symptom: wu:prune prints Unmanaged worktree: <path> (branch <name>), for example a
worktree a desktop coding tool made under .claude/worktrees.
Cause: LumenFlow creates lane worktrees under its worktrees directory on lane/<lane>/<wu-id>
branches and micro-worktrees on tmp/* branches. A worktree outside the worktrees directory on any
other branch was made by something else, so wu:prune reports it once, does not count it as an error
and never removes it. A non-lane branch inside the worktrees directory, or a lane/* worktree with no
WU record, is still an error.
Fix: None needed. Remove it with the tool that created it if it is stale.
wu:done fails with “not a fast-forward”
Section titled “wu:done fails with “not a fast-forward””Symptom:
Cause: Main has new commits since you started your WU.
Fix:
wu:done fails with “Worktree has uncommitted changes”
Section titled “wu:done fails with “Worktree has uncommitted changes””Symptom:
Cause: wu:done requires a clean, committed worktree before it can merge.
Fix:
If the changes were accidental, restore the files, then retry.
Hooks block my commit
Section titled “Hooks block my commit”Symptom:
Cause: You’re trying to commit WU work from the main directory.
Fix:
Commit message missing WU ID
Section titled “Commit message missing WU ID”Symptom:
Fix: Include the WU ID in the commit message, for example:
Branch drift warning
Section titled “Branch drift warning”Symptom:
Fix:
Parallel completions detected
Section titled “Parallel completions detected”Symptom:
Cause: Other WUs merged to main while your WU was in progress.
Fix: Use the governed lane rebase from the WU worktree, then retry:
Rebase a worktree pinned to an older CLI
Section titled “Rebase a worktree pinned to an older CLI”If a long-lived worktree is pinned to an older CLI (such as 6.0.6 or 6.0.8), its wu:rebase may
hard-abort on a spec-YAML content conflict because older versions did not pause for governed
conflict resolution. Run the newer CLI from the main checkout and target the old worktree with
--base-dir:
This uses the main checkout only for its newer CLI; --base-dir keeps the rebase, conflict state,
and Git operations scoped to the worktree. Do not upgrade or reinstall the old worktree’s pinned
dependencies to get the newer CLI, and do not resolve or stage paths that the command did not
report. Use --dry-run first to inspect the rebase, or --abort with the same --base-dir to
discard a paused rebase. Once it succeeds, run pnpm wu:prep in the worktree and finish with
wu:done from main.
Rebase in an installed consumer with configured metadata paths
Section titled “Rebase in an installed consumer with configured metadata paths”wu:rebase reads the target repository’s configured WU, backlog and status paths,
including docs/tasks/wu. It auto-resolves only the active WU spec and configured
lifecycle projections. A conflict in a different WU’s spec aborts the rebase.
A present malformed workspace.yaml refuses; fix the reported configuration with
the supported configuration commands before retrying.
Installed consumers need no Turbo or framework refresh script.
The runtime spawns a fresh Node process from the executing software-delivery
pack’s ./commands/wu/wu-rebase-baseline-refresh entry, resolved and confined to
that installed pack. A consumer-owned script is never used for refresh. The entry
is an internal executable boundary; calling it directly creates no grant or
claim authority. Owner, caller, origin claim or receipt, worktree, branch and
single-use continuation checks remain required.
Framework source targets retain their content-hash workspace build and artifact
coherence checks. A failed build stops before refresh; a failed continuation names
the WU and child exit status. Resolve that output and rerun wu:rebase, then use
normal wu:prep and its printed wu:done command. Do not hand-edit a baseline,
copy a session capability or run a consumer-provided refresh script.
Rebase audit evidence appears
Section titled “Rebase audit evidence appears”When wu:done resolves governed metadata conflicts during an automatic rebase, LumenFlow appends
an audit row to the meta-rebase audit log (meta-rebase-audit.ndjson under .lumenflow/). The file is append-only local evidence. It
is intentionally ignored by Git and remains on disk across lumenflow init and
lumenflow:upgrade.
A manually resolved wu:rebase content session appends a terminal row to the same file after a
validated --continue or --abort. It includes old and final commits, target ref and SHA, all
conflicted paths, explicitly authorized staged paths, resolver identity, timestamps, and the
continued or aborted outcome. The ignored pause record (wu-rebase-conflict.json in the Software Delivery state namespace) remains available while the
rebase is paused and is cleared after the terminal operation succeeds.
Do not commit the audit and do not delete it to satisfy gates. Incremental format checks exclude
only this exact normalized runtime path because Prettier does not parse NDJSON; changed source files
are still formatted and checked normally. If an older workspace shows the file as untracked, run
lumenflow:upgrade to reconcile .gitignore. Reconciliation adds the canonical ignore entry
without deleting or rewriting existing audit bytes.
--continue aborts with “no resolvable conflicts” and no other detail
Section titled “--continue aborts with “no resolvable conflicts” and no other detail”If git rebase --continue itself fails for a real reason after you resolve a content conflict,
wu:rebase now names the exact git or hook output in the abort error, instead of only the generic
“rebase paused with no resolvable conflicts; aborted to avoid a half-rebased state”. The abort also
always clears the pause record (wu-rebase-conflict.json in the Software Delivery state namespace), so the next wu:rebase attempt
starts cleanly. Read the surfaced git/hook text to fix the underlying cause before retrying. A
leftover pause record from before this fix is local, gitignored scratch state; it is safe to remove
by hand.
Control-plane runtime files keep the checkout dirty
Section titled “Control-plane runtime files keep the checkout dirty”The control-plane sidecar writes its latest-tick snapshot and durable outbox under
.lumenflow/state/. These files are local runtime state, including when LumenFlow runs inside a
nested workspace. Run pnpm lumenflow:upgrade --latest to reconcile the canonical root and nested
ignore rules.
If an older installation already tracked
.lumenflow/state/control-plane-sidecar-last-tick.json, upgrade removes the file from Git’s index
without deleting or rewriting the working copy. It does not untrack repository-bound lifecycle
evidence: wu-events.jsonl, migration-complete.json, and WU done stamps remain versioned.
lumenflow:upgrade refuses with .lumenflow/flow.log (changed) before publication
Section titled “lumenflow:upgrade refuses with .lumenflow/flow.log (changed) before publication”Symptom: on a checkout whose state-namespace migration has not run (no migration-complete.json),
the upgrade stops before it publishes with Consumer upgrade state conflict: legacy Software Delivery inputs changed since staging: .lumenflow/flow.log (changed). The upgrade’s own process, and any hook
or agent running a CLI command meanwhile, appends log lines to the legacy .lumenflow/flow.log after
staging.
Fix: none needed from 1.0.116. The staging baseline records the legacy flow log’s byte length and
accepts the file when its first bytes are unchanged and everything after them is complete JSON-object
lines. Those lines are added to the migrated flow.log before it is restored into your checkout, in
order and exactly once, and the legacy file is never rewritten. A rewritten, truncated or reordered
flow log, a partial or non-JSON line, a deleted file and a change to any other legacy input still stop
the upgrade with the same message. On an older release, check that nothing else is writing to
.lumenflow/flow.log, then re-run the upgrade. A line appended after the carry, or left
behind when the upgrade stops before restoration, stays in the legacy flow.log: it is not lost, but
it is not carried into the migrated log.
lumenflow:upgrade failed after publication
Section titled “lumenflow:upgrade failed after publication”Symptom: the upgrade pushed chore: upgrade @lumenflow packages to <version>, then exited 1
with The <version> upgrade commit is published ... but reconciling local state failed after publication. Up to 1.0.11 the message was Consumer upgrade state conflict: legacy Software Delivery inputs changed since staging ... Both copies are preserved, most often because the
consumer still tracked old .lumenflow/telemetry/* files that the upgrade itself untracks.
The legacy baseline stores digests only, so no copy of earlier bytes exists. Since 1.0.116 one more
cause is possible: the carry of legacy .lumenflow/flow.log lines into the staged snapshot refused
(the legacy log no longer starts with the bytes the staging copy read, or the snapshot holds no
flow.log). The recovery message names it, and the legacy file is untouched.
Fix: do not re-run the upgrade; its commit is complete. Run the commands it printed, in order:
The version check reads the installed manifest by path, because the CLI’s exports map does not
expose package.json. The message names every changed path and the staged state snapshot it kept
under the upgrade cache; compare that snapshot with .lumenflow/state/packs/software-delivery/
before copying any file it lacks. Old telemetry files the fast-forward removed from the working tree
remain in Git history (git show <upgrade commit>^:<path>).
From 1.0.12 the check compares only legacy files copied from the main checkout, skips entirely once
migration-complete.json exists, ignores agent-session and conductor writes under
.lumenflow/state/, and lets live claims prune their lifecycle-enforcement/mutations/ receipts.
Software Delivery State Namespace
Section titled “Software Delivery State Namespace”Software Delivery state (the WU
event log, stamps, archive, merge lock, telemetry, flow log, incidents, and
commands log) resolves under a pack-owned namespace instead of generic shared
.lumenflow/<name> paths:
Agent session state (.lumenflow/sessions/) stays at the shared kernel path —
it’s a cross-pack contract with the agent-runtime pack and is not part of
this namespace.
Detection and migration: every CLI command checks for the sentinel file
above before running main(). If it’s missing and legacy .lumenflow/state,
.lumenflow/stamps, etc. exist, LumenFlow runs a one-way, atomic migration:
it copies the legacy content into the namespace through a private staging
directory, then promotes it with a single atomic rename, then records the
sentinel (source digest, target schema version, per-category file counts —
no secrets). The legacy directories are never deleted or written back to
— they remain on disk as a historical record. Once the sentinel exists,
subsequent CLI invocations skip re-verification (a single file-existence
check), so steady-state startup cost is unaffected.
Recovery: the migration is fail-closed. If it’s interrupted mid-run, the
next invocation either resumes cleanly (a crash before the atomic promote
just leaves disposable scratch space that gets cleared) or finalizes cleanly
(a crash after the promote but before the sentinel just needs the sentinel
written — no data is re-copied or duplicated). If it detects the legacy
source has changed since a prior successful migration (tampering or manual
edits), or that the target namespace already contains data that doesn’t match
what the legacy source would produce, it refuses to proceed and leaves both
locations untouched — inspect .lumenflow/state/packs/software-delivery/
manually before retrying.
Removal: lumenflow:uninstall cleans up both the new namespace and any
remaining legacy directories.
Configuration: workspace.yaml’s software_delivery.state.* fields (all
except base, the kernel-shared .lumenflow root) may still be overridden,
but overrides must resolve inside the pack namespace — an override that
points outside .lumenflow/state/packs/software-delivery is rejected at
startup instead of silently redirecting state to a shared writer path.
CLI Issues
Section titled “CLI Issues”Command not found: wu-create
Section titled “Command not found: wu-create”Symptom:
Fix:
Config validation errors
Section titled “Config validation errors”Symptom:
Fix:
- Check the error message for the specific field
- Compare with the Configuration Reference
- Fix the YAML syntax or values
Validate without running:
Memory checkpoint fails
Section titled “Memory checkpoint fails”Symptom:
Fix:
wu:done warns about missing checkpoints
Section titled “wu:done warns about missing checkpoints”Symptom:
Fix: Create checkpoints during long sessions:
Context Validation Errors
Section titled “Context Validation Errors”LumenFlow commands include context-aware validation that checks your location, WU status, and git state before running. When validation fails, you’ll see an error with a copy-paste fix.
WRONG_LOCATION
Section titled “WRONG_LOCATION”Symptom:
Fix: Run the command from the correct location. wu:done runs from main; wu:prep runs from the worktree.
WRONG_STATUS
Section titled “WRONG_STATUS”Symptom:
Fix: Claim the WU first: pnpm wu:claim --id <wu-id> --lane "Your Lane"
GIT_DIRTY
Section titled “GIT_DIRTY”Symptom:
Fix: Commit your changes before running wu:prep:
Configuring Validation
Section titled “Configuring Validation”You can adjust validation behavior in workspace.yaml:
Enforcement Hooks
Section titled “Enforcement Hooks”pnpm lumenflow:integrate is a deprecation no-op — it prints migration guidance and exits 0 without writing any files. LumenFlow no longer generates or manages AI-client-specific enforcement hooks. If your project has a .claude/hooks/ directory from an older LumenFlow version, it is a snapshot — your file now, never regenerated or touched by LumenFlow again. Adapting an enforcement-hook layer into your own tool is a task for your repository maintainers; see AI Coding Assistant Integrations for a copyable example.
In a Git repository, init and pnpm lumenflow:enable activate the generated
.husky/pre-commit hook through local core.hooksPath=.husky only when Git has
no configured hook path or existing default pre-commit hook. They preserve
custom core.hooksPath values and existing default hooks. If your project uses
a custom hook path, integrate the LumenFlow managed block there; doctor reports
the active path and the required remediation when Git will not invoke LumenFlow.
Doctor follows a husky v9 shim directory (core.hooksPath=.husky/_, generated by
husky) to the real .husky/pre-commit it runs, and reports that file as the one
it checked when the LumenFlow block is missing.
Ordinary commits on main or master are blocked with WU workflow guidance
once lanes are locked. While the lanes recorded in the committed
workspace.yaml are still unconfigured or draft, the hook admits the
documented setup commits on main, because wu:create cannot run before
pnpm lane:lock. The hook reads the committed HEAD, so staging a
workspace.yaml edit cannot reopen setup after lanes are locked. Temporary
tmp/* lifecycle commits and the existing release exemption remain available
to their governed commands.
If a hook blocks your operation, check:
- Are you in a worktree? Hooks block edits outside worktrees. Run
pwdand verify. - Do you have a claimed WU? Some hooks require an active WU for edits.
- Is the file in your code_paths? Hooks may restrict edits to scoped files.
See Agent Safety for the full enforcement architecture.
pnpm lumenflow:doctor reports lifecycle enforcement as enforced,
non-compliant-disabled, missing, stale, tampered, or unavailable. For every remediable
non-compliant state, run pnpm lumenflow:enable; it atomically writes the generic delegators and
digest-bound manifest, preserving marker-external user bytes. unavailable means repository storage
cannot be read safely: restore storage access before retrying. Only enforced is certifying.
Disabled and uninstalled consumers cannot claim lifecycle certification.
Local remediation assumes the process follows repository policy.
Bootstrap And Local Runtime Prerequisites
Section titled “Bootstrap And Local Runtime Prerequisites”Clean WU worktrees may not have the CLI built locally. Root LumenFlow package
scripts should route through the LumenFlow CLI launcher so commands can use an existing main
checkout build or build on demand instead of failing with MODULE_NOT_FOUND.
If a package script edit adds or changes LumenFlow CLI aliases, run:
The live probes cover common agent entry points such as mem:create, orchestrate:monitor,
lumenflow:commands, and lumenflow:setup-prereqs.
Before local database or local model workflows, run:
The report checks Supabase CLI, Docker, and Supabase local stack readiness when Supabase config or
deps are detected. It also checks Ollama binary, OpenAI-compatible local API, and configured model
readiness when Ollama deps or OLLAMA_* env vars are present.
Official setup references:
- Supabase local development
- Supabase CLI getting started
- Ollama API introduction
- Ollama OpenAI compatibility
The prereq report intentionally omits local service keys and token-shaped values. Do not paste raw
supabase start, .envrc, or provider-token output into issues, WU notes, or prompts.
Pack refuses every job after a CLI upgrade
Section titled “Pack refuses every job after a CLI upgrade”Pack manifest version mismatch: expected <old>, got <new> on every governed
command right after upgrading @hellmai/lumenflow-cli means workspace.yaml
still carries the software-delivery pack pin the previous CLI wrote.
The CLI self-heals this automatically: a source: builtin pack pin is
resolved from the CLI’s own bundled, unmodifiable packs directory, so a
version or integrity drift against that pin is trusted and loaded rather than
refused, and an advisory notes the drift. Run pnpm lumenflow:upgrade to
clear the advisory by repinning workspace.yaml to the installed CLI’s
bundled version through the same governed pin-write path the upgrade command
already uses. If you installed the CLI directly (npm install @hellmai/lumenflow-cli@latest) rather than through lumenflow:upgrade, or a
standalone Connected Compute runner picked up a new CLI version on its own,
run lumenflow:upgrade once to write the repaired pin and silence the
advisory for good.
Native addon fails to load on a runner (“compiled against a different Node.js version”)
Section titled “Native addon fails to load on a runner (“compiled against a different Node.js version”)”A standalone runner install can carry a native addon (for example
better-sqlite3) built for a different Node ABI than the Node binary the
runner process runs under — most often after upgrading the runner’s Node
version without rebuilding native dependencies, or after copying a cli
directory between machines with different Node versions.
Run pnpm compute:doctor --experimental --runner-dir <path>: the report’s native addon ABI line names every mismatched package and the fix
(npm rebuild <package>) before the runner accepts more work, instead of the
runner re-claiming and re-failing the same assignment repeatedly.
The disk is filling with LumenFlow scratch
Section titled “The disk is filling with LumenFlow scratch”Engine temp directories carry a name prefix and an ownership marker, and they are removed when the command ends. A crash can still leave some behind, and release artifacts accumulate per version.
Check: pnpm lumenflow:doctor prints a “Scratch and Disk” section with the size of each scratch root
and the free space of its disk, and warns above software_delivery.cleanup.scratch_warn_gb (default 10)
or below software_delivery.cleanup.disk_free_warn_gb (default 20).
Deep TMPDIR: when TMPDIR is so deep that tsx’s own socket path would pass the Unix limit, wu:prep’s docs-drift and parity checks run tsx with a short temporary directory and remove it afterwards, instead of failing with listen EINVAL.
Fix: pnpm state:cleanup --scratch --dry-run previews the sweep, then pnpm state:cleanup --scratch
deletes engine-owned scratch older than software_delivery.cleanup.tmp_scratch_max_age_days (default 7;
override with --scratch-max-age-days <n>). It also clears a crashed run’s leftovers from your per-user socket scratch root, with no age wait: only your own private entries whose owning process has ended, and only if that root passes the same ownership and sticky-parent check used to create it (an absent root is skipped silently; a root that exists and fails the check is not swept and the reason is reported). Only directories with both the engine prefix and the ownership
marker are touched; sessions, worktrees, lifecycle roots and your own directories never are. A removed
checkout’s git worktree registration is pruned with it. If a command refuses on a registration whose
directory is already gone, pnpm wu:prune --tmp-only --execute removes the dangling scratch registrations.
A dangling detached registration outside the worktrees root no longer blocks wu:claim or wu:rebase: it is
skipped with a warning naming the path. Only a branchless one at the claim’s own path or under the worktrees
root still refuses (the root is compared by real path, so a symlinked spelling counts as inside). While a
rebase of the branch is paused, any dangling branchless record also refuses wu:rebase --continue until
pnpm wu:prune --tmp-only --execute removes it. The engine’s scratch helper drops the registrations a
removed scratch directory leaves dangling; a leftover from a session’s own scratch outside the engine still
needs that prune.
The check fails closed by design: a dangling detached record beneath a directory that cannot be traversed, or on Windows on a disconnected drive or an unreachable network host, still blocks claims until the record is pruned or the location is reachable again.
A successful release keeps only the newest software_delivery.cleanup.release_artifacts_keep (default 2)
release-artifact versions. Set thresholds with pnpm config:set.
Still Stuck?
Section titled “Still Stuck?”If these solutions don’t help:
- Check logs: read
flow.login the Software Delivery state namespace. - Run with debug:
DEBUG=lumenflow:* pnpm wu:done --id <wu-id> - Review the FAQ for common pitfalls.
Include in your question:
- The exact error message
- Your
workspace.yaml(redact secrets) - Output of
pnpm wu:status --id <wu-id> - Your Node.js version:
node --version
Windows shell commands and worktree hints
Section titled “Windows shell commands and worktree hints”Windows guidance labels commands for cmd.exe, PowerShell and Git Bash. Copy only the
command after the label for your shell. Git Bash paths use forward slashes and POSIX
quoting. For a UNC directory, cmd.exe uses pushd, which maps a temporary drive;
run popd when finished to restore the previous directory and release the mapping.
For a path containing %, the cmd.exe line is a notice, not a command: use the
PowerShell or Git Bash line, because cmd.exe can expand paired environment variables
even inside quotes. Session-prefixed commands follow the same rule.
Git Bash’s //server/share spelling is shell-quoted, but MSYS native argument conversion
has corner cases; shell parsing alone does not verify access to a network share or
the installed pnpm launcher. See MSYS2 filesystem paths.
When the local main checkout has not yet received the claim, a brief run in the matching
worktree uses that actual worktree root in its next-command hint.