Skip to content

release: promote workflow, memory, goal, and DAG fixes - #242

Merged
LeXwDeX merged 41 commits into
mainfrom
dev
Aug 13, 2026
Merged

release: promote workflow, memory, goal, and DAG fixes#242
LeXwDeX merged 41 commits into
mainfrom
dev

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Promote the validated dev integration branch to main.

This release includes:

  • workflow authoring validation and composable DAG configuration support
  • project-owned Memory lifecycle and identity safety fixes
  • Goal transition and Session automation fencing fixes
  • DAG execution-location and parent-wake reliability fixes
  • reproducible frozen-lockfile CI and release gates

Evidence before promotion

Release policy

  • Merge only after all required main PR checks pass.
  • Trigger release-fork from main only after the merge.

LeXwDeX and others added 30 commits August 11, 2026 12:08
Introduce one validated source-to-graph boundary, internalize composable block capabilities, and gate config/release packaging with compatible provenance.
feat(dag): unify workflow authoring and blocks
…(CI-LOCK-02)

A clean checkout could not reproduce dependencies: packages/app/package.json
pinned ghostty-web to the mutable `#main` ref while bun.lock recorded a stale
commit (513463a), so `bun install --frozen-lockfile` failed with 'lockfile had
changes'. CI also ran non-frozen installs via the shared setup-bun action, so it
could silently test a different dependency set than the lock declared.

Fixes:
- Pin ghostty-web to the reviewed immutable commit 83c0a07b8628b748aed073b232cb4b52a6ca11c1.
- Sync bun.lock to that pin (manifest + lock entry).
- Add `--frozen-lockfile` to both setup-bun installs (Linux + Windows) and to the
  release-fork package-templates install.
- Add a static repository-policy gate (test/policy/repo-dependencies.test.ts) that
  fails closed if any git dependency is not pinned to a full 40-char SHA, or any
  `bun install` in CI (.github/**/*.yml) is not frozen. It validates config only —
  no second install flow.

Mutation-proven: reverting the SHA to `#main`, or dropping `--frozen-lockfile`
from any CI install, flips the gate Red. Clean checkout `bun install
--frozen-lockfile` now passes with a stable lock.

Co-Authored-By: Claude <noreply@anthropic.com>
fix(ci): pin ghostty-web to immutable SHA and freeze all CI installs (CI-LOCK-02)
Phased reconstruction of the lost ProjectMemoryAuthority redesign on top of
the d7b011738 process-safe baseline. 8 phases (P1-P8), each a commit.
P1=identity+atomic store API, P6=fromDirectory cutover (1C), P7=crash harness (1A).

Co-Authored-By: Claude <noreply@anthropic.com>
…rotocol

Adversarial review (5 critics, ultracode) found the lost ADR-0004 is
unrecoverable, so 'faithful reconstruction' is unverifiable. Add blocking
product decisions D1-D4 (Policy-in-Home vs ADR-0001, source-Home preserve
retention, retirement-as-merge/lineage, new journal/Revision/ChangeMemory
surface) requiring user sign-off, a mandatory P0 (recreate ADR-0004 +
CONTEXT for approval before P1), technical revisions (P4 before P3, FK
per-table rules, mutation-gate fixes, ledger locations, phase enum), and
a resume protocol for fresh sessions.

Co-Authored-By: Claude <noreply@anthropic.com>
…osed)

P0 design-of-record for the ProjectMemoryAuthority redo, reconstructing
the lost ADR-0004 (uncommitted WIP, /tmp-cleaned). Resolves the §7.A
product decisions D1-D4 and encodes the user principles confirmed
2026-08-12: one shared Memory per Project (worktrees share it, no
per-worktree memory), Memory never forks, and identity upgrade is
imperceptible. Status: Proposed (P0 gate) — P1 is blocked until the user
approves this ADR.

Co-Authored-By: Claude <noreply@anthropic.com>
Fix #1)

migrateProjectId deleted the old ProjectTable row, which ON DELETE CASCADE
silently destroyed every DAG workflow and every saved permission whenever a
repo gained its first remote (root -> first-remote identity upgrade). Repoint
both project_id FKs inside the existing immediate transaction before the old
row is deleted. A (newID, action, resource) collision on permission fails the
transaction closed (no data loss).

Extended 'migrates cached root project data when origin becomes available' to
seed a workflow + permission and assert both survive the upgrade. Mutation gate:
removing the repointing flips the test Red (rows cascade-deleted).

Co-Authored-By: Claude <noreply@anthropic.com>
…04 redesign

User applied Occam's Razor: the 8-phase ProjectMemoryAuthority redesign is
over-engineered for the real needs (shared/no-fork memory already in baseline;
imperceptible upgrade + no data loss via small in-place fixes). ADR-0004 →
Rejected. Plan §10 = 4 targeted fixes; Fix #1 already done.

Co-Authored-By: Claude <noreply@anthropic.com>
…h complete

Re-assessed the remaining Occam fixes against the actual code:
- #3 (ABBA): unreachable — migrateHome is one-way (root→remote), no reverse
  caller, so the two project flocks are never taken in opposite orders.
- #4 (destructive rescan): already handled — worktree reconcileLegacyMemory
  invalidates the admission cache before ensure, forcing a fresh rescan.
- #2 (typed errors): deferred — full propagation is a multi-file cascade for a
  marginal HTTP-status gain on a rare conflict (.orDie preserves the diagnostic
  in the Die cause). Awaits user decision (Occam cut vs invariant #5).

Fix #1 (the real data-loss gap) stands; full regression green.

Co-Authored-By: Claude <noreply@anthropic.com>
…entity (MEM-PR01-00)

Every commit-less repository resolves to the same ProjectV2.ID.global, and the
branch keys Memory Home by project ID. Before this change an enabled global
config activated Memory for all commit-less repos at once: one shared Home
leaked topics across unrelated repositories, and the first commit moved the
identity to root/remote while migrateProjectId never migrates away from
global — silently orphaning everything written pre-commit.

Fix with the minimal Occam seam: one fail-closed guard in Memory.configuration
(the single activation gate behind active/prepare/search/checkpoint/setEnabled)
returning undefined while the project identity is global. Memory activates
normally once the repository gains a real identity; migrating the shared
bucket is structurally infeasible (no per-repo provenance) and pre-existing
orphans belong to the deferred retention/GC decision.

- Red: search must report "unavailable" and /memory on must stay off for a
  commit-less repo even with an enabled global config and seeded topics
- Green: single guard; identity-scoped (repos with a commit activate normally)
- Mutation: removing the guard turns both Red tests red again
- Domain regression: memory+project suites 162 pass / 0 fail; opencode+core
  typecheck clean
- redo plan: record Fix #5 decision; reopen #3 (ABBA reachable via
  remote→remote identity change, MEM-PR01-R1-24)
