c53d3aec381d9b93a0426cd266cda34e4c8bded0
139 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1322a1bb11 |
docs(solutions): the optional-flags seam kept four green suites blind to their own conversion — and why I did not ship a ratchet for it (#2748)
Docs only. No code, no census movement. ## The finding, measured Four consecutive files in this program had **fully green suites at conversion time** that could not have detected the conversion — correct or broken: | file | pre-existing cases blind to the change | | --- | --- | | `github-tracking-reconciler.ts` | **33** (fake store had no workflow reader) | | `TaskReviewTab.tsx` | **45** (`columnFlags` omitted everywhere) | | `plan-approval-hold-invariant` drain | **25** (`opts.lifecycle` omitted everywhere) | | `task-age-staleness.ts` | **12** (`context.lifecycle` omitted everywhere) | The cause is structural. Every conversion here uses the same seam — the caller passes resolved flags, the helper falls back to the legacy id when they are absent — and every pre-existing test omits the flags. So the suite passes **before** the conversion, **after a correct one**, and **after a wrong one**, as long as the fallback is intact. "The suite is green" carries no information about the change. I reported this observation four times in PR bodies. Restating it a fifth time is worth less than writing it where the next worker will actually find it. ## It also corrects the obvious test The natural property is "hold the traits fixed, change the id, behaviour is identical". That is only half the invariant. It does not catch: ```ts // Not a fallback — an OVERRIDE. The id wins even when traits disagree. return column === "in-review" || flags?.mergeBlocker === true; ``` Renaming `in-review` → `checking` leaves that correct, because the trait arm answers. The defect appears in the **converse** direction — a column that still *carries* a lifecycle name while its traits say otherwise, which is what you get by repurposing a default column rather than renaming one. That is the direction that found a live **"Merge & Close" offered on a mid-implementation card** in #2718. ## Why this is not a ratchet — a negative result, recorded I tried to automate it, and I am shipping the reason it failed rather than a guard I do not trust. The **consumer scan is sound**: AST-based, 31 files, 66 role-helper call sites. The **coverage half is not**. The renamed ids this program uses — `building`, `checking`, `converted`, `published`, `backlog` — are ordinary English words that appear in unrelated test prose, and a test merely *importing* the module under test does not prove it exercises the role path. My scan reported `TaskCard.tsx` as covered by `Column.test.tsx` on a **filename coincidence**. A guard built on that reports coverage that does not exist, which is worse than no guard, so it is not shipped. A sound alternative — pin the consumer set and make each new file declare its status — was also rejected: a 31-entry status inventory would conflict with every concurrent fleet PR that adds coverage. That is the same churn already removed from the census baseline by dropping its derived aggregates. The attempt is written down so the next person does not repeat it from scratch, and the requirement lives as a review criterion until someone finds a sound signal. ## What it asks for 1. **A flags-supplying case** — if every case omits the new parameter, the conversion is untested in both directions. 2. **Both directions where both are reachable** — renamed lane, and repurposed column. 3. **A non-vacuous companion** — assert what the widened predicate must still *exclude*, or a predicate matching every column satisfies your new cases. (Both `TaskReviewTab` and the dispatch filters needed this.) 4. **Run the revert and record the failure text.** Twice in this program a new case passed with the change reverted: once because the branch was gated behind an unwired handler (`refine` needs `onOpenRefine`), once because the hook was dispatched by trait and the test IR did not declare that trait, so it never ran at all. Cross-linked both ways with the adjacent `store-fake-defects` entry, with the distinction stated so the two are not confused: **there** a fake is missing a method so a branch never runs and production looks wrong; **here** the fake is complete and the test is correct, but a parameter is absent so production takes its documented fallback. ## Verification `pnpm lint` clean · census `--strict` exits 0 (unmoved — this PR changes no code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
11aba0394e |
docs(solutions): converting a column literal to a role makes it async — the four forms that ship green (#2710)
Four review rounds across `TaskCard.tsx` and `TaskDetailModal.tsx` each found a **real defect**. None was in the conversion itself — every one came from the same property change. The fleet has ~600 guards left to convert against the same helpers, so this is written down rather than left in four commit messages. ## The property that changes ```ts task.column === "in-progress" // stable for the lifetime of the render tree isWipColumn // derived from fetched trait flags — CHANGES after first paint ``` Column trait flags arrive from a board-workflows fetch. Until it lands they are `undefined` and every role helper falls back to the legacy id. So a converted role is `false`, then `true`, within one mounted component. ## The four forms | | form | symptom | |---|---|---| | 1 | **stale memo** — deps still keyed only on `task.column` | timers, labels, completion dates frozen at first-paint values (4 instances in TaskCard) | | 2 | **frozen `useState` initializer** | the section does not start collapsed — it *appears later, already collapsed*, on a card nobody touched | | 3 | **eager action on a guess** — effect mutates state before flags resolve | a tab opens and instantly bounces; the correction never lands because the action destroyed the state it would have corrected | | 4 | **stale identity** — flags resolved, but for the *previous* entity | roles resolve from another task's workflow: confidently wrong rather than merely stale | **Form 4 defeats the obvious fix for form 3.** A `metadata === null` guard asks whether data *loaded*, not whether it describes the entity currently open — and it only appears in components that stay **mounted across entity changes**, which is why TaskCard never showed it and the modal did. ## Why a doc rather than four commit messages All four ship **green**: types pass, existing tests pass, and the **default board behaves identically** — because on the default lineage the legacy fallback and the resolved role agree. They diverge only on a **renamed board**, which is precisely the case the conversion exists to support. So the failure mode is: census count reaches zero, everything is green, and the feature is broken exactly where the programme was meant to fix it. A reviewer catching these one at a time is the expensive path, and it has now cost four rounds on two files. Also relevant: **this repo has no `react-hooks/exhaustive-deps` rule**, so form 1 has no automated backstop at all. ## Contents A checklist a converter can run against a component file, and the concrete fix shape for each form — including tagging fetched metadata with the id it describes, and applying that guard to the **role bindings** rather than only the effects (reordering effects fixes the call sites you noticed and leaves the bindings stale for everything else). Follows the convention already established by the engine-side scoping note in `architecture-patterns/fleet-self-healing-cluster-scoping.md`, which records the equivalent hazard for sync workflow reads. ## Verification `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `pnpm lint` clean. Docs-only; no changeset. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bb30d37e59 |
fleet: self-healing.ts 110 → scoped (NOT converted) — sync workflow reads make this cluster unsafe to batch (#2683)
Claiming the largest unclaimed cluster per the work order, then **handing it back sized rather than half-converted.** Docs only; census unchanged (722 / triage 0). ## The cluster `packages/engine/src/self-healing.ts` — **110 guards**, largest single file in the order. ``` by column: in-review 48 · in-progress 20 · done 17 · todo 13 · archived 12 by receiver: column 100 · to 7 · from 3 ``` ## Why the mechanical conversion is unsafe here **The engine has no synchronous way to learn a task's workflow.** `resolveTaskWorkflowIrSync` returns the DEFAULT IR for every task in production — `getTaskWorkflowSelection` returns `undefined` unconditionally (a PG-cutover stub), so the reader always takes its `!workflowId` branch. It is typed non-optional, so **no caller can detect the substitution.** A conversion routed through it: compiles, reads better than the literal, **counts as census progress**, and is wrong for every custom workflow, silently. That is strictly worse than leaving the literal — the literal is at least honest about being one. It is the "guard that cannot fire" pattern wearing better clothes, and the ratchet would score it as a win. The correct form uses `resolveTaskLifecycleColumns(store, taskId)` (async, store-aware), which needs resolved lanes **in scope per method**. Sampled sites (926, 932, 984) do sit in `async` methods so it is reachable — but that is a per-sweep restructuring, not a per-line substitution, and these sweeps iterate task lists, so a naive per-task resolve turns one sweep into N store reads. **In-tree precedent:** `triage.ts` `discoverReadyPlanningTasks` solved this exact problem — store-free `couldBeCandidate` prefilter, bounded (8) concurrent resolve over the survivors, decision stays synchronous over a resolved map. Any batch here should follow that shape per sweep. ## Recommended split, by SWEEP not by column 110 sites cannot honour *"census before/after, baseline shrinks by exactly the converted count"* while also restructuring six-plus sweeps in one PR. 1. **the review/merge sweeps** (`in-review` 48) — largest, and the one where a wrong lane silently changes **merge eligibility**. First and alone. 2. **WIP/rebound sweeps** (`in-progress` 20, `todo` 13). 3. **terminal sweeps** (`done` 17, `archived` 12) — read `complete`/`archived`; most mechanical of the three. 4. **the 10 `from`/`to` sites** — these are MOVE-transition arms, not task-column reads. Different question (*"is this transition into a review lane?"*), so they must not ride along with the `task.column` work. ## Why I am not doing item 1 myself I am near the end of a long session — this is the same context in which I produced a confidently-wrong structural finding earlier today (retracted in #2667, where I trusted a hand-rolled brace counter over a comment in the file). A 48-site restructuring of the merge-eligibility sweeps is exactly the work that should not be done by a worker in that state, and the fleet rules' *flag-and-skip* discipline is the right call over guessing. **What a fresh worker gets from this PR:** the site census, the async-scope survey, the hazard with its root cause, the in-tree pattern to copy, and a four-way split with the risky piece isolated. That is the expensive part of the job already done. ## Fleet rule this cluster proves, worth adding to the brief **Never resolve a workflow synchronously in a converted guard.** Use `resolveWorkflowIrForTaskWithProvenance` (branch on `source`) or `resolveTaskLifecycleColumns`; if neither is reachable at the site, flag and skip. --------- 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> |
||
|
|
9dbc98f1b3 |
Audit: every sync workflow-IR read answers for the DEFAULT workflow (not a PG-only problem) (#2653)
Docs only. This came out of a #2593 review thread that reported the problem as PostgreSQL-specific. **It is unconditional**, and it has consequences well outside the guard I was fixing — including one that looks like a live production break for custom workflows. ## The chain, each link checkable 1. `TaskStore.getTaskWorkflowSelection(taskId)` delegates straight to `getTaskWorkflowSelectionImpl` — **no mode branch** (`store.ts:2545`). 2. `getTaskWorkflowSelectionImpl` **returns `undefined` unconditionally** (`workflow-definitions.ts:505-512`). Its own comment: *"sync selection reader is incomplete-PG; use getTaskWorkflowSelectionAsync."* A PG-cutover stub that never got finished. 3. So `resolveTaskWorkflowIrSyncImpl` always takes its `if (!workflowId)` branch and returns `resolveDefaultWorkflowIr()`. Its `isBuiltinWorkflowId` and `SELECT ir FROM workflows` branches are **unreachable in production**. `resolveTaskWorkflowIrSync` is typed `WorkflowIr`, non-optional — so callers cannot detect the substitution. There is no `undefined` to check and the IR that arrives looks valid. **Why tests don't catch it:** test stores stub `getTaskWorkflowSelection` with a real selection, so the reader works under test and substitutes only in production. Any test written against a stubbed store proves the caller's logic and never the reader's behavior. ## Consequences, severity descending 1. **Custom fields appear to be rejected on custom workflows.** `resolveTaskCustomFieldDefsSyncImpl` returns `ir.fields` — the DEFAULT workflow's. `task-update.ts:128-136` validates against them, and its own comment states the outcome: *"a write against a workflow with no fields (the default) is rejected with a typed CustomFieldRejectionError."* 2. **Per-workflow capacity pools collapse** — `resolveEffectiveWorkflowIdSyncImpl` reads the same selection, so every task resolves to `resolveCapacityPoolId(undefined)`. 3. **Plugin transition hooks re-run against the wrong IR** (`lifecycle-ops.ts:1052`, crash recovery). 4. **Terminal-node detection degrades** to `nodeId === "end"` (`branch-and-pr-entities.ts:578`). 5. **A U7 guard was inert** — fixed in #2593. Its fail-closed arm was `workflowIr ? … : true`, dead code against a non-optional return. **#1 and #2 are REASONED FROM SOURCE, NOT OBSERVED.** I did not execute those paths, and I am labelling them that way in the doc rather than reporting them as confirmed. No test in `packages/core` covers `CustomFieldRejectionError` or `resolveTaskCustomFieldDefsSync` — consistent with the gap, but absence of a test is not proof of a break. **Reproduce before fixing.** I would rather hand you a labelled hypothesis than a confident claim I did not verify. ## Why this matters for the fleet, specifically The census work replaces column literals with trait lookups. A conversion that resolves its traits through a **sync** reader produces a guard that reads the DEFAULT workflow's traits for every task — plausible, wrong, and invisible. **It converts a visible literal into a hidden bug**, and the ratchet counts it as progress. Suggested addition to the fleet brief: conversions must resolve through `resolveWorkflowIrForTaskWithProvenance` and branch on `source`; `resolveTaskWorkflowIrSync` is never acceptable in a converted guard. ## Not fixed here Each consequence needs its sync call path made async — a real slice per site, not an end-of-turn edit. #2593 fixed only the one that was mine. Census unchanged (781 / triage 5); this PR adds and converts no guards. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added an architecture-pattern finding documenting a workflow-reading limitation that can cause synchronous reads to use the default workflow. * Described resulting effects on custom workflow updates, crash recovery, capacity-pool handling, and terminal-node detection. * Documented testing gaps and guidance to avoid synchronous task workflow reads. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2771408bba |
ci: enforce the lifecycle-column ratchet — it has never actually run (#2654)
**The ratchet was advisory.** `scripts/lifecycle-column-census.mjs` existed only as `pnpm census:lifecycle-columns` — without `--strict` — and **no workflow invoked it**. Nothing has ever compared the tree to the baseline. Every "the baseline ratchet holds them" assumption in this program rested on a check that does not run. That explains both classes of hole: **1. Three PRs lowered counts without re-recording,** leaving allowances the deleted guards could return through while every check stayed green. I've tightened them across #2593 and earlier PRs, but nothing stops the next one. **2. #2621 GREW the count while its own title claimed "count 0 → 0".** It added `column === "triage"` and `column === "todo"` at `register-task-workflow-routes.ts:2681`, taking that file to **23 against an allowance of 22**. It landed unchallenged. This is the failure mode the ratchet exists to prevent, and it happened *inside this program*, in a PR that asserted the opposite. ## The change Adds `check:lifecycle-columns` (the census with `--strict`) to the `pr-checks.yml` lint job, next to `check:changesets` and `check:routes-modular` — the established pattern. **~1.8s over ~1950 files**, so this is not a slow-test addition. ## Proven to fail, in both directions A guard that reports success without checking anything is worse than no guard, so: | injected defect | result | |---|---| | `const __probe = (c: string) => c === "triage"` added to `moves.ts` | `count ROSE — moves.ts: 39 -> 40`, exit 1 | | run against main's current baseline | exit 1 on `mission-feature-sync.ts: allows 5, tree has 0` | Both reverted; exit 0 restored. Note the second row: **this check is RED on main right now**, which is the point. ## Merge order **Stacked on #2593**, which carries the `DELIBERATE-LITERAL` marker for the #2621 site (a v1 IR declares no roles, so no trait can answer that question) plus the baseline re-record. Standalone on main this PR is red — correctly. **Merge #2593 first**, then this. I stacked rather than duplicating those two edits because I already caused one conflict today by appending related content from two branches, and #2651 merged a correction ahead of the section it corrected. Same-content edits in two PRs is the same mistake. ## Census Unchanged by this PR: **776 total, triage 5, reviewed 16** — it adds no guards and converts none. It only makes the numbers enforceable. ## For the fleet This should land before the 776-guard fleet launches. The brief says "the baseline ratchet must shrink by exactly the converted count" — until now nothing verified that claim, so a batch worker could report a shrink that did not happen, or grow the count while converting, and CI would agree. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5481c27729 |
docs(solutions): finding 6 — read the implementation before claiming its output is wrong (#2649)
Completes `proving-a-code-path-actually-runs.md` (merged as #2642) with the rule its own author broke three times while writing it. **Docs only.** ## Why this belongs in that document rather than a new one Findings 1-5 are about proving **your own** claim: does this path run, can this test fail, is this negative result observable. Finding 6 is the mirror image — the claims we make against **other people's** work — and it is the same underlying error pointed outward. Splitting them would let a reader take the first five as "be rigorous about my code" and miss that the identical discipline applies when reviewing someone else's. ## The three cases, all mine, all in one day | What I claimed | What was actually true | |---|---| | The census undercounts triage guards, 13 vs 10 | `summarize()` counts `byColumnId` only for `kind === "column"`. My patched counter summed `role`, `status` and `deliberate` too. The three "missing" ones were exactly the ones it classifies correctly — and I reported this against the instrument the program had just adopted as authoritative. | | `resolvePlannerLanesForTask` silently disables two recovery paths for legacy cards — escalated across four messages | The file's own header had already reasoned it through and documented why that answer is correct. And `TaskStore` implements `getTaskWorkflowSelectionAsync`, which the resolver prefers — so real projects never take the path my `{ getTask }`-only probe forced. | | `executor.ts` is clean of triage guards | A receiver-specific grep missed three under `from` and `originColumn`. Same error one step earlier: trusting a reconstruction of the thing instead of the thing. | Every one was: reconstruct behaviour from outside → compare to actual output → find a difference → report a defect, **without reading the implementation.** ## The rules it adds - Read the implementation and its header comment before reporting anything as wrong. On this codebase the reasoning is usually already written down, and the FNXC note frequently answers the exact objection — twice today it answered mine verbatim. - **A fixture is not a measurement of production.** When a probe and the real system disagree, suspect the probe: ask what it had to stub, and whether production ever supplies that shape. - Retract precisely and immediately. A false defect report against shared infrastructure costs more than the bug would have — it sends people to verify something already correct, and spends the credibility needed for the next report that is real. Also updates the count in the intro (five → six) and adds an `applies_when` entry so the doc surfaces for "about to report a tool as defective", which is when it is needed and not when someone is already debugging. `pnpm lint` clean. No changeset — internal documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0c07584d51 |
U11 fallout: disprove the coding-ideas column collapse, and correct a U11 note that recorded the merge backwards (#2651)
Two findings, no behavior change. Both are about **recorded reasoning that was wrong** — the kind that sends the next person the wrong way. ## 1. The coding-ideas column collapse does not work (IR change reverted) I implemented it — deleted `ideas`, moved its `intake`/`autoTriage: false` onto Planning, repointed the `start` anchor, updated the IR suites to the merged shape (they went green, 44/44). Then the wider suites failed and showed why it cannot work. **The manual gate IS the column boundary.** `replan-target.ts` names the discriminator in its own comment: *"The real discriminator is which lane the triage service SCANS, which depends on the intake column's `autoTriage` config."* So `ideas` is unscanned, `todo` is scanned, and "promote" means moving the card from one into the other. Merge them and one column must be both: | if… | consequence | |---|---| | `autoTriage: false` wins | never scanned → nothing is ever planned → the capacity hold releases an **unplanned** card into `in-progress`, violating FN-7648 | | scanning wins | `autoTriage: false` is meaningless → the manual gate is gone → the preset duplicates the default Coding workflow | **8 tests fail, and they are not fixtures** — they encode the promotion flow itself, e.g. `store-create-intake-column.test.ts` › *"promotes an Ideas-parked task to todo without planning it (still bootstrap-stub PROMPT.md)"*. Rewriting them would have meant inventing what "promote" means with no destination column, which is how a broken flow gets blessed by a green suite. **What it would actually take:** a promoted flag the triage scan reads, so one column can hold both "not yet promoted" and "being planned". That is a new lifecycle signal, not a column merge — the same shape as the deferred `needs-replan` follow-up. Happy to scope it. **I also corrected my own earlier checklist** in this doc, which said to delete the now-dead `isUnplannedStartCreate` arm. Wrong: `autoTriage` is a general trait field (`builtin-traits.ts`), so any custom workflow can declare a manual intake with `intake !== hold`. The arm is dead only for this preset. ## 2. `replan-target.ts` recorded the U11 merge backwards The note claimed U11 deletes `todo` and keeps `triage`. It is the reverse — Shape B kept the id `todo` and deleted `triage`, precisely so the ~120 `column === "todo"` guards kept their meaning and no data migration shipped. The default lineage now declares `todo, in-progress, in-review, done, archived`. The lookups are correct today, but **for the opposite reason to the one recorded**: the default lineage falls *through* the `triage` lookup and lands on `todo`, its merged planning column. `triage` still matches the workflows that genuinely declare it (Lead generation, PR review). Also flagged without changing (it would be a behavior change): the `return "triage"` fallbacks on the no-match and throw paths name a column the default lineage no longer declares, so a workflow with neither `triage` nor `todo` gets a nonexistent target. ## Census **Unchanged: 781 total, triage 5.** This PR adds no guards and converts none — `workflowHasColumn(ir, "triage")` is a call argument, not a comparison, so it is outside what the census counts either way. ## Verification 41/41 engine replan-target suites (including the existing `replan-target-merged-planning-column` suite that covers the corrected behavior) · engine typecheck clean · the reverted IR restores the tree to main's content for those three files, verified by `git checkout --`. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7ab6506c0f |
docs(solutions): proving a code path actually runs — the five ways U8 shipped code that never executed (#2642)
Durable write-up of U8's verification findings. **Docs only — no code change, no CI risk beyond lint.** These currently exist only in PR bodies, which nobody greps. `docs/solutions/` is where this project keeps exactly this kind of thing, and every one of the five will recur: the handler-pair shape and the resolved-vs-guessed fork both have more call sites than U8 touched. ## The five 1. **Two prompt-node handlers exist; only one runs.** `createDefaultNodeHandlers` prefers the primitives handler whenever `deps.primitives` is set, and `executeWorkflowGraph` always sets it — so every seam entry in `createAuthoritativeWorkflowSeams` is unreachable for prompt nodes. A lifecycle announcement sat there through two PRs. It type-checked and its unit tests passed, because a seam-level test calls the seam object directly and therefore always can. 2. **A negative instrumentation result is worthless without a control.** No output from an instrumented seam is only evidence once you have shown writes from that module are visible under the harness. One `process.stderr.write` at module load separates "never ran" from "output swallowed" — opposite conclusions. 3. **Source-string ratchets prove syntax, not behavior.** Three were torn down in review. The sharpest guarded a never-executed-code bug with a source search, reproducing the bug one level up; measured, the behavioural version fails an inverted dispatch and the textual one passes it. Includes the sub-rules paid for the hard way: use the AST not regex (a brace in a string truncated an extraction to 13 lines and every count read a *passing* zero), guard the guard, anchor by index rather than a character window. 4. **A green test on first try, on a path with no prior coverage, is a warning.** Two conversions were reverted in one day because their tests passed with the change reverted. Negative assertions succeed trivially when the method returns early — `recoverCompletedTask` has seven guards before the converted line, and the fixture has to satisfy all of them. 5. **A named workflow selection is not a resolved one.** Provenance cannot be inferred from the returned value, because a fallback IR and a valid id-less IR are structurally identical — the resolver that knows has to report it. This is the fork every remaining lifecycle-column conversion hits. ## Why this rather than another conversion Everything left in my area is now owned and further along than I could take it: `executor.ts` → #2628 (which solved the `recoverCompletedTask` fixture I could not), `self-healing.ts` → #2560 (independently hit all three traps I catalogued), the dashboard cluster → #2625/#2626/#2636. Duplicating that would be motion, not progress. Turning findings that cost real cycles into something greppable is the useful thing I can still add. `pnpm lint` clean. No changeset — internal documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added a best-practices guide for verifying that workflow code paths actually execute. * Covers reliable behavioral assertions, instrumentation controls, regression-proof tests, source validation, and detection of fallback behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6d10683dbd |
docs(solutions): store fakes that lie — six fixture defects that each looked like a production bug (#2534)
Six consecutive slices of U7 produced **six test-fixture defects, and every one first presented as a bug in the code under test.** Not one was real. Each cost 15–60 minutes debugging the wrong file. **Two would have shipped a false green** — a test passing while asserting nothing — if the failure had happened to look plausible rather than implausible. This is not a story about carelessness. Every one of these fakes was modelled on an existing fixture in this repo, and the repo's fixtures are inconsistent about exactly the things that matter. ## The catalogue | # | Defect | How it presented | Real cause | |---|---|---|---| | 1 | `moveTaskIf` ignores its predicate | Test passed; in-txn guard untested and indistinguishable from absent | Fake never invoked the callback | | 2 | `updateTaskAtomic: vi.fn()` never invokes its callback | *Every* finalize bailed before the branch under test | Success is derived from whether the callback ran | | 3 | Harness default parameter swallows the input | "Task vanished" case became a duplicate of the control | `harness(undefined)` triggers the default | | 4 | `logEntry: vi.fn()` returns `undefined` | Sweep appeared to match only one column | `.catch` on a non-promise throws, aborting the loop after item one | | 5 | Harness lets `poll()` reach the real `specifyTask` | **exit 1 with every test green** | Real agent path threw *asynchronously*, after assertions passed | | 6 | `updateTask: vi.fn()` returns `undefined` | Branch "did not run" | Same as #4 | **4 and 6 are the same shape, found a week apart, because nothing prevented the second.** That is the argument for writing this down. ## The three rules 1. **Every store method a fake exposes returns what the real one returns** — overwhelmingly a promise. Production writes `await store.m(...).catch(h)` as a fail-soft idiom; `.catch` on `undefined` throws a `TypeError` that unwinds into a broad *"never let housekeeping break the poll"* handler and vanishes. Symptom is never "your fake is wrong" — it is *"the loop only processed the first item"*. 2. **A fake handed a predicate or callback must invoke it.** Ignoring it makes the guarded and unguarded implementations *indistinguishable*, so a test named for the guard cannot detect the guard's removal. Includes the `onLockedRead` hook, without which an in-transaction recheck stays untestable even once the predicate is invoked. 3. **Stub the agent-dispatch boundary.** `poll()` ends in "start an agent", which in a unit test throws *after* the test resolved — `17 passed`, exit code 1, which on CI reads as infrastructure noise. > Never accept a non-zero exit on a green run. It is the only signal that something escaped your assertions entirely. ## Also covered - **How to spot a fixture defect fast** — the tell is *failing for the wrong reason*. Three concrete checks before you open the production file. - **Why differential tests earn their keep** even when they feel redundant: the default-vocabulary half doubles as a fixture self-check, because it asserts behavior that is by definition already shipping. On this program, "both halves failed" was the signal that found three of the six. - **The connection to guards that cannot fire** — six of those on this program too, including a ratchet I wrote that matched only a double-quoted literal (#2527). Same discipline either way: *prove the check fails on the thing it claims to catch before trusting that it passes.* Including the warning that one ratchet injection silently failed to apply, leaving a green run that would have "proven" the ratchet worked. ## The concrete next step, stated plainly A shared `createTaskStoreFake({ tasks, workflowIr })` with promise-resolving, callback-invoking defaults would remove this whole class in one small PR. **It is not built here** because it is cross-unit and needs adopters — building it inside U7 and hoping others find it is how conventions die. The doc says: if you are about to hand-roll a seventh store fake, build the helper instead and link it. Docs-only; no changeset (AGENTS.md excludes internal docs). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added guidance on six store-fake defect patterns that can resemble production bugs during testing. * Documented best practices for creating reliable store fakes, including promise handling, callback invocation, and async dispatch isolation. * Added diagnostic techniques for distinguishing fixture issues from genuine application defects. * Included guidance for validating production guards and links to related documentation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9a11e0b136 |
U2b reproduction: the live move path accepts the column U11 deleted (characterized, not patched) (#2601)
Found while proving U11's caveat 2. **Characterization plus guard-rails
— no production change, deliberately.**
## The defect
A default-workflow card in Planning can be moved **into `triage`** — a
column its workflow no longer declares — re-creating exactly the
stranded state `reconcileUndeclaredTaskColumns` exists to repair.
Measured on a fresh store:
```
experimentalFeatures.workflowColumns null ← no production writer
createTask(...) column = "todo"
moveTask("todo" → "triage") ACCEPTED
moveTask("todo" → "bogus-column") REJECTED: "Valid targets: in-progress, triage, archived"
```
The second rejection is the tell. Validation is real — but it is the
**legacy `VALID_TRANSITIONS`** table talking, and that table does not
know the card's workflow. Its `todo` row still lists `triage`.
## Why the workflow-aware check does not run
`moves.ts` gates its adjacency block — including
`workflowHasColumn(workflowIr, toColumn)` — on
`isWorkflowColumnsCompatibilityFlagEnabled`, which reads the raw
`experimentalFeatures.workflowColumns` key. Nothing writes it, so the
block is dead on the path every real project takes.
**Corollary, already reported:** U11's undeclared-source escape hatch in
`resolveAllowedColumns` also does not run in production. It was added
with #2515 so a stranded card would have a legal move instead of `Valid
targets: none`; on the live path that rescue comes from the legacy table
instead. Mutation-verified — stubbing the hatch back to `[]` leaves the
operator-move test green.
## Why I did not fix it
PR #2499 un-gated the capacity check and **explicitly scoped validation
out**:
> SCOPE, deliberately narrow: only the CAPACITY check is un-gated.
`workflowIr` stays flag-gated so transition VALIDATION keeps its current
behavior — the inline path's bare-Error/"Valid targets:" contract is
unchanged, and none of the Phase A2 divergences are flipped here.
That is a considered decision by the owner of this function, and several
suites pin the contract it protects. Overriding it from outside would
flip an error shape I do not own.
**What has changed since that decision is U11:** the legacy table now
offers a target the default workflow does not declare, which it never
did before. That is new input to the scoping call, not licence to ignore
it — so this lands as a reproduction for U2b rather than a patch.
U2b's branch (`feature/workflow-move-path-convergence`) is stale — HEAD
predates several merged PRs, clean tree — so nothing is being raced.
## What ships
The defect is **characterized, not asserted-as-correct**: the test pins
today's behaviour so it is visible and measurable, and an `it.todo`
states the intended behaviour. Writing it as a passing "refuses" test
would have required the fix; writing it as a failing test would redden
CI; asserting the current behaviour as *correct* would be a lie.
Characterization plus `it.todo` is the honest third option.
Four guard-rails pin what a fix must **not** break:
- every declared lifecycle move (`todo → in-progress → in-review →
done`)
- archiving
- a `recoveryRehome` deliberately reaching an undeclared column — the
path that rescues already-stranded cards, and the one a careless fix
would break
- a premise test asserting the compatibility flag really is unset, so
the suite fails loudly if that ever changes rather than silently testing
a different code path
## Exposure
Narrow but real. U10 already fixed the dashboard move menu to offer only
workflow-declared targets, so the board does not present this. The
**write path** does — REST API, CLI, plugins, any stale client — which
is why the guard belongs in `moves.ts` rather than only in the UI.
5 passed + 1 todo; 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**
* Added coverage for task moves involving workflow-declared and
undeclared columns.
* Documented a known issue where tasks can currently be moved into the
deleted `triage` column.
* Preserved valid moves, archiving, and recovery re-homing behavior.
* **Documentation**
* Added reproduction steps, affected move paths, and guardrails for
addressing the issue.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
30e0a8f291 |
U11 P0 audit: no hard stall in the recovery block — and one obvious fix is wrong (#2570)
Docs only. Answers the P0 question per site: **does it still fire, what silently stops happening, is there a backup?** ## Headline: no hard stall The alarming reading — *"the orphaned-planning-status sweeps stop finding default cards, so a card whose planner died sits with `status:"planning"` forever, invisible to discovery"* — **does not hold.** `triage.ts`'s `sweepStalePlanningStatuses` is the **periodic primary** for that repair and already tests `column !== "triage" && column !== "todo"`. It covers the merged column. The two self-healing sweeps perform the same repair and are **redundant nets**, not the sole rescue. That is the difference between a P0 and a cleanup, and it is only visible by reading the **backup** path rather than the broken guard. Recorded so nobody re-derives the panic. ## Self-healing block, by blast radius | site | fires? | what stops | backup | verdict | |---|---|---|---|---| | `:12106`, `:12427` | no | clearing a stale `planning` status | `triage.sweepStalePlanningStatuses` | redundant net lost — **cleanup** | | `:2961/2981/3016` `recoverAdvancedTriageTasks` | no | re-homing a card with a worktree + durable IR pin to its **pinned** resume column | hold-release still releases it on capacity (real spec ⇒ `isUnplannedForExecution` false) | **degraded, not stuck** — fix first | | `:12254` | no | a bounded priority nudge | none needed; the doc says nudge, not rescue | **low** | | `:12151`, `:9151` | **yes** | — | already OR-paired | **safe** | **Second-order trap at `:3016`.** It skips when `resumeColumn === "triage"`, guarding against resuming a card into the column it already occupies. Post-merge the pinned column is `todo`, which is **not** skipped — so pairing the literal at `:2961` *without* also pairing `:3016` produces a `todo → todo` move. **Repair the three together.** ## Two sites in the ownership split are already handled - **`usage-limit-detector.ts:126`** (assigned to u8) — already fixed in **PR #2567**. Real breakage: the planning lane stopped being recognised, so a card being planned was neither parked when its provider hit a usage limit nor resumed when it recovered. - **`spec-staleness.ts:95`** (assigned to u7) — already proven safe as-is, merged with #2515. **Its obvious fix is wrong.** I tried `|| task.column === "todo"` and it turned an existing test red: it breaks the parked-preserved-progress path. ## The generalisation, which is the most useful thing here **On the merged column, `todo` answers two different questions.** After the merge `todo` is both the planner column *and* the capacity-hold column. So any site that used `triage` to mean *"is being planned"* **cannot simply be paired with `todo`**, because `todo` also means *"is parked waiting for capacity"*. Those sites need **status or a trait**, not a wider literal. That is precisely the mistake a bulk conversion makes, and `spec-staleness.ts` is the worked example: the guard was already asking status, and widening the column would have destroyed the distinction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
67904f8a2c |
U11: merge Todo into Planning on the default lineage (+ the migration mechanism, and a measured safety audit that cuts the work list 32%) (#2515)
**Merges Todo into Planning on the operator's real default workflow.** Held from merge pending the `triage` literal audit below — see *Gating*. ## The board change `builtin:coding` → `BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR` → clones `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`. That IR now declares **five** columns, and `plan`, `plan-review`, `plan-replan` and `start` all live in the merged Planning column: ``` columns: todo="Planning", in-progress, in-review, done, archived start -> todo plan -> todo plan-review -> todo plan-replan -> todo parse -> in-progress (first implementation node) ``` The id stays `todo`, the display name becomes "Planning". That is the cheaper half: `todo` was already the hold column, so every trait lookup, task row, stored selection and the 121 `column === "todo"` guards keep their meaning, and **no stored row needs re-homing**. Promoting `triage` instead would have produced the same board while making those guards workflow-*dependent* — live for Coding (Ideas), silently dead for Coding. `builtin:legacy-coding` keeps its six-column shape, per the operator's decision. It exists to be the old thing. ## Entry contract, before and after each IR edit | | result | |---|---| | before the default-lineage edit | **15 passed** | | after the edit | **13 passed, 2 failed** | | after reading both | **15 passed** | Neither failure was routed around. One was a genuine expectation change (two planning entry points became one); the other was my own `mergeTodoIntoPlanning` helper throwing *"source IR is not the split-column shape this merge transforms"* — because production **is** the merged shape now. I **deleted** the helper rather than making it tolerant: a transform that has silently become a no-op asserts nothing. ## The safety argument, proven not asserted Entering at `start` is exactly what dragged cards backward in the three earlier reverted attempts. `merged-planning-start-node-no-move.test.ts` proves against the **real** boundary controller and **real** default IR that entering `start` performs no move (`moveTask` is never *called*), reaches no hold→wip capacity seam, and **still moves on a genuine crossing** so the no-op is same-column rather than a disabled boundary. Removing the controller's same-column short-circuit turns exactly the two no-move tests red. ## The migration mechanism A card can outlive its column. `resolveAllowedColumns` derives targets from graph adjacency, and an undeclared source has none — so it returned `[]` and **every** move was rejected with "Valid targets: none", including the one that would rescue the card. An undeclared source now resolves to the workflow's rebound target. Escape hatch, not relaxation: declared columns are untouched, and it offers the rebound target *only*, so a stranded card gets back **into** the lifecycle rather than a free jump past review. ## A real regression this surfaced `isDefaultWorkflowColumns` matched the legacy **six** ids as a set. The merged default declares five, so the match stopped firing and the default board fell through to neighbor-only adjacency, which **drops legal moves and invents an illegal one**: | edge | effect | |---|---| | `in-progress → done` | **dropped** — the mission-validation cross edge | | `in-review → todo` | **dropped** — review work back to planning | | `todo/done → archived` | **dropped** — the FN-4892 direct-archival edges | | `done → in-review` | **invented** — a backward edge no rule allows | Adjacency now derives from lifecycle **roles**. The load-bearing assertion: the legacy six still reproduce `VALID_TRANSITIONS` **verbatim**. Applied only when a workflow declares the full role set, so custom boards keep neighbor adjacency. ## Failure accounting (core package, vs a 49-failure baseline) | stage | failed | new | |---|---:|---:| | after the merge | 65 | 18 | | after the escape hatch | 52 | 5 | | after role-derived adjacency | 53 | 4 | The 4 remaining are 3 `builtin-workflows` expectations encoding the pre-merge shape and 1 create-intake expectation naming `triage` on `builtin:coding`. Two `schema-applier` and two `workflow-reconciliation-production-shape` failures appeared in intermediate runs and are **not mine** — both files pass in isolation (75/75 and 7/7). I re-ran each before attributing them, which is why the earlier "priority" flag on the reconciliation pair was withdrawn. Gate: **309/309**. Lint clean. ## Gating: the `triage` audit (`docs/solutions/architecture-patterns/u11-triage-literal-safety-audit.md`) Program tracking cited **58** `triage` comparisons. Measured with the same pattern: | | count | |---|---:| | raw comparisons | 87 | | inside comments | 1 | | **not a lifecycle column at all** | **15** | | column comparisons | 71 | | OR-paired with `"todo"` in the same expression | 32 | | **exclusive `triage` — the real work list** | **39** | **15 do not compare a column.** `role === "triage"`, `surface === "triage"`, `sessionPurpose === "triage"`, `entry.agent === "triage"` name the planning **agent**. Converting them would be actively wrong, and the failure — a planning agent that can't resolve its prompt template — would look nothing like a column bug. **One site changes an operator-visible affordance**, which is why per-site review beat a sweep: `TaskCard.tsx:1927` — `taskColumnFlags?.intake === true && task.column !== "triage"`. The literal is a **narrowing**, not a match. After the merge a Planning card has `intake === true` and `column === "todo"`, so the narrowing stops applying and **Start begins rendering on default Planning cards where it previously did not.** A sweep would have "converted" the literal and shipped the new affordance silently. These guards do not go **dead**, they go **workflow-dependent** — `triage` stays live for legacy-coding, Ideas, every linear built-in and any user workflow (R11) — which is harder to detect than dead. Work list and ownership are in the audit doc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c5d07ab01 |
docs: workflow-owned lifecycle program plan + column-placement contract (#2463)
Docs only — no code. Companion to #2462. ## `docs/solutions/architecture-patterns/workflow-node-column-placement-and-graph-entry-contract.md` Why a workflow node's `column` is a lifecycle contract rather than a display choice: it decides **who can drive the node**, **whether the card holds a WIP slot**, and **whether anything can move it onward**. Contents: - The graph **entry contract** (`resolveColumnResumeNode`, shipped in #2462) with the resume table. - The **plan-in-place chain** — triage → finalize → continuation seed → drain → resume → capacity suspend → release — annotated with the check each link performs. Notably `todo`, not `triage`: an intake column has no releaser, so a card parked there waits for a human. - Why the pre-release gate must be narrow (column match **and** enablement). - The measured failure table from three reverted placement attempts. - Why removing a column is a lifecycle-vocabulary refactor, not a workflow edit: **82 guards** that silently stop matching, **43 writes** to a column that no longer exists, **59 dashboard literals**. A guard that never fires doesn't fail a test — it disables a recovery path. ## `docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md` The program that finishes the job, in four movements: 1. Resolve lifecycle columns from the workflow instead of ~207 string literals. 2. Move every lane — planning, execution, review, merge — behind graph nodes; lane services keep substrate only (storage, leases, timers, supervision, capacity, recovery, audit). 3. A **post-commit event seam**: transitions commit transactionally, *then* emit; subscribers react and may enqueue durable work items, but no subscriber performs a transition. Enforced by test — dropping every subscriber must change no lifecycle outcome. 4. Only then merge Todo into a single Planning column. Phased so each phase lands green independently, with the IR change deliberately **last** (KTD-7). Changing the workflow first makes the suite green over dead guards — that's how the earlier attempts hid their own breakage. The merge lane **adopts** the existing design in `docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md` (slices S02–S08, still `draft-stack-handoff`) rather than authoring a competing one, with a note to re-validate against current `main` since it was drafted seven weeks ago. Scale is stated honestly: ~48k lines across the four lane services, with the executor unit explicitly landing across several commits rather than one sweep. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a516c8b409 |
docs(FN-8600): capture the live-planning-worktree reclaim incident
Documents why self-healing force-removed a worktree a planning session was using and parked the card branch-conflict-unrecoverable: planning gained a task worktree but never took an active-session lease, so the reclaim sweep's liveness guard had nothing to see, and a zero-commit branch classifies as tip-already-merged by construction. Captures the investigation's dead ends too — including reading maxConcurrent from a multi-tenant config table without filtering by project_id, which produced a confidently wrong root cause — and the three ways the first version of the fix was itself wrong. CONCEPTS.md: adds planning to the Active-session lease kinds (the entry had gone stale), states the converse invariant that an unheld path reads as proof nothing is running, and defines Top-level agent slot — the capacity concept whose conflation with the worktree limit derailed the first hour of diagnosis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0c85613313 |
fix(engine): address code-review findings on the planner/worktree recovery fixes
Review of |
||
|
|
3c3acbf769 |
docs(settings): record why global settings are not all in postgres
Global settings are already split three ways -- values in settings.json, the revision journal in postgres, and globalMaxConcurrent/defaultProjectId in central tables -- so the recurring "finish the cutover" proposal keeps getting re-litigated from scratch. Write down the two hard constraints (startup-factory reads embeddedPostgresMaxConnections to start postgres; createFusionAuthStorage is synchronous and host-agnostic), the recovery argument, and the one real motivation for a partial move (multi-node policy consistency), plus the machine-tier vs operator-policy-tier rule for placing new keys. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
faeb491ea4 |
fix(dashboard): harden terminal paste contract and manual-start button per review
Review follow-up to
|
||
|
|
51fc34f585 |
FN-8465: align skill toggles with canonical paths
Align session skill filtering with Skills-view path identities. - Match session inclusions, exclusions, and diagnostics by skills-relative body path. - Ignore stale flat toggle keys for categorized skills and cover the display/session invariant. - Document the behavior and add a patch changeset. Files changed: .../fn-8465-legacy-skill-toggle-path-match.md | 7 ++ docs/dashboard-guide.md | 2 +- .../legacy-flat-skill-toggle-session-divergence.md | 41 ++++++++ .../engine/src/__tests__/skill-resolver.test.ts | 73 ++++++++++++++ packages/engine/src/skill-resolver.ts | 105 ++++++++++++++------- 5 files changed, 191 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-8465 Fusion-Task-Lineage: 042fc7f4-08a3-42f4-9a6f-52f066d585e8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ac52438554 |
FN-8192: prevent mobile confirmation ghost-click dismissal
Keep mobile task-delete confirmations open through delayed compatibility clicks. - Gate backdrop dismissal until the opening gesture settles. - Cover deliberate backdrop dismissal and mobile ghost-click behavior. - Document the regression and add a patch changeset. Files changed: .changeset/fn-8192-mobile-confirm-ghost-click.md | 7 +++ .../confirm-dialog-mobile-ghost-click-dismiss.md | 53 ++++++++++++++++++++++ .../dashboard/app/components/ConfirmDialog.tsx | 23 +++++++++- .../components/__tests__/ConfirmDialog.test.tsx | 34 ++++++++++++-- .../app/hooks/__tests__/useConfirm.test.ts | 32 ++++++++++++- 5 files changed, 143 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8192 Fusion-Task-Lineage: ea08a6c0-0f2e-4ec3-8ad7-23cdc4a5d7f1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
bc7dfe4bbf |
FN-8178: fix task-card context menu autofocus dismissal
Prevent portaled task-card menus from closing when autofocus creates a scroll event. - Focus the first menu action with preventScroll while preserving keyboard access. - Cover all card menu entry points and intentional dismissal paths. - Document the interaction lifecycle fix and publish a patch changeset. Files changed: .changeset/fn-8178-context-menu-flash-dismiss.md | 7 +++ docs/dashboard-guide.md | 2 +- .../task-card-context-menu-flash-dismiss-layers.md | 35 +++++++++++ .../dashboard/app/components/TaskContextMenu.tsx | 9 ++- .../app/components/__tests__/TaskCard.test.tsx | 69 ++++++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8178 Fusion-Task-Lineage: 2eb94ffd-e922-497d-8904-1aea8bf760b1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
d0ce7829c0 |
FN-7953: fix mobile OAuth code submit taps
Submit Anthropic OAuth manual codes on the first mobile tap instead of requiring keyboard dismissal first. - Add a reusable touch action gesture hook that handles touch/pointer activation before synthetic clicks. - Wire the OAuth manual code Submit button to invoke submission on the first touch while preventing duplicate click handling. - Cover the mobile double-tap regression and document the UI bug pattern for future fixes. Files changed: .../oauth-manual-code-mobile-double-tap-submit.md | 60 +++++++++++ .../app/components/OAuthManualCodeForm.tsx | 31 +++++- .../__tests__/OAuthManualCodeForm.test.tsx | 110 +++++++++++++++++++++ .../hooks/__tests__/useTouchActionGesture.test.ts | 110 +++++++++++++++++++++ .../dashboard/app/hooks/useTouchActionGesture.ts | 89 +++++++++++++++++ 5 files changed, 399 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7953 Fusion-Task-Lineage: d387cdbd-25a7-4b7d-add6-27a1ded5cbea Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
725ce45c5d |
FN-7799: fix false-negative Git repository detection blocking task execution
Replace the boolean isGitRepository() check with a tri-state Git detection so environmental git failures (dubious ownership, missing git binary, timeouts) are no longer misreported as "not a Git repository", which previously blocked all task execution in valid repos and survived engine restarts. - Add detectGitRepository() in worktree-pool.ts returning repo / not-repo / error (with reason: dubious-ownership, git-missing, timeout, unknown), classified from git's stderr; bound the git rev-parse call with a 10s timeout and maxBuffer; keep isGitRepository() as a backward-compatible wrapper - Route the executor dispatch preflight guard through detectGitRepository(): only emit the original "not a Git repository / run git init" fatal on a positive not-repo verdict; on error, throw a distinct accurate error naming the real git failure, including the safe.directory remedy for dubious ownership - Route the in-process runtime startup warning through the same tri-state detection so it only warns "not a Git repository" on a positive not-repo verdict - Add a regression test locking extractWorktreeConflictInfo() to NOT misclassify a dubious-ownership git worktree add failure as not-git-repo - Add targeted tests across worktree-pool, executor-worktree, and in-process-runtime test suites covering repo/not-repo/dubious-ownership/git-missing/timeout classifications on Windows OneDrive-style and POSIX paths - Add changeset and a docs/solutions/logic-errors write-up of the false-negative root cause and fix Files changed: .changeset/fn-7799-git-detection-false-negative.md | 7 +++ .../logic-errors/git-detection-false-not-repo.md | 54 ++++++++++++++++ .../engine/src/__tests__/executor-worktree.test.ts | 61 +++++++++++++++++++ .../engine/src/__tests__/worktree-pool.test.ts | 71 +++++++++++++++++++--- packages/engine/src/executor.ts | 38 +++++++++--- .../runtimes/__tests__/in-process-runtime.test.ts | 53 ++++++++++++++-- packages/engine/src/runtimes/in-process-runtime.ts | 16 ++++- packages/engine/src/worktree-pool.ts | 66 ++++++++++++++++++-- 8 files changed, 334 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-7799 Fusion-Task-Lineage: 25a84283-bf47-472b-8a98-a10bf7e494de Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
6a13ad175e |
FN-7732: remove dangling release-authorization UI/i18n/docs scaffolding
Narrative: The triage release-authorization gate itself was already removed in b5b0458; this cleans up the leftover scaffolding it left behind — an unemitted activity type, a dead TaskCard badge/label/CSS, orphaned i18n keys across all 6 locales, and a stale solutions doc — so the codebase no longer references a gate that no longer exists. - Drop the unused `task:release-authorization-required` ActivityEventType and its label/rendering in ActivityFeed.tsx and ActivityLogModal.tsx - Remove the dead `isReleaseAuthorizationHold` badge logic and `.awaiting-release-authorization` CSS class from TaskCard.tsx/TaskCard.css - Simplify TaskDetailModal.tsx comments/logic now that legacy release-authorization holds render as ordinary manual plan-approval holds - Delete orphaned i18n keys `tasks.awaitingReleaseAuthorization` and `taskDetail.plan.releaseAuthorizationHold` across en/es/fr/ko/zh-CN/zh-TW locales and resources.d.ts - Delete the stale docs/solutions/architecture-patterns/release-triage-requires-user-authorization.md doc - Update docs/workflow-steps.md and docs/settings-reference.md to describe the gate as removed (superseded by FN-7732) instead of documenting still-active behavior - Add changeset for @runfusion/fusion (patch/internal) Files changed: .changeset/fn-7732-remove-release-authorization-block.md | 7 +++++ docs/settings-reference.md | 2 +- docs/solutions/architecture-patterns/release-triage-requires-user-authorization.md | 33 ---------------------- docs/workflow-steps.md | 6 ++-- packages/core/src/types.ts | 8 ++++-- packages/dashboard/app/components/ActivityFeed.tsx | 5 ---- packages/dashboard/app/components/ActivityLogModal.tsx | 6 ---- packages/dashboard/app/components/TaskCard.css | 11 -------- packages/dashboard/app/components/TaskCard.tsx | 13 +++------ packages/dashboard/app/components/TaskDetailModal.tsx | 14 +++------ packages/i18n/locales/en/app.json | 3 -- packages/i18n/locales/es/app.json | 5 +--- packages/i18n/locales/fr/app.json | 5 +--- packages/i18n/locales/ko/app.json | 5 +--- packages/i18n/locales/zh-CN/app.json | 5 +--- packages/i18n/locales/zh-TW/app.json | 5 +--- packages/i18n/src/resources.d.ts | 3 -- 17 files changed, 30 insertions(+), 106 deletions(-) Fusion-Task-Id: FN-7732 Fusion-Task-Lineage: d4137bd8-9056-4062-9f2a-c6f5d47295f4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
a24b0fac1a |
FN-7721: cap heartbeat worktree-acquisition retries and record exhaustion failures
Bounds durable-agent heartbeat worktree acquisition to a fixed retry count instead of requeuing to todo indefinitely across heartbeat cycles. - Add MAX_HEARTBEAT_WORKTREE_ACQUISITION_RETRIES (3) in agent-heartbeat.ts, reusing Task.recoveryRetryCount as a cross-heartbeat counter (no schema migration) - On cap exhaustion, terminally mark the task status:"failed" with an explanatory error, log the entry, and reopen to todo with preserveStatus so the failed status isn't wiped by reopen-to-todo semantics - Add onTaskAcquisitionExhausted callback wired in in-process-runtime.ts to CentralCore.recordTaskCompletion(taskId, false) so exhausted acquisitions count toward totalTasksFailed - Add regression tests in agent-heartbeat-worktree.test.ts and in-process-runtime.test.ts covering the retry cap and completion recording - Add changeset (patch) and a docs/solutions/logic-errors writeup documenting the investigation and other worktree-collision sub-gaps found not to reproduce on HEAD Files changed: .changeset/fn-7721-worktree-heartbeat-retry-cap.md | 7 ++ docs/solutions/logic-errors/heartbeat-worktree-acquisition-unbounded-requeue.md | 84 ++++++++++++++++++++++ packages/engine/src/__tests__/agent-heartbeat-worktree.test.ts | 58 +++++++++++++++ packages/engine/src/__tests__/in-process-runtime.test.ts | 11 +++ packages/engine/src/agent-heartbeat.ts | 72 ++++++++++++++++++- packages/engine/src/runtimes/in-process-runtime.ts | 12 ++++ 6 files changed, 242 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7721 Fusion-Task-Lineage: caad671c-f360-4c1c-8aaa-5b48fca5a55b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
0755fc5747 |
fix(FN-7693): mobile terminal blank — exempt xterm measurement from * { max-width: 100% }
Real root cause of the blank mobile terminal (the FN-7692 remeasure guard did not
fix it and is reverted here). styles.css has a mobile-only reset
`@media (max-width: 768px) { * { max-width: 100% } }` to prevent horizontal
overflow. That universal selector also matches xterm's hidden character-measurement
subtree (`.xterm-helpers` / `.xterm-char-measure-element`). That subtree's containing
block (`.xterm-helpers`) is a 0x0 absolutely-positioned box, so `max-width: 100%`
resolves to `max-width: 0` and hard-caps xterm's character-cell measurement at 0.
FitAddon.fit() then proposes 0 columns/rows and `.xterm-screen` (plus the WebGL
canvas) collapses to 0x0 — the prompt streams in and is written into xterm's row DOM
but paints into a zero-size box, so the terminal is blank. Mobile-only, which is why
desktop always rendered fine.
Reproduced live via mobile emulation: `.xterm-char-measure-element` measured 0 while
an identical monospace span in the same container measured ~295px; `max-width: none`
on the measure element restored ~295px, and reopening the terminal with the exemption
active rendered the prompt with `.xterm-screen` sized 369x760. No amount of
remeasure/refit can fix this — the CSS re-caps the measurement to 0 every time — so
the FN-7692 CharSizeService guard is removed.
- Exempt `.xterm-helpers` / `.xterm-char-measure-element` from the mobile max-width
reset in styles.css (covers both TerminalModal and SessionTerminal)
- Revert the ineffective FN-7692 remeasure guard and its tests
- Update changeset (patch) and the docs/solutions write-up to the real root cause
Note: root cause + fix validated in the automation browser via mobile emulation
(393px, iPhone UA, forced touch), not a physical device.
Fusion-Task-Id: FN-7693
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b7b1b71cae |
fix(FN-7692): recover blank mobile terminal when xterm screen collapses to 0x0
The mobile terminal rendered blank even though the WebSocket was Connected and the shell prompt had already streamed in. Root cause (reproduced live): on the mobile fullscreen layout xterm's CharSizeService can measure a 0-width character cell, so FitAddon.fit() proposes 0 columns/rows and .xterm-screen (plus the WebGL canvas) collapses to 0x0 — prompt bytes arrive and are written into xterm's row DOM but paint into a zero-size box. Renderer-independent and mobile-layout- specific; not fixed by resize/font-size re-fits because prior guards only validate the container width and font load, never the resulting measured screen/cell width. Add guardAgainstCollapsedTerminalScreen (app/utils/terminalPreferences.ts) and arm it from both terminal surfaces (TerminalModal + SessionTerminal) right after their initial fit. While the container has a width but .xterm-screen does not, it forces a genuine DOM-strategy remeasure (forceTerminalFontRemeasure) + fit, re-driven by a ResizeObserver until the screen has a real width. It waits (does not give up) while the container is not yet measurable, is bounded so it never spins, and is disposed on every re-init/close path. Recurrence of FN-7620/FN-7686. - Add isTerminalScreenCollapsed + guardAgainstCollapsedTerminalScreen with tests - Wire + dispose the guard across all xterm (re)init/close paths in both surfaces - Add changeset (patch) and a docs/solutions write-up Note: reproduced via mobile emulation (393px, iPhone UA, forced touch), not a physical device; the guard is the structural fix — confirm on a real device. Fusion-Task-Id: FN-7692 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
07507f5264 |
FN-7688: add slow login-shell profile latency hint and docs
Investigated whether --login in TerminalService first-prompt latency is a meaningful contributor and added a one-time diagnostic hint plus documentation of findings. - Add SLOW_LOGIN_PROFILE_HINT_MS (2000ms) threshold and one-time, non-blocking console.info hint in createSession()'s PTY onData handler when a login shell is slow to produce first output - Track spawnStartedAt and loginProfileHintLogged per session, and whether the succeeding spawn attempt used --login, without altering spawn args, timeouts, or the retry-without-login fallback - Add regression tests covering the slow-login-profile hint behavior in terminal-service.test.ts - Document the investigation and findings in docs/solutions/developer-experience/login-shell-profile-latency.md and link it from docs/dashboard-guide.md - Add a patch changeset for @runfusion/fusion describing the new server-log hint Files changed: .changeset/fn-7688-login-shell-profile-latency.md | 7 ++ docs/dashboard-guide.md | 17 +++ .../login-shell-profile-latency.md | 79 ++++++++++++++ .../src/__tests__/terminal-service.test.ts | 121 +++++++++++++++++++++ packages/dashboard/src/terminal-service.ts | 57 ++++++++++ 5 files changed, 281 insertions(+) Fusion-Task-Id: FN-7688 Fusion-Task-Lineage: 08d5dd47-ea9f-4973-9f0e-a8d5fdeae111 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
a832b7979f |
FN-7686: skip redundant session-list round trip on fresh terminal load
Speed up initial terminal load by short-circuiting the no-op server session list call when there are no persisted local tabs to validate. - useTerminalSessions: when readTabsFromStorage returns zero tabs, skip the listTerminalSessions HTTP call entirely and mark bootstrap ready immediately, unblocking auto-create/WebSocket connect instead of serializing behind a provably-discarded round trip - Reload-with-persisted-tabs path is unchanged and still awaits the list call since its result is decision-relevant there - Add regression tests covering the fresh-load fast path and the persisted-tabs path - Add changeset (patch) and a docs/solutions write-up of the bootstrap-list-serialized-before-auto-create issue Files changed: .changeset/fn-7686-slow-terminal-initial-load.md | 7 ++ ...bootstrap-list-serialized-before-auto-create.md | 87 ++++++++++++++++++++++ .../hooks/__tests__/useTerminalSessions.test.ts | 73 ++++++++++++++++++ .../dashboard/app/hooks/useTerminalSessions.ts | 23 +++++- 4 files changed, 189 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7686 Fusion-Task-Lineage: 9c708329-6362-4c2e-967f-aea12849c47c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ca7c987ab5 |
FN-7621: fix mobile terminal shortcut bar horizontal scroll defeated by ancestor touch-action lock
Root-caused and fixed the third recurrence of the mobile terminal shortcut bar not scrolling horizontally: styles.css's mobile lockdown resets touch-action to pan-y across ancestors, and touch-action's used value is the intersection of the touched element's and every ancestor's value, so the leaf .terminal-shortcut-panel's pan-x was silently defeated even though it was already correct. - Opt the terminal overlay and modal ancestors (.modal-overlay.terminal-modal-overlay, .modal.terminal-modal--mobile, plain-media-query mobile modal, and the shortcut/status footer) into touch-action: pan-x pan-y so descendant leaf touch-action values can take effect - Add FNXC:Terminal comments documenting the ancestor-intersection root cause and recurrence history (FN-7550/FN-7560) - Add a documented solution note under docs/solutions/ui-bugs/ for the ancestor-intersection touch-action pattern - Add regression tests asserting the modal/overlay/footer ancestors carry the pan-x pan-y opt-in - Add a changeset for the fix Files changed: .../fn-7621-mobile-terminal-shortcut-scroll.md | 7 ++ ...on-ancestor-intersection-defeats-leaf-scroll.md | 57 +++++++++++ .../dashboard/app/components/TerminalModal.css | 38 ++++++++ .../components/__tests__/TerminalModal.test.tsx | 106 +++++++++++++++++++++ 4 files changed, 208 insertions(+) Fusion-Task-Id: FN-7621 Fusion-Task-Lineage: 771fd79e-e193-43b0-908b-0e8fe2fc2c70 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
8d73b18c35 |
FN-7620: fix mobile terminal rendering blank via ResizeObserver recovery
Fixes the mobile dashboard terminal sometimes rendering completely blank on open by having TerminalModal recover from a zero/collapsed container box. - TerminalModal now attaches a persistent ResizeObserver directly on the xterm container, mirroring SessionTerminal's existing pattern - When the container reports a zero/collapsed box on the first post-open fit, it now re-fits once the real box settles instead of staying stuck at FitAddon's degenerate 2x1-cell floor - Added regression tests covering TerminalModal and SessionTerminal zero-geometry recovery - Documented the root cause and fix in docs/solutions/ui-bugs/mobile-terminal-blank-render-zero-geometry-container.md - Added a patch changeset for @runfusion/fusion Files changed: .changeset/fn-7620-mobile-terminal-blank-render.md | 7 + docs/solutions/ui-bugs/mobile-terminal-blank-render-zero-geometry-container.md | 112 +++++++ packages/dashboard/app/components/TerminalModal.tsx | 49 +++ packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx | 66 ++++ packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 358 +++++++++++++++++++++ 5 files changed, 592 insertions(+) Fusion-Task-Id: FN-7620 Fusion-Task-Lineage: 103f5b17-9a6e-4e9a-ab61-65ccb2203a8d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
e347062e1f |
FN-7603: force xterm DOM-based char measurement to fix mobile terminal spacing
Fixes recurrence #5 of mobile terminal inter-character spacing by unifying xterm's cell-width measurement pipeline with WidthCache's DOM-based glyph measurement, validated against real xterm instead of the jsdom mock. - Add withDomBasedTerminalCharacterMeasurement() in terminalPreferences.ts: transiently hides window.OffscreenCanvas during terminal.open() so CharSizeService's constructor throws and self-selects its own DOM-based fallback strategy, unifying dimensions.css.cell.width with WidthCache.get('W') measurement - Wire withDomBasedTerminalCharacterMeasurement() around terminal.open() calls in SessionTerminal.tsx and TerminalModal.tsx - Add FNXC:Terminal comments documenting the Canvas-vs-DOM measurement divergence root cause, grounded in the installed @xterm/xterm@5.5.0 source - Add docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md recurrence #5 section - Expand TerminalModal.test.tsx coverage for the new measurement-forcing behavior - Add changeset fn-7603-mobile-terminal-spacing.md (patch, fix) Files changed: .changeset/fn-7603-mobile-terminal-spacing.md | 7 + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md | 101 ++++++ packages/dashboard/app/components/SessionTerminal.tsx | 16 +- packages/dashboard/app/components/TerminalModal.tsx | 16 +- packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 363 ++++++++++++++++++++- packages/dashboard/app/utils/terminalPreferences.ts | 63 ++++ 6 files changed, 554 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-7603 Fusion-Task-Lineage: 6c7d980f-953e-4fa9-908e-b24125904cbe Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
7c0be5311a |
FN-7567: re-bake xterm letter-spacing after fit() to fix mobile terminal spacing recurrence
Fixes recurrence #4 of the mobile terminal excess character-spacing bug: SessionTerminal and TerminalModal now force a second genuine xterm font remeasure AFTER fitAddon.fit() settles the post-fit column count, since handleResize() never re-bakes DomRenderer's letter-spacing compensation itself. - SessionTerminal.tsx: call forceTerminalFontRemeasure() again after fit()/sendResizeMessage() in the resize handler, re-baking spacing against the settled (post-fit) column count instead of the stale pre-fit one. - TerminalModal.tsx: same second forceTerminalFontRemeasure() call after fitAddon.fit()/sendResize() in its resize handling path. - Expanded SessionTerminal.test.tsx and TerminalModal.test.tsx coverage to assert the post-fit remeasure occurs. - Added docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md documenting recurrence #4 root cause (DomRenderer._setDefaultSpacing() never recomputes from handleResize()). - Added changeset fn-7567-mobile-terminal-spacing.md (patch, category fix). Files changed: .changeset/fn-7567-mobile-terminal-spacing.md | 7 + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md | 89 ++++++ packages/dashboard/app/components/SessionTerminal.tsx | 23 ++ packages/dashboard/app/components/TerminalModal.tsx | 43 ++- packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx | 127 +++++++- packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 334 ++++++++++++++++++++- 6 files changed, 619 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7567 Fusion-Task-Lineage: 5da20522-82d3-4c4d-9008-db71bc5b4d75 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
9d4a45b0f9 |
FN-7561: fix mobile terminal excess inter-character spacing after font settle
Root-cause fix for xterm text rendering with wide inter-character gaps on mobile after fonts settle post-load. - Added forceTerminalFontRemeasure() in terminalPreferences.ts to work around xterm's OptionsService setter being a no-op when reassigning an already-current fontFamily/fontSize - Applied the remeasure helper at every post-waitForTerminalFontMetrics() settle site in TerminalModal.tsx and SessionTerminal.tsx - Added regression tests covering the remeasure invariant across SessionTerminal, TerminalModal, and terminalPreferences - Documented root cause and fix in docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md - Added changeset for @runfusion/fusion (patch) Files changed: .changeset/fn-7561-mobile-terminal-spacing.md | 7 + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md | 120 ++++++++++ packages/dashboard/app/components/SessionTerminal.tsx | 11 +- packages/dashboard/app/components/TerminalModal.tsx | 11 +- packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx | 108 ++++++++- packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 257 ++++++++++++++++++++- packages/dashboard/app/utils/__tests__/terminalPreferences.test.ts | 55 +++++ packages/dashboard/app/utils/terminalPreferences.ts | 38 +++ 8 files changed, 601 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-7561 Fusion-Task-Lineage: ad9da396-abf4-46ea-8c60-f2ed40fa4b01 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
7300bf54ce |
FN-7460: disable WebKit text scaling in terminals
Disable WebKit terminal text scaling so iPhone Safari preserves exact xterm cell metrics. - Set terminal xterm host subtrees to disable text-size adjustment instead of pinning it at 100%. - Update terminal CSS contract coverage for modal and session terminal surfaces. - Document the real-iPhone recurrence and add a patch changeset for the published CLI package. Files changed: .changeset/fn-7460-ios-terminal-spacing.md | 7 +++++++ .../ui-bugs/xterm-async-font-remeasure-paste-dedupe.md | 7 ++++--- packages/dashboard/app/__tests__/terminal-input.test.ts | 14 +++++++------- packages/dashboard/app/components/SessionTerminal.css | 7 +++++-- packages/dashboard/app/components/TerminalModal.css | 9 ++++++--- .../app/components/__tests__/TerminalModal.test.tsx | 7 +++++++ 6 files changed, 36 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-7460 Fusion-Task-Lineage: 00542017-457b-40b8-9139-084218bc3533 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
8d7abb8c33 |
FN-7447: fix Android terminal mobile viewport sizing
Fix Android keyboard-open terminal layout by sizing mobile shells from the visual viewport. - Track visualViewport width in the terminal modal and mobile keyboard detection. - Apply keyboard-constrained mobile terminal sizing even when CSS media queries see a wider layout viewport. - Cover Android visualViewport width behavior with modal, session terminal, and hook regressions. - Document the fix pattern and add a patch changeset for the published Fusion package. Files changed: .../fn-7447-android-mobile-terminal-spacing.md | 7 ++ ...ndroid-mobile-terminal-visual-viewport-width.md | 26 ++++++++ .../dashboard/app/components/TerminalModal.css | 21 +++++- .../dashboard/app/components/TerminalModal.tsx | 8 +++ .../__tests__/SessionTerminal.mobile.test.tsx | 40 +++++++++++- .../components/__tests__/TerminalModal.test.tsx | 76 ++++++++++++++++++++++ .../app/hooks/__tests__/useMobileKeyboard.test.ts | 38 +++++++++++ packages/dashboard/app/hooks/useMobileKeyboard.ts | 10 ++- 8 files changed, 222 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7447 Fusion-Task-Lineage: c01faf0f-842e-4632-bf8f-ae54070c3bf8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
18b07b5b7d |
FN-7388: Fix folded Android terminal mobile sizing
Fix Android foldable terminal surfaces so initial folded panes use mobile viewport geometry.\n\n- Use touch visualViewport dimensions when classifying terminal mobile mode.\n- Apply mobile TerminalModal shell styles outside media queries for folded Android panes.\n- Cover folded Android viewport behavior in terminal and viewport-mode tests.\n- Document the folded viewport baseline invariant and add a patch changeset.\n\nFiles changed:\n .changeset/fn-7388-android-folded-terminal-spacing.md | 7 +++\n docs/solutions/ui-bugs/mobile-terminal-folded-viewport-baseline.md | 8 ++-\n packages/dashboard/app/components/SessionTerminal.tsx | 18 +++++-\n packages/dashboard/app/components/TerminalModal.css | 37 +++++++++++\n packages/dashboard/app/components/TerminalModal.tsx | 31 ++++++++-\n packages/dashboard/app/components/__tests__/SessionTerminal.mobile.test.tsx | 31 +++++++++\n packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 73 ++++++++++++++++++++++\n packages/dashboard/app/hooks/__tests__/useViewportMode.test.ts | 29 ++++++++-\n packages/dashboard/app/hooks/useViewportMode.ts | 26 +++++++-\n 9 files changed, 251 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-7388 Fusion-Task-Lineage: cdf2ef0d-be1f-4414-9113-17ffebdecde5 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
8f65186bdc |
FN-7289: fix folded-phone terminal keyboard spacing
Preserve mobile terminal geometry when folded phones open with the keyboard already visible. - Use layout viewport height when computing keyboard overlap so focused keyboard-open folded postures keep a valid baseline. - Allow keyboard-closed width/posture changes at short folded heights to refresh the terminal viewport baseline. - Add regression coverage for TerminalModal and SessionTerminal folded-phone keyboard spacing and document the recurrence. Files changed: .../fn-7289-mobile-terminal-folded-keyboard.md | 7 ++ .../mobile-terminal-folded-viewport-baseline.md | 11 +- .../dashboard/app/components/TerminalModal.tsx | 20 ++-- .../__tests__/SessionTerminal.mobile.test.tsx | 30 +++++ .../components/__tests__/TerminalModal.test.tsx | 127 +++++++++++++++++++++ 5 files changed, 183 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-7289 Fusion-Task-Lineage: 82134835-ca72-4796-89da-c8d071efc481 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ac87b1e8e4 |
FN-7281: fix folded terminal viewport baselines
Keep mobile terminals sized against the settled folded viewport before keyboard overlap is applied. - Re-baseline terminal keyboard metrics when folded devices settle to a new closed-posture width. - Avoid replacing the baseline from focused keyboard-open samples that would erase real keyboard overlap. - Cover TerminalModal and SessionTerminal mobile keyboard spacing behavior with regression tests. - Document the folded-phone terminal spacing fix and add a patch changeset. Files changed: .changeset/fn-7281-mobile-terminal-spacing.md | 7 ++ .../mobile-terminal-folded-viewport-baseline.md | 56 +++++++++++++++ .../dashboard/app/components/TerminalModal.tsx | 57 ++++++++++++++-- .../__tests__/SessionTerminal.mobile.test.tsx | 39 +++++++++++ .../components/__tests__/TerminalModal.test.tsx | 79 ++++++++++++++++++++++ .../app/hooks/__tests__/useMobileKeyboard.test.ts | 53 ++++++++++++++- packages/dashboard/app/hooks/useMobileKeyboard.ts | 31 +++++++-- 7 files changed, 309 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7281 Fusion-Task-Lineage: 90ab7398-c583-4312-8cf6-0ef7d6328c8d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
c17d745665 |
FN-7109: notify operators when CLI agents await input
Notify external providers when terminal-backed CLI agents pause for tool permission or user input. - Add a dedicated cli-agent-awaiting-input notification event to settings, schema defaults, dashboard options, and provider tests. - Dispatch CLI-agent waiting-on-input notifications from the in-process runtime with task context and prompt-scoped dedupe keys. - Document the operator behavior and add a published package changeset for the new notification surface. Files changed: .changeset/fn-7109-cli-agent-notification.md | 7 ++ docs/agents.md | 4 + docs/settings-reference.md | 4 +- .../cli-agent-permission-notifications.md | 13 +++ .../core/src/__tests__/global-settings.test.ts | 1 + packages/core/src/settings-schema.ts | 1 + packages/core/src/types.ts | 6 ++ .../SettingsModal.remote-notifications.test.tsx | 1 + .../components/__tests__/settings-mobile.test.tsx | 2 +- .../settings/sections/NotificationsSection.tsx | 3 + .../src/__tests__/in-process-runtime.test.ts | 56 ++++++++++- packages/engine/src/__tests__/notifier.test.ts | 6 +- .../engine/src/__tests__/ntfy-provider.test.ts | 4 + .../engine/src/__tests__/webhook-provider.test.ts | 2 + .../engine/src/cli-agent/__tests__/runtime.test.ts | 39 +++++++- .../__tests__/notification-service.test.ts | 56 ++++++++++- .../src/notification/notification-service.ts | 9 +- packages/engine/src/notification/ntfy-provider.ts | 7 ++ .../engine/src/notification/webhook-provider.ts | 2 + packages/engine/src/notifier.ts | 1 + packages/engine/src/runtimes/in-process-runtime.ts | 111 +++++++++++++++++++++ 21 files changed, 326 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-7109 Fusion-Task-Lineage: 636c1875-92a2-46a7-847e-1f4955c0822f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
04f8fe5d59 |
FN-7107: load CE stage skills for interactive sessions
Ensure Compound Engineering stages request and expose their registered skills when launching interactive sessions. - Add plugin-local skill resolution checks that warn when a stage skill install is missing. - Pass a stable additional skill path and requested stage skill into every CE interactive session. - Add coverage for stage skill loading options and update skill reachability expectations. Files changed: ...d-skills-not-loading-in-interactive-sessions.md | 1 + .../src/__tests__/skill-reachability.test.ts | 71 +++++++++++++--------- .../src/__tests__/stage-skill-loading.test.ts | 69 +++++++++++++++++++++ .../src/session/orchestrator.ts | 48 ++++++++++++++- 4 files changed, 157 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-7107 Fusion-Task-Lineage: 0504e9ba-c2e7-4317-9c90-55693baf8170 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
d9efea9e27 |
docs(workspace): capture Task-field persistence gotcha learning
Adds a docs/solutions learning: a Task field is silently dropped on persist unless it has a SQLite column + defineTaskColumn + rowToTask mapping (applyTaskPatch writes the DB-round-tripped task back over task.json). Plus a CONCEPTS.md entry for the active-session lease (the path-keyed exclusivity/liveness registry whose key choice caused the concurrent-workspace collision). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4faadd4f75 |
Merge remote-tracking branch 'origin/main' into conflict-resolution-1712
# Conflicts: # packages/dashboard/app/components/WorkflowNodeEditor.tsx # packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts |
||
|
|
ec1d29e818 |
FN-6922: guard worktree acquisition returns from repo root
Prevent task worktree acquisition from handing the project root back to executors. - Add a repo-root return guard across resume, pool, and fresh acquisition paths. - Clear invalid repo-root task assignments and create a fresh worktree fallback when safe. - Share repo-root canonicalization and expand regression coverage for acquisition liveness. - Document the acquisition guard and add a patch changeset. Files changed: .changeset/fn-6922-repo-root-acquisition-guard.md | 5 + .../repo-root-task-worktree-requeue-loop.md | 9 +- .../__tests__/executor-worktree-liveness.test.ts | 35 +++ .../worktree-acquisition-secrets-env.test.ts | 9 +- .../src/__tests__/worktree-acquisition.test.ts | 91 ++++++- packages/engine/src/worktree-acquisition.ts | 272 +++++++++++++-------- packages/engine/src/worktree-pool.ts | 8 +- 7 files changed, 304 insertions(+), 125 deletions(-) Fusion-Task-Id: FN-6922 Fusion-Task-Lineage: c16cee1b-de16-4d5f-90e0-132f94fa8377 |
||
|
|
68d3c5820e |
docs(FN-6880): capture optional-group toggle-id collision learning
Document the code-review P1 as a logic-errors learning: a per-task graph toggle (enabledWorkflowSteps, keyed by optional-group node id) collided with the legacy step-template namespace and was silently remapped by the store resolver, bypassing an enabled group. Cross-references the per-task-override blast-radius cousins as the id-namespace-collision variant of that class. Seeds an "Optional step group" entry in CONCEPTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7ee1fec22c |
FN-6861: reject repo-root task worktrees
Prevent stale task metadata from treating the project repository root as a reusable task worktree. - Classify root-equal task worktree paths as repo-root before registered-worktree checks. - Clear stale resumed repo-root assignments so acquisition creates a fresh configured task worktree. - Add structured executor audit metadata and regression coverage for repo-root liveness collisions. - Document the repo-root requeue-loop invariant and recovery behavior. Files changed: docs/architecture.md | 2 +- .../repo-root-task-worktree-requeue-loop.md | 49 ++++++++++++++++ .../__tests__/executor-worktree-liveness.test.ts | 23 +++++++- .../src/__tests__/worktree-acquisition.test.ts | 67 +++++++++++++++++++++- .../src/__tests__/worktree-pool-liveness.test.ts | 25 ++++++++ packages/engine/src/executor.ts | 22 ++++++- packages/engine/src/worktree-acquisition.ts | 4 ++ packages/engine/src/worktree-pool.ts | 13 ++++- 8 files changed, 198 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-6861 Fusion-Task-Lineage: d6a4922e-5a21-4037-b2c2-86ff53d8e9e3 |
||
|
|
7e478fb473 |
Merge pull request #1703 from Runfusion/gsxdsm/workflow-optional-steps
feat(dashboard): workflow optional steps — authoring, full-modal parity, stepwise seam |
||
|
|
cb64e874de |
docs: enrich engine singleton-lock learning after PR #1704 review
- Fix frontmatter enums (root_cause: logic_error, component: tooling); add #1699 to related_prs and last_updated - Reflect post-review behavior: external marker cleared at lock-acquire time; reconcile/startAll/onProjectAccessed swallow EngineAlreadyRunningError - Add rejected approach (don't merge has()/hasRunningEngine) and the keep-them-distinct invariant - Tighten socket-name detail (sha1[:16]); cross-link the browser-testing doc Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7d312b416d |
docs: capture engine singleton-lock learning (EngineAlreadyRunningError != no engine)
Document the false "engine not running" banner root cause and fix as a docs/solutions learning, and add an "Engine Singleton Lock" entry to CONCEPTS.md: a failed per-machine lock acquisition is proof an engine is running elsewhere, not "no engine." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9479f0b313 | docs(solutions): capture eslint exhaustive-deps CI-lint gotcha |