Merge branch 'main' into gsxdsm/compound
This commit is contained in:
@@ -5,7 +5,6 @@ A comprehensive guide to creating Fusion plugins that extend the task board with
|
||||
## Table of Contents
|
||||
|
||||
1. [Getting Started](#1-getting-started)
|
||||
- [External authoring guide](./plugins/external-authoring.md)
|
||||
2. [Plugin Manifest Reference](#2-plugin-manifest-reference)
|
||||
3. [Plugin Settings Schema](#3-plugin-settings-schema)
|
||||
4. [Available Hooks and Signatures](#4-available-hooks-and-signatures)
|
||||
@@ -26,6 +25,8 @@ A comprehensive guide to creating Fusion plugins that extend the task board with
|
||||
|
||||
---
|
||||
|
||||
See also: [External Plugin Authoring guide](./plugins/external-authoring.md)
|
||||
|
||||
## 1. Getting Started
|
||||
|
||||
### What Are Fusion Plugins?
|
||||
|
||||
@@ -1779,7 +1779,7 @@ Reliability-layer changes are in scope. Interaction regression backstops live in
|
||||
- FN-5830 backstop: `packages/engine/src/__tests__/reliability-interactions/branch-group-promotion.test.ts` guards branch-group completion-gate + promotion lifecycle so promotion happens exactly once after all members land, re-calls are idempotent, and gated paths emit `merge:branch-group-promotion-gated` without default-branch promotion.
|
||||
- FN-5819/FN-5846 backstop: `packages/engine/src/__tests__/reliability-interactions/shared-group-member-integration.test.ts` and `shared-branch-group-lifecycle.test.ts` guard the scoped autoMerge-off exception and deterministic finalize path so shared members integrate into the single group branch, produce `mergeTargetSource: "branch-group-integration"`/`mergeTargetBranch`, do not land on main, and are not moved backward by self-healing maintenance.
|
||||
- FN-5901 backstop: `packages/engine/src/__tests__/reliability-interactions/mission-validator-run-reaper.test.ts` guards stale mission-validator-run recovery across manual and automatic trigger types, verifies `mission:validator-run-reaped` audit metadata, ensures archived/complete parents keep their terminal feature state untouched, and proves reaped active features resume validation instead of staying wedged behind abandoned `running` rows.
|
||||
- FN-5738 backstop: `packages/engine/src/__tests__/reliability-interactions/mission-validation-trigger-gap.test.ts` extends coverage so zero-assertion auto-pass deterministically advances `loopState` to `passed`, sets `lastValidatorStatus="passed"`, emits `validation_auto_passed_no_assertions`, and does not re-fire on repeated recovery passes.
|
||||
- FN-5738 backstop (superseded by FN-5902): `packages/engine/src/__tests__/reliability-interactions/mission-validation-trigger-gap.test.ts` no longer permits zero-assertion auto-pass. Current coverage proves legacy zero-link features lazily restore a managed assertion, route through validator runs, and do not emit `validation_auto_passed_no_assertions` during recovery replays.
|
||||
- FN-5741 backstop: `packages/engine/src/__tests__/reliability-interactions/merge-request-shadow-handoff.test.ts` guards Phase-1 merge-request contract shadow writes: flag OFF is a no-op, flag ON writes marker/record strictly after legacy handoff, and `autoMerge:false` remains `manual-required` without shadow running transitions.
|
||||
- FN-5742 backstop: `packages/engine/src/__tests__/reliability-interactions/dual-observe-merge-seam.test.ts` guards Phase-2 dual-observe invariants: legacy dependency satisfaction remains authoritative while parity diffs emit, and shadow dequeue selection never advances `manual-required` rows.
|
||||
- FN-5743 backstop: `packages/engine/src/__tests__/reliability-interactions/merge-request-cancel-on-hard-cancel.test.ts` and `packages/core/src/__tests__/merge-request-record.test.ts` guard Phase-3 cutover invariants: transient merge retries mutate merge-request state (no column rebound), user hard-cancel after accepted handoff cancels pending merge requests, and non-user rebounds preserve legacy fail-soft semantics.
|
||||
|
||||
@@ -19,6 +19,20 @@ Thanks for contributing to Fusion.
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
### pnpm build-scripts approval policy
|
||||
|
||||
pnpm v10 blocks dependency `preinstall`/`install`/`postinstall` scripts by default and reports any uncategorized packages in the install output.
|
||||
|
||||
When a package is flagged:
|
||||
|
||||
- Approve it only when this repo genuinely needs that dependency's build script for supported runtime/build paths.
|
||||
- Ignore it when prebuilt artifacts, optional-native fallbacks, or release-only tooling make the script unnecessary for normal workspace verification.
|
||||
- Record every reviewed package in exactly one bucket: `onlyBuiltDependencies` or `ignoredBuiltDependencies`.
|
||||
|
||||
Fusion currently keeps the reviewed decision set documented in the root `package.json` `pnpm` block and mirrored in `pnpm-workspace.yaml`, which is the effective pnpm v10.33 install-time config read by `pnpm install`.
|
||||
|
||||
The guard test `node --test scripts/__tests__/pnpm-build-scripts-config.test.mjs` (also covered by `pnpm test:scripts`) enforces that the reviewed dependencies stay categorized, deduped, and non-overlapping so the ignored-build-scripts warning cannot silently return.
|
||||
|
||||
### Build workspace packages
|
||||
|
||||
```bash
|
||||
|
||||
@@ -211,7 +211,7 @@ Mailbox view shows inbox/outbox communication threads and unread state.
|
||||
- reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading
|
||||
- mailbox now includes an **Approvals** tab with pending and history filters (`approved` / `denied` / `completed`), approval detail context, and inline approve/deny actions for pending requests
|
||||
- in the **Agents** tab, the agent selector now includes **All agents**, which shows one combined agent-to-agent stream (with sender + recipient labels); selecting a specific agent still shows Inbox/Outbox subtabs
|
||||
- mailbox entry points now show pending-approval indicators: Header mailbox toggle dot, Header overflow mailbox badge, Mobile mailbox tab dot, and Mobile More → Mailbox badge
|
||||
- mailbox entry points now show unread/pending indicators: the desktop Header mailbox toggle shows a pending-approval dot first or an unread dot when unread mail exists without pending approvals, while Header overflow + Mobile mailbox entry points continue to surface mailbox badges/dots
|
||||
- approval lifecycle SSE events (`approval:requested`, `approval:updated`, `approval:decided`) trigger mailbox approvals refresh without manual reload
|
||||
- when a task newly enters `awaiting-approval`, the app shows a persistent approval banner above project content with an **Open Mailbox** CTA; dismissals are remembered per approval item until that item advances or a different one arrives
|
||||
- Visible message history/threading is driven by explicit `message.metadata.replyTo.messageId` links
|
||||
@@ -756,6 +756,8 @@ For setup prerequisites, security caveats for tokenized URLs/QR links, and troub
|
||||
|
||||
## Skills API
|
||||
|
||||
The Skills view now supports the full browse-and-install loop for skills.sh entries: use **Skills Catalog** to search the catalog, click **Install** on any card with a source repository, and the dashboard will run the same installer as the CLI (`npx skills add <owner/repo> -y -a pi`, with `--skill <slug>` when applicable). On success, the view refreshes **Discovered Skills** immediately so the newly installed skill appears without a page reload.
|
||||
|
||||
The Skills API provides endpoints for managing execution skills. Skills are toggled via project-scoped settings in `.fusion/settings.json`.
|
||||
|
||||

|
||||
@@ -871,6 +873,49 @@ Toggle a skill's enabled/disabled state.
|
||||
{ "error": "Skills adapter not configured", "code": "adapter_not_configured" }
|
||||
```
|
||||
|
||||
### POST /api/skills/install
|
||||
|
||||
Install a catalog skill into the current project.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"source": "owner/repo",
|
||||
"skill": "example-skill"
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Validates `source` in `owner/repo` format before spawning anything
|
||||
- Runs `npx skills add <source> -y -a pi`
|
||||
- Appends `--skill <skill>` when `skill` is provided
|
||||
- Uses the scoped project root as `cwd`, so installed files land in the current project's skill directories
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request` — missing source
|
||||
```json
|
||||
{ "error": "source is required", "code": "invalid_body" }
|
||||
```
|
||||
- `400 Bad Request` — malformed source
|
||||
```json
|
||||
{ "error": "Invalid source format. Use owner/repo.", "code": "invalid_source" }
|
||||
```
|
||||
- `404 Not Found` — adapter not configured
|
||||
```json
|
||||
{ "error": "Skills adapter not configured", "code": "adapter_not_configured" }
|
||||
```
|
||||
- `502 Bad Gateway` — installer failed/timed out/could not start
|
||||
```json
|
||||
{ "error": "installer failed", "code": "install_failed" }
|
||||
```
|
||||
|
||||
### GET /api/skills/catalog
|
||||
|
||||
Fetch the skills.sh catalog with optional authentication.
|
||||
@@ -1057,6 +1102,8 @@ Breakpoints: 768px (primary mobile), 1024px (tablet `min-width: 769px and max-wi
|
||||
|
||||
**Bottom spacing:** `--mobile-nav-height` (44px) + `env(safe-area-inset-bottom, 0px)` + `--standalone-bottom-gap` (0/8px PWA). All bottom-positioned mobile elements compose those. When the soft keyboard opens, the mobile nav bar stays pinned to page bottom cross-platform; the executor footer keyboard-collapse pin is iOS-only. On Android (`interactive-widget=resizes-content`), the footer keeps its stacked position above the nav bar to avoid overlap after keyboard dismiss.
|
||||
|
||||
**Footer-safe fill layouts:** View wrappers that reserve footer/mobile-nav space (for example `.project-content`) should be flex containers with `min-height: 0` / `min-width: 0`, and child surfaces like `.board` should use `flex: 1 1 auto` plus the same min-size guards. This keeps the board/columns stretched between the header and fixed bottom bars across desktop, tablet, and mobile while allowing internal scroll regions to own overflow.
|
||||
|
||||
**Touch targets:** Standing button-freeze directive supersedes per-button touch-target guidance. For non-button elements, primary controls (nav bar, FAB, tab action rows, modal CTAs, list-row tap targets, form controls) must be ≥36px on mobile. Secondary controls inside a card/list-row where the row itself is the tap target stay compact (24–28px or small chips).
|
||||
|
||||
**Safe area:** `max(var(--space-md), env(safe-area-inset-left, 0px))` for notch-aware horizontal padding.
|
||||
|
||||
@@ -126,7 +126,7 @@ FN-5416 extends resume-correlation coverage to stream-focused hooks and their pr
|
||||
- `useDevServerLogs`: `project-context-change`, `sse-open`, `sse-reconnect`
|
||||
- `useResearch`: `sse-open`, `sse-reconnect`
|
||||
- `useBackgroundSessions`: `sse-open`, `sse-reconnect`
|
||||
- `useAgentLogs`: `project-context-change`, `sse-open`, `sse-reconnect` on `/api/tasks/:id/logs/stream`
|
||||
- `useAgentLogs`: `project-context-change`, `sse-open`, `sse-reconnect` on `/api/tasks/:id/logs/stream` (live tail via SSE; historical reads are backed by `.fusion/tasks/{ID}/agent-log.jsonl`)
|
||||
- Route shells
|
||||
- `DevServerView`: `remount` / `route-active` / `route-inactive`
|
||||
- `ResearchView`: `remount` / `route-active` / `route-inactive`
|
||||
|
||||
@@ -2,175 +2,93 @@
|
||||
|
||||
## Status
|
||||
|
||||
- **Decision date:** 2026-05-30
|
||||
- **Task:** FN-5718
|
||||
- **Depends on enforcement behavior from:** FN-5715 (reference implementation of the trigger/recovery path)
|
||||
- **Scope:** Product contract and implementation requirements only (no code changes in this task)
|
||||
- **Implementation status:** Realized by FN-5733 (loop auto-pass advancement, mission/store guard telemetry, MissionManager label reconciliation)
|
||||
- **Decision date:** 2026-06-02
|
||||
- **Current contract task:** FN-5902
|
||||
- **Supersedes:** FN-5718 baseline contract
|
||||
- **Depends on runtime trigger/recovery behavior from:** FN-5715
|
||||
- **Implementation status:** Realized by FN-5902
|
||||
|
||||
## Problem
|
||||
## Decision
|
||||
|
||||
Mission validation has had a recurring ambiguity: users can see feature acceptance text and milestone "completion criteria" text, but autopilot enforcement is actually driven by assertion linkage + validator outcomes. This document defines the canonical enforced gate so mission completion cannot silently stall or be misread.
|
||||
Mission completion now uses an **all-criteria AI-run contract**:
|
||||
|
||||
## Canonical Completion Gate (Enforced)
|
||||
|
||||
### Decision
|
||||
|
||||
A feature is autopilot-complete only when **its linked contract assertions are satisfied**.
|
||||
|
||||
Canonical authored source and enforcement path:
|
||||
|
||||
1. `MissionFeature.acceptanceCriteria` is the canonical authored criteria text (authoring surface).
|
||||
2. MissionStore must maintain a **store-managed per-feature `MissionContractAssertion`** derived from feature content, with text priority:
|
||||
1. `MissionFeature.acceptanceCriteria` is the canonical authored feature criteria text.
|
||||
2. MissionStore must maintain or lazily restore **one store-managed per-feature `MissionContractAssertion`** derived from feature content with text priority:
|
||||
- `feature.acceptanceCriteria`
|
||||
- `feature.description`
|
||||
- `Verify implementation of: {feature.title}`
|
||||
3. The mission validator enforces completion using the feature's **linked assertions** (including its store-managed assertion and any additional linked milestone assertions).
|
||||
4. Feature/slice/mission advance is gated by the validator outcome (or explicit no-assertions auto-pass behavior defined below).
|
||||
3. The mission validator must run for every feature completion trigger. Runtime validation may lazily call `ensureFeatureAssertionLinked(feature.id)` before starting the validator so legacy missing-link rows still become validator-backed.
|
||||
4. `milestone.acceptanceCriteria` is also part of the enforced gate by being threaded into the validator prompt for every feature in that milestone.
|
||||
5. Feature, slice, milestone, and mission advancement are gated by the validator result — **not** by an informational-only path.
|
||||
|
||||
### Precedence and interpretation rules
|
||||
## Enforcement Model
|
||||
|
||||
- `MissionFeature.acceptanceCriteria` is the canonical authoring field for feature-level intent.
|
||||
- The **linked assertion set** is the canonical enforcement set.
|
||||
- Milestone `MissionContractAssertion` rows are additive contract rows. They are enforced **only when linked to a feature**.
|
||||
- `milestone.acceptanceCriteria` is descriptive/informational milestone text and is not directly executed by the validator.
|
||||
### Feature-level enforcement
|
||||
|
||||
### Worked examples
|
||||
A feature is autopilot-complete only when the validator passes after evaluating:
|
||||
|
||||
1. **Feature has acceptance criteria; store-managed assertion linked; validator passes**
|
||||
- Result: feature may move to done/passed and contribute to slice completion.
|
||||
2. **Feature has acceptance criteria; additive milestone assertion also linked; one linked assertion fails**
|
||||
- Result: feature is not complete; no slice advance.
|
||||
3. **Feature has acceptance criteria visible, but no linked assertions (legacy FN-5696 shape)**
|
||||
- Result: data inconsistency; must not be interpreted by operators as a separate enforced gate. Repair links (FN-5696 backfill) so enforcement matches displayed intent.
|
||||
- the feature's linked contract assertions, including its store-managed assertion, and
|
||||
- the parent milestone's `acceptanceCriteria` text when present.
|
||||
|
||||
## Enforced vs. Informational Surfaces
|
||||
### Milestone-level enforcement
|
||||
|
||||
| Surface | Category | Contract meaning |
|
||||
|---|---|---|
|
||||
| `MissionFeature.acceptanceCriteria` | Informational authoring source | Canonical authored feature criteria text; enforcement happens through derived/linked assertions |
|
||||
| Store-managed per-feature `MissionContractAssertion` | Enforced | Primary validator gate for the feature |
|
||||
| Additive milestone `MissionContractAssertion` (linked to feature) | Enforced | Additional validator gate for that linked feature |
|
||||
| Additive milestone `MissionContractAssertion` (unlinked) | Informational until linked | Contract candidate, not yet a feature gate |
|
||||
| `milestone.acceptanceCriteria` | Informational | Milestone summary/pass-bar text for humans; not directly validator-executed |
|
||||
| MissionManager `milestone-feature-acceptance-rollup` UI (`data-testid="milestone-feature-acceptance-rollup"`) | Informational display | Display-only rendering of feature acceptance text, not a separate enforcement mechanism |
|
||||
`milestone.acceptanceCriteria` is no longer informational-only. FN-5902 enforces it by threading the milestone pass-bar text into the validator prompt for each feature under that milestone.
|
||||
|
||||
## Zero-Assertion Behavior and FN-5696 Failure Shape
|
||||
This is intentionally **prompt-threading**, not per-feature milestone assertion row synthesis:
|
||||
|
||||
### Zero-assertions runtime behavior (canonical FN-5738 path)
|
||||
- store-managed per-feature assertions remain the canonical feature assertion rows,
|
||||
- milestone acceptance text remains milestone-authored prose,
|
||||
- the validator sees both and must satisfy both.
|
||||
|
||||
When a feature reaches completion trigger points and has **zero linked assertions**, mission execution must take exactly one canonical auto-pass path (not a silent stall and not a competing behavior):
|
||||
### Legacy data and lazy repair
|
||||
|
||||
- mark feature terminal as `status="done"`, `loopState="passed"`, `lastValidatorStatus="passed"`,
|
||||
- emit explicit observability/audit evidence with mission event code `validation_auto_passed_no_assertions`,
|
||||
- continue normal slice/mission advancement checks idempotently (no duplicate re-fire on repeated recovery).
|
||||
Legacy missions can still contain features with missing assertion links. Runtime enforcement no longer depends on pre-running backfill:
|
||||
|
||||
### FN-5696 legacy shape clarification
|
||||
- mission execution lazily restores the store-managed feature assertion just before validation, and
|
||||
- `fn_mission_backfill_assertions` / `backfillFeatureAssertions()` remain available as operator repair tooling for data hygiene and visibility.
|
||||
|
||||
A feature can show acceptance text while links are missing (legacy pre-repair data). This must be treated as a **linkage/data integrity problem**, not as proof that milestone text alone is enforced. Assertion authoring/backfill (FN-5696) is outside the execution loop; the loop must not synthesize `mission_feature_assertions` rows. The contract prevents ambiguity by separating:
|
||||
## Removed behavior (FN-5902 inversion)
|
||||
|
||||
- authored/informational text surfaces, from
|
||||
- linked assertion enforcement surfaces.
|
||||
FN-5718's zero-assertion auto-pass behavior is superseded.
|
||||
|
||||
Operators should use the mission assertion backfill operator path to restore expected store-managed linkage for FN-5696 legacy rows:
|
||||
Removed contract:
|
||||
|
||||
- Agent/tool: `fn_mission_backfill_assertions` with `{ missionId?, dryRun? }` (defaults to dry-run).
|
||||
- API: `POST /api/missions/:missionId/backfill-assertions` with body `{ dryRun?: boolean }` (defaults to `true`).
|
||||
- Run dry-run first, then apply (`dryRun=false`) once repaired rows look correct.
|
||||
- This remediation is additive: it derives/links one store-managed assertion per unlinked feature so runtime enforcement uses validator-linked assertions rather than the zero-assertion auto-pass branch.
|
||||
- no `validation_auto_passed_no_assertions` completion path,
|
||||
- no silent or explicit rubber-stamp pass because assertions were missing,
|
||||
- no informational-only feature criteria bucket in MissionManager.
|
||||
|
||||
## Slice Status and Mission Autopilot Advance Derivation
|
||||
Instead, features are routed through validator execution after lazy assertion ensure.
|
||||
|
||||
Autopilot may advance only when each active-slice feature is resolved under this contract:
|
||||
## Worked examples
|
||||
|
||||
- Feature with linked assertions: all linked assertions must pass.
|
||||
- Feature with zero linked assertions: explicit auto-pass path completes it.
|
||||
- Feature with failed/blocked validation: slice remains incomplete.
|
||||
- Feature stranded without a task link in an active autopilot slice (`taskId == null`): startup + maintenance reconciliation must repair it (title-match link first, otherwise defined-status re-triage) so `allDone` remains reachable instead of stalling on never-triaged features.
|
||||
1. **Feature has acceptance criteria; no linked assertion row is present yet**
|
||||
- Runtime calls `ensureFeatureAssertionLinked(feature.id)`.
|
||||
- Validator runs against the restored managed assertion.
|
||||
- Result gates completion normally.
|
||||
|
||||
Then:
|
||||
2. **Feature has acceptance criteria and milestone acceptance criteria**
|
||||
- Validator evaluates the linked feature assertion(s).
|
||||
- Validator also evaluates the milestone acceptance text in the prompt.
|
||||
- Feature passes only when both are satisfied.
|
||||
|
||||
1. All features resolved complete → slice flips to `complete`.
|
||||
2. Completed active slice with pending next slice → next slice activates.
|
||||
3. All milestone slices complete → milestone complete.
|
||||
4. All mission milestones complete → mission complete.
|
||||
3. **Operator runs backfill on legacy data**
|
||||
- Backfill pre-restores missing managed assertions for visibility/reporting.
|
||||
- Runtime behavior is unchanged because lazy ensure already guarantees validator-backed enforcement.
|
||||
|
||||
This keeps completion logic deterministic and consistent with FN-5715 trigger/recovery behavior.
|
||||
## UI contract
|
||||
|
||||
## UI Reconciliation Requirements (for follow-on engineering task)
|
||||
MissionManager must present mission criteria as **AI-validated** rather than informational:
|
||||
|
||||
✅ Implemented in FN-5733 with MissionManager labels:
|
||||
- `Contract assertions (autopilot gate)` + enforced indicator
|
||||
- `Feature acceptance criteria (informational)` + not-enforced indicator
|
||||
- warning badge when `hasProseButNoAssertions === true`
|
||||
- assertion heading text reflects AI validation,
|
||||
- informational / not-enforced labels are removed,
|
||||
- zero-assertion warning guard is removed,
|
||||
- fallback feature-criteria rollups, when shown for missing loaded assertions, describe runtime AI validation rather than non-enforced prose.
|
||||
|
||||
Target surface: `packages/dashboard/app/components/MissionManager.tsx`
|
||||
## Success invariant
|
||||
|
||||
1. **Disambiguate labels**
|
||||
- Use distinct wording for:
|
||||
- feature-authored acceptance text, and
|
||||
- milestone contract assertions.
|
||||
- Do not reuse "Completion criteria" to refer to both categories.
|
||||
- Required wording baseline (or semantically equivalent copy):
|
||||
- Feature rollup heading: `Feature acceptance criteria (informational source)`
|
||||
- Assertion list heading: `Contract assertions (validator-enforced when linked)`
|
||||
For any mission feature that reaches validation trigger points:
|
||||
|
||||
2. **Per-row enforcement indicator**
|
||||
- Every displayed row in the assertions/criteria area must show whether it is:
|
||||
- `Enforced gate` (validator-blocking when linked), or
|
||||
- `Informational` (display-only).
|
||||
|
||||
3. **Empty-state contract-correct copy**
|
||||
- Replace the current implication that completion criteria are absent when assertion rows are empty.
|
||||
- Empty-state text must acknowledge when feature acceptance text exists but no assertion rows are defined/linked.
|
||||
- Required behavior:
|
||||
- If feature acceptance text exists but no assertion rows are present, show copy equivalent to: `No contract assertions are linked yet. Feature acceptance criteria are present below and remain informational until assertions are linked.`
|
||||
- If neither feature acceptance text nor assertions exist, show copy equivalent to: `No feature acceptance criteria or contract assertions defined yet.`
|
||||
|
||||
4. **No button/mobile scope expansion**
|
||||
- No button touch-target/mobile-reflow requirements (standing directive).
|
||||
|
||||
## Engineering Acceptance Criteria (follow-on implementation)
|
||||
|
||||
✅ Implemented in FN-5733:
|
||||
- Auto-pass path now advances `loopState` to `passed` and emits mission event code `validation_auto_passed_no_assertions` while preserving the `validation:passed` emit contract (`"No assertions linked"` summary).
|
||||
- Milestone rollup/store guard now exposes `hasProseButNoAssertions` and emits warning mission event code `milestone_missing_structured_assertions` (debounced on transition into condition).
|
||||
- MissionManager UI now distinguishes enforced assertion gate vs informational feature acceptance criteria.
|
||||
|
||||
1. **Data/model contract**
|
||||
- Preserve the canonical relationship: feature-authored criteria -> store-managed assertion -> linked assertion enforcement.
|
||||
- If any model/UI metadata is added for enforced-vs-informational badges, it must be backward compatible with existing mission rows.
|
||||
|
||||
2. **Validator/loop behavior**
|
||||
- Maintain FN-5715 invariants:
|
||||
- done mission-linked tasks with linked assertions trigger validation,
|
||||
- completion-trigger starts loop if needed,
|
||||
- startup recovery replays done-implementing features with unpassed assertions,
|
||||
- periodic self-heal maintenance replays the same `recoverActiveMissions` path so historically stranded `implementing` features recover without restart,
|
||||
- zero-linked-assertions path remains explicit canonical auto-pass.
|
||||
|
||||
3. **UI behavior**
|
||||
- Implement the Step-2 label reconciliation and per-row indicator requirements.
|
||||
- Ensure no shared ambiguous terminology remains between feature acceptance text and assertion rows.
|
||||
|
||||
4. **Regression coverage**
|
||||
- Add at least one regression test pinning the Goals-mission shape:
|
||||
- feature has `acceptanceCriteria`,
|
||||
- parent milestone has zero `MissionContractAssertion` rows / no links,
|
||||
- autopilot behavior is deterministic and observable (explicit auto-pass path, no silent stall).
|
||||
|
||||
5. **Operational observability**
|
||||
- Ensure mission/audit surfaces make no-assertions auto-pass and subsequent advance decisions queryable in logs/events.
|
||||
|
||||
## Success Metric
|
||||
|
||||
For 30 days after the follow-on implementation ships:
|
||||
|
||||
- **Primary metric:** zero autopilot stalls of the FN-5715 class (done mission task + unresolved validation trigger gap) in production mission runs.
|
||||
- **Evidence source:** mission audit/event stream (`feature_completed`, `slice_completed`, `mission_completed`) plus `mission_validator_runs` records showing:
|
||||
- explicit no-assertions auto-pass evidence (summary/reason path such as `No assertions linked` when no validator run is started), and
|
||||
- downstream advancement evidence without stalled active slices.
|
||||
|
||||
## Follow-on Task Requirement
|
||||
|
||||
Implementation must land in a separate engineering task that references this document and FN-5715 as the enforcement baseline.
|
||||
- a validator run must occur,
|
||||
- the feature must not auto-pass due to missing assertion links,
|
||||
- milestone acceptance text must be visible to the validator when present,
|
||||
- advancement decisions must derive from validator outcomes only.
|
||||
|
||||
@@ -23,9 +23,9 @@ Mission: Improve Reliability
|
||||
Task: FN-214
|
||||
```
|
||||
|
||||
## Mission ↔ Goal persistence
|
||||
## Mission → Goal linkage
|
||||
|
||||
Missions and goals are stored independently, but Fusion now persists an optional many-to-many linkage in the `mission_goals` join table.
|
||||
Missions and goals are stored independently, with an optional many-to-many linkage persisted in the `mission_goals` join table.
|
||||
|
||||
- Columns: `missionId`, `goalId`, `createdAt`
|
||||
- Primary key: `(missionId, goalId)`
|
||||
@@ -33,14 +33,24 @@ Missions and goals are stored independently, but Fusion now persists an optional
|
||||
- Delete behavior: both foreign keys use `ON DELETE CASCADE`, so removing either parent deletes only the corresponding join rows
|
||||
- Reverse lookups are indexed via `idxMissionGoalsGoalId`
|
||||
|
||||
`MissionStore` owns the linkage CRUD surface:
|
||||
`MissionStore` owns the persisted linkage CRUD surface:
|
||||
|
||||
- `linkGoal(missionId, goalId)` — idempotently create a link and return `{ missionId, goalId, createdAt }`
|
||||
- `unlinkGoal(missionId, goalId)` — remove a link and report whether anything changed
|
||||
- `listGoalIdsForMission(missionId)` — list linked goals in deterministic creation order
|
||||
- `listMissionIdsForGoal(goalId)` — list linked missions in deterministic creation order
|
||||
|
||||
Existing missions are **not** backfilled with goal links as part of this schema change; that decision is deferred to FN-5898.
|
||||
### No-backfill decision
|
||||
|
||||
Existing missions are intentionally **not** auto-linked to any goals. Fusion does not run a migration backfill for pre-existing missions, so a mission with no links should be treated as genuinely unlinked until an operator or agent associates it with one or more goals.
|
||||
|
||||
### Manual linkage workflow
|
||||
|
||||
Mission ↔ goal links are created and removed deliberately as part of normal planning and operations work. Read surfaces can show current associations, and operator-facing write surfaces can add or remove links when a mission should explicitly support a goal. The workflow is intentionally manual so teams can choose the correct strategic relationship per mission instead of inheriting guessed links from older data.
|
||||
|
||||
### Unlinked mission indicator
|
||||
|
||||
Mission Manager shows an **Unlinked** indicator on active mission cards when `linkedGoalCount` is zero. This is a read-only attention badge so operators can quickly find active missions that still need an explicit goal association.
|
||||
|
||||
## Creating Missions
|
||||
|
||||
@@ -107,18 +117,20 @@ Fusion surfaces the persisted mission↔goal linkage through REST, CLI, and pi-e
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|---|---|
|
||||
| `GET /api/missions/:missionId` | Return `MissionWithHierarchy`, including `linkedGoals` as an always-present array of `Goal` objects for the selected mission and optional `eventCount` as the authoritative unfiltered mission activity total. |
|
||||
| `GET /api/missions/:missionId/goals` | List linked goals for a mission. Returns `{ goals }`. |
|
||||
| `PUT /api/missions/:missionId/goals` | Replace the full linked-goal set with body `{ goalIds: string[] }`. Duplicate ids are deduplicated before reconciliation. |
|
||||
| `POST /api/missions/:missionId/goals/:goalId` | Idempotently link one goal to a mission. |
|
||||
| `DELETE /api/missions/:missionId/goals/:goalId` | Idempotently unlink one goal from a mission. |
|
||||
|
||||
All four endpoints validate mission/goal identifier formats and return `404` for missing mission/goal rows.
|
||||
The mission detail payload keeps `linkedGoals` separate from the milestone tree so read paths can surface strategy context without traversing slices/features. All five endpoints validate mission/goal identifier formats and return `404` for missing mission/goal rows.
|
||||
|
||||
### CLI
|
||||
|
||||
- `fn mission goals <mission-id>` — list linked goals for a mission.
|
||||
- `fn mission link-goal <mission-id> <goal-id>` — idempotently link a goal.
|
||||
- `fn mission unlink-goal <mission-id> <goal-id>` — idempotently unlink a goal.
|
||||
- Mission detail screens in the dashboard render linked-goal chips in the mission header; selecting a chip opens the Goals view and scrolls/highlights the anchored goal card.
|
||||
|
||||
## Mission Planning Tools (pi extension)
|
||||
|
||||
@@ -128,7 +140,7 @@ The canonical per-parameter tool reference lives in `packages/cli/skill/fusion/r
|
||||
|---|---|
|
||||
| `fn_mission_create` | Create a mission with title/description, optional `baseBranch`, and optional auto-advance behavior. |
|
||||
| `fn_mission_list` | List missions and their current status. |
|
||||
| `fn_mission_show` | Show mission details with milestone/slice/feature hierarchy, including milestone/feature acceptance criteria and slice verification when present. |
|
||||
| `fn_mission_show` | Show mission details with milestone/slice/feature hierarchy, including a **Linked Goals** section plus milestone/feature acceptance criteria and slice verification when present. |
|
||||
| `fn_mission_list_goals` | List the goals linked to a mission. |
|
||||
| `fn_mission_link_goal` | Idempotently link a goal to a mission. |
|
||||
| `fn_mission_unlink_goal` | Idempotently unlink a goal from a mission. |
|
||||
@@ -510,7 +522,7 @@ A feature transitions to `blocked` when:
|
||||
|
||||
On engine restart, `recoverActiveMissions()` re-enqueues features in `validating` or `needs_fix` states, ensuring no validation work is lost. It also re-triggers `implementing` features whose linked task is already `done`/`archived` and whose assertion validation has not passed yet. When the stale-run reaper has already converted an abandoned validator run into `needs_fix`, `processTaskOutcome()` promotes the feature back through `implementing` and re-validates instead of skipping it. The same recovery path is replayed during periodic self-heal maintenance, so historically stranded `implementing` features can self-heal without requiring an engine restart.
|
||||
|
||||
For features with zero linked assertions, the completion path is explicit: the loop marks the feature `done`, advances `loopState` to `passed`, emits `validation:passed` with summary `"No assertions linked"`, and records mission event code `validation_auto_passed_no_assertions`. Contract details (including canonical no-assertions behavior and FN-5696 assertion-authoring separation) are defined in [Mission Completion Gate Contract](./missions-completion-contract.md).
|
||||
For features with missing linked assertions, the completion path is now validator-first: the loop lazily restores the store-managed per-feature assertion just before validation, then runs the AI validator instead of auto-passing. Milestone `acceptanceCriteria` is threaded into the validator prompt for every feature in that milestone, so all mission criteria are AI-evaluated. Contract details are defined in [Mission Completion Gate Contract](./missions-completion-contract.md).
|
||||
|
||||
### Autopilot / Scheduler Interplay
|
||||
|
||||
@@ -541,8 +553,7 @@ These are independent tracking mechanisms — autopilot monitors mission progres
|
||||
**MissionEvent audit types:**
|
||||
- `slice_activated`, `feature_planned`, `feature_completed`
|
||||
- `validation:started`, `validation:passed`, `validation:failed`, `validation:blocked`
|
||||
- `validation_auto_passed_no_assertions` (reason: `"No assertions linked"`)
|
||||
- `milestone_missing_structured_assertions` (warning when prose criteria exist with zero structured assertions)
|
||||
- `milestone_missing_structured_assertions` (legacy-data warning surface; enforcement still lazy-restores managed assertions at runtime)
|
||||
- `fix_feature:created`, `feature:blocked`
|
||||
|
||||
**Validator run telemetry:**
|
||||
|
||||
@@ -45,7 +45,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
|
||||
| `ntfyTopic` | `string` | `undefined` | ntfy topic name. |
|
||||
| `ntfyBaseUrl` | `string` | `undefined` | Optional custom ntfy server base URL (must use `http://` or `https://`). If blank/unset, Fusion uses `https://ntfy.sh` for both runtime and test notifications. |
|
||||
| `ntfyAccessToken` | `string` | `undefined` | Optional ntfy access token. When set, Fusion sends `Authorization: Bearer <token>` with ntfy publish requests, including Settings → Notifications test sends. Leave blank/unset to publish without authentication. |
|
||||
| `ntfyEvents` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "board-stall-unrecovered" \| "fallback-used" \| "task-created" \| "memory-dreams-processed" \| "message:agent-to-user" \| "message:agent-to-agent" \| "message:room" \| "oauth-token-expired" \| "token-budget")[]` | `["in-review","merged","failed","awaiting-approval","awaiting-user-review","planning-awaiting-input","gridlock","board-stall-unrecovered","fallback-used","memory-dreams-processed","message:agent-to-user","message:agent-to-agent","message:room","oauth-token-expired","token-budget"]` | Event types that trigger ntfy notifications. `planning-awaiting-input` fires when planning mode is waiting on user input. `gridlock` fires when all schedulable todo tasks are blocked; delivery is cooldown-throttled (first alert immediately, then suppressed for 15 minutes until gridlock resolves). `board-stall-unrecovered` fires only after a board-stall auto-recovery sweep runs and a follow-up verification tick still sees zero progress. `fallback-used` fires when Fusion recovers from a retryable model failure by switching to a configured fallback model. `task-created` fires when an agent creates a new task (requires `sourceAgentId`) and is opt-in/off by default. `memory-dreams-processed` fires when manual dream processing writes a new `DREAMS.md` entry (project and/or agent); disable it via ntfy/webhook event filters if you want to opt out. `message:agent-to-user` fires when an agent sends a direct message to the user. `message:agent-to-agent` fires when an agent sends a message to another agent (including replies). `message:room` fires when an agent posts an assistant reply in a chat room. `oauth-token-expired` fires when a provider OAuth credential reaches its expiry and needs re-authentication. `token-budget` fires when a task crosses token soft/hard caps. If you use a custom `ntfyEvents` list, these message events must be present (or `ntfyEvents` must be unset so defaults apply) for the corresponding notifications to send. |
|
||||
| `ntfyEvents` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "board-stall-unrecovered" \| "fallback-used" \| "task-created" \| "memory-dreams-processed" \| "message:agent-to-user" \| "message:agent-to-agent" \| "message:room" \| "oauth-token-expired" \| "token-budget")[]` | `["in-review","merged","failed","awaiting-approval","awaiting-user-review","planning-awaiting-input","gridlock","board-stall-unrecovered","fallback-used","memory-dreams-processed","message:agent-to-user","message:agent-to-agent","message:room","oauth-token-expired","token-budget"]` | Event types that trigger ntfy notifications. `planning-awaiting-input` fires when planning mode is waiting on user input. `gridlock` fires when all schedulable todo tasks are blocked; delivery is cooldown-throttled (first alert immediately, then suppressed for 15 minutes until gridlock resolves). `board-stall-unrecovered` fires only after a board-stall auto-recovery sweep runs and a follow-up verification tick still sees zero progress. `fallback-used` fires when Fusion recovers from a retryable model failure by switching to a configured fallback model. `task-created` fires when an agent creates a new task (requires `sourceAgentId`) and is opt-in/off by default. `memory-dreams-processed` fires when manual dream processing writes a new `DREAMS.md` entry (project and/or agent); disable it via ntfy/webhook event filters if you want to opt out. `message:agent-to-user` fires when an agent sends a direct message to the user. `message:agent-to-agent` fires when an agent sends a message to another agent (including replies). `message:room` fires when an agent posts an assistant reply in a chat room. `oauth-token-expired` fires when a provider OAuth credential reaches its expiry and needs re-authentication; Fusion also throttles that notification and the matching startup expiry warning to at most once per provider every 12 hours, and the throttle persists across server restarts. `token-budget` fires when a task crosses token soft/hard caps. If you use a custom `ntfyEvents` list, these message events must be present (or `ntfyEvents` must be unset so defaults apply) for the corresponding notifications to send. |
|
||||
| `ntfyDashboardHost` | `string` | `undefined` | Dashboard host used to build deep links in notifications. |
|
||||
| `taskTokenBudget` | `{ soft?: number; hard?: number; perSize?: { S?: { soft?: number; hard?: number }; M?: { soft?: number; hard?: number }; L?: { soft?: number; hard?: number } } }` | `undefined` | Global fallback per-task token budget policy. Project `taskTokenBudget` overrides this. |
|
||||
| `webhookEnabled` | `boolean` | `false` | Enable webhook notifications for task lifecycle events. Part of the legacy flat settings; prefer `notificationProviders` for new setups. |
|
||||
@@ -161,7 +161,7 @@ When `id` is `"ntfy"` in `notificationProviders`, the provider `config` supports
|
||||
| `topic` | `string` | _required_ | ntfy topic name (1–64 chars, alphanumeric + `-_`). |
|
||||
| `ntfyBaseUrl` | `string` | `"https://ntfy.sh"` | Optional custom ntfy server URL. |
|
||||
| `ntfyAccessToken` | `string` | `undefined` | Optional access token. When set, provider sends `Authorization: Bearer <token>` on ntfy publishes. |
|
||||
| `events` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "board-stall-unrecovered" \| "fallback-used" \| "task-created" \| "memory-dreams-processed" \| "message:agent-to-user" \| "message:agent-to-agent" \| "message:room" \| "oauth-token-expired")[]` | `DEFAULT_NTFY_EVENTS` | Event filter list used by the provider. For `gridlock`, enabled events are still cooldown-throttled at runtime (15-minute suppression window, reset on full resolution). `board-stall-unrecovered` is emitted when board-stall verification fails after an attempted auto-recovery sweep. `task-created` is available as an opt-in event and only fires for agent-created tasks (`sourceAgentId` required). `memory-dreams-processed` is emitted when manual dream processing appends a new project/agent `DREAMS.md` entry. `message:agent-to-user`/`message:agent-to-agent` are emitted for mailbox messages and deep-link to the specific message when `dashboardHost` is configured. `message:room` is emitted for assistant replies in chat rooms and deep-links to the room when `dashboardHost` is configured. `oauth-token-expired` is emitted when a provider OAuth credential has expired. |
|
||||
| `events` | `("in-review" \| "merged" \| "failed" \| "awaiting-approval" \| "awaiting-user-review" \| "planning-awaiting-input" \| "gridlock" \| "board-stall-unrecovered" \| "fallback-used" \| "task-created" \| "memory-dreams-processed" \| "message:agent-to-user" \| "message:agent-to-agent" \| "message:room" \| "oauth-token-expired")[]` | `DEFAULT_NTFY_EVENTS` | Event filter list used by the provider. For `gridlock`, enabled events are still cooldown-throttled at runtime (15-minute suppression window, reset on full resolution). `board-stall-unrecovered` is emitted when board-stall verification fails after an attempted auto-recovery sweep. `task-created` is available as an opt-in event and only fires for agent-created tasks (`sourceAgentId` required). `memory-dreams-processed` is emitted when manual dream processing appends a new project/agent `DREAMS.md` entry. `message:agent-to-user`/`message:agent-to-agent` are emitted for mailbox messages and deep-link to the specific message when `dashboardHost` is configured. `message:room` is emitted for assistant replies in chat rooms and deep-links to the room when `dashboardHost` is configured. `oauth-token-expired` is emitted when a provider OAuth credential has expired; Fusion suppresses repeat delivery for the same provider for 12 hours even across server restarts, and applies the same persisted window to the startup expiry warning log. |
|
||||
| `dashboardHost` | `string` | `undefined` | Dashboard host for deep links in notifications. |
|
||||
|
||||
Disable daily update checks globally:
|
||||
@@ -265,7 +265,7 @@ Sandbox backend precedence is:
|
||||
|
||||
| `pushAfterMerge` | `boolean` | `false` | Auto-push to remote after successful direct merge. Includes pulling latest and AI conflict resolution. |
|
||||
| `pushRemote` | `string` | `"origin"` | Git remote (and optional branch) to push to after merge. |
|
||||
| `worktreeInitCommand` | `string` | `undefined` | Shell command run after worktree creation. For pnpm repos, prefer `pnpm install --frozen-lockfile` for deterministic bootstrap. |
|
||||
| `worktreeInitCommand` | `string` | `undefined` | Shell command run after worktree creation and again to bootstrap the merge worktree before AI merge verification. Useful for project-specific setup beyond package install (for example `pnpm install --frozen-lockfile`, `cp .env.local .env`, or codegen/bootstrap scripts). |
|
||||
| `testCommand` | `string` | `undefined` | Merge-time test command (hard gate). When unset, Fusion auto-detects from lockfile. |
|
||||
| `buildCommand` | `string` | `undefined` | Merge-time build command (hard gate). |
|
||||
| `recycleWorktrees` | `boolean` | `false` | Default: off (opt-in). Reuse worktrees from a pool for faster startup. |
|
||||
@@ -430,6 +430,8 @@ Default notes:
|
||||
| `showQuickChatFAB` | `boolean` | `false` | Show floating quick-chat button (chat remains available via More menu). |
|
||||
| `chatAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-cleanup retention window for idle chat sessions and chat rooms. `0` is off (default). When enabled, periodic self-healing maintenance deletes rows with `updatedAt` older than the configured day window. |
|
||||
| `mailAutoCleanupDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `0` | Auto-prune retention window for inbox/outbox mail messages. `0` is off (default). When enabled, periodic self-healing maintenance deletes `messages` rows where `updatedAt < cutoff` for the configured day window. Suggested setting: `7`. |
|
||||
| `operationalLogRetentionDays` | `0 \| 7 \| 14 \| 30 \| 60 \| 90` | `30` | Retention window for SQLite operational-log tables (`activityLog`, `runAuditEvents`, `agentHeartbeats`). `0` is off. Lower values mean Reliability metrics/charts and the Activity feed will not show history older than the configured window; per-task task detail history is unaffected. Periodic maintenance prunes rows older than this many days using each row's `timestamp`. |
|
||||
| `agentLogFileRetentionDays` | `number` | `0` | Retention window for per-task `.fusion/tasks/{ID}/agent-log.jsonl` files after a task is soft-deleted or archived. Periodic maintenance removes JSONL entries older than this many days; active tasks are never pruned. Set `0` to disable pruning. |
|
||||
| `chatRoomRecentVerbatimMessages` | `number` | `25` | Number of newest chat-room messages kept verbatim in responder context before older entries are compacted (about 2× prior default history). |
|
||||
| `chatRoomCompactionFetchLimit` | `number` | `200` | Upper bound on room messages fetched for transcript compaction per responder turn (raised to support larger retained context windows). |
|
||||
| `chatRoomSummaryMaxChars` | `number` | `3000` | Hard cap for the synthesized “Earlier room context” summary block (about 2× the prior summary budget). |
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
| 5. Soft-delete an `in-progress` task with an active workflow-step session and reviewer subagent | Live `in-progress`; workflow step child session exists | Delete succeeds; no public recovery/undelete flag | New execution attempts refuse; workflow-step + reviewer abort/cleanup is **pending FN-5142** | N/A | N/A | SSE removes card; reload stays clean | Preserved until FN-5143 | Hidden from live readers once FN-5140 lands | FN-5142 |
|
||||
| 6. Soft-delete an `in-review` task that is currently being merged | Live `in-review`; active merge session in flight | Delete succeeds; live readers omit afterward | Scheduler must not requeue it | Active merge abort, queue removal, and controller cleanup are **pending FN-5142** | N/A | SSE removes card; reload keeps it absent | Preserved until FN-5143 | Hidden from live readers once FN-5140 lands | FN-5142 |
|
||||
| 7. Soft-delete an `in-review` task queued for merge but not yet active | Live `in-review`; merge queued only | Delete succeeds; row stays for forensics only | Scheduler/executor must not pick it up again | Merge queue must filter it out; pending FN-5142 covers deterministic abort/filter assertions | N/A | SSE removes card; reload keeps it absent | Preserved until FN-5143 | Hidden from live readers once FN-5140 lands | FN-5137 / FN-5142 |
|
||||
| 8. Soft-delete a `done` task with archived/visible agent logs and saved task documents | Live `done`; has task docs + agent logs | Delete succeeds; live task readers omit afterward; forensic reads still allowed internally | Not runnable after any engine tick or restart | N/A | N/A | SSE removes card; refresh does not show it in board/ListView/TodoView | `agentLogEntries` must clear atomically; **pending FN-5143** | `/api/documents` and per-task docs must disappear while DB rows remain; **pending FN-5140** | FN-5140 / FN-5143 |
|
||||
| 9. Soft-delete an archived task | Task already archived / moved out of live `tasks` table | Current contract is not pinned; matrix gate requires deterministic error-or-no-op behavior. Follow-up filed as **FN-5196**. | Must never affect active queues either way | N/A | N/A | No dashboard resurrection; exact UX blocked by FN-5196 | N/A | N/A | FN-5196 |
|
||||
| 8. Soft-delete a `done` task with archived/visible agent logs and saved task documents | Live `done`; has task docs + agent logs | Delete succeeds; live task readers omit afterward; forensic reads still allowed internally | Not runnable after any engine tick or restart | N/A | N/A | SSE removes card; refresh does not show it in board/ListView/TodoView | `agent-log.jsonl` file is preserved, but `getAgentLogs*` / `getAgentLogCount` return zero once `deletedAt` is set | `/api/documents` and per-task docs must disappear while DB rows remain; **pending FN-5140** | FN-5140 / FN-5143 || 9. Soft-delete an archived task | Task already archived / moved out of live `tasks` table | Current contract is not pinned; matrix gate requires deterministic error-or-no-op behavior. Follow-up filed as **FN-5196**. | Must never affect active queues either way | N/A | N/A | No dashboard resurrection; exact UX blocked by FN-5196 | N/A | N/A | FN-5196 |
|
||||
| 10. Soft-delete a task that is checked out by an agent (`checkedOutBy` set) | Live task with lease / checkout metadata | Delete succeeds; linked agent task references clear with delete | Soft-deleted checked-out task must not be auto-claimed or executed after refresh/tick; extra deterministic coverage filed as **FN-5195** | If merge-owned, FN-5142 owns active merge abort details | If triage-owned, FN-5142 owns active triage abort details | SSE removes card; refresh must not show stale checked-out task | Preserved until FN-5143 | Hidden from live readers once FN-5140 lands | FN-5137 / FN-5195 |
|
||||
| 11. Retry-exhausted `in-review` blocker is soft-deleted (`mergeRetries >= 3`) | Row remains in `tasks` with `deletedAt` set; downstream live tasks may still reference blocker in `dependencies`/`blockedBy` | Default task readers still hide it; opt-in surfaces expose it (`GET /api/tasks/exhausted-in-review?includeDeleted=true`, `GET /api/tasks/:id?includeDeleted=true`, `fn_task_show` fallback, `fn_task_list includeDeleted`) | Deadlock/stuck-merge/in-review-stall scans must exclude soft-deleted rows via `listTasks` (`ACTIVE_TASKS_WHERE`), plus per-sweep `task.deletedAt` guards as belt-and-suspenders | No merge-state mutation; blocker remains terminal unless explicit operator action | N/A | ReliabilityView panel explicitly lists hidden exhausted blockers + blocked dependents; main board stays unchanged | Existing logs preserved | Documents readable via opt-in task-detail fetch; no automatic restore | FN-5513 / FN-5528 |
|
||||
|
||||
@@ -40,7 +39,7 @@
|
||||
| 1,10 | Triage abort on `task:deleted` | No deterministic triage abort assertion in current corpus | Missing active triage session + subagent abort coverage | `packages/engine/src/__tests__/triage-soft-delete-abort.test.ts` | FN-5142 |
|
||||
| 11 | Deadlock/stuck-merge/in-review-stall scan exclusion for soft-deleted exhausted blockers | Added in this task | GREEN — defensive sweep guards + script `WHERE deletedAt IS NULL` backstop | `packages/engine/src/__tests__/reliability-interactions/soft-delete-deadlock-scan-exclusion.test.ts`, `scripts/__tests__/recover-stale-blocked-by.test.mjs` | FN-5528 |
|
||||
| 2,11 | Soft-delete blocker residue + legacy column drift reconciliation (`deletedAt` + non-archived column) | Added in this task | GREEN — in-transaction blocker cleanup, periodic/startup column-drift reconciler, and audit mutation `task:soft-delete-column-reconciled` | `packages/engine/src/__tests__/reliability-interactions/soft-delete-blocker-residue.test.ts`, `packages/core/src/__tests__/store-delete-task-blocker-residue.test.ts` | FN-5566 (re-land FN-5446) |
|
||||
| 8 | `agentLogEntries` cleared on soft-delete | No dedicated coverage today | Missing atomic clear + post-delete empty-reader assertion | `packages/core/src/__tests__/soft-delete-agent-logs.test.ts` | FN-5143 |
|
||||
| 8 | Preserved `agent-log.jsonl` file is hidden from live readers after soft-delete | `packages/core/src/__tests__/soft-delete-agent-logs.test.ts` | GREEN — read APIs return zero while the on-disk file remains available for forensics | — | FN-5143 / FN-5911 |
|
||||
| 8 | `/api/documents` and per-task docs exclude soft-deleted parents | No dedicated soft-delete document visibility assertion today | Missing store + route coverage | `packages/core/src/__tests__/task-documents.test.ts` and `packages/dashboard/src/__tests__/routes-tasks.test.ts` | FN-5140 |
|
||||
| 3 | Lineage-unlink 409 flow through API + UI | Store lineage guards are covered; route/UI flow is not | Missing 409 payload + confirm-retry UX coverage | `packages/dashboard/src/__tests__/routes-tasks-ops.test.ts`, `packages/dashboard/app/utils/__tests__/taskDelete.test.ts`, `packages/dashboard/app/components/__tests__/TaskCard.test.tsx`, `packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx`, `packages/dashboard/app/components/__tests__/ListView.test.tsx` | FN-5139 |
|
||||
| Stream-wide | `fn_task_delete` tool / skill terminology | No regression asserting soft-delete wording | Missing user-facing copy coverage | `packages/cli/src/__tests__/extension.test.ts` | FN-5141 |
|
||||
@@ -65,7 +64,7 @@
|
||||
Supported forensic access is internal only:
|
||||
|
||||
- `readTaskFromDb(id, { includeDeleted: true })` in `packages/core/src/store.ts`
|
||||
- direct SQL against `tasks`, `task_documents`, and `agentLogEntries`
|
||||
- direct SQL against `tasks` and `task_documents`, plus on-disk reads of `.fusion/tasks/{ID}/agent-log.jsonl`
|
||||
|
||||
No public API flag exposes deleted-task forensics today. Adding one requires a new FN with its own review.
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Per-task auto-merge override ignored by trigger-layer gates
|
||||
date: 2026-06-03
|
||||
category: logic-errors
|
||||
module: engine
|
||||
problem_type: logic_error
|
||||
component: background_job
|
||||
symptoms:
|
||||
- "Tasks with per-task autoMerge:true never auto-merged when global settings.autoMerge was off"
|
||||
- "Override tasks reached in-review and sat there indefinitely with no error surfaced"
|
||||
- "In-review self-healing sweeps short-circuited on the global setting and never enqueued the merge"
|
||||
root_cause: logic_error
|
||||
resolution_type: code_fix
|
||||
severity: high
|
||||
related_components:
|
||||
- merger
|
||||
- self-healing
|
||||
- store
|
||||
tags:
|
||||
- auto-merge
|
||||
- per-task-override
|
||||
- merge-queue
|
||||
- self-healing
|
||||
- engine
|
||||
- trigger-gate
|
||||
---
|
||||
|
||||
# Per-task auto-merge override ignored by trigger-layer gates
|
||||
|
||||
## Problem
|
||||
|
||||
A per-task `autoMerge: true` override was honored only by the merger itself, but every *trigger-layer* gate (engine enqueue, 19 self-healing sweeps, store stall-signal hydration) checked the global `settings.autoMerge` alone. With global auto-merge OFF, override tasks were never enqueued and sat in `in-review` forever. Fixed in PR Runfusion/Fusion#1356.
|
||||
|
||||
## Symptoms
|
||||
|
||||
- User disabled auto-merge globally but enabled it on individual tasks.
|
||||
- Those individually-enabled tasks reached `in-review` and stayed there indefinitely — never picked up, never merged.
|
||||
- No error surfaced: the tasks were simply never *triggered* into the merge pipeline, so the merger's per-task handling never ran.
|
||||
|
||||
## What Didn't Work
|
||||
|
||||
- **Assuming the downstream merger check was enough.** The only code consulting `task.autoMerge` was the merger (`packages/engine/src/merger.ts` ~7958: `task.autoMerge === false` → `manual-required`). That runs *after* enqueue. The enqueue gate `allowInReviewMergeProcessing` (`packages/engine/src/project-engine.ts:1386`) and 19 self-healing sweeps short-circuited on `settings.autoMerge` before the task ever reached the merger — so the per-task flag was dead code from the user's perspective. Notably, the feature issues (Runfusion/Fusion#1150, #1152, #1153) shipped the data model, a resolver (`resolveEffectiveAutoMerge`), and the dashboard control — #1152 even claimed engine merge-gating used the resolved value — but no trigger gate actually consulted it.
|
||||
- **Reaching for `resolveEffectiveAutoMerge` at the gates.** The existing resolver `task.autoMerge ?? settings.autoMerge` (`packages/core/src/task-merge.ts`) looks like the natural gate, but using it would *regress* the global-ON + `autoMerge:false` case: those tasks must still flow into the merger so it can park them as `manual-required` (and so merged-task finalization sweeps still finalize them). Plain resolution would skip them at the trigger, stranding manually-merged tasks in `in-review`.
|
||||
- **Slim-projection gotcha.** Per-task gating reads `task.autoMerge` off rows from slim task projections. If the `autoMerge` column were missing from `getTaskSelectClause` (`packages/core/src/store.ts` ~1976), the gate would silently see `undefined` and the override would fail with no error. (Verified present — but a real trap when adding per-row predicates.)
|
||||
|
||||
## Solution
|
||||
|
||||
New core predicate, **additive** to the global setting (`packages/core/src/task-merge.ts`):
|
||||
|
||||
```ts
|
||||
export function allowsAutoMergeProcessing(
|
||||
task: Pick<Task, "autoMerge">,
|
||||
settings: Pick<Settings, "autoMerge">,
|
||||
): boolean {
|
||||
return settings.autoMerge !== false || task.autoMerge === true;
|
||||
}
|
||||
```
|
||||
|
||||
Applied at three trigger layers:
|
||||
|
||||
1. **Enqueue gate** (`project-engine.ts:1386`), which fronts all four enqueue paths (startup sweep, periodic retry, unpause, task-moved fast path):
|
||||
|
||||
```ts
|
||||
// before
|
||||
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext">, settings: Pick<Settings, "autoMerge">): boolean {
|
||||
return settings.autoMerge || isSharedBranchGroupMemberIntegration(task);
|
||||
}
|
||||
// after
|
||||
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext" | "autoMerge">, settings: Pick<Settings, "autoMerge">): boolean {
|
||||
return allowsAutoMergeProcessing(task, settings) || isSharedBranchGroupMemberIntegration(task);
|
||||
}
|
||||
```
|
||||
|
||||
2. **All 19 self-healing sweeps** (`self-healing.ts`): the function-level early returns (`if (settings.autoMerge === false) return 0;`) were replaced by per-task filtering inside each sweep's candidate set, e.g.:
|
||||
|
||||
```ts
|
||||
const candidates = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(t, settings) &&
|
||||
!t.paused && /* ... */);
|
||||
```
|
||||
|
||||
3. **Store stall-signal hydration** (`store.ts`, 6 sites): `autoMerge: settings.autoMerge` → `autoMerge: allowsAutoMergeProcessing(task, settings)` in the `getInReviewStallReason` / `getInReviewStalledSignal` contexts, so board diagnostics reflect that override tasks *are* being processed.
|
||||
|
||||
The self-healing contract also changed: from "skip the whole sweep when global is off" to "list tasks, but mutate nothing without a per-task override." FN-5147 tests that asserted `listTasks` was never called were updated to assert the mutation-free guarantee instead. This extends — and stays consistent with — the AGENTS.md `autoMerge: false` callout (FN-5147): self-healing still never moves override-less `in-review` tasks when auto-merge is off.
|
||||
|
||||
## Why This Works
|
||||
|
||||
The root cause was a flag consulted only where the *action* runs, not where processing is *triggered*. Adding the override evaluation to every trigger gate closes the gap.
|
||||
|
||||
Additive (`settings.autoMerge !== false || task.autoMerge === true`) is deliberately chosen over resolution (`task.autoMerge ?? settings.autoMerge`):
|
||||
|
||||
- **Global ON:** `settings.autoMerge !== false` is already `true`, so the predicate is a no-op — every task flows through exactly as before, including `autoMerge:false` tasks that the merger then parks as `manual-required`. Resolution would have excluded those, breaking manual-required parking and finalization.
|
||||
- **Global OFF:** the first term is `false`, so only `task.autoMerge === true` tasks proceed — exactly the missing override path.
|
||||
|
||||
It changes nothing when global is ON and adds only the explicit-true path when global is OFF.
|
||||
|
||||
## Prevention
|
||||
|
||||
When adding a per-entity override to a behavior that's gated on a global setting, the override must be consulted **where the behavior is TRIGGERED, not just where the action runs.** A check at the merger (the action) is invisible if upstream enqueue/sweep gates already filtered the entity out.
|
||||
|
||||
- **Grep every gate on the global setting** before declaring the override wired: here `settings.autoMerge` appeared at 1 enqueue gate, 19 sweep guards, and 6 hydration sites — all needed updating. A search for the global key, not just the new override field, surfaces the dead-flag sites.
|
||||
- **Prefer additive gating over effective-value resolution for *processing* gates.** Resolution collapses three states (global-on/off × per-task true/false/unset) into one boolean and can starve a needed downstream branch (the manual-required parking path). Gate on "should this be processed at all," resolve the actual behavior later.
|
||||
- **Check existing regression contracts before re-scoping a gate.** Review of the fix PR suggested exempting `todo`/`in-progress` candidates (execution-stage repair) from the auto-merge gate — but the repo's FN-5704 regression test ("short-circuits reclaim when autoMerge is false") deliberately keeps execution-stage reclaim inert in manual-review projects. Per-task gating applied uniformly preserves that contract while enabling overrides; exempting execution-stage recovery would be a separate, deliberate behavior change.
|
||||
- **Watch slim projections:** per-row predicates require the override column in the SELECT clause, or they silently read `undefined`.
|
||||
- **Test matrix must cross global × per-task.** The fix shipped red-first unit tests for the predicate (`packages/core/src/__tests__/task-merge.test.ts`), the gate including the shared-group exemption (`packages/engine/src/__tests__/project-engine.test.ts`), and a self-healing test proving an **override task is processed while an override-less sibling stays skipped** (`packages/engine/src/__tests__/self-healing.test.ts`) — the latter is the canonical shape: two tasks differing only in `autoMerge` under global-OFF, asserting divergent outcomes.
|
||||
|
||||
## Related Issues
|
||||
|
||||
- Runfusion/Fusion#1356 — the fix PR
|
||||
- Runfusion/Fusion#1150, Runfusion/Fusion#1152, Runfusion/Fusion#1153 — the per-task auto-merge feature trio (data model + resolver, engine gating, dashboard control); #1152's gating claim is the gap this bug exposed
|
||||
- Runfusion/Fusion#753 (FN-5147), Runfusion/Fusion#690 (FN-5052) — prior global `autoMerge:false` stall/lifecycle handling that the sweeps' guards came from
|
||||
- AGENTS.md → "`autoMerge: false` callout (FN-5147)" — standing lifecycle rule this fix extends to per-task granularity
|
||||
@@ -15,10 +15,20 @@
|
||||
- Archived-task flows (`archiveTask`, archived cleanup/migration) still hard-delete from the active `tasks` table after copying to cold storage (`archive.db`).
|
||||
- ID reservation is unchanged: soft-deleted IDs remain reserved. `distributed-task-id` and `task-id-integrity` intentionally scan all task rows (including soft-deleted rows), and must not filter on `deletedAt`.
|
||||
|
||||
### Agent log clearing (FN-5143)
|
||||
### Agent log storage + soft-delete visibility (FN-5143 / FN-5911)
|
||||
|
||||
- `TaskStore.deleteTask` now clears `agentLogEntries` rows for the soft-deleted task in the same transaction that writes `deletedAt`, so downstream `getAgentLogs*` / `getAgentLogCount` calls observe zero logs immediately.
|
||||
- This is soft-delete-specific cleanup; archived-task agent log snapshot behavior (`taskToArchiveEntry` / `archiveTask`) is unchanged.
|
||||
- Agent logs are no longer stored in SQLite. Each task now appends newline-delimited JSON records to `<rootDir>/.fusion/tasks/{ID}/agent-log.jsonl`.
|
||||
- `TaskStore.deleteTask` keeps that JSONL file on disk for forensics, but all live read APIs (`getAgentLogs*`, `getAgentLogCount`) gate on task liveness and return zero entries once `deletedAt` is set.
|
||||
- Archived-task snapshot behavior (`taskToArchiveEntry` / `archiveTask`) is unchanged in spirit: archive payloads still embed a capped agent-log snapshot, now sourced from the JSONL file instead of `fusion.db`.
|
||||
- Retention is now independent from SQLite operational-log pruning. `settings.agentLogFileRetentionDays` controls age-based pruning of JSONL entries for soft-deleted and archived tasks only. Default: `0` (disabled).
|
||||
|
||||
### Activity-log no-op `task:moved` cleanup (FN-5940)
|
||||
|
||||
- `TaskStore` now defends the invariant that `activityLog` never records a `task:moved` row when `metadata.from === metadata.to`.
|
||||
- Defense is layered: the `task:moved` listener skips same-column transitions, and source emitters skip no-op `archived -> archived` / same-column polling re-emits before subscribers see them.
|
||||
- Existing junk rows are removed by a one-time init migration guarded by `__meta.noOpTaskMovedActivityCleanupVersion = "1"`.
|
||||
- The cleanup deletes only rows matching `type = 'task:moved'` where `json_extract(metadata, '$.from') = json_extract(metadata, '$.to')`; legitimate distinct-column moves are preserved.
|
||||
- The migration does **not** run `VACUUM` automatically. After the delete lands on a large disk-backed DB, run `fn db --vacuum` manually to reclaim the freed space from the SQLite file.
|
||||
|
||||
### Dashboard delete-event handling (FN-5135)
|
||||
|
||||
|
||||
@@ -98,3 +98,12 @@ Prefer `it.each` over copy-pasted `it()` blocks. When trimming, keep: first case
|
||||
- Do **not** add net-new real-network calls, real-`setTimeout` polling loops, or mock-the-world component shells when a narrower seam exists.
|
||||
- Use the canonical taxonomy in **What NOT to write** and **What TO keep unconditionally** when deciding trim vs keep.
|
||||
- See `docs/test-speed-audit-FN-5048.md` for the measured baseline offender list and optimization priorities.
|
||||
|
||||
### Surface Enumeration checklist
|
||||
|
||||
Copy this checklist into a bug-fix task's `## Surface Enumeration` section and make the regression tests prove the invariant across every checked surface. See `AGENTS.md` → **Standing Rule: Fix the Invariant, Not the Repro (FN-5893)** for the enforced planning/review contract.
|
||||
|
||||
- [ ] Providers / bridges / execution paths touched by the invariant
|
||||
- [ ] Desktop + mobile breakpoints / platforms that exercise the behavior
|
||||
- [ ] Empty / undefined / duplicate / populated data states
|
||||
- [ ] Shared hooks / components / modules / helpers reusing the logic
|
||||
|
||||
Reference in New Issue
Block a user