- remove leftover no-assertion diagnostic scaffold (repro-scope-finding);
  its scenario is captured in finding MEM-PR01-R1-06 for the M-C slice

Co-Authored-By: Claude <noreply@anthropic.com>
…erge, FK collision, deadlock-freedom (MEM-PR01 M-A)

Two-round review confirmed four P2 defects in the identity-upgrade path; all
fixed at the existing seams with Red→Green→mutation evidence per finding.

R1-12  inspectHome wedged every upgrade after a crash: the store's own
       atomicWrite residue (manifest.json.<pid>.<uuid>.tmp) was rejected as
       foreign state. Tolerate the store's own temp pattern; foreign files
       still fail closed (pinned).

R1-15  The merge compared full topic JSON, so controller metadata drift from
       MemoryStore.markMatched (last_matched_at/match_count/revision/
       updated_at) registered as a user-visible ConflictError and wedged the
       upgrade. Compare content only; the target's own copy stays
       authoritative; real content differences still conflict (pinned).

R1-11  Permission FK repoint used a bulk UPDATE that violated the unique
       (project_id, action, resource) index whenever the successor identity
       already held the same (action, resource) — the immediate transaction
       died and the whole upgrade wedged on every retry. Repoint per row;
       on collision the successor row wins and the duplicate old row is
       dropped; disjoint rows still repoint.

R1-24  The redo plan claimed ABBA unreachable because retirement was "one-way"
       — false: a changed origin URL yields remote→remote transitions, and the
       old lock structure (hold flock(old) across the merge while
       updateTopics locks flock(new) inside) deadlocks opposite-direction
       migrations (Red: 20 s test timeout on the legacy structure). Sorted
       pre-acquisition is impossible because the flock is non-reentrant, so
       fix by construction: a sorted pair lock serializes the two directions
       and the merge is restructured into three phases that never hold more
       than one memory-project:* lock at a time. A source that changed
       mid-merge now fails closed with retryable SourceChangedError instead
       of risking deletion of new data. Crash-retry convergence pinned
       (R1-13).

- Domain regression: memory+project suites 169 pass / 0 fail; opencode+core
  typecheck clean; lint ratchet unchanged (0 new warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
…t reconcile, TOCTOU revalidation, retired-identity inertness (MEM-PR01 M-C)

R1-06 (blocking): worktree remove/reset reconciled admission against a SINGLE
directory, so a lone sandbox legacy config could be promoted to the project
config past disagreeing siblings (order-dependent, silent effective-config
flip). Both call sites now pass the complete snapshot (primary + every
registered sandbox); disagreeing siblings fail closed with no promotion.

R1-03: configuration() fell back to the stale instance context when the
identity row was gone, letting a process holding a retired identity fork a
Home under it. The fallback is removed: missing row = inert.

R1-04: admission deleted scanned legacy topic/config files without re-reading
them; a writer outside the admission flock (older runtime, hand edit) landing
between scan and delete lost content. Each file is now re-read and compared
immediately before removal; changed content is preserved and surfaced as a
conflict. Deterministic TOCTOU test pins the scan→delete window via the store
flock.

R1-08: worktree remove/reset migration ran for uninitialized projects despite
the memory path's inertness rule; reconcile is now gated on time.initialized
(residue still fails closed). Existing migration tests stamp initialized.

R1-10: admission's explicit-config choice used a localeCompare sort that put
memory.json before memory.jsonc, disagreeing with MemoryConfig.load. The scan
now keeps loader precedence and a jsonc/json fork in the project directory is
diagnosed as config.conflict instead of silently picking a side; legacy
configs equal only to the non-effective file are no longer deleted as
duplicates.

Pins: R1-07 (/memory writes the project config to the project worktree from a
non-primary instance context) and R1-23 (runtime admission snapshot covers
every registered sandbox).

- Domain regression: memory+project suites 176 pass / 0 fail; opencode+core
  typecheck clean; lint ratchet unchanged (0 new warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
…to remove (MEM-PR01 M-D)

R1-16 (blocking): list() ran `git worktree prune` and deregistered sandboxes
for every merely-prunable entry. "prunable" does not prove a worktree is gone
— git also marks inaccessible directories (unmounted volume, locked parent)
and broken gitdir links whose directories still exist, so a read call could
destroy git admin data and live registrations. list() is now a pure
observation path: prunable entries stay hidden from the listing but are
otherwise untouched.

The destructive cleanup moves to the action path, where each case can be
proven:
- remove() gains a prunable branch: reconcile legacy memory fail-closed,
  prune the admin data, remove the directory if it still exists, delete the
  branch, drop the registration.
- remove() gains a git-unknown recovery branch (R1-18): a registered worktree
  with no git record previously failed forever with a false "not registered"
  error and no remediation; it now reconciles legacy memory fail-closed and
  drops the stale registration without ever deleting the directory.
- Registration cleanup drops every canonically-equal entry, not just the
  first — symlinked paths (/var vs /private/var) could register the same
  worktree twice and leave a zombie entry that broke serialized removal.

Pins (both mutation-proven):
- R1-17: reset fails closed over invalid legacy memory and preserves it.
- R1-19 (blocking): reset/remove invalidate the admission cache before the
  rescan; a reset-primed clean cache must never hide a legacy file that
  appears before a later destructive operation.

- Updated the prune-era list test to the new semantics (list hides but does
  not touch; explicit remove cleans up).
- Domain regression: memory+project suites 180 pass / 0 fail; opencode+core
  typecheck clean; lint ratchet unchanged (0 new warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
…em_count, torn-commit (MEM-PR01 M-E)

R1-02 (P2 test-gap): the corrupt-manifest fail-closed guards had no test, so
reverting them would let migrateHome delete an unread Memory Home. Now pinned:
an invalid manifest and a manifest referencing a missing generation both fail
readSnapshot, and migrateHome fails closed on the merge path with the source
Home preserved. Both guards proven load-bearing by mutation (fail-open revert
turns the test Red).

R1-20 (P3 test-gap): decodeTopic item_count/items.length consistency was only
covered by a since-deleted test. Re-pinned at both the decoder and the
writeSnapshot generation gate (mutation-proven).

R1-21 (P3 test-gap): a crash mid-writeSnapshot leaves an orphaned staging
generation whose manifest was never published; pinned that it never shadows
the committed generation and the store still commits cleanly (mutation-proven).

Test-only change; no production code touched.
- memory+project suites 183 pass / 0 fail; opencode+core typecheck clean;
  lint ratchet unchanged (0 new warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
…-process commit conflict (MEM-PR01 M-F)

R2-02: the branch collapses MEMORY config onto one project-primary file,
written by three paths under mutually disjoint locks — /memory on|off
(in-process KeyedMutex), admission promotion (memory-admission flock), and
readConfig's normalization rewrite (no lock). atomicWrite prevents torn bytes
but not whole-document last-writer-wins across worktrees/processes. All config
file writes now serialize on a per-file cross-process flock
(memory-config:<file>): writeProject, writeGlobal, and the normalization
rewrite. Pinned by a blocking-observation test; mutation-proven (dropping the
lock lets a concurrent writer complete while the lock is held).

Residual, documented rather than fixed (Occam): decision-level
read-modify-write across processes is not CAS-protected — only the write
primitives are serialized. A full cross-process RMW protocol would be
over-engineering for the exposure.

R2-03: the cross-process commit protocol's explicit-conflict guarantee
(ADR-0002) was only exercised within one process. A new spawned-worker test
commits with a stale expectedRevision from a second OS process and observes
CommitConflictError deterministically (the pre-existing updateTopics race test
only overlaps probabilistically).

- memory+project suites 185 pass / 0 fail; opencode+core typecheck clean;
  lint ratchet unchanged (0 new warnings).

Co-Authored-By: Claude <noreply@anthropic.com>
…esign (MEM-PR01 M-G)

R1-01 (blocking): CONTEXT.md still shipped the rejected ADR-0004 authority
design as the domain's governing self-doc. Rewritten to the actual authority
structure (Store/Config/Admission/migrateHome/worktree guard + project
identity migration): rejected-design glossary and invariants removed (Identity
Alias, Canonical Project ID, tombstone retirement, opaque Revision, destruction
guard); source Home described as migrate-then-remove with retention deferred;
Project Configuration described as the unversioned .opencode/memory.jsonc; the
read-leniency split stated (runtime read projects empty, strict reads and
migration fail closed); ADR-0001's policy clause restored as live; ADR-0004
marked Rejected; the M-A…M-F behaviors reflected (global inertness,
content-only conflicts, non-destructive list, fail-closed reset/remove,
per-file config lock).

R1-25: redo-plan internal consistency — header status no longer says PLANNING;
the §10 resume protocol is marked superseded (no pending autonomous fix, only
user decisions remain).

R1-09 (spec-gap → decision): the git-exclusion narrowing to the two config
candidates is intentional and now documented: preserved fail-closed legacy
topic files stay visible in git status and committable; surfacing repair-
pending files beats silently excluding user data.

R1-14 (spec-gap → requirement): the openspec workspace is untracked, so this
plan now carries the identity-upgrade requirement ("Identity upgrade preserves
Project Memory and Project-owned references") with its scenarios, pinned by the
M-A/M-B/M-C/M-E tests.

Docs-only; no production code touched.

Co-Authored-By: Claude <noreply@anthropic.com>
…oped prune, proof-after-hook (MEM-PR01 M-H)

R3-P2-a in-flight old-identity writer could recreate a retired Home: writers
(prepare/search/checkpoint) now hold a cross-process memory-identity:<id> flock
around their whole read-modify-write and re-check identity liveness inside it;
migrateHome takes the same identity lock inside the sorted pair lock, so it
waits for in-flight writers and moves their writes with the Home. Lock order
admission -> migrate(pair) -> identity -> project is cycle-free.
R3-P2-d git worktree prune is repo-global; it now runs only when the removed
entry is the sole prunable one (else stale admin data is left for explicit
cleanup), so sibling worktrees' admin data is not destroyed.
R3-P2-e the WorktreeRemove hook now fires BEFORE the reconcile proof on all
remove paths, so the proof observes everything the hook produced.
R3-P2-f remove/reset no longer fall back to the stale instance identity
(?? ctx.project); they fail closed when the identity row is gone.
R3-P2-b cleanupLegacyDirectory re-checks the listing immediately before
removing each dir.
Pins (mutation-proven): SourceChanged verify-before-delete guard (R3-P2-c);
store write paths fail closed on a corrupt manifest (R4-P2-a); unresolved
admission results are never cached (R4-P2-b).
Docs: ADR-0002 updated to the three-phase merge + identity-lock protocol;
rejected ADR-0004 no longer claims to supersede live clauses; redo-plan #3
ABBA narrative corrected.

Also drops two no-op non-null assertions (session/summary.ts, format/index.ts)
surfaced by type-aware churn from the identity-migration FK fix, returning the
tree to the 4852 lint ratchet with no behavior change.

Co-Authored-By: Claude <noreply@anthropic.com>
…-PR01 M-I)

Closes the two Round 5+6 convergence findings:

- P1-a (writer fence in the wrong lock dir): memory.ts's writer fence
  (prepare/search/checkpoint) now passes home.locks so it lives in the same
  lock namespace as identity migration. Previously it fell back to the default
  XDG-state lock dir, a DIFFERENT directory, so the writer fence and the
  migration fence never actually serialized.

- flock-leak P2 (fence released before row deletion): ProjectIdentityMigration
  .migrate now holds memory-identity:<oldID> for the WHOLE retirement — the
  Memory Home migration AND the caller's reference/row retirement — via a
  retireReferences callback. The fence is no longer released between the Home
  move and the old-row deletion, so an in-flight writer under oldID cannot slip
  into the gap. Callers pass their row retirement as the callback and no longer
  touch the fence themselves (single authority for the fence).

Mutation check: removing retireReferences() from inside the fence turns the
MEM-PR01-R1-11 permission-collision test Red (FK repoint no longer happens),
confirming the seam wiring is load-bearing.

Co-Authored-By: Claude <noreply@anthropic.com>
…(MEM-PR01 M-J)

Round 7 converged on one residual P1 (F1) and one P2 (F2), both pre-existing:

- F1 (P1): admission.ensure took the memory-identity fence but never
  re-checked identity liveness inside it. A retirement could complete while
  admission waited on the fence, and admission would then import legacy
  topics into the re-created retired Home AND delete the legacy source files
  (permanent orphaning — the identity cache already points at the successor,
  so no migration would ever run for the pair again).

- F2 (P2): the phase-1 Home rename raced a concurrent newID writer creating
  the target between existsSafe and rename (ENOTEMPTY), dying fromDirectory.
  Self-healing (next boot retries into the merge path), no data loss.

The identity-race TOCTOU class has now been found in three consecutive review
rounds, and the memory-identity protocol (key + lock dir + in-fence liveness
recheck) was hand-duplicated at four sites across three modules — exactly why
admission could diverge from the writer discipline. Per the redesign rule this
is a seam redesign, not a patch:

- New MemoryIdentityFence (memory/identity-fence.ts) is the single authority
  for the protocol: key() builds the lock key, withLiveIdentity() holds the
  fence on home.locks AND re-checks the identity row inside the fence (None =
  retired, callers fail closed). A future path cannot forget the recheck.
- Writers (prepare/search/checkpoint) and admission route through it; the
  retirement seam (ProjectIdentityMigration) stays the only raw fence holder
  (it deletes the row inside the fence) and builds its key from key().
- admission.ensure now fails with a tagged IdentityRetired error when the row
  is gone; configuration() stays inert, the worktree guard proceeds (the
  migration is moot after a completed retirement).
- F2: rename failure with a target that appeared falls through to the
  snapshot-merge path; genuine FS failures still rethrow.

Verification: Red test MEM-PR01-R7-F1 (ensure after row retirement must not
import nor delete the legacy files) confirmed Red before, Green after;
mutation removing the in-fence recheck turns it Red again. memory+project
189 pass / 0 fail; typecheck clean; lint flat at 4852.

F2's mutation is registered as a test-gap: the race window is between two
file ops with no observable state between them, so no deterministic
public-seam test exists (the fallback routes into the already-tested merge
path).

Co-Authored-By: Claude <noreply@anthropic.com>
…ve (MEM-PR01 M-K)

Round 9 product-invariant review found P2-A (introduced by M-J): the
reconcileLegacyMemory guard returned undefined on IdentityRetired, so a
worktree remove could proceed past the fence and `git worktree remove
--force` would destroy legacy .opencode/memory content that was never
admitted into any Home. The window sits between removeLocked's own
row-liveness check and the admission fence recheck — widened by the
WorktreeRemove hook (user scripts) that runs between the two.

Fix: on IdentityRetired the guard now returns a blocker message (fail
closed), matching the reset path's existing stance. A retry under the
successor identity imports the legacy content first and then removes
safely.

Red-first + mutation evidence:
- New test MEM-PR01-R9-P2A (worktree-remove.test.ts): holds the
  memory-admission flock so the remove blocks inside ensure after its own
  row check passed, retires the identity row, then releases — asserting
  the removal fails and the never-admitted legacy file survives. Red
  before the fix, Green after; reverting the blocker to undefined turns
  it Red again.

Also fixes a standards-P2: reindents the retirement transaction body in
project.ts (pure whitespace, no behavior change).

Registered, not fixed here (out of PR scope): EffectFlock stale-break can
silently lose a cross-process update (P2-B, pre-existing core infra,
recorded as a residual for the final audit).

Co-Authored-By: Claude <noreply@anthropic.com>
fix(memory): make Project Memory process-safe and identity-safe (MEM-PR-01)
…tity

The dev push CI (linux Unit Tests) failed in MEM-PR01-R1-03: the first
search returned "unavailable" in isolation while the same file passes
locally. Root cause (CI log analysis + code trace): the test's "active"
pre-condition depends on the InstanceStore's BOOT-TIME project resolution,
which runs in a separate Effect graph (its own :memory: Database) and
silently degrades to the shared global identity when a git subprocess
fails transiently on a loaded runner (every git failure collapses to
"exit 1, empty output" in core git.ts run()). The test body's own
resolution milliseconds later returns the real root-commit identity, so
the identity assertions pass while memory fails closed against the stale
global context.

Fix: resolve the identity once in the test body and hand it to the
instance store (provideInstance now accepts a full LoadInput; boot skips
its own fromDirectory when project+worktree are given). Applied to the
three identity-scoped tests (R1-03, R1-23, R1-00 third) that assert
active memory.

Registered separately (product hardening, out of this PR): identity
resolution should not silently degrade to the global identity on
transient git errors — discover/rootCommits should retry or propagate
instead of collapsing to exit 1.

Co-Authored-By: Claude <noreply@anthropic.com>
test(memory): stamp the instance store with the already-resolved identity (CI fix)
Second dev-CI failure investigation (run 31648866555) with the
identity-fixture fix in place: the same assertion still failed, still
with no visible warning. Verified root cause: bun test runs all files in
ONE process, sequentially, sharing one XDG_CONFIG_HOME. A file that runs
before this one and triggers global-memory initialization (memory.test
prepare, or instance boot via bootstrap.ts memory.init) leaves a VALID
global memory.jsonc whose model this file's fake provider does not know.
writeGlobal then silently no-ops over the valid file (config.ts returns
false), configuration() loads the foreign model, resolveModel fails (the
warning is captured by TestConsole and never reaches CI logs), and
search fails closed with "unavailable". R1-00's identical flow passes
because R1-07's finalizer removes the global file in between. CI-only
because bun's file order is deterministic per filesystem state and the
fresh CI checkout orders a contaminator before this file.

Fix: pin a private OPENCODE_CONFIG_DIR per file (beforeAll/afterAll,
live env getter — globalConfigDir reads it at call time) so the global
file can never be contaminated by earlier files, plus a tripwire
assertion after writeGlobal that the loaded global config carries the
expected model.

Registered separately (product hardening): writeGlobal should log when
it declines to overwrite an existing valid config.

Co-Authored-By: Claude <noreply@anthropic.com>
…ation

test(memory): isolate the global config dir per test file (CI fix)
…state (GOAL-FP-01-01/-03)

The DAG automation-lease registration lifecycle was bound to WAKE DELIVERY
instead of workflow state, leaking dag registrations that permanently block
the session's goal (owner() prefers dag, so the goal can never claim).

- Startup wake sweep registered every workflow in the wake snapshot,
  including terminal workflows with wake_reported=true, which are never in
  the wake batch and therefore never unregistered (-01).
- Terminal event handlers (WorkflowCompleted/Failed/Cancelled) never
  unregistered, so a workflow terminalizing without a successful wake
  delivery kept its registration indefinitely (-03).

Fix:
- Sweep: register only non-terminal workflows. Verified safe for
  terminal-but-unreported workflows: tryDeliverWake registers every
  workflow in its batch itself right before claiming the wake lease, so
  redelivery does not depend on the sweep.
- Terminal handlers: unregister the dag registration on workflow
  terminalization. Identity verified: the projector writes
  WorkflowTable.id = event dagID, so the unregister key
  { kind: "dag", id: evt.data.dagID } matches every registration key
  (adoption, recovery, sweep, delivery).

TDD evidence (test/dag/dag-lease-lifecycle.test.ts, real DagLoop init over
in-memory DB + real SessionAutomationLease + real Goal/store):
- Red (current code): -01 "goal claimable after restart" failed with
  claim(goal) = none (dag leaked by the sweep); -03 "dag lease released on
  terminal event without wake delivery" timed out (registration persisted).
- Green after fix: 2/2 pass.
- Mutation 1 (revert sweep filter): -01 goes Red. Restored.
- Mutation 2 (remove handler unregister): -03 goes Red. Restored.

Verification: bun test test/goal test/session/automation-lease.test.ts
test/dag → 564 pass / 0 fail; bun typecheck clean; bun lint → 4852
warnings (ratchet unchanged, 0 new).

Co-Authored-By: Claude <noreply@anthropic.com>
…ry (GOAL-FP-01-03 follow-up)

P2-A residual on the -01/-03 seam: the terminal-handler unregister was gated
by Stream.filter(runtimes.has(dagID)), so a workflow registered by the
startup wake sweep but never adopted into a runtime entry (recoverWorkflow
aborted at startup, e.g. an unreadable persisted row) could only ever be
unregistered by a successful wake delivery — a control-op terminalization
left a permanent dag registration and the goal permanently blocked.

Fix (same seam, loop.ts only):
- Terminal handlers no longer filter on runtimes.has. The handler remains a
  no-op for events not concerning this instance: the evalLock cleanup and
  the wake fork stay gated on the runtime entry, and the new no-entry
  release is scoped by the durable row's project (the same cross-instance
  guard every adoption path uses).
- When the terminal event has no runtime entry, the handler releases the
  registration from the durable row: store.getWorkflow(dagID) →
  WorkflowRow.sessionId (verified: DagStore.Interface.getWorkflow returns
  WorkflowRow with sessionId — no store changes needed), then
  automation.unregister(SessionID.make(wf.sessionId), { kind: "dag",
  id: dagID }) with the project guard.

TDD evidence (test/dag/dag-lease-lifecycle.test.ts, same real-DagLoop
harness):
- Red: new test "releases a swept registration when a workflow with no
  runtime entry is terminalized by a control op" timed out — the dag lease
  survived WorkflowCancelled (the recovery failure is simulated as a
  session-store defect that aborts reconcileWorkflow, leaving a non-terminal
  row with no runtime entry; sweep registers it; dag.cancel terminalizes it).
- Green after fix: 3/3 in the file.
- Mutation (remove the no-entry unregister branch): the new test goes Red
  (timeout). Restored.

Verification: bun test test/dag test/session/automation-lease.test.ts
test/goal → 565 pass / 0 fail; bun typecheck clean; bun lint → 4852
warnings (ratchet unchanged, 0 new).

Co-Authored-By: Claude <noreply@anthropic.com>
LeXwDeX and others added 11 commits August 13, 2026 09:19
…AL-FP-01-02)

The final DAG lease unregister (U2) lands AFTER the last wake turn's idle
event: the runner emits the session idle status before completing its
awaiter, so GoalLoop's idle-driven claim still sees the dag registration and
yields; after U2 lands there is no second idle and the active goal silently
stalls until the next external idle.

SessionAutomationLease.unregister now detects the dag -> goal/none owner
transition (before/after compare under the per-session KeyedMutex, generation
bump semantics preserved) and re-triggers the goal evaluation by reusing the
EXISTING idle status event mechanism (SessionStatus.set idle) — no new event
or GoalLoop consumer. The publish runs after the lock (unconditional
fire-and-forget enqueue, cannot lose or duplicate; Set.delete is idempotent
and only the last dag removal flips the owner). A busy-session gate avoids
spurious judge calls mid-turn: a busy turn always re-emits idle on
completion, which re-drives the claim with the dag already released. This is
also the GOAL-FP-01-11 mitigation surface: a claim that lost the ownership
race gets another chance once the owner actually transfers.

TDD evidence:
- Red: test/dag/dag-goal-wake-retrigger.test.ts fails on pre-fix code with
  "goal was not re-evaluated after the dag lease release (GOAL-FP-01-02)"
  after the workflow completes and the wake is reported, no further idle
  events published (saved /tmp/red-goal-fp-01-02.txt).
- Green: real DagLoop wake delivery end-to-end (U2 fires in the delivery
  tap) + real GoalLoop on the shared bus; goal claimed, judge runs,
  turns_used advances, continuation dispatched.
- Mutation: reverting the unregister re-trigger makes the test Red again
  (saved /tmp/mutation-red-goal-fp-01-02.txt); restored to Green.
- e2e-loop "DAG owner arbitration" updated to the new contract: the dag
  release alone re-drives the goal (manual second idle publish removed);
  its SessionStatus wiring switched to provideMerge so the lease re-trigger
  is visible from the test body context.

Verification: bun test test/dag test/goal test/session/automation-lease.test.ts
= 566 pass / 0 fail; bun typecheck (tsgo --noEmit) clean; bun lint = 4852
warnings (at the ratchet threshold, 0 errors).

Co-Authored-By: Claude <noreply@anthropic.com>
…AL-FP-01-02 follow-up)

R1: the GOAL-FP-01-02 unregister re-trigger publishes a duplicate idle for
every dag release, so the turn-idle fiber B (whose claim landed after U2) and
the retry fiber D both hold valid same-generation goal tokens. The harmful
interleavings on the synthetic no-text verdict path: 4a — B commits and is
interrupted by D's registerLoopFiber between commit and continuation
dispatch, D's stale-revision commit noops, goal silently stalls; 4b — D
double-commits (turns inflation) or spurious-pauses on the busy status check.

Candidate analysis: (a) per-session serialization of afterIdle alone still
lets the second fiber commit again after the first dispatched (4b survives);
(c) generation bump on goal re-register invalidates only the OTHER fiber's
token — the revision guard still admits D's fresh-load commit (inflation) and
does not stop the interrupt from killing B post-commit (4a survives);
skip-if-alive on the fiber map races the fiber's unwinding window (branch-4
contract). Chosen fix (b): a per-session blocked-claim flag in the lease.

Mechanism: claim records "a goal claim was rejected by the dag owner"
(blockedGoalClaims); a successful (or non-dag-rejected) goal claim clears it;
unregister CONSUMES it (Set.delete) inside the same per-session KeyedMutex
critical section as the owner-transition decision, so the re-trigger fires
exactly once per blocked claim, atomically with claim serialization. The
blocked claim's evaluation fiber yields at the claim itself, so the retry it
spawns is the only evaluation in flight.

Unconstructibility arguments:
- 4a: D is forked only if the flag was set, i.e. only after an evaluation's
  claim was rejected and that fiber yielded at the claim. B in flight
  post-commit implies B's claim succeeded, which cleared the flag under the
  same lock before U2's consume — no publish, no D, no interrupt. The
  commit→dispatch tail of the sole evaluation can no longer be raced.
- 4b: D implies the flag was set and not cleared since, so no evaluation
  committed in between; D loads fresh state and commits once. A turn-boundary
  fiber whose claim succeeds clears the flag before any release decision, so
  one commit per boundary. The busy→pause path is unreachable for D (no turn
  is in flight when D runs).

No loss: the retry obligation is only dropped by a successful claim (the
evaluation then happened) or by the busy-gate consume — whose session
re-emits idle on turn completion and re-drives the claim (runner onIdle →
SessionStatus.set idle).

TDD evidence:
- Red: new e2e-loop test "an unblocked goal is evaluated exactly once when
  the dag releases before the boundary idle" fails deterministically on the
  unfixed re-trigger with turns_used 2 for one real boundary (Expected: 1,
  Received: 2), pinned by a second-dispatch gate — saved
  /tmp/red-goal-r1.txt.
- Green: real GoalLoop + real lease + synthetic no-text verdict; the dag
  release stays silent when no claim was ever blocked, the boundary
  evaluation commits exactly once.
- Mutation: reverting the blocked-claim gate to the unconditional publish
  makes the test Red again (Expected: 1, Received: 2) — saved
  /tmp/mutation-red-goal-r1.txt — then restored.
- The GOAL-FP-01-02 dag wake test now reproduces the faithful production
  sequence: the prompt mock emits the wake turn's idle event (as the real
  runner does before its awaiter resolves), the blocked claim arms the
  re-trigger, and U2's retry drives the goal with no idle after U2.

Verification: bun test test/dag test/goal test/session/automation-lease.test.ts
= 567 pass / 0 fail; bun typecheck (tsgo --noEmit) clean; bun lint = 4852
warnings (at the ratchet threshold, 0 errors).

Co-Authored-By: Claude <noreply@anthropic.com>
…elete (GOAL-FP-01-05/-06/-16)

TDD: red test first (test/session/session-remove-cleanup.test.ts, 3 fail on
current code), minimal green, mutation (revert Session.defaultLayer cleanup
provides -> 3 fail), restore -> green.

Wiring diagnosis (-05): Session.remove resolved Goal via
Effect.serviceOption(Goal.Service) captured at layer construction. In the
production AppLayer (effect/app-runtime.ts) Goal.defaultLayer and
Session.defaultLayer are Layer.mergeAll siblings; mergeAll builds members
concurrently against the parent context only, so Goal was never in Session's
build context and the cleanup silently no-op'd - `opencode session delete`
orphaned the goal_state row. Fixed by making Goal, SessionAutomationLease and
Dag hard requirements of Session.layer: Session.defaultLayer self-provides all
three (each is self-contained, requirements=never), Session.node lists their
nodes, and tsgo now enforces the wiring at every composition site (4 raw-layer
test harnesses updated). No layer cycle: Goal -> SessionStatus/Lease,
Dag -> DagStore/DagProjector, none depends on Session.

Cleanup (-06): Session.remove now (1) purges goal rows via Goal.purgeSession,
(2) cancels owned non-terminal workflows via the existing Dag.cancel authority
(durable terminalization; the DagLoop terminal handler aborts child sessions
and releases the dag lease - no second runtime authority), and (3) purges the
session's lease registrations via the new SessionAutomationLease.purgeSession
(under the per-session KeyedMutex). Each step catches its cause and logs a
warning; deletion itself still cannot fail.

Ordering + crash window: cleanup runs BEFORE the Deleted publish (the
SessionProjector deletes the session row inside that transaction; FK cascade
then wipes workflow rows). A crash mid-way leaves a live session with no
goal/workflows (consistent, recoverable) - never orphan goal rows or
re-adoptable workflows under a deleted session. No shared transaction exists
(three separate aggregates: goal tables, workflow events, lease map); each
step is individually atomic.

-16: goal_outcome rows now deleted in the same durable transition transaction
as the goal_state row (transition seam gained a deleteOutcomes flag;
Goal.purgeSession sets it, Goal.clear keeps outcome history).

Verification: bun test test/session test/goal test/dag -> 964 pass, 0 fail;
bun typecheck clean; bun lint 4852 (ratchet).

Co-Authored-By: Claude <noreply@anthropic.com>
…-01-05 follow-up)

P2-A (ordering inversion): Session.remove published the Deleted event BEFORE
the cleanup block, contradicting the block's own comment. Inside the publish
transaction the SessionProjector deletes the session row and the workflow FK
cascade wipes the workflow rows, so dag.store.listBySession in the cleanup
always returned [] — the cancel loop was dead code, the WorkflowCancelled
event never fired, the DagLoop terminal handler never aborted running DAG
child sessions, and a crash between publish and cleanup orphaned
goal_state/goal_outcome rows.

Fix: reordered remove() to goal purge -> workflow cancel -> lease purge ->
Deleted publish -> event-log removal. The SettingsHook SessionEnd trigger now
runs BEFORE the destructive steps (its documented contract is to observe the
session before removal; the event-wiring test asserts trigger contents only,
no Deleted-vs-hook ordering, so no consumer conflict).

