aedee4b8231bf050c3240a00ab6645ede5d87ee9
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
083f8f5c5b |
fix(glasses): every card on a renamed board badged "todo", including cards in review (#3015)
## Every card on a renamed board badged `todo` — including cards in
review
```ts
export function statusBadge(column: Task["column"]): string {
return COLUMN_BADGES[column] ?? "todo";
}
```
`COLUMN_BADGES` maps the six legacy ids to themselves. On a board whose
lanes are named anything else **every lookup misses**, so every card
badges `todo` — a card sitting in review tells the wearer it is
un-started, and the whole board carries one identical badge.
On a display with room for a single word, that is worse than an
unrecognised lane: it is a *confident wrong answer* rather than a
missing one.
Reached from `taskToCard` (the main card) and `notificationCard` (the
notification badge).
## Fix, and the dead weight it exposed
The badge **is** the column id, so the function now says so:
```ts
export function statusBadge(column: Task["column"]): string {
return column;
}
```
That also retires `COLUMN_BADGES`. Once the fallback is the id, a table
mapping each legacy id to *itself* decides nothing — six lane literals
sat in this file doing no work. It was module-private with `statusBadge`
as its only consumer and it was a pure identity, so behaviour on legacy
boards is byte-identical: this is not a behaviour change riding along
with a cleanup, it is the dead weight the fix exposed, removed rather
than left as a decoy.
Mirrors `columnLabel` in the CLI (`COLUMN_LABELS[column] ?? column`) for
the same reason: a board that calls its lane `checking` should read
`checking`. No resolution needed; the id is in hand at the call site.
Note the census count for this file does **not** move — those six were
object keys, not comparisons, which is exactly the scope the census
documents for itself.
## This is a miss in my own #2968
That PR fixed the summary card's counts **in this same file** and never
looked one function further at the per-card badge those counts sit
above. Worth saying plainly, because it is the practical reminder behind
the census finding I have been repeating all run: *a file having had a
defect fixed is not evidence about its neighbours* — and here the
neighbour was nine lines away, in a function I had read.
## Revert proof
```
AssertionError: expected 'todo' to be 'checking'
Tests 1 failed | 9 passed (10)
```
The paired case ("still badges the legacy ids exactly as before") passes
both ways by design — it guards against the fallback change altering
known boards, so I am not counting it as coverage of the defect.
## Verification (measured)
- plugin suite — **198 passed / 19 files**
- `tsc --noEmit`, `eslint` — clean
- `lifecycle-column-census --strict`, `check-lane-wiring`,
`check-sql-column-literals`, `check-inert-flag-seams`,
`check-fnxc-future-dates` — green
No changeset: the plugin is `private: true` and is not bundled into the
published CLI.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Status badges now accurately display a card’s actual lane, including
previously unrecognized lanes.
* Preserved existing badge behavior for known lanes.
* **Tests**
* Added coverage to verify accurate lane reporting and legacy behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
eb9cd431c1 |
fix(glasses): settings silently discarded the operator's own lane names, so notifications never fired (#2973)
## The operator configures their lanes, and the plugin throws it away
`TaskColumn` was the closed legacy union and `COLUMN_SET` gated every
settings read against it:
```ts
.filter((value): value is TaskColumn => COLUMN_SET.has(value as TaskColumn));
```
**`notifyOnColumns` is a free-text string array in the schema** (`type:
"array", itemType: "string"`) — the UI invites any lane name. So an
operator on a renamed board types `checking`:
1. `getNotifyColumns` filters it out — not in the legacy five
2. `columns.length === 0`, so it substitutes `DEFAULT_NOTIFY_COLUMNS` =
`["in-review"]`
3. their board has no `in-review`
4. `notifier.ts` builds `new Set(getNotifyColumns(...))`, and
`diffSnapshots` tests `notifyOnColumns.has(task.column)`
**No notification ever fires.** The feature is silently off while the
setting reads as configured, and nothing surfaces an error.
`quickCaptureDefaultColumn` had the same shape with an extra irony: it
was rewritten to `todo` *before* `normalizeCaptureColumn` saw it — and
that function already validates against the board's **declared** columns
and falls back to the workflow's own intake lane. The pre-filter
destroyed the operator's answer immediately before the code that could
have honoured it.
## Fix
Validation is now **structural** (non-empty string) rather than
**vocabulary-based**. `TaskColumn` mirrors core's `ColumnId`
(`LegacyTaskColumn | (string & {})`), so legacy ids keep autocomplete
while custom ids are admitted. `COLUMN_SET` survives only as the
quick-capture dropdown's suggestions, not as a gate.
**Deliberately not resolving the board's columns.** That needs a
board-columns endpoint `FusionApiClient` does not have — a new read, not
a rename — and that gap is already recorded in this file by an earlier
audit. This change is orthogonal to it: accepting operator input
requires no resolution at all, which is why it doesn't wait on the
endpoint.
**Trade-off, taken knowingly:** a typo'd lane is now honoured and will
match no card. That is the milder failure. Before, a *correct* custom
lane was discarded categorically, so renamed boards could not use the
feature at all; now the only broken case is one the operator typed wrong
and can see in their own settings.
## Two existing assertions asserted the defect
```ts
expect(getNotifyColumns({ notifyOnColumns: ["nope"] })).toEqual(["in-review"]);
expect(getQuickCaptureColumn({ quickCaptureDefaultColumn: "bad-column" })).toBe("todo");
```
I checked provenance before touching them: they date to the plugin's
original commits (FN-3738 / FN-3970), when the five ids genuinely were
the whole vocabulary. They carry no reasoning comment and no later
change defended custom-lane rejection as a contract — so this is a stale
assumption being corrected, not a peer's tested decision being
overwritten. Structural rejection (non-strings, blanks, whitespace-only)
and trimming are still asserted, because that part was always right.
## Revert proof
```
AssertionError: expected [ 'in-review' ] to deeply equal [ 'checking', 'shipped' ]
AssertionError: expected [ 'todo' ] to deeply equal [ 'todo', 'spaced' ]
Tests 2 failed | 5 passed (7)
```
## Verification (measured)
- plugin suite — **196 passed / 19 files**
- `tsc --noEmit`, `eslint` — clean
- `lifecycle-column-census --strict`, `check-fnxc-future-dates` — green
No changeset: the plugin is `private: true` and is not bundled into the
published CLI.
|
||
|
|
5d01164994 |
test(glasses): re-green 15 agent-action tests, and revive a guard that asserted nothing (#2969)
## 15 tests have been red on `main`
`startWork`, `requestReview` and `approvePlan` gained a third `moveTask`
argument, `{ moveSource: "user" }`. The assertions in
`agent-actions.test.ts` kept the two-argument form:
```
AssertionError: expected "vi.fn()" to be called with arguments: [ 'FN-1', 'building' ]
Received: [ "FN-1", "building", + { "moveSource": "user" } ]
Tests 15 failed | 41 passed (56)
```
Reproduces on clean `origin/main`. This suite is outside the merge gate,
which is why it went red unnoticed.
## The part that is worse than staleness
Three of these are **negative** assertions, and they did not fail — they
went **dead**. `expect(fn).not.toHaveBeenCalledWith(id, column)` cannot
match a three-argument call, so it passes whether or not the forbidden
move happened. `requestReview`'s "never lands on the legacy `in-review`"
guard has been asserting nothing since the option landed.
Verified rather than reasoned about — a scratch case, run and then
deleted:
```ts
const fn = vi.fn();
fn("FN-1", "in-review", { moveSource: "user" }); // the forbidden move HAPPENED
expect(fn).not.toHaveBeenCalledWith("FN-1", "in-review"); // old form: passes anyway ✓
```
Both cases passed, confirming the old form is vacuous and the
three-argument form throws.
## Applied per action, not uniformly
Only **3 of 5** product `moveTask` calls take the option, and the split
is deliberate:
| action | source | why |
| --- | --- | --- |
| `startWork`, `requestReview`, `approvePlan` | `{ moveSource: "user" }`
| the wearer's tap is a human gesture, matching the dashboard move route
|
| `returnToAgent`, `retryTask` | default (engine) | per the Move-Task
contract a user-source move parks the row `userPaused`, defeating the
return/retry intent |
So `returnToAgent`/`retryTask` assertions are **correct as
two-argument** and are left untouched — including their negatives, which
genuinely assert because the real call is also two-argument.
Attribution was done by scoping each assertion to its enclosing `it(`
block, not to the nearest preceding call: the latter mis-attributes the
`await expect(startWork(...)).resolves` form, which reads as `expect`.
Blocks mixing a user-source and a default-source action were excluded
from rewriting (there were none).
I did **not** "fix" the 3/5 split. It is documented in the product with
its reasoning and it matches the Move-Task contract; changing it would
be a behavior change riding in a test-only commit.
## Verification (measured)
- `agent-actions.test.ts` — **56/56 passed** (was 15 failed / 41 passed)
- full plugin suite — **192 passed / 19 files** (was 180 passed, 15
failed)
- `lifecycle-column-census --strict`, `check-sql-column-literals`,
`check-fnxc-future-dates` — green
Tests only; no product file is touched. An FNXC note now records the
arity contract and which actions take which source, so the next
assertion added here doesn't reintroduce a dead guard.
|
||
|
|
1c19540f50 |
fix(glasses): the summary card reported five hardcoded lanes, so a renamed board read as empty (#2968)
## The defect
`boardSummaryCardFromCounts` built its text from five hardcoded lane
ids:
```ts
`Triage ${counts.triage} Todo ${counts.todo} Doing ${counts["in-progress"]} Review ${counts["in-review"]} Done ${counts.done}`
```
`boardSummary` seeds exactly those five keys to `0`, then counts by real
`task.column`. On a board whose lanes are named anything else, **every
interpolated value is the seeded zero** — so the summary card, which is
the first card in every deck and the entire body of `GET
/board/summary`, reads:
```
Triage 0 Todo 0 Doing 0 Review 0 Done 0
```
…while the real work sits in lanes it never mentions. The wearer is told
the board is empty.
It is also wrong on the **default** board today: U11 (#2515) deleted the
`triage` lane, so `Triage 0` is permanently dead text — 9 of the 24
characters this display gets per line.
## Why there was no test
The only summary coverage in `cards.test.ts` exercised
`boardSummaryCard`, an **export no production file calls** (its sole
caller is that test). The function the deck actually ships had none.
That gap is why five hardcoded ids survived here.
## The fix, and one deliberate choice
The lanes are derived from `counts` instead of taken as an optional
resolved-lanes parameter.
That is on purpose. This program's recurring defect is precisely the
"optional lane answer + documented literal fallback" shape shipped
without wiring the caller — `unwired-lane-parameter-guard.test.ts`
documents **five live on `main` at once**, and in four of five the
parameter was unreachable because the caller held a larger defect.
`counts` is already keyed by real `task.column` values, so the
vocabulary is in hand with **no resolution, no new plumbing, and nothing
that can be left unwired**.
Zero-count lanes are dropped so the scarce line budget goes to lanes
with work; legacy ids keep their familiar labels and order, unknown ids
sort after them alphabetically so output is deterministic. `counts`
itself is untouched — the route returns it as the API body and consumers
still see every key.
## Revert proof
Restoring only `cards.ts` fails all three new cases, printing the defect
verbatim:
```
AssertionError: expected 'Triage 0 Todo 0 Doing 0 Review 0 Done…' to contain 'backlog 2'
AssertionError: expected 'Triage 0 Todo 1 Doing 0 Review 0 Done…' not to match /Triage/
AssertionError: expected 'Triage 0 Todo 0 Doing 0 Review 0 Done…' to be 'No active work'
Tests 3 failed | 5 passed (8)
```
## Verification (measured)
- `cards.test.ts` — **8/8 passed**
- full plugin suite — **180 passed**, 19 files
- `eslint`, `tsc --noEmit` — clean
- `lifecycle-column-census --strict`, `check-sql-column-literals`,
`check-fnxc-future-dates` — green
No changeset: `@fusion-plugin-examples/even-realities-glasses` is
`private: true` and is not bundled into the published CLI.
## Pre-existing failure, NOT from this change
The plugin suite also has **15 failures in
`src/__tests__/agent-actions.test.ts`**. They reproduce identically on
clean `origin/main` with this branch stashed (`15 failed | 41 passed`),
so they are not caused by this PR and are not fixed by it. Flagging
separately — this suite is outside the merge gate, which is why it has
been red unnoticed.
|
||
|
|
189f237a07 |
fix(security): make the approval and permission gates actually enforce (#2457)
## Why
An AI agent autonomously deleted a live task (FN-8609) via
`fn_task_delete`. Investigating that led to two audits, which found the
gate machinery is well-built and its *enforcement* well-tested — but the
*decision boundary* was unguarded and the tools that matter never
reached the gate at all.
The headline defect: all ~76 tools in `packages/cli/src/extension.ts`
are delivered through `hostExtensionPaths` → `DefaultResourceLoader`,
which never passes through `wrapToolsWithActionGate`. `fn_task_delete`
*was* classified as gated in `gating-classifications.ts` — that
classification was dead code. Roughly 15 destructive tools were ungated
even for an operator who had locked their policy all the way down.
Second: `POST /api/approvals/:id/decision` took the approver identity
from the request body, validated for shape only, with no check that the
decider was human or differed from the requester. Combined with a bash
tool whose command string was never inspected (only `params.cwd`), an
agent could read the daemon token from `~/.fusion/settings.json`, find
its own pending request, and approve it.
## Scope
This does **not** tighten defaults. The `unrestricted` preset is
byte-for-byte unchanged — `git diff` on `agent-permission-policy.ts` is
empty — and regression tests assert that an out-of-the-box install
behaves exactly as before. The bug was never "the default is too
permissive"; it was "strict policy doesn't enforce." This makes turning
security up actually work.
The one deliberate exception: the containment that stops an agent
escalating its *own* privileges (reading the daemon token / credentials,
calling the approvals API to self-approve) applies at every preset
including `unrestricted`. That is a privilege-escalation boundary rather
than a permission preference — if it only engaged under strict policy it
would not have prevented the incident that prompted this.
## What changed
8 bisectable commits:
- **Approval lifecycle** — self-approval blocked via server-derived
deciders; same-verdict replay 409s; decide re-reads and re-validates
inside the transaction; expiry TTLs; `markCompleted` ownership check;
session identity registry in core.
- **Engine gates enforce for real** — unclassified tools resolve to a
policy-governed category instead of hardcoded `allow`; missing-policy
fail-open closed; bash containment floor + exact-command approval
binding.
- **Dashboard decision routes** — stop trusting client-supplied actors
(decision, bypass-review, worktrunk → 403 on forged actors).
- **`fn serve` authenticated by default** — auto-mints a token following
the existing `fn dashboard` precedent; `--no-auth` opts out.
- **Sibling entry points closed** — user-sourced hard-cancel moves, ACP
execute-once approvals, plugin task-store gating.
- **pi-extension principal resolution** — the extension resolves the
acting principal and can withhold or policy-gate the previously ungated
destructive tools.
- **Root-cause bonus fix** — `findLatestByDedupeKey` was broken in
PostgreSQL backend mode (already-parsed jsonb fed through a string-only
parser), so approved-grant redemption **never matched in production**,
minting duplicate requests. This explains the live DB state of 17
approved / 0 completed. *(Also cherry-picked to `main` as `a9b30013bb`,
since it is an active production defect on its own.)*
- **Review follow-ups** (`627f1b1fa8`) — operator-configured
provisioning privilege and a configurable grant TTL; see below.
## Review follow-ups
**Provisioning privilege is operator-configured, not role-derived.**
`isCallerPrivileged` had gone from `caller.reportsTo == null` (every
top-level agent privileged — permanent escalation by creating a
manager-less agent) to `caller.role === "ceo"`, which swapped an
implicit rule for a magic string: any agent config can claim that role,
while an operator who genuinely wants a privileged agent had no
supported way to say so. Privilege now derives solely from
`agentProvisioning.trustedAgentIds` / `trustedRoles` and fails closed
when settings are unresolvable.
It is also no longer forwarded to `resolveAgentProvisioningPolicy` as
`isPrivileged`, because that flag short-circuits ahead of
`alwaysApproveDelete` — a trusted caller was bypassing delete approval
entirely. The policy applies the same trusted rules itself, in the right
order. The function now governs only the org-chart escape hatch (acting
outside your own direct reports).
**Grant TTL defaults to 1 hour and is configurable.** Approval →
redemption is not instantaneous: an operator approving from their phone,
an engine restart, a queued lane, or a task waiting on a worktree all
routinely exceeded 15 minutes, after which the grant expired and the
agent silently re-requested. One hour remains far short of the
"redeemable forever" hazard the TTL exists to bound. Override via
`FUSION_APPROVAL_GRANT_TTL_MS` or `configureApprovalRequestTtls()`;
invalid overrides are ignored rather than widening the window to
infinity or collapsing it to zero.
## Behavior changes requiring operator review before rollout
1. `fn serve` requires a bearer token by default (`--no-auth` opts out);
unauthenticated clients get 401.
2. Agents can no longer run withheld destructive tools
(`fn_task_delete`, `fn_task_bypass_review`,
mission/milestone/slice/feature/workflow deletes, `experiment_finalize`,
`skills_install`). Operators keep them via CLI/dashboard. **This is the
incident fix.**
3. Agents get provisioning privilege only when the operator lists them
in `agentProvisioning.trustedAgentIds` / `trustedRoles`; the
provisioning gate is now live in production. Previously-implicit
privilege (top-level position, or a `ceo` role) no longer grants
anything on its own.
4. Decision replay 409s (was 200); pending approvals expire after 24h,
approved grants after 1h (configurable); bash approvals bind per exact
command.
5. Forged/body actors on decision, bypass-review, worktrunk routes →
403; `archive-all-done` requires `{confirm:true}` (external scripts
affected).
6. `fn_secret_get` approvals grant exactly one reveal (previously
granted nothing and looped forever); ACP approvals are execute-once
(previously infinite reuse).
7. Bash containment denies token/credential/approvals-API commands in
all agent sessions at every preset.
## Verification
Independently re-run against the branch, not just self-reported:
- 5 typechecks (core, engine, cli, dashboard `tsconfig.json` +
`tsconfig.app.json`) — clean
- `pnpm lint` — clean
- `pnpm test:gate` — 379 passed
- `pnpm build --force` — green (a plain `pnpm build` skips packages as
unchanged and does **not** compile the branch)
- `pnpm check:changesets` — clean
- ~650 file-scoped tests including new negative-path suites for the
decision boundary, which previously had **zero** test coverage
`packages/engine/src/__tests__/plugin-runner.test.ts` fails 56/80 —
**verified pre-existing**, reproducing identically at base commit
`93a403af67` on `main`. Not in the merge gate.
### A mutation check that failed to fail
Worth recording, because it nearly shipped an untested security fix. The
first mutation check on the provisioning change reintroduced the `ceo`
hardcode and **all 17 tests still passed** — the tests asserted through
the policy path, which can no longer observe `isCallerPrivileged` at
all, precisely because `isPrivileged` is no longer forwarded there.
Org-chart cases that do exercise the function were added; the hardcode
now fails exactly 1 of 19, and restoring is green. A green mutation run
is only meaningful if the test can actually see the code under test.
## Known limitations (stated, not papered over)
- The bash containment floor is string-matching: a cost-raiser, not a
sandbox. Quoting, encoding, `$HOME`, symlinks, or an interpreter
one-liner can evade it. The durable protection is the decision route
refusing agent-originated deciders — the filter is the belt, not the
braces.
- Approval expiry is lazy (evaluated at decide/complete/redeem), not
swept, so an expired pending row stays visible in lists until touched.
- The extension's require-approval path returns a pending message but
cannot suspend a pi session mid-turn; engine-side pause hooks cover
engine lanes only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Security**
* Hardened approval and permission gating with server-side decider
attribution, self-approval blocking, ownership checks, replay/race
protection, and status/TTL enforcement.
* Added fail-closed behavior for sensitive/unclassified tools and
sandbox provisioning approvals.
* Blocked credential/approval access via bash containment; plugin
destructive task operations now require explicit permission.
* **New Features**
* `fn serve` now defaults to bearer-token auth, with `--no-auth` as the
explicit opt-out.
* **Bug Fixes**
* Improved task move-source attribution (`moveSource: "user"`) and
tightened dashboard archive/bypass confirmation and operator attribution
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
92d82b7a17 |
fix(guard): the unwired-lane check reported 0 because its question was too weak (#2852)
A guard nobody has proven can fail is a number, not a check. This one
was returning a clean `[]` while **18** real unwired lane declarations
sat on `main` — including one it was specifically built to catch.
## The escape that found it
`diffSnapshots` in the glasses plugin:
```ts
opts: { notifyOnColumns: ReadonlySet<ColumnId>; completeColumnsByTaskId?: ReadonlyMap<...> }
...
const completeColumns = opts.completeColumnsByTaskId?.get(task.id);
const isComplete = completeColumns ? completeColumns.has(task.column) : task.column === "done";
```
**No file anywhere builds that map.** The conversion was decorative —
the literal decided every real poll, so on a renamed board the wearer is
notified of every column transition *except the card finishing*, the one
they care about. The name was already in the guard's vocabulary list,
the declaration was exported and optional; it satisfied every condition
the guard checks, and the guard said nothing.
## Three independent blind spots
| # | blind spot | why it mattered |
|---|---|---|
| 1 | `SCANNED_PACKAGES` omitted `plugins/` | plugins hold lane logic
like anything else — this one resolves workflow IRs and decides what
"finished" means |
| 2 | inline options-object types were not walked | only bare parameters
and *named* interfaces were. Whether a lane answer arrives as a
parameter, an interface property, or an inline field is a style choice —
**a check evadable by a style choice is decorative** |
| 3 | the mention rule was `source.includes(parameter)` **anywhere** |
satisfied by coincidence for any ordinarily-named parameter |
Fixing 1 or 2 alone would still have missed it: **measured on `main`,
the guard found 0 unwired across 1753 files, and 0 again across 2114
once `plugins` was added**, because the shape was invisible too.
### On (3), I proved it on myself
Renaming the unwired parameter from `completeColumnsByTaskId` to
`completeColumns` — a better name, chosen for good reasons — **silenced
the guard instantly**, because 15 unrelated production files declare a
local called `completeColumns`. The check had not been satisfied; it had
been switched off by a rename. That is exactly the failure the code
comment two lines up condemns, committed one edit later.
The fix is the cause, not a name blocklist: a file that never references
`diffSnapshots` cannot be the thing that wires `diffSnapshots`'s
options. Still deliberately loose — a co-occurrence test, not call-graph
analysis — which keeps the low false-positive rate that makes the guard
bearable while removing a false **negative** that scaled with how
ordinary a parameter's name was.
## The 17 this uncovered
Tightening (3) surfaced 17 further unwired declarations across core,
engine and dashboard. **Spot-checked, not assumed**:
`buildUnblockWeightMap` in `task-priority.ts` declares `terminalColumns`
and `reviewColumns`, and the only files that pass either are its own
tests — the production caller silently uses the built-in `{done,
archived}` default. That is the inert-conversion shape this module
exists to name.
They span three other batches, so they are recorded as a **ratcheted
baseline** in the shape `scripts/lifecycle-column-census.mjs` already
uses here — keyed on `file + parameter` so an unrelated edit above them
cannot manufacture a failure. A new one fails immediately; these can
only leave the list. Listing them beats pretending for another week that
they do not exist.
## The glasses fix
`completeColumnsByTaskId` -> a flat `completeColumns` set, matching its
sibling `notifyOnColumns` in the same options object, resolved **once
per poll** by `notifier.ts` via `resolveProjectColumnsForRoles`.
Project-scoped and not per task because this runs on a polling timer
over the whole board — a per-card workflow read would scale with the
board on every tick. Best-effort: a failed resolve leaves the diff on
its documented legacy default rather than dropping a poll.
Still gated by `alsoNotifyOnDone`, which the production caller passes as
`false`, so it remains unobservable at runtime. Wired anyway: the day
someone enables the flag the resolution must already be right — and now
the guard will say so if the wiring is removed.
## Revert proofs (measured, one per fix)
| revert | failure |
|---|---|
| unwire `notifier.ts` | baseline gains `plugins/…/diff.ts
completeColumns` |
| drop the inline-options walk | "covers an INLINE options-object type"
fails `expected [] to deeply equal [ 'completeColumns' ]`, and the repo
scan loses the glasses entry |
| drop the owner scoping | the repo scan loses **all 17** pre-existing
entries |
| restore `task.column === "done"` | both new `diff.test.ts` cases fail
|
The new diff cases assert **both** directions — a card in the resolved
lane fires, and a card in the legacy `done` does *not* once the caller
resolved other lanes. The second is what proves the resolved set
replaces the default rather than being unioned with it.
## Verification
- `pnpm test:gate` — 161 / 487 / 13 / 71 passed
- `pnpm lint` — clean
- `tsc --noEmit` — clean
- guard suite — 9/9; full glasses plugin — 188 passed across 19 files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
21e688e1f9 |
fix(glasses): ?columns= silently returned the WHOLE board on a renamed board (#2849)
A lane-literal defect the census cannot see — the literals are
**Set/Array members**, not comparisons — in a live plugin, with **no
test coverage on the filter at all**. That absence is how the inversion
survived.
## The bug
```ts
const ALLOWED_COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
function parseColumns(raw) {
const parsed = raw.split(",").filter(v => ALLOWED_COLUMNS.includes(v));
return parsed.length ? new Set(parsed) : null; // ← `null` ALSO means "no filter requested"
}
```
On a board whose lanes are named anything else, every requested id is
discarded, `parsed.length` is `0`, and the function returns `null` —
**the same value it returns when no filter was requested**. The caller
then does `columns ? all.filter(...) : all`, so the route answers `200`
with the **entire board**.
Asking for one column returns all of them. Nothing in the response says
the filter was dropped. The list also still named `triage`, a column U11
deleted, so it described a board that no longer exists in either
direction.
## The fix
No allow-list can be correct here and none is needed. Valid ids are
whatever the project's workflows declare, and `Task["column"]` is
already `ColumnId = Column | (string & {})` — open by construction.
Filtering directly on the requested ids needs **no resolution source at
all**, which is why this literal, unlike the display ordering in
`cards.ts` (documented DELIBERATE-LITERAL: this package depends on
`@fusion/plugin-sdk` only, so there is no IR or store to resolve from),
is a defect rather than a deferral.
**Deliberate behaviour change:** `?columns=nonsense` now returns an
**empty deck** instead of the whole board. "Show me column X" answered
with every column is not a lenient default — it is the bug wearing a
200.
## Revert proof (measured)
Restore the allow-list and **both** new cases fail:
```
FAIL > filters on a RENAMED lane instead of silently returning the whole board
expected [ { id: 'summary', …(5) }, …(2) ] to have a length of 2 but got 3
FAIL > answers an unknown column with an EMPTY deck, not with everything
expected [ { id: 'summary', …(5) }, …(2) ] to have a length of 1 but got 3
```
Both directions are asserted on purpose: a filter that matched *nothing*
would satisfy the renamed-lane case alone while being equally broken.
## Not changed, and why
`plugins/fusion-plugin-even-cards` carries the **identical** bug — I
wrote the fix there first. It was removed from the pnpm workspace
(`858bab2`, "remove `fusion-plugin-even-cards` from the active workspace
package list to avoid duplicate user-facing integrations") and its
README names this plugin as its replacement, so it is not built, tested,
or shipped by anything. Fixing it would only imply it still runs.
Reverted and left alone.
## Verification
- `pnpm test:gate` — 161 / 487 / 13 / 71 passed
- `pnpm lint` — clean
- `tsc --noEmit` (`@fusion-plugin-examples/even-realities-glasses`) —
clean
- full plugin suite — 188 passed across 19 files
- census `--strict` — exit 0 (unchanged: these literals are Set members,
invisible to it)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d252c4e0cf |
fix(glasses): finished cards were crowding live work off the deck on a renamed board (#2854)
Not a labelling bug. `boardToDeck` filters finished cards out of a deck that is **capped at `maxCards`**, so on a board whose complete lane is named anything but `done`/`archived`, every finished card **consumes a slot and displaces live work**. The wearer sees fewer live tasks the more the team finishes — which reads as "nothing is happening", not as a defect. ```ts const active = tasks.filter((task) => task.column !== "archived" && task.column !== "done") .sort(...) .slice(0, Math.max(0, maxCards - 1)); // ← the cap is what makes this bite ``` ## Correcting an earlier DELIBERATE-LITERAL call This site was marked **DELIBERATE-LITERAL — no resolution source in this package**. That reasoning was inherited from the **deprecated** `fusion-plugin-even-cards`, which depends on `@fusion/plugin-sdk` alone. **This** package lists `@fusion/core` as a runtime dependency and already calls `resolveWorkflowIrById` / `resolveLifecycleColumns` in `quick-capture.ts` and `agent-actions.ts`. The half that *was* right: `cards.ts` genuinely cannot resolve anything — it takes plain `Task` rows. So the lane answer becomes a parameter and the **route** supplies it: one `listWorkflowDefinitions()` read per request regardless of board size, matching how `?columns=` already treats the board as a single pool. Best-effort, so a failed resolve leaves the deck on its documented default rather than failing the request — a slightly-wrong deck beats no deck on a pair of glasses. `terminalColumns` is in the `unwired-lane-parameter` vocabulary, so an unwired version of this parameter fails the build instead of sitting here looking converted. (That guard only learned to see this class in #2852.) ## Revert proof (measured) ``` FAIL > does not let a card in a RENAMED complete lane displace live work expected [ 'summary', 'FN-SHIPPED' ] to deeply equal [ 'summary', 'FN-LIVE' ] ``` `maxCards: 2` in the fixture is load-bearing — one summary card plus exactly one task slot, so an unfiltered finished card **displaces** the live one rather than merely joining it. A larger cap would let both through and the case would pass either way. The second case pins the degraded default (legacy `done`/`archived` still filtered when the caller resolved nothing), since most boards never rename anything. ## Not changed `cards.ts:187` — `task.column === "in-review" ? "In review" : "Moved in"` in the notification title. Genuinely cosmetic: a review card on a renamed board reads "Moved in" instead of "In review". No slot is lost and no decision is made from it, and `notificationCard` has no options object to thread a lane answer through, so converting it means widening a signature for a label. Left with the finding stated rather than silently swept in. ## Verification - `pnpm test:gate` — 161 / 487 / 13 / 71 passed - `pnpm lint` — clean - `tsc --noEmit` (`@fusion-plugin-examples/even-realities-glasses`) — clean - full plugin suite — 188 passed across 19 files - unwired-lane guard — 6/6, no new entries - census `--strict` — exit 0 Touches `board-routes.ts`, which #2849 also edits (different hunk — `parseColumns` vs the handlers), so the two merge cleanly in either order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ed6d54485b |
glasses plugin: the review actions could never resolve a review lane (4 guards + 3 invisible destinations) (#2816)
Four agent actions still keyed on literals, with three census-invisible `moveTask` destinations between them. `agent-actions.ts` already had `laneContext`/`destination` from an earlier partial conversion — these were simply never migrated. ## Census | file | main | here | | --- | ---: | ---: | | `plugins/fusion-plugin-even-realities-glasses/src/agent-actions.ts` | 4 | **0** | Plus 3 hardcoded `moveTask` destinations the census cannot see (`requestReview` → `in-review`, `returnToAgent` → `todo`, `retryTask` → `todo`). ## The real finding: this plugin could never resolve a review lane `resolveLifecycleColumns` keys its `review` role on the **`mergeOrchestration` trait alone**. A board whose review column carries only `merge-blocker` and/or `human-review` — the common custom shape, since `merge` is opt-in — resolves **no review lane at all**. So every review-gated action here (`requestReview`, `acceptReview`, `returnToAgent`, `retryTask`) compared against `undefined` and **refused every card**, and `requestReview` had nowhere to move one. This is not a regression from converting them; it is why they *could not* be converted with `lanes.review` as-is. Converting the four guards without noticing would have shipped four actions that fail closed on exactly the boards this program exists to support — a conversion that looks complete, passes its suite, and makes the plugin useless on a custom board. **Widened in `laneContext`, not in the shared resolver.** `resolveLifecycleColumns` is consumed well beyond this plugin, and its `review` role deliberately means "the merge-orchestration column" for the merge queue. The gap is already recorded in `notification-renamed-lifecycle-columns.test.ts` and in #2807 — reconciling the two definitions is a core-level decision, not one to take from a plugin. `mergeBlocker` is preferred over `humanReview` because a card cannot leave a merge-blocking column until the gate clears, which is the closer analogue of the legacy `in-review`. ## The suite caught an over-reach of mine My first version put a blanket `if (degraded) conflict(...)` at the top of `retryTask`, which broke a pinned invariant the test names outright: **"a degraded workflow does not block retries that move nothing."** The status-only retry just clears fields; refusing it because the workflow could not be read breaks a recovery that needs no lane at all. Degraded now blocks only the branches that actually **move**. Same reasoning applied to `acceptReview`, which also moves nothing. The existing `startWork` convention — conflict on degraded — is right precisely *because* it moves. ## Ordering `returnToAgent` and `retryTask` now resolve their destination **before** the field clear. Both cleared first, so a rejected move left the assignee and status — or the worktree, branch and base refs — nulled with the card exactly where it was. That is the fifth instance of this half-applied shape in the audit, and it is rule 3 in the class doc. ## Revert results (measured, each independently) | conversion | reverted → | | --- | --- | | `requestReview` destination | 1 failed — moves to the literal `in-review`, which this workflow does not declare | | `returnToAgent` destination | 1 failed — moves to the literal `todo`, same | Plus a non-vacuous companion: a renamed card *not* in the wip lane must still be refused by `requestReview`, so a gate admitting everything would not pass. ## Verification - Plugin suite — **186/186** - `pnpm test:gate` — 161 + 487 + 13 + 71, green - `tsc` on the plugin — clean - `pnpm lint`, `check:changesets`, census `--strict` — all clean (run explicitly) |
||
|
|
4184fde08d |
batch-cli-plugins: 7 guards — 3 were a foreign enum, and fn pr create refused every card on a renamed board (#2775)
`batch-cli-plugins` — the u7 worker's mega-batch: `packages/cli` + `plugins` + anything left. ## The batch is 7 guards, and 3 of them are not guards at all The census's per-file list gives this batch seven sites. Reading them, **three are a foreign vocabulary the census matches on the string alone**: | file | site | verdict | |---|---|---| | `plugins/fusion-plugin-reports/store/report-store.ts` | `next === "archived"` ×2 | **not a column** — `next` is a `ReportStatus` | | `plugins/fusion-plugin-reports/store/report-types.ts` | `to === "failed" \|\| to === "archived"` | **not a column** — same enum, its own terminal states | The reports plugin has its own status lineage (`draft → generating → review_* → approved → published`, plus `failed`/`archived`) that shares two spellings with the lifecycle vocabulary. A report is not on a board and has no workflow, so resolving an IR there would answer a question nobody asked. All three are marked `DELIBERATE-LITERAL` with the reason at the site. **This cuts the other way from #2763.** That PR establishes the census total as a *floor* (25 membership predicates it structurally cannot see). This is the opposite error in the same number: a foreign enum inflating it. The total is neither a ceiling nor a floor — it is an estimate with error in both directions, and the per-file list is worth reading before trusting a file's count. ## Converted (census before → after, per file) | file | before | after | |---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | **0** | | `plugins/…/even-realities-glasses/notifications/diff.ts` | 1 | **0** | | `plugins/…/reports/store/report-store.ts` | 2 | **0** (deliberate) | | `plugins/…/reports/store/report-types.ts` | 1 | **0** (deliberate) | ### `fn pr create` refused every card on a renamed board The live defect in this batch. The gate was `task.column !== "in-review"`, and its error told the operator to move the task to a column their board does not have: ``` Error: Task must be in 'in-review' column to create a PR (current: signoff) ``` There is no way to satisfy that short of renaming the workflow back. Now resolved through core's `resolveReviewColumns`, and the message names the lanes that actually exist. **The SET, not `lifecycle.review`.** A board may declare more than one review lane, and a card parked in a `humanReview`-only lane is still a card you can open a PR from. A single-id answer keeps refusing those — the same narrowing #2728's review caught in the CLI retry gate, which is why the test pins both lanes. ## Skipped, with the reason **`plugins/fusion-plugin-even-cards` (2 guards) — blocked on packaging, not on analysis.** The defect is real: `boardToDeck` filters with `column !== "archived" && column !== "done"`, so on a renamed board every finished card stays in the deck, fills `maxCards`, and pushes the active cards off the display. The wearer sees a board that never finishes anything. I implemented the fix and **reverted it**: this plugin is not in `pnpm-workspace.yaml` and depends only on `@fusion/plugin-sdk` — it has no `@fusion/core` dependency, so the route cannot reach `resolveTaskLifecycleColumns`. Adding one is a packaging change, which this program's rules put out of scope. Shipping only the injected parameter without a caller was the alternative, and that is precisely the decorative conversion #2759 documents: the census would drop by 2 and the deck would keep the bug. Flagged for whoever owns the plugin's dependency surface. The glasses plugin next door *does* depend on `@fusion/core`, so this is a one-plugin problem, not a plugin-wide one. ## Honest note on the glasses conversion `diff.ts`'s completion branch is **currently unreachable** — the only production caller (`notifier.ts`) passes `alsoNotifyOnDone: false`. So that conversion changes nothing at runtime today. It is converted rather than marked deliberate because the literal is not deliberate: it is wrong, and would ship the bug the day someone turns the flag on. Stated here rather than left for a reviewer to discover. ## Verification - new CLI suite **4 passed**; `pr-command` + `pr-automerge-cleanup` + `bin-pr-router` **35 passed** - glasses plugin **181 passed (19 files)** · reports plugin **110 passed (23 files)** - `pnpm test:gate` — **158 / 10 / 487 / 71** · `pnpm lint` clean · `--strict` exits 0 **Revert proof, measured.** Restoring `if (task.column !== "in-review")` fails 3 of the 4 new cases (`process.exit:1` on both renamed lanes, and the refusal message reverts to naming `in-review`). The unresolvable-workflow case keeps passing — it is the legacy path — so the negative cases alone do not pin the fix and all four are required. ## Handoff to `batch-engine` `packages/engine/src/project-engine.ts` **5 → 0** is finished, green, and pushed as `handoff/project-engine-lanes-for-batch-engine` (`34dbb35209`) for the capacity worker to cherry-pick — it is engine-owned, not mine to land. It fixes two live defects: a card that **had merged** reported as a failed merge to `fn task merge` and the dashboard button (`merged: finalTask?.column === "done"`), and the three post-finalize `column === "done" && mergeConfirmed` fast-path checks, which on a renamed board sent an already-landed card down the bounce path — re-queued, retry-counted, and in the capped branch parked `failed` with its merge sitting on main. Plus `hasAutoHealableVerificationBufferFailure`, which returned false for every card on a renamed board, so a buffer-overflow verification failure was never auto-healed. 8 new tests, revert-proven (restoring the literal fails 4 of 8), gate green. --- ## Completion pass (u7) — the batch is now closed Two workers converged on this branch. I rebased onto the first-landed commit rather than force-pushing over it, took its wording wherever the conclusion was identical, and added what was missing. ### What this pass added 1. **`even-cards` (2 sites)** — the only in-scope file the first pass left open. Marked DELIBERATE-LITERAL: the package depends on `@fusion/plugin-sdk` only, and the SDK does not re-export the lifecycle role helpers, so there is no IR, no store, and no trait flags to resolve *from*. Fixing it properly means the SDK exposing role flags on the task shape it hands plugins — a structural change, out of scope, and recorded at the site as the correct home. Live consequence is cosmetic: a finished card on a renamed board shows as active in the glasses deck. 2. **A red test in the `fn pr create` conversion.** The incoming version rendered `Task must be in 'in-review' to create a PR`, dropping the word `column`. `task.test.ts:3422` pins `must be in 'in-review' column`, so that hunk failed `runTaskPrCreate > exits with error when task not in in-review column`. Restoring the word makes the single-lane message **byte-identical** to the pre-conversion one, which is what a vocabulary conversion should be — the guard's own test now passes unmodified. Marked at the site so it is not "simplified" back. 3. **Duplicate imports** — the two independent conversions each added `resolveWorkflowIrForTask`/`resolveReviewColumns`, which does not compile. Deduped in its own commit. ### Census Measured with `--json` on `origin/main` and on this branch. | file | before | after | action | |---|---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | 0 | converted | | `plugins/fusion-plugin-reports/src/store/report-types.ts` | 1 | 0 | marked | | `plugins/fusion-plugin-reports/src/store/report-store.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-cards/src/cards/board-cards.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-realities-glasses/.../diff.ts` | 1 | 0 | marked | Backlog **415 → 408** (−7, exactly the in-scope count). Deliberate **40 → 46** (+6 marked); 6 + 1 converted = 7. `--strict` exits 0. **Nothing remains in `cli` + `plugins` + everything-else — there is no follow-up batch behind this one.** ### One note on the `even-realities-glasses` site Worth recording beyond "cannot resolve": its only production caller (`notifier.ts:80`) passes `alsoNotifyOnDone: false`, so that arm is **unreachable today**. Converting it could not have changed observed behaviour either way. ### Verification (measured, on the merged branch) - `pnpm --filter @runfusion/fusion exec tsc --noEmit` → exit 0 - `pnpm lint` → 0 errors - CLI `task.test.ts` → 144 passed, including the `runTaskPrCreate` guard test - `@fusion-plugin-examples/reports` → 110 passed; `even-realities-glasses` → 181 passed **Pre-existing failures, not from this change:** the 5 `runTaskImportFromGitHub` / `runTaskImportGitHubInteractive` tests fail identically on `origin/main` — verified by stashing this diff and re-running (5 failed / 144 passed both ways). --- ## Census audit (unowned follow-on) After closing the batch scope I audited whether the **392** column-backlog number is inflated by foreign vocabularies — the class this batch found in the reports plugin, where `"archived"` is a `ReportStatus` rather than a board lane. If that class were widespread, every remaining batch would be chasing sites that must not be converted. **It is not. The number is real.** A receiver-level pass over all 392 column-category sites found exactly **3** false positives, all in `plugins/fusion-plugin-reports` (`next`, a `ReportStatus`), all now marked in this PR. What was checked and cleared: - **Property-reached foreign enums** (`step.status`, `feature.status`, `mission.status`) — already correctly bucketed into the separate `status` category (185), not the column backlog. Verified against `merge-queue-ops.ts`: 11 lifecycle-spelled literals in the file, census counts **1**, and that 1 is the genuine `.column` guard. - **Bare step-status variables** (`status`, `currentStatus`, `liveStatus` compared to `"done"`/`"skipped"`) — likewise excluded. - **Every other receiver in the backlog** — `to`, `from`, `column`, `fromColumn`, `toColumn`, `latestColumn`, `state`, `preArchiveColumn`. All resolve to genuine task columns. `executor.ts`'s 15 sites were spot-checked line by line: all 15 are real. The gap the classifier genuinely cannot close is a foreign enum held in a **bare variable** — the receiver name carries no type information, so `next === "archived"` is indistinguishable from a lifecycle guard by AST alone. That is why the reports sites need a marker rather than a classifier fix, and it is now documented in `lifecycle-column-census-ast.mjs`'s header alongside the measured scope, so the remaining batches do not re-run this hunt. Census tests: **43 passed**. The change is comment-only. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dca20496f4 |
consolidate/u7: plugins to zero + 8 executor rebound guards + resume lanes (supersedes #2607, #2635, #2640) (#2644)
Consolidation branch for U7, per the new one-branch working mode. **Supersedes #2607, #2635, #2640** — the three of my PRs that were stuck on review threads. My other seven (#2602, #2605, #2606, #2611, #2621, #2628, #2633) are green with **zero unresolved threads** and are deliberately left alone for the merge sweep. ## What is in here, file by file | file | change | guards before → after | |---|---|---| | `plugins/…/glasses/src/agent-actions.ts` | gates, destinations and degraded-resolution refusal all resolve from the task's own workflow | 2 → 0 | | `plugins/…/glasses/src/quick-capture.ts` | accepted capture columns come from the board; default no longer names the deleted column | 1 → 0 | | `plugins/…/glasses/src/settings.ts` | quick-capture default was `triage`, the column #2515 removed | (assignment, uncounted) | | `plugins/…/dependency-graph/src/GraphTaskNode.tsx` | redundant column condition deleted | 1 → 0 | | `packages/engine/src/executor.ts` | 8 rebound guards compare the resolved column; 4 resume-eligibility literals share one resolver | 151 → 143 (+4 off-bar) | | `packages/engine/src/__tests__/` | 4 new suites, 26 cases | — | `plugins/` reaches **zero** column guards with this branch. ## The three threads it closes **#2607 — five findings, all mine, all the same rule.** I kept *qualifying* a legacy-id fallback instead of removing it: | attempt | rule | hole review found | |---|---|---| | 1 | fall back to `todo` when the role is missing | moved cards to phantom columns | | 2 | …only if the workflow **declares** `todo` | aliased **review** lane named `todo` | | 3 | …and only if no other role is assigned to it | **traitless** parking column named `todo` | The qualifications were the mistake. Once `resolveLanes` returns a lane set the workflow *has* a column vocabulary, so "no column carries the hold trait" is a complete answer — refuse. `destination()` is two lines now, with no aliasing surface left to qualify. Plus a sixth, which is a genuinely different state: **degraded resolution is indistinguishable from the default board.** `resolveWorkflowIrForTask` is total by design — a missing definition silently returns the *default* coding IR — so a card on a custom board whose definition could not be read resolved to `todo`/`in-progress`. `undefined` lanes cannot express that (it means "no workflow at all", where the legacy ids *are* the answer). The actions now refuse with 409. #2618 would replace this check with resolver provenance; it is not merged, so this does not depend on it. **#2635 — "seven rebound sites remain untested."** Fair; my "same shape" note was an assertion, not coverage. Seven of the eight need a live graph run to reach, so the *shape* is pinned instead: a static check that no guard in front of a rebound move compares against a column literal, with a vacuity case (the same detection run against the original shape) and a match-count floor (≥8), because a guard reporting success on zero matches is worse than no guard. **#2640 — duplicate workflow resolution.** Framed as I/O; it is also a correctness bug. Eligibility and re-entry are two halves of one decision and resolved the workflow separately, so a workflow edit landing between them has the halves reading *different boards*. Now one caller-owned memo per decision — caller-owned because a process-lifetime cache would have to guess when a mid-flight workflow edit invalidates it. ## Behavioural findings, not tidying - **The last-resort recovery for completed-but-stranded work did not exist off the default lineage.** `promotedFromPlannerColumn` was false on a renamed board, so finished work resting in planning was never promoted; the code fell through to a review handoff that role adjacency rejects, and the card stayed stuck with its work complete. - **Rebound guards could not see the column their own move targeted.** U5b converted the move target; the eight `column !== "todo"` checks in front of it were left literal, so on a renamed board the engine moved a card into the column it was already in — and `moveTaskInternal` runs reset-on-entry on every real move, so at the `preserveProgress: false` site it reset step progress a second time. - **The FN-1404 `task:move` audit row was lying**, recording `to: "todo"` while the move target was resolved. A run-audit trail that disagrees with the move it describes is worse than none. Not a comparison, so no census counts it. - **A task interrupted by an engine pause never resumed on a renamed board** (off-bar, `in-review`/`in-progress` literals): four comparisons decided one question and had to agree; two of them disagreed on a renamed board, so re-entry silently never fired. ## Revert proofs, isolated per site | reverted | result | |---|---| | `destination()` back to attempt 3 | 3 of 38 fail | | degraded-resolution refusals removed | 2 of 42 fail | | capture set back to the legacy five | 2 of 3 fail (renamed-board suite) | | forward exclusions → literals | 1 of 14 fails | | missing-wip refusal removed | 2 of 14 fail | | `promotedFromPlannerColumn` → literals | 3 of 7 fail | | promotion target → `"in-progress"` | 3 of 7 fail | | one rebound guard → `!== "todo"` | 1 of 3 fails (static shape) | | resume lanes → legacy trio | 1 of 5 fails | Every conversion is paired with a negative — a forward move, a not-a-planner-lane card, a default-lineage card, an unresolvable workflow — so neither "always fire" nor "never fire" can pass for "resolve the role". ## Commit discipline Twelve commits, each one thing: the code move (`resolvePlannerLanes` out of `triage.ts`) is separate from every behavior change, and each review fix is its own commit with its own revert proof. ## Verification - `pnpm test:gate` **71/71** - 162/162 across the glasses plugin's 19 files; 26/26 across the four new engine suites - engine + glasses typecheck clean; `pnpm lint` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Engine recovery and retries now work correctly with renamed or customized workflow columns. * Tasks in manual-intake columns are no longer automatically planned. * Agent actions and quick capture now respect each board’s declared columns and lifecycle stages. * Awaiting-approval tasks are recognized regardless of their current column. * Command Center SDLC funnel stages now accurately reflect customized workflows. * **Documentation** * Added guidance for safely changing workflow-column logic and interpreting lifecycle-column checks. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a242f1b449 |
fix(FN-7952): migrate bundled plugins to PostgreSQL (#2111)
## Summary Bundled plugins now persist shared runtime state in project-scoped PostgreSQL tables instead of maintaining independent SQLite authority. Reports, CLI Printing Press, Compound Engineering, Roadmap, Even Realities, and WhatsApp all follow the same ownership and startup contract as Fusion core. ## Design decisions - Plugin schema hooks run through the host’s PostgreSQL owner and enforce project isolation. - The SDK exposes the host contract needed by bundled plugins without importing engine internals. - Legacy Roadmap ownership fixtures use the supported empty-owner sentinel, preserving current composite primary/foreign keys while exercising backfill behavior. - The lockfile travels with the Even Realities PostgreSQL dependency so packaged installs remain reproducible. ## Validation - All six affected plugin builds pass. - Affected plugin suites pass: 773 tests across Printing Press, Compound Engineering, Even Realities, Reports, Roadmap, and WhatsApp. - `pnpm test:gate` passes all 478 gate tests. - This PR changes 40 files. ## Stack - Depends on #2110 → #2109 → #2108. - The documentation/release PR completes the stack. Related: #2105 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Breaking Changes** * PostgreSQL is now required for runtime storage; SQLite files are used only as one-time migration inputs. * The legacy `FUSION_NO_EMBEDDED_PG` fallback has been removed. * **New Features** * Added project-isolated PostgreSQL storage for plugins, reports, tasks, notifications, and other plugin data. * Added agent tools for reports and CLI service drafts. * Added PostgreSQL schema initialization support for plugin authors. * **Bug Fixes** * Improved migration and recovery of legacy plugin state. * Prevented cross-project data access and strengthened transactional schema updates. * **Documentation** * Updated storage, migration, deployment, plugin authoring, CLI, and dashboard guidance for PostgreSQL. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
0b1c8fb56b |
fix(ci): widen Column to ColumnId in plugins + drop unused import after #1403 widening
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
007efcab0b |
feat(FN-3970): consolidate Even plugin APIs into unified package with webho
Collapsed the Even plugin architecture into a single unified plugin by merging board card routes and replacing the transport stub with a real webhook implementation, removing the separate cards plugin from the workspace. The new unified plugin (`fusion-plugin-even-realities-glasses`) now exports con Fusion-Task-Id: FN-3970 |
||
|
|
c744ef83c2 |
fix(FN-3794): pass unload context and isolate WhatsApp sessions per project
- Extend PluginOnUnload to receive runtime context and wire ctx through plugin-loader unload hooks - Scope WhatsApp chat plugin connections by project root to avoid cross-project session leakage - Update plugin authoring docs and add a patch changeset for @runfusion/fusion - Align plugin test suites across WhatsApp and example/runtime plugins with the new onUnload context contract Fusion-Task-Id: FN-3794 |
||
|
|
fe5148cc19 |
feat(FN-3744): add agent action routes, tools, and roadmap plugin extractio
Merges agent lifecycle tools (create_agent, delete_agent) with full API routes, web fetch tool implementation and tests, and full roadmap UI extraction into a bundled plugin — including schema hook, chat scroll fixes, and autostash orchestrator cleanup. Also adds mesh auth snapshot replication acros Fusion-Task-Id: FN-3744 |
||
|
|
089c139bad |
feat(FN-3743): add polling task notifications for even realities plugin
Commits merged: - feat(FN-3743): add polling task notifications for even realities plugin Files changed: .../fusion-plugin-even-realities-glasses/README.md | 27 +++ .../package.json | 2 + .../src/__tests__/cards.test.ts | 82 +++------ .../src/__tests__/diff.test.ts | 76 ++++++++ .../src/__tests__/notification-card.test.ts | 40 +++++ .../src/__tests__/notification-routes.test.ts | 78 ++++++++ .../src/__tests__/notification-store.test.ts | 71 ++++++++ .../src/__tests__/notifier.test.ts | 138 +++++++++----- .../src/__tests__/transport.test.ts | 4 +- .../src/agent-actions.ts | 4 +- .../src/cards.ts | 127 ++++++++++--- .../src/index.ts | 22 ++- .../src/notifications/diff.ts | 77 ++++++++ .../src/notifications/store.ts | 46 +++++ .../src/notifications/types.ts | 19 ++ .../src/notifier.ts | 199 +++++++++++++-------- .../src/routes/notification-routes.ts | 103 +++++++++++ pnpm-lock.yaml | 31 +++- 18 files changed, 929 insertions(+), 217 deletions(-) Fusion-Task-Id: FN-3743 |
||
|
|
8c112ffda8 |
feat(FN-3742): unify agent action gating classifications across permanent a
Merges a significant refactor of agent action gating into a unified `gating-classifications.ts` module shared across permanent agents and executor gating logic, plus new glasses quick-capture support with dedicated routes and parser, and a fix restoring step progress visibility during task execution Fusion-Task-Id: FN-3742 |
||
|
|
cafaa68a5f |
feat(FN-3738): add fusion-plugin-even-realities-glasses plugin package with
Implements a new Fusion plugin package (`fusion-plugin-even-realities-glasses`) providing settings schema, a Fusion HTTP API client, cards, quick capture actions, a notifier, and transport stub — plus plugin routes and lifecycle hooks wired into the pi extension. The branch concludes with a small fi Fusion-Task-Id: FN-3738 |