Commit Graph

10 Commits

Author SHA1 Message Date
gsxdsm
8c9b84ae38 batch-core: packages/core + dashboard/src lifecycle conversion (129 → 92) (#2780)
## batch-core — `packages/core` + `packages/dashboard/src`

Shared branch: two workers are converting into it. Opening the PR
because the branch was green with none, and a branch without a PR merges
nothing.

### Census

Measured with `node scripts/lifecycle-column-census.mjs --json`.

| | guards |
|---|---|
| batch-core scope at branch point | 129 |
| batch-core scope now | **92** (51 files) |
| repo total now | 358 |

Files closed so far: `store.ts` 11→0, `task-merge.ts` 6→0,
`live-agent-count.ts` 6→0 (marked, not converted — see #2762),
`task-update.ts` 3→0, display-ordering + Wake Delta ranking 5→0,
`register-git-github.ts` 4→0.

### The `register-git-github.ts` slice

Three PR routes — `pr/create`, `pr/push-branch`, `pr/resolve-conflicts`
— plus the `CHANGES_REQUESTED` handler each compared `task.column !==
"in-review"`. On a renamed board **none** of them matched, so every PR
affordance the dashboard offers was refused for a card sitting in the
lane that board calls review, and the refusal named a column that does
not exist there.

All four now share one helper, `reviewColumnsForTask`, which gets two
things right that this program has repeatedly gotten wrong:

- **Membership, not a single id.** It takes the broad review set
(`mergeOrchestration ∪ mergeBlocker ∪ humanReview`).
`resolveLifecycleColumns` returns the *first* column per trait, so a
single-id answer silently ignores a board that declares a merge lane
**and** a separate human sign-off lane. These guards only refuse or
permit — they never move the card — so over-admitting costs nothing
while under-admitting refuses a request that should have worked.
- **An empty resolved set means UNEXPRESSED, not absent.**
`synthesizeDefaultColumns` upgrades a v1 graph by emitting every default
column with `traits: []`, so a v1-upgraded workflow resolves to an empty
review set while its `in-review` column plainly exists and holds the
card. Reading empty as "this board has no review lane" would refuse
these routes on **every pre-v2 project** — a worse regression than the
one being fixed, and invisible to any v2 test.

This is the dashboard twin of the `fn pr create` guard in
`packages/cli/src/commands/pr.ts` (#2775). The two surfaces answer the
same question and now agree — FN-5893 surface enumeration.

### Testing note: why the seam and not the routes

I wrote route-level HTTP tests first and **deleted them**. An express
fixture over `registerGitGitHubRoutes` hangs — every case, including the
pure refusals, times out at 4s, because registering the router starts
background work the fixture never satisfies. Making it run would mean
mocking git, the GitHub client, and the pollers: a mock-the-world shell,
which is what the project's do-not-add-slow-tests rule (FN-5048) says to
avoid in favour of a narrow seam.

`reviewColumnsForTask` *is* the narrow seam — it holds the entire
decision, and the four call sites now do nothing but ask it and render
its answer. Six cases pin it: the renamed lane is returned and
`in-review` is not, a two-lane board returns both, a v1-upgraded board
falls back, an unresolvable workflow falls back, and the refusal renders
lanes an operator can act on.

**Mutation-verified, both directions:** reverting the helper to the
legacy literal fails 2 of 6; treating an empty set as an answer fails 1
of 6.

One fixture bug worth recording, since it would have made the two-lane
case vacuous: the trait id is kebab-case `human-review`, not
`humanReview`, and the built-in traits must be registered via `import
"@fusion/core"` before flags resolve.

### Verification

- `pnpm --filter @fusion/dashboard exec tsc --noEmit -p tsconfig.json` →
0 errors
- `pnpm lint` → 0 errors
- `register-git-github.review-lanes.test.ts` → 6 passed

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 09:28:27 -07:00
gsxdsm
18641ba5d2 fleet: the age-staleness hydration site #2746 missed (3rd time in this file), and a blocker that blocked forever (#2749)
## Census

| | before | after |
|---|---|---|
| `packages/core/src/task-age-staleness.ts` | 4 | **0** |
| `packages/core/src/blocker-fanout.ts` | 4 | **1** (the marked
no-metadata fallback) |
| repo backlog | 581 | **573** |

Baseline re-recorded; `--strict` exits 0.

## Both were the unconverted sibling in an already-converted file

That is the shape this program keeps re-finding, and both files here
even carry notes about *previous* P1s on the same question.

### 1. Age staleness never fired

`getTaskAgeStalenessSignal` returns `undefined` unless the card is in
wip **or** review, then picks its warning/critical thresholds by which
of the two it is. Keyed on the literals, a renamed board produced **no
age-staleness badge at all**.

That is the worst shape a monitoring failure can take: **a missing
warning is indistinguishable from health**. Nothing looks broken — "this
card has been sitting in progress for a day" simply stopped being said.

`reads.ts` already resolves `holdColumn` and `reviewColumn` per row for
the sibling signals. Its own comments record a P1 where exactly this
role was threaded into a helper but **omitted at both hydration sites**
— "same defect, same file, one role over". So this adds the third
resolver (`resolveWipColumnForTask`, mirroring the review twin) and
threads **both** lanes at **both** sites, off the same per-pass IR
cache.

The signal's reported `column` deliberately stays on the legacy id: that
field is its public shape, which consumers switch on, so renaming it is
a separate breaking change rather than part of resolving a guard.

### 2. A blocker that blocked forever

`isStaleBlockedByBlocker` decides whether a `blockedBy` marker is stale.
Keyed on the literals, a **finished** blocker on a renamed board never
read as stale — so the dependent kept its marker permanently and its
"waiting on" badge pointed at work that shipped days ago. Every path
that clears a stale marker consults this predicate first, so nothing
else rescues it.

`computeBlockerFanoutMap` — the **only** production caller — already
takes `terminalColumns`/`holdColumn`/`classify`, and the file documents
two separate P1s about getting this right. The predicate sat on the
literals and the call passed nothing.

**Both are fixed, and that matters more than it sounds:** converting the
predicate alone would have changed *nothing at runtime* while the census
scored it as a 4-site win. That is the half-conversion trap, and it is
why the wiring gets its own revert proof below.

## Revert proof — each reverted alone

| reverted | result |
|---|---|
| age-staleness lanes → literals | **3 failed** / 8 passed |
| blocker predicate → literals | **2 failed** / 9 passed |
| fanout **wiring** (`classify` not consulted) | **1 failed** / 10
passed |
| none (shipped) | **11 passed** |

Each group also carries a paired negative — a non-active lane still
raises no staleness signal, and a live blocker is still not stale — so
neither fix can degrade into "always fires".

## Verification

- 4 core suites (blocker/staleness/age/reads) — **33 passed**, no
regressions
- new suite — **11 passed**
- `pnpm test:gate` — **10 / 158 / 487 / 71** · `pnpm lint` clean · core
`tsc --noEmit` **0 errors**

## The 1 remaining

`blocker-fanout.ts` keeps one `DELIBERATE-LITERAL`: the no-metadata
fallback for an unconverted caller. Deleting it makes an unresolved
caller read every blocker as non-terminal, so stale markers would never
clear **at all** — strictly worse than the legacy behaviour it would
replace.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 08:35:24 -07:00
gsxdsm
5d0f1ef631 Phase B slice B1: lifecycle column roles in the U6 policy modules (4 guards, red-green) (#2479)
**Stacked on #2469** → #2468 → #2467. Base is
`feature/workflow-capacity-ground-truth`.

This is **slice B1 of Phase B, not all of Phase B.** Sizing escalation
sent separately; the census is below.

## Why this is a slice

Measured census of code lines referencing a lifecycle column literal
(comments excluded):

| Unit | Files | Sites |
|---|---|---:|
| U4 | `self-healing.ts` | 203 |
| U5 | `executor.ts` 171, `scheduler.ts` 55, `replan-target.ts` 20,
`merger-ai.ts` 5, `hold-release.ts` 4, `mesh-lease-manager.ts` 4,
`task-agent-sync.ts` 3 | 262 |
| U6 | `moves.ts` 34, `default-workflow-hooks.ts` 13, `board-config.ts`
9, `blocker-fanout.ts` 6, `task-priority.ts` 5,
`dependency-blocked-todo-report.ts` 2, `stale-paused-todo.ts` 1 | 70 |
| | **Total** | **535** |

The plan's "~207" counts the guard category only. Under the phase's
non-negotiable rule — a test that **fails before** conversion, per guard
— that is ~200 red-green cycles. Doing it as one sweep would reproduce
exactly the failure this phase exists to prevent: converted guards
nobody proved still fire.

`moves.ts` and `default-workflow-hooks.ts` stay **parked** per the
dispatch constraint (move-path convergence and the pool-id sentinel are
on an operator decision).

## Guards converted (4), each red-green

Every case below was written **first** and observed failing against the
literal implementation.

| Module | Guard | Before → After |
|---|---|---|
| `stale-paused-todo.ts` | stall detection | `column !== "todo"` →
resolved **hold** column |
| `blocker-fanout.ts` | active | `ACTIVE_COLUMNS.has(col)` →
`!terminalColumns.has(col)` |
| `blocker-fanout.ts` | hold-wait metric | `col === "todo"` → resolved
**hold** column |
| `task-priority.ts` | unblock active | `UNBLOCK_ACTIVE_COLUMNS`
**deleted**, folded into the terminal set |

Three of the seven new cases are **regression floors** that pass before
and after. One of them earned its keep immediately: it failed on my own
fixture (`activeCount` vs the public `totalCount`), catching a bad test
rather than bad code — which is the point of asserting the default path
alongside the renamed one.

### The `task-priority` finding

`UNBLOCK_ACTIVE_COLUMNS` and `DONE_COLUMNS` encoded **one concept
twice**, two lines apart, and disagreed for any custom column:
dependency counting treated a `drafting` card as unmet (correct) while
the active check treated it as inactive (wrong), zeroing the blocker's
unblock weight. The enumeration wasn't just legacy-shaped — it
contradicted its own neighbour.

## ⚠️ Behavior change, not a pure refactor

Inverting active from enumeration to exclusion means **a card in a
column that is neither terminal nor in the legacy enum now counts as
active where it previously did not.** That is the plan's stated intent,
but it is a real change for any project already using a custom column —
**Coding (Ideas)' `ideas` column is the in-tree case.** Fan-out counts
and unblock weights for such cards will rise.

## Verification

- Four affected suites green (45 tests), each conversion observed
red→green.
- `pnpm lint`, `tsc --noEmit` (core) green.

**Not verified / not done, stated plainly:**

- **Call sites are not wired.** These modules now *accept* resolved
roles; every parameter still defaults to the legacy set, so at the call
sites the vocabulary is unchanged. A caller that cannot resolve a
workflow keeps literal behavior. Threading `resolveLifecycleColumns`
through `reads.ts` and `self-healing.ts` is follow-on work — until then
the guards are *convertible*, not *converted end-to-end*.
- `dependency-blocked-todo-report.ts` and `board-config.ts` are
untouched in this slice.
- 19 core-suite failures exist on this branch; all confirmed
**pre-existing** by stashing and re-running on a clean tree
(`duplicate-guard`, `log-severity-spam-contract`, `settings-parity`,
`task-delete-caller-attribution`, `settings-defaults`).

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

---

**Supersedes #2470**, which GitHub force-closed when its base branch was
deleted by the merge of #2469 and refuses to reopen. Same head branch,
same commits (rebased onto `main`), now based on `main` directly. The
two P1 review threads on #2470 were resolved there — one of them with a
correction noting the threading half landed in code that was
subsequently deleted as a dead feature in #2477.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Dependency and blocker reports now correctly recognize custom hold,
active, and terminal workflow columns.
* Blockers in renamed terminal columns are no longer incorrectly
reported as active.
* Stale paused-task badges and self-healing now work with
workflow-specific hold columns.
* Mixed boards with different workflow column names are handled
consistently.
* Existing default workflow behavior remains compatible, including
fallback handling when workflow details cannot be resolved.

* **Enhancements**
* Reporting and task-priority calculations now support configurable
single or multiple hold and terminal columns.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 14:19:32 -07:00
gsxdsm
49114a27ad fix(dashboard): show Merging badge during AI-merge reviewing/landing
AI merge spends most of its time in reviewing and landing, not merging.
Treat the full merge pipeline as active so cards, workflow switcher, and
stall suppression show Merging… while the pump owns a task.
2026-07-15 10:36:10 -07:00
gsxdsm
e2a3a37b26 FN-6569: make auto-merge retry cap configurable
Adds a project setting that controls how many auto-merge conflict retries run before recovery paths give up.

- Add maxAutoMergeRetries to project settings defaults, schema, types, and dashboard controls.
- Resolve the retry cap in engine merge handling, self-healing recovery, stall detection, and blocker fanout logic.
- Cover custom retry caps with core, dashboard, and engine regression tests.

Files changed:
 .changeset/fn-6569-max-auto-merge-retries.md       |  5 ++
 docs/architecture.md                               |  2 +-
 docs/settings-reference.md                         |  1 +
 .../core/src/__tests__/settings-defaults.test.ts   | 12 ++++
 packages/core/src/in-review-stall.ts               | 16 ++++-
 packages/core/src/index.ts                         |  1 +
 packages/core/src/settings-schema.ts               |  6 ++
 packages/core/src/task-priority.ts                 |  4 +-
 packages/core/src/types.ts                         |  8 +++
 .../dashboard/app/components/SettingsModal.tsx     |  8 +++
 .../components/settings/sections/MergeSection.tsx  | 26 +++++++
 .../app/hooks/__tests__/useBlockerFanout.test.ts   |  5 +-
 packages/dashboard/app/hooks/useBlockerFanout.ts   |  3 +-
 .../auto-merge-retry-cap-settings.test.ts          | 79 ++++++++++++++++++++++
 packages/engine/src/project-engine.ts              | 77 +++++++++++++--------
 packages/engine/src/self-healing.ts                | 30 +++++---
 16 files changed, 236 insertions(+), 47 deletions(-)

Fusion-Task-Id: FN-6569
Fusion-Task-Lineage: 42242d6a-68bc-41f1-b2d9-af2e6f168eed
2026-06-17 04:51:31 -07:00
gsxdsm
02ba659dd8 feat(FN-4969): merge fusion/fn-4969 2026-05-17 22:57:15 -07:00
Fusion (runfusion.ai)
52b167879b fix(FN-4663): make expedite route persistence-safe and idempotent
Fusion-Task-Id: FN-4663
Fusion-Task-Lineage: 80975e9f-51e2-42ee-9f61-5e065a3fed24
2026-05-15 23:28:22 -07:00
Fusion (runfusion.ai)
1aad59aeb2 feat(FN-4663): complete Step 4 — add stranded refinement API endpoints
Fusion-Task-Id: FN-4663
Fusion-Task-Lineage: 80975e9f-51e2-42ee-9f61-5e065a3fed24
2026-05-15 23:28:22 -07:00
Fusion
fbf91c74a9 feat(FN-3596): unify board/list task ordering, split TaskDetailModal test m
The merge introduces a major TaskDetailModal test refactor (splitting a 6.7K-line monolith into five focused suites), significant merger improvements including autostash race-rescue, deduplication, and advisory logging for destructive operations, a new TUI narrow-mode log-split feature for the dashb

Fusion-Task-Id: FN-3596
2026-05-06 18:37:23 -07:00
Fusion
52168d7f2c feat(FN-2383): persist task priority across core storage
- Add task-priority contract, normalization helpers, and exports in @fusion/core types/index
- Store task priority in SQLite and migrate existing databases with default values
- Update task store behavior and sorting tests to preserve and order by persisted priority
- Add migration/regression coverage for archived tasks and refresh storage/task-management docs
2026-04-24 08:54:25 -07:00