P2-B (vacuous assertion): the cancellation test asserted
expect(cancelledEvent).not.toBeNull(), which passes vacuously — drizzle
.get() returns undefined for a missing row. Changed to toBeDefined().

TDD evidence:
- Red (vacuity proof): toBeDefined() on the publish-first code fails with
  Received: undefined — the cancel event was indeed absent (2 pass / 1 fail).
- Green: after the reorder, the event is actually present (3 pass / 0 fail).
- Mutation: moved the publish back before the cleanup -> 1 fail (event
  absent). Restored -> green.

Verification: bun test test/session test/goal test/dag -> 964 pass, 0 fail;
bun typecheck clean; bun lint 4852 (ratchet).

Co-Authored-By: Claude <noreply@anthropic.com>
GoalLoop was purely event-driven: the idle-status subscription was the
only driver, and no component emits idle for sessions that already
existed at startup. An active goal that survived a crash slept until
the next user interaction; with turns_used > 0 the D6 zombie guard also
never fired (it runs inside afterIdle). The automation obligation — an
active goal keeps advancing — was lost across restart.

Add a startup scan to GoalLoop.init:

- The durable snapshot is captured at instance boot inside the
  InstanceState builder (Goal.listActiveSessions — new accessor
  returning session ids whose goal_state row is "active", plus the
  goal revision), then the per-session triggers are forkScoped after
  the idle subscription is armed. Building from init's caller context
  would not work: evaluation fibers resolve services from their
  ambient runtime context, and the builder runs under the ScopedCache
  layer-build environment — the same context the idle subscription
  sees (this is why the test-injected GoalLoopJudgeLLM is visible).
- The scan reuses the EXISTING evaluation path verbatim — the idle
  handler body was extracted into triggerEvaluation (active pre-check,
  fork afterIdle, registerLoopFiber, identity-scoped self-clean) and
  is now shared by both drivers. No new evaluation logic.
- Mutual exclusion stays with the lease claim: a dag-owned session is
  rejected inside afterIdle exactly as on a real idle, and the
  GOAL-FP-01-02 blocked-claim re-trigger re-evaluates it once the dag
  releases (harmless + self-healing; covered by a test).
- Busy sessions are gated via SessionStatus exactly like the idle
  path (the automation-lease re-trigger gate), plus afterIdle's
  post-judge status check and promptIfIdle; covered by a test.
- Crash window between snapshot and trigger: terminal changes are
  absorbed by the active-status re-check; non-terminal changes (the
  scan fiber scheduled late, after the session's own idle event
  already evaluated the boundary) are absorbed by the expectedRevision
  gate — revision bumps on every durable transition, so a stale
  trigger cannot double-commit turns (the R1 turns-inflation harm,
  caught by the existing dag-release test before the gate existed).
- The scan runs once, forkScoped; query and per-session failures are
  logged and swallowed, never fatal to init.

TDD: Red — seeded a goal in the durable store before boot, published
ZERO idle/status events, polled 5s for the judge/continuation: 3 tests
failed with "startup scan never evaluated … (5s timeout)", goal stayed
dormant. Green — added the scan; all 3 pass. Mutation — removed the
scan trigger: same 3 tests go Red; restored → green.

Verified: bun test test/goal test/dag
test/session/automation-lease.test.ts (570 pass, 0 fail),
bun typecheck (packages/opencode) clean, bun lint 4852 warnings (≤ 4852).

Co-Authored-By: Claude <noreply@anthropic.com>
…n failure handling (GOAL-FP-01-04 follow-up)

Domain review of the GOAL-FP-01-04 startup scan: one P1 (D-1) and three
P2s (D-2/D-3/D-4), plus one registered residual.

D-1 (P1): the scan was not scoped to the instance. goal_state has no
directory column and the Database is the shared global opencode.db, so
any instance boot evaluated/committed/paused/drove the active goals of
EVERY project — judge budget burn, pause prompts injected into foreign
sessions, cross-project agent turns with the wrong cwd. Fix: the scan
query (Goal.listActiveSessions) now inner-joins goal_state.session_id
→ session.id and filters session.directory = the instance directory.
The session table (core session sql, directory column) is the single
directory authority; no schema change was needed. The directory is
resolved in GoalLoop.init from the caller context (InstanceRef, which
instance boot provides) and handed to the instance-state builder via a
ref set before the first InstanceState.get — the builder runs under the
ScopedCache layer-build environment, which does NOT include InstanceRef
in production (reading InstanceState.directory there would die).

D-2 (P2): "query failures logged and swallowed, never fatal" was false.
tapError/orElseSucceed only handle Cause.Fail, so a boot-time DB defect
killed the state builder, closing the ScopedCache entry scope and
taking the idle subscription down with it until restart. Fix: the query
is wrapped in Effect.catchCause, which in this effect version catches
Fail AND Defect (there is no catchAllCause) — any failure degrades to
no-scan + a log. Per-session triggers keep their catchCause guards.

D-3 (P2): undecodable goal_state rows were skipped silently. The skip
now logs a warning with the session id and the decode error, so the
dormancy is visible (asserted via TestConsole).

D-4 (P2): the boot-snapshot revision gate was not airtight: if an idle
evaluation committed between the scan's gate load and its afterIdle
entry load, the scan's evaluation would commit again (matchesExpected
passes on the re-loaded revision) — double-commit of the same boundary.
Fix: replaced the snapshot-revision comparison with a per-process
evaluatedRevisions map — afterIdle records the committed revision on
every successful updateAfterJudge; the scan path (triggerEvaluation
gate + afterIdle entry gate, flagged by scanResume) skips when the
recorded revision equals the current revision. The idle path never
consults the gate, so it keeps re-evaluating the same revision across
new turn boundaries. This also fixes the D-5 cross-process false
negative: a revision bumped by a touch-without-evaluation (incl. by
another process before this boot) no longer suppresses the resume.
The map is overwritten by every commit and deleted at the same terminal
points where afterIdle unregisters the goal automation.

