Commit Graph

13 Commits

Author SHA1 Message Date
gsxdsm
9b573268a4 docs(u9): audit every non-scheduler execute() call site — exactly ONE ignores pause (#2747)
## What this closes

`executor-prompt.test.ts` has 3 failures asserting that `execute()`
refuses to dispatch a user-paused card during a global pause. The guard
lives in the **scheduler** (`scheduler.ts:1515`, a hard stop that never
reaches `.execute(`), so those tests call a layer that has never
enforced it.

I flagged this in #2719 without being able to say how large the real gap
was. This answers that — and the answer is much narrower than "execute()
is unguarded".

## Every non-scheduler call site

Enclosing method and guard status resolved **programmatically**, not by
reading nearby lines:

| Site | Enclosing method | Guarded? | Reading |
|---|---|---|---|
| `executor.ts:3333` | `dispatchUnpauseResume()` | no | **Correct
as-is** — it *is* the unpause path; a guard here is self-contradictory |
| `executor.ts:3495` | `constructor()` — `task:moved` sub, `to ===
"in-progress"` | **no** | **The one real gap** |
| `executor.ts:5702` | `resumeTaskForAgent()` | yes | `globalPause \|\|
enginePaused` + `!task.paused` |
| `executor.ts:5858` | `resumeOrphaned()` | yes | same guard earlier in
the method |
| `in-process-runtime.ts:2255` | `drainWorkflowContinuations()` |
indirect | gated by `status !== "active"`; engine pause is expected to
leave the runtime non-active |

**Exactly one path can reach `execute()` without consulting pause
state.** So the open question is not *"does `execute()` need a guard"*
but *"can a `task:moved` → `in-progress` event fire while paused"* —
narrow, and answerable by whoever owns the pause contract.

## A measurement correction worth recording

My first pass used a 40-line window above each call and **mis-attributed
two sites**: `:5702` and `:5858` looked unguarded because their guard
sits earlier in the same method, above the window. Resolving the
enclosing method properly flipped both to guarded and cut the apparent
gap from three sites to one.

That is the difference between reporting "3 of 5 paths ignore pause" and
the truth. A proximity heuristic is not an enclosing-scope analysis.

## Still not decided, deliberately

Three options, and they are not equivalent:

1. **Guard the `task:moved` subscription** — narrowest; keeps the
scheduler as the single pause authority. Does *not* make the three tests
pass, since they call `execute()` directly.
2. **Give `execute()` its own guard** — makes the tests pass, but must
not refuse the legitimate internal re-dispatch paths.
`dispatchUnpauseResume()` would break outright: it exists to resume a
card the operator just unpaused.
3. **Retire the three direct-`execute()` assertions**, covering the
invariant at the scheduler layer where it is enforced.

(2) and (3) both touch coverage of **user pause** — a safeguard this
program re-ratified and told workers not to narrow. Choosing either
silently inside a test-repair PR is how a safeguard gets weakened by
accident.

## What is NOT verified

Whether that subscription is actually **reachable** while paused.
Proving it needs a trace of who emits `task:moved` with `to ===
"in-progress"` under a global pause; if every emitter is itself gated,
the gap is theoretical. That trace is the remaining work before
preferring option 1 over the status quo.

Docs-only — no source, no tests. Lint clean.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added an audit documenting workflow pause behavior and identifying an
event-driven execution path that can create sessions during a global
pause.
* Recorded findings from existing test failures and reviewed available
enforcement points for pause handling.
* Documented trade-offs and the remaining decision on where pause guards
should be applied.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-30 08:51:32 -07:00
gsxdsm
ae23be79f7 fleet: scheduler.ts 28 → triaged (NOT converted) + repo-wide reachability measurement — the work order sorts on a number that doesn't predict convertibility (#2687)
## Claim

`packages/engine/src/scheduler.ts` — the largest **unclaimed** cluster
(28). Triaged, **not converted**, for the reason below. Census
unchanged: **722 → 722**. No baseline movement is claimed, because
nothing was converted.

## Why not converted

Three of us independently hit the same wall on our first file — #2683
and #2684 (`self-healing.ts`), #2685 (helper coverage). This measures
the whole backlog **once** so the remaining workers don't each pay that
cost.

Two constraints gate conversion. Neither is visible in the per-file
counts the work order sorts on.

### 1. Location — the helpers aren't importable from 80% of the backlog

`isIntakeColumnRole` / `isPreImplementationColumnRole` /
`isHoldColumnRole` live in
`packages/dashboard/app/utils/columnRoles.ts`, a dashboard-**app**
module.

| Location | Guards | Share | Importable? |
|---|---:|---:|---|
| `packages/engine/**` | 316 | 43% | no |
| `packages/dashboard/app/**` | 150 | 20% | **yes** |
| `packages/core/**` | 148 | 20% | no |
| `packages/dashboard/src/**` | 78 | 10% | no |
| `packages/cli/**` | 24 | 3% | no |
| plugins | 6 | 1% | no |

**150 of 722 (20%)** can call them at all. Widening the helper *set*
(#2685, correctly) does not move this number — it's the module's
location, not its coverage. Core already exports `resolveColumnFlags`,
so a core-side predicate module would be *the same* abstraction made
reachable, not a new one. It is a prerequisite for the other 80% and
**not sufficient** — see below.

### 2. Flag scope — the binding constraint

A role predicate needs resolved trait flags. Most guards run in
functions handed a bare task row with no IR to resolve from. Threading
one in changes a signature and its call graph: a **behavior change, out
of scope**.

File-level proxy over the 572 non-dashboard guards: **339** in files
that reference an IR/flags resolver, **233** in files with none.

**That proxy overstates convertibility, and the overstatement is the
finding.** Reachability varies *within* one file, so a file-level
verdict is unusable. In my claimed cluster:

| Site | Context | Convertible? |
|---|---|---|
| `scheduler.ts:1690` | `resolveWorkflowIrById(...)` +
`resolveColumnFlags(c)` in the same block | **yes** |
| `scheduler.ts:231` | `isLegacyDependencySatisfied(dep: Task \|
undefined)` | no — task only |
| `scheduler.ts:341` | `shouldHoldActiveFileScopeLease(...)` | no — task
only |

So "convert the file" is not a unit of work that exists in this backlog,
and the rule *"the baseline must shrink by exactly your converted
count"* cannot be satisfied per-file until the count is per-site.

## A guard that must be skipped, not guessed

`packages/core/src/task-merge.ts:254`:

```ts
if (!options.skipColumnIdentityCheck && task.column !== "in-review") {
```

The parameter is `Pick<Task, "column" | "paused" | ...>` — no IR,
deliberately. The in-source FNXC comment records that callers who *have*
resolved the `merge-blocker` trait pass `skipColumnIdentityCheck` rather
than spoofing `{ ...task, column: "in-review" }`. The trait-aware path
already exists *beside* this literal.

Converting it wouldn't remove a legacy id — it would delete the fallback
the option was introduced to make explicit. **Flagged and skipped.**

## Suggested census upgrade (not done here)

Emit per-site whether trait flags are resolvable in the enclosing scope.
That turns the work order from "largest file" into "largest
**convertible** cluster" and makes baseline shrinkage predictable. I did
not touch `scripts/lifecycle-column-census.mjs` — it is the shared
instrument and changing it unannounced would invalidate everyone's
in-flight before/after numbers.

## Method correction worth propagating to every fleet worker

Claim-collision scans must compare a branch to its **merge base**, not
to `origin/main`. `git diff origin/main origin/<branch> -- <file>`
reports a difference when the branch is merely *stale* (the file didn't
exist at its base) — it flagged dashboard and CLI PRs as touching engine
E2E files. I nearly skipped a free cluster on that false signal.

`feature/code-organization-wave17` is excluded from collision checks:
**1556 files, 150 commits behind main, already `DIRTY`**. It must rebase
wholesale regardless, and counting it as a claim marks *every* cluster
in the backlog as taken.

Docs-only — no source, no test, no census change.
2026-07-30 02:53:18 -07:00
gsxdsm
73338502e5 fix(test) + E2E: re-green main's lifecycle release leg, and prove the MERGED board + REVISE rework (#2634)
**Second batch.** Three commits, no production code. `pnpm test:gate`
green, `pnpm lint` clean, all three E2E suites together **3 files / 41
tests, exit 0**.

## 1. main's lifecycle E2E is RED right now — this fixes it

Independently of my work, on a detached `origin/main`: **2 failed / 18
passed**. Scenarios 1 and 2 fail with `sweep.released` **empty**.

**Cause:** `seedTask` relied on task creation's PROMPT.md, which is a
bootstrap seed (`"# <id>\n\n<description>"`). FN-7648's
`isUnplannedForExecution` reads that file for any card resting in an
intake- **or** hold-trait column and refuses to move an unplanned card
into a processing column. The sweep reported `held: [{ reason:
"move-rejected-or-no-slot" }]`.

**That is the gate working.** The fixture was asking the scheduler to
release a card that had never been specified. The fix is the one the
graph-entry contract doc already prescribes: *"Scheduler/release test
fixtures must model a card that cleared the gate ... A held unreviewed
card is the gate working."* `seedTask` now writes a planned PROMPT.md.

**Verified it repairs main, not just this branch:** applying only that
file to a detached `origin/main` leaves scenarios 1 and 2 **passing**,
with the 4 residual failures being scenarios 3 and 6 — which need the
fixture-options commit main does not have.

### I was wrong in #2627 and this corrects it

In #2627 I named the in-transaction capacity gate (#2488/#2499) as the
likely cause. **It was not.** Two hypotheses died, both recorded in the
code comment so nobody re-runs them:

| Hypothesis | Result |
|---|---|
| E2E settings lack `maxConcurrent` → capacity gate rejects the move |
added `maxConcurrent`/`maxWorktrees` → **still 2 failed**. Not the
cause. |
| the move itself is refused | a direct `moveTask(id, wip)` →
**succeeded**. Never the blocker. |

Only then did probing the two release gates give
`isTaskBlockedOnApproval=false`, `isUnplannedForExecution=true`, and
dumping the file show the stub. I've flagged the wrong lead on #2627 too
— a plausible-sounding cause pointed at another worker's PR is worse
than no lead.

## 2. E2E evidence: the MERGED intake+hold board

U11's shape — one column carrying intake **and** hold — had no
end-to-end coverage; every prior E2E drove intake and hold as separate
columns.

- shared fixture gains opt-in `mergedIntakeAndHold`, plus `MERGED_VOCAB`
(legacy ids, so a failure is attributable to the **role** merge alone)
and `MERGED_RENAMED_VOCAB` (ids move too).
- lifecycle scenario 3 drives the full spine: planning runs **in place**
on the dual-role column, the real `runHoldReleaseSweep` releases
**from** it, the graph runs to complete.
- 4 merge-safeguard cases on the merged board (finalize, proofless
refusal with the same reason, merged+renamed landing no legacy id,
at-most-once).

## 3. E2E evidence: a REVISE routes back through rework

The plan's `InReview → InProgress: review requests changes` had **no**
live-engine evidence on any board — the fixture's review seam always
succeeded.

Two things the engine taught me, both corrected here:
- the **IR validator refused** my rework edge: it is only legal into a
node with `config.reworkRegion: true`. A real contract, and the
validator catching it is the system working. `exec` now declares it (the
shape the builtin uses on `merge-attempt`).
- my first assertion was wrong. A REVISE does **not** leave the card in
wip — rework re-enters `exec` within the same run, review approves on
its second call, and the card finishes at complete. The evidence is the
**seam sequence**
`["planning","execute","review","execute","review","merge"]`, not an
intermediate column the run has already passed. Asserting the final
column alone would have been satisfied by a graph that ignored the
REVISE entirely.

## Both families are mutation-attributed

| Scenario | Mutation | Result |
|---|---|---|
| 3 — merged intake+hold | `isHeldTask` treats intake/hold as exclusive
| **exactly its 2 tests** fail |
| 6 — REVISE → rework | disable rework re-entry in
`workflow-graph-executor` | **exactly its 2 tests** fail |

Both fixture options are opt-in; the two pre-existing suites are
behaviourally unchanged (27 → 29 → 41 passed across the additions, no
existing assertion touched).

## Still not shipped: safeguard 2's graph E2E

Attempted twice, deleted both times. Attempt 1 passed and then survived
mutating `merge-gate` to ignore `task.autoMerge` — the card parked on
the review column's `merge-blocker` trait, not the gate. Attempt 2
removed that trait to isolate the gate, and the **control** case parked
too. Isolating it needs a merge path mirroring the builtin (`merge-gate
→ merge node → end`) rather than a direct edge to `end` — a real
redesign, not a speculative edit. The enforcement that holds today is
`allowInReviewMergeProcessing` in `project-engine` (unit-mutation
verified, NEW=9; gated via #2526).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:23:32 -07:00
gsxdsm
99be8e6153 docs(U9): correct the safeguard baseline — safeguards 1 and 4 are NOT covered (#2520)
**U9, PR4.** Docs-only correction to a document already on `main`
(#2511). No changeset.

## I got #2511 wrong, and it matters

#2511's table claimed all six merge safeguards were verified. **Two of
them were not**, and the error is the same family this program exists to
stamp out: I reported the **absolute** failure count under mutation,
with **no baseline**.

`merge-error-recovery.test.ts` (10 failures) and `self-healing.test.ts`
(1) are **already red on clean `main`**. The "11 failed" I credited to
the row 1 mutation *was that pre-existing red*. The mutation added
nothing. I even flagged the identical `11 failed` on rows 1 and 3 as "a
red flag" in my own notes and then did not chase it.

## Re-measured as deltas

Baseline fail-SET vs mutated fail-SET on the identical selection,
reporting only NEW failures, each named:

| # | Safeguard | Baseline | Mutated | **NEW** | Verdict |
|---|---|---|---|---|---|
| 1 | user pause | 11 | 11 | **0** | **NOT COVERED** |
| 2 | `autoMerge:false` | 0 | 9 | **9** | covered |
| 3 | dependency gating | 0 | 5 | **5** | covered |
| 4 | capacity single-flight | 10 | 10 | **0** | **NOT COVERED** |
| 5a | merge-proof (pre-enqueue) | 0 | 1 | **1** | covered, thin |
| 5b | file-scope | 0 | 6 | **6** | covered |
| 6 | at-most-once | 0 | 3 | **3** | covered |

**Four hold. Two do not.**

- **Safeguard 1** is the pause invariant re-ratified in #2486. Removing
`task.paused || task.userPaused` from the merge admission provider
admits a **user-paused card into the merge pump** — and nothing fails.
- **Safeguard 4** is the single-flight guard that serializes merge.
Removing it permits concurrent `drainMergeQueue` entry — and nothing
fails.

Both guards **work correctly today**. What is missing is any test that
would notice if they stopped. That is exactly the state U9 must not
convert on top of — and #2511 said the opposite.

## Also corrected: nothing here is defended by blocking CI

The one gate-admitted file (`merger-merge-lifecycle.test.ts`) is not the
file that proves any surviving row. Rows 2/5a/6 rest on
`project-engine.test.ts`, row 3 on core's `task-merge.test.ts` — neither
is in the gate (core's gate is two PG tests via `test:pg-gate`).

## Three distinct ways the first pass was wrong

All recorded in the doc, because each produced a confident wrong answer:

1. **Absolute counts with no baseline** — rows 1 and 4. A mutation run
must diff fail-sets and report only new failures.
2. **Too-narrow selection** — an earlier pass measured rows 1 and 3 at
zero and I nearly filed two false gaps. Widening fixed row 3 but is also
how the pre-existing red crept in. Both directions need the baseline
diff.
3. **A harness that silently matched nothing** — the delta harness's
regex required a `|project|` segment in vitest's `FAIL` line.
`@fusion/core` does not emit one, so it parsed **zero** failures at both
baseline and mutation and printed "NOT COVERED" for row 3, which is
covered by 5 tests. A verification tool that reports success without
checking anything is worse than no tool; it must be tested against a
known-failing case first.

The harness now aborts on a no-op patch, asserts a clean restore, and I
validated its parser against a known-failing run before trusting it.

## Next

**PR5 writes the missing tests for safeguards 1 and 4**, then gate
admission. Neither should wait for U8 — they guard code that exists
today, and the conversion needs them in place first. That is the
reversible call I'm making rather than asking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:19:35 -07:00
gsxdsm
aeef592187 docs(U9): safeguard baseline — six merge safeguards verified by mutation (#2511)
**U9, PR3.** Docs-only, one new file, zero production changes. No
changeset (internal docs). Independent of #2504.

This is the six-row safeguard table required as a U9 artifact —
delivered **verified**, replacing the partial, unearned version I put in
#2494.

## Every row proven by mutation

Break the guard in production, run the cited tests, confirm red,
restore. A cited test that does not fail is not evidence.

| # | Safeguard | Consulted at | Result under mutation |
|---|---|---|---|
| 1 | user pause | `project-engine.ts:645` (merge admission filter) |
**11 failed** / 1865 passed |
| 2 | `autoMerge:false` | `project-engine.ts:2797`
`allowsAutoMergeProcessing` | **9 failed** / 250 passed |
| 3 | dependency gating | `task-merge.ts:402` unresolved-dependency
reason | **5 failed** / 94 passed |
| 4 | capacity | `project-engine.ts:3178` single-flight `mergeRunning` |
**11 failed** / 1445 passed |
| 5a | merge-proof (pre-enqueue) | `project-engine.ts:2609`
`getTaskMergeBlocker` consult | **1 failed** / 272 passed |
| 5b | merge-proof (file scope) | `merger-file-scope.ts:200`
`FileScopeViolationError` | **6 failed** / 172 passed |
| 6 | at-most-once | `project-engine.ts:2730` `mergeActive` dedupe | **3
failed** / 263 passed |

**All six hold. Nothing is currently broken.** Per-row test attribution
is in the doc.

## Finding 1 — one of nine safeguard test files runs in blocking CI

Only `merger-merge-lifecycle.test.ts` is in the `engine-core`
allow-list. The core gate is two PG tests (`test:pg-gate`) and does not
include `task-merge.test.ts`.

AGENTS.md: CI blocks on Lint/Typecheck/Build/Gate, and "a red
non-blocking run is information, not a merge stopper."

So a change breaking **user pause on merge admission, dependency gating,
capacity single-flight, or the file-scope invariant** does not block a
PR today — it goes red in full-suite, after the merge.

Acceptable for a lane nobody is rewriting. Wrong for the lane U9
rewrites next. **Recommendation: admit the highest-value safeguard tests
to the gate before conversion begins, with the budget cost measured** —
engine-core is 5.36s against a ~60s ceiling, so there is room, but I
won't assume it. Proposed as PR4.

## Finding 2 — safeguard 5a rests on a single non-gate test

Removing the pre-enqueue merge-blocker consult fails exactly one test.
Thinnest of the six, on a destructive-risk gate. Its sibling 5b is well
covered (6 tests), so the invariant isn't unguarded — but the consult
that keeps a blocked task out of the queue very nearly is.

## Methodology note, because it cost an hour

**Rows 1 and 3 initially measured ZERO failures and looked like coverage
gaps. Both were wrong** — the test selection was too narrow. Widening
row 1 from three files to
`project-engine|merge|concurrency|self-healing` turned 0 failures into
11. Row 3's real coverage lives in `@fusion/core`'s suite, which `pnpm
--filter @fusion/engine` never runs, even though the engine config
aliases `@fusion/core` to source so the mutation *was* live.

I nearly reported two false gaps. Recorded as two rules: a narrow
mutation run cannot prove absence of coverage, and cross-package guards
need cross-package runs.

## Scope

New file only — deliberately **no** edit to
`docs/workflow-policy-ownership-map.md`, because #2504 already edits
that file at the same anchor and I want both PRs independently
revertable. Cross-link follows once both are on main.

Verified: no production diff, all mutations restored, `pnpm lint` clean.

## Not covered, stated rather than implied

Reviewer-lane safeguards; FN-7720 operator bypass; FN-8492
orphaned-pending-step rewrite; branch-group promotion sequencing. Each
needs its own verified row before the matching conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:50 -07:00
gsxdsm
6ee20d9817 docs(U9): correct merge-stack slice statuses to measured wiring state (#2504)
**U9, PR2.** Docs-only, no changeset (AGENTS.md: internal docs).

## Why

All seven slices in `docs/plans/workflow-owned-merge-stack/` were marked
`draft-stack-handoff` — accurate when drafted 2026-06-09, wrong now. **A
worker picking up this stack cold would have re-implemented S04, which
has already landed.** I nearly did.

## Measured, against `main @ 46f35323c`

| Slice | Was | Now | Evidence |
|---|---|---|---|
| S02 projection | draft | `landed-unwired` |
`projectMergeRequestToWorkflowWorkItem` implemented, **0 production
callers** |
| S03 scheduler claim | draft | `landed-unwired` |
`claimDueWorkflowWorkItem` implemented; its only caller is S05's
processor, itself unwired |
| S04 IR regions | draft | **`landed`** | `merge-gate`, `merge-retry`,
`manual-merge-hold`, `merge-attempt`, `recovery-router` present in the
coding IR |
| S05 runtime driver | draft | `landed-unwired` | `runWorkItem` /
`processDueWorkflowWorkItem` implemented, exported from `index.ts`, **0
production callers** |
| S06/S07/S08 | draft | `not-started` | merge still runs through
`merger.ts` + the live `ProjectEngine.mergeQueue` pump |

## The finding that changes U9's sequencing

`WorkflowWorkItemKind` is `task | merge | retry | manual-hold |
recovery`. The only live pump —
`InProcessRuntime.drainWorkflowContinuations` — filters `kinds:
["task"]`. The generic processor that would claim the other four kinds
has **no production caller**.

So the entire merge-lane work-item vocabulary is dormant: **zero
writers, zero readers.**

**I checked whether this is a live bug and it is not.** Nothing in
production writes a non-`task` kind — the only two writers
(`plan-review-continuation.ts`, `workflow-column-boundary-hooks.ts`)
both go through `replaceActiveTaskWorkflowContinuation`. Nothing is
stranded today. I'd rather say that plainly than let a scary-sounding
finding stand unqualified.

But it produces a hard ordering constraint, now recorded in S07:

> **S07 must not land before S03/S05 are actually driven.** S07 is the
slice that starts writing `merge`-kind work items. If it lands first,
those items are created and never claimed — a card that reaches the
merge boundary and silently stops.

This also reframes U9's job on S02/S03/S05: **wire them, don't build
them.**

## Scope discipline

Docs-only — `git diff --stat` is 9 files, all under `docs/`. No
production code, no tests, no behavior. `pnpm lint` clean.

I also fixed the three parent-plan lines asserting the slices are "all
still `draft-stack-handoff`", and the four landed slices' Stack Role
paragraphs that would otherwise contradict their own new Measured State
block. Leaving those stale would recreate exactly the defect this PR
fixes.

Related: #2494 pins the S04 caveat — the IR regions are declared but
their config is read by nothing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Updated workflow-owned merge planning documents with current
implementation and wiring statuses.
- Added measured wiring details showing which workflow capabilities are
active, implemented but unused, or not started.
- Clarified sequencing requirements to ensure merge processing is not
enabled before prerequisite workflow paths are operational.
- Corrected slice metadata and references to reflect the latest measured
state.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:16 -07:00
gsxdsm
c5b824137e feat(FN-000): process workflow-owned merge work
Fusion-Task-Id: FN-000
2026-06-11 08:43:28 -07:00
gsxdsm
296f191a83 feat(FN-000): create workflow merge work on handoff
Fusion-Task-Id: FN-000
2026-06-11 08:43:27 -07:00
gsxdsm
53d644714c feat(FN-000): extract workflow merge node capability
Fusion-Task-Id: FN-000
2026-06-11 08:43:27 -07:00
gsxdsm
fa4445138a feat(FN-000): run workflow work items through runtime
Fusion-Task-Id: FN-000
2026-06-09 20:44:45 -07:00
gsxdsm
265496d0b6 feat(FN-000): model merge policy in builtin workflow IR
Fusion-Task-Id: FN-000
2026-06-09 20:44:45 -07:00
gsxdsm
0ec7b496ae feat(FN-000): claim workflow work in scheduler substrate
Fusion-Task-Id: FN-000
2026-06-09 20:44:45 -07:00
gsxdsm
5a36ac1cd6 feat(FN-000): project merge requests to workflow work
Fusion-Task-Id: FN-000
2026-06-09 20:44:36 -07:00