D-4 testability: the A-commits/B-scan interleaving is not
deterministically constructible through the public seam — the scan's
gate load and afterIdle's entry load are adjacent in the same fiber
with no injectable pause between them, and the fiber map's
interrupt-on-replace kills any earlier evaluation a test could park.
The committed D-4 test instead deterministically parks the scan's
evaluation at the judge (Deferred, not sleep), races an idle evaluation
into the same boundary, and asserts exactly one commit — the tightest
public-seam construction of the race. The record gate's exact
interleaving is argued above rather than exercised.

Registered residual (not fixable in-process): the cross-process mirror
of D-4 — two live GoalLoop instances in the same process group could
both evaluate the same boundary (each has its own record map). Trigger
conditions: two instances booted against the same session/goal_state
simultaneously. Rare; would need a cross-instance lease or a
directory-level claim, out of scope for this slice.

TDD: D-1 Red — a foreign-directory goal got evaluated (foreignJudgeCalls
1, turns 1, expected 0); D-2 Red — dropping goal_state killed init
(test body died); D-3 Red — no skip log captured. D-4 regression guard
green on HEAD. Green after the fix: all four pass. Mutations: removed
the directory filter → D-1 Red; restored tapError/orElseSucceed +
orDie (pre-fix defect channel) → D-2 Red. Restored → green.

Verified: bun test test/goal test/dag
test/session/automation-lease.test.ts (574 pass, 0 fail, 3x stable
e2e-loop reruns), bun typecheck (packages/opencode) clean, bun lint
4852 warnings (≤ 4852).

Co-Authored-By: Claude <noreply@anthropic.com>
…14/-15)

- GOAL-FP-01-07: updateAfterJudge `expected` (goalID+revision) is now a
  required parameter — the stale-judge protection is the contract, not a
  caller convention. matchesExpected's optional short-circuit is gone;
  typecheck enforces every caller passes the pre-judge identity.
- GOAL-FP-01-08: Goal.set now unregisters the previous goal id from the
  automation lease atomically with the overwrite, so a replaced goal can no
  longer leave a double id in the registration set (owner() returned the
  stale first id and silently starved the new goal's claim).
- GOAL-FP-01-09: the goal tool's `complete` no longer shows "✓ 目标已达成"
  when markDone no-ops (clear/complete race) — it reports the no-op instead
  of presenting a goal that no longer exists as achieved.
- GOAL-FP-01-12: the dispatch-failure path now pauses via pauseGoal
  (pauseAndPublish + inline lease unregister), symmetric with every other
  pause site instead of depending on the trailing afterDispatch load.
- GOAL-FP-01-13 (test): one integration test drives the goal continuation
  through the REAL SessionRunState.startIfIdle admission gate — real busy
  flip, real admission rejection, and the REAL Runner onIdle re-driving the
  loop to done with no manual idle events. Remains mocked: SessionPrompt
  admitPrompt/runLoop (full app layer — disproportionate), Session,
  Provider, judge LLM.
- GOAL-FP-01-14: wake delivery dedupes on retry — a summary whose transcript
  part was already written is only re-marked, never re-prompted (in-process;
  the crash-between-write-and-mark residual on the restart sweep is
  registered — a durable delivering-marker would need a schema change). The
  delivery failure log now carries the cause.
- GOAL-FP-01-15: the done confirmation prompt failure is logged instead of
  silently swallowed (no retry — a retried line could re-inject after a new
  goal is set; the crash-window transcript loss is inherent to the
  durable-leads-presentation invariant and the event stream still notifies
  consumers).

Tests: red-first pinning tests for -08 (lease), -09 (tool API), -12
(lease after a defecting trailing load), -14 (wake retry dedupe); -13's test
is the artifact. Mutation-verified for -08/-09/-12/-14.

Co-Authored-By: Claude <noreply@anthropic.com>
…fensive scan ref, lease SessionStatus requirement

Standards deep review follow-ups on the GOAL-FP-01-04 startup scan and
the GOAL-FP-01-02 dag-release re-trigger (S-1..S-3, all P2).

S-1: missing session.directory index. The boot scan joins goal_state →
session filtered on session.directory, so every instance boot linearly
scanned the whole channel-global session table. Added the inline
index("session_directory_idx").on(table.directory) to the session table
definition and generated the migration + snapshot via the sanctioned
generator (packages/core/script/migration.ts): new migration file
20260813020344_bored_skaar, schema.json / schema.gen.ts /
migration.gen.ts regenerated; `bun run script/migration.ts --check` is
clean. The regeneration also reconciled pre-existing snapshot drift:
the hand-written 20260811060000_goal_outcome migration had never been
baked into schema.json/schema.gen (the check was already red at HEAD);
the generator's duplicate of it was discarded so existing installs
never re-run the DDL.

S-2: scanDirectoryRef fragile-by-construction. The unset-ref invariant
lives only in the init→builder call order. The builder now reads the
ref defensively: if it is unset at build time, log an ERROR and skip
the scan (loud no-op) instead of querying with an empty directory that
silently matches no session. The alternative (threading the directory
through the ScopedCache key or InstanceState.make input) would require
modifying shared instance-state.ts beyond the listed files; the
defensive read is the accepted fallback. Not covered by a test: the
unset path is unreachable through the public seam — init sets the ref
before the only call site of InstanceState.get — so no injectable
unset-ref path exists without exposing internals.

S-3: serviceOption(SessionStatus) unsanctioned in the lease. The
dag-release re-trigger silently degraded to a dropped re-trigger when
SessionStatus was absent. SessionStatus.Service is now a HARD
requirement of the lease layer (Layer.sync → Layer.effect; the
serviceOption/None branch is gone); defaultLayer self-provides
SessionStatus.defaultLayer, and the node lists SessionStatus.node
(added to Session's node list — the documented "missing wire fails
silently" invariant). All production and test consumers already build
via defaultLayer, so only the standalone lease test needed wiring; it
now also gains a re-trigger test asserting the blocked goal claim is
re-driven through the real SessionStatus idle publish (typed via the
event definition's data schema, no unsafe assertions).

Verified: bun test test/goal test/dag
test/session/automation-lease.test.ts (579 pass, 0 fail) and
bun test test/session (408 pass, 0 fail), bun typecheck clean,
bun lint 4852 warnings (≤ 4852).

Co-Authored-By: Claude <noreply@anthropic.com>
fix(goal): close registration lifecycle, ownership re-trigger, and session-delete cleanup clusters (GOAL-FP-01)
fix(opencode): fence goal and dag automation
@LeXwDeX
LeXwDeX merged commit 7fb4e0f into main Aug 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant