A lane literal the census cannot see — it is a **call argument**, not a
comparison — and the two sibling hooks disagree about how to record the
same kind of fact:
```ts
onTaskMoved: async (task, fromColumn, toColumn, ctx) => {
await store.enqueueSyncAsync({ …, toColumn }); // the real column
},
onTaskCompleted: async (task, ctx) => {
await store.enqueueSyncAsync({ …, toColumn: "done" }); // ← a guess
},
```
On a board whose complete lane is named anything else, the sync-queue
row names a column that board does not have.
## Why fix a field nothing reads
`toColumn` is written to `ce_pipeline_sync_queue` and **never consumed
by any logic** — I checked every reference; it appears only in the
schema, the store's insert, and the row type. It is audit metadata.
That is both why it went unnoticed and why it is worth one token: the
single thing a wrong audit row costs you is the ability to reconstruct
what happened after the fact. A queue that says a card went to `done` on
a board with no `done` is worse than a queue with no column at all,
because it reads as authoritative.
## Structural ratchet, and I would rather say so than imply otherwise
`getCePipelineStore` requires a live PostgreSQL `AsyncDataLayer` and
throws without one, so driving the hook means standing up PG to
re-assert a one-token substitution — against the standing rule on slow
tests. The repo already takes this trade in the same shape
(`packages/core/src/__tests__/analytics-timing-roles-resolved.test.ts`,
whose analytics aggregators have the identical problem).
The test comment states plainly what it does and does not prove: it pins
that the hook reads the card's own column and holds no completion
literal; it does not exercise the write.
Comments are stripped before the negative assertion — the test's own
explanation names the old literal, and a ratchet that matches its own
prose passes forever without checking anything. (That mistake is already
in this program's history, which is why it is guarded here.)
## Revert proof (measured)
Restore `toColumn: "done"` — **both** assertions fail (the positive one
on the missing `task.column`, the negative one on the literal).
## Verification
- `pnpm test:gate` — 161 / 487 / 13 / 71 passed
- `pnpm lint` — clean
- `tsc --noEmit` (`@fusion-plugin-examples/compound-engineering`) —
clean
- full plugin suite — 319 passed across 32 files
🤖 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**
* Sync audit records now capture the task’s actual completion lane
instead of assuming it is named “done.”
* Improved audit accuracy for boards with custom completion lane names.
* **Tests**
* Added coverage to verify completion records use the task’s real
destination lane.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
216 lines
9.5 KiB
TypeScript
216 lines
9.5 KiB
TypeScript
import { definePlugin } from "@fusion/plugin-sdk";
|
|
import { COMPOUND_ENGINEERING_SKILLS } from "./skills.js";
|
|
import { installBundledCeSkills, resolveDefaultInstallTargetRoot } from "./skill-installation.js";
|
|
import {
|
|
installBundledCeAgents,
|
|
resolveDefaultAgentsInstallTargetRoot,
|
|
} from "./agent-installation.js";
|
|
import { ensureCeSchema } from "./schema.js";
|
|
import { createSessionRoutes } from "./routes/session-routes.js";
|
|
import { createArtifactRoutes } from "./routes/artifact-routes.js";
|
|
import { recoverStaleSessionsForContext } from "./session/session-recovery.js";
|
|
import { getCePipelineStore } from "./sync/pipeline-store.js";
|
|
import { reconcileCePipelines } from "./sync/reconciler.js";
|
|
import { settingsSchema } from "./settings.js";
|
|
import { getReconcileOnHooks } from "./settings.js";
|
|
|
|
export { COMPOUND_ENGINEERING_SKILLS } from "./skills.js";
|
|
export {
|
|
installBundledCeSkills,
|
|
resolveBundledSkillsRoot,
|
|
resolveDefaultInstallTargetRoot,
|
|
isPluginLocalPath,
|
|
} from "./skill-installation.js";
|
|
export {
|
|
installBundledCeAgents,
|
|
resolveBundledAgentsRoot,
|
|
resolveDefaultAgentsInstallTargetRoot,
|
|
isPluginLocalAgentsPath,
|
|
} from "./agent-installation.js";
|
|
export { ensureCeSchema } from "./schema.js";
|
|
export { CeSessionStore, getCeSessionStore } from "./session/session-store.js";
|
|
export { CePipelineStore, getCePipelineStore } from "./sync/pipeline-store.js";
|
|
export type {
|
|
CePipelineLink,
|
|
CreateCePipelineLinkInput,
|
|
CePipelineState,
|
|
CePipelineStatus,
|
|
CeSyncQueueEntry,
|
|
CeSyncReason,
|
|
} from "./sync/pipeline-store.js";
|
|
export { CeReconciler, reconcileCePipelines } from "./sync/reconciler.js";
|
|
export type { ReconcileResult } from "./sync/reconciler.js";
|
|
export {
|
|
CeOrchestrator,
|
|
WORK_STAGE_ID,
|
|
CE_PLUGIN_ID,
|
|
CE_WORK_SOURCE_TYPE,
|
|
} from "./session/orchestrator.js";
|
|
export { getStage, listPipelineStages, listStages, registerStage } from "./session/stage-registry.js";
|
|
export {
|
|
settingsSchema,
|
|
getDefaultProvider,
|
|
getDefaultModelId,
|
|
getDisabledStages,
|
|
getEnabledStages,
|
|
getReconcileOnHooks,
|
|
getReconcileIntervalMinutes,
|
|
} from "./settings.js";
|
|
|
|
const plugin = definePlugin({
|
|
manifest: {
|
|
id: "fusion-plugin-compound-engineering",
|
|
name: "Compound Engineering",
|
|
version: "0.1.0",
|
|
description: "A dedicated dashboard surface for compound-engineering artifacts and interactive ce-* sessions.",
|
|
author: "Fusion Team",
|
|
fusionVersion: ">=0.1.0",
|
|
skills: COMPOUND_ENGINEERING_SKILLS.map((s) => ({ skillId: s.skillId, name: s.name })),
|
|
settingsSchema,
|
|
},
|
|
state: "installed",
|
|
skills: COMPOUND_ENGINEERING_SKILLS,
|
|
hooks: {
|
|
// Idempotent DDL for plugin-local CE tables; runtime handlers use the host PostgreSQL layer.
|
|
onSchemaInit: ensureCeSchema,
|
|
// INBOUND board→pipeline sync (U8 / FN-5719). The 5s hook budget
|
|
// (plugin-runner invokeHookSafe) means these MUST be fast: resolve the link,
|
|
// ENQUEUE a sync signal, and return. Heavy advancement (board reads, outbound
|
|
// task creation) happens in the reconciler, NOT inline here. A reconcile
|
|
// drain is fired-and-forgotten (never awaited) so a slow sweep cannot blow
|
|
// the hook budget; correctness does not depend on it firing because the next
|
|
// reconcile() sweep re-derives the transition from board truth.
|
|
onTaskMoved: async (task, fromColumn, toColumn, ctx) => {
|
|
const store = getCePipelineStore(ctx);
|
|
// Use the async sibling so backend (PG) mode queries Drizzle instead of
|
|
// throwing on the null sync Database. In SQLite mode it falls back to the
|
|
// sync path internally.
|
|
const link = await store.findByTaskIdAsync(task.id);
|
|
if (!link) return; // not a CE-linked task → ignore fast.
|
|
await store.enqueueSyncAsync({
|
|
cePipelineId: link.cePipelineId,
|
|
taskId: task.id,
|
|
reason: "task_moved",
|
|
fromColumn,
|
|
toColumn,
|
|
});
|
|
// Setting-gated auto-drain (U9): when disabled, the enqueue still happens
|
|
// so an on-demand reconcile (route/refresh) converges later; we just skip
|
|
// the inline sweep.
|
|
if (!getReconcileOnHooks(ctx.settings)) return;
|
|
void reconcileCePipelines(ctx)
|
|
.catch((err) => ctx.logger.warn(`CE reconcile (onTaskMoved) failed: ${String(err)}`));
|
|
},
|
|
onTaskCompleted: async (task, ctx) => {
|
|
const store = getCePipelineStore(ctx);
|
|
const link = await store.findByTaskIdAsync(task.id);
|
|
if (!link) return;
|
|
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-30-16:05:
|
|
Record the lane the card ACTUALLY reached, not the built-in board's name for "finished".
|
|
|
|
`onTaskMoved` two hooks up already records the real `toColumn` it is handed; this one wrote the
|
|
literal `"done"`, so on a board whose complete lane is named anything else the sync-queue row
|
|
claimed a column that board does not have. Nothing reads `toColumn` — it is audit metadata, not
|
|
a control signal, which is precisely why it went unnoticed and precisely why it matters: the
|
|
one thing a wrong audit row costs you is the ability to reconstruct what happened.
|
|
|
|
`task.column` is the completed card's own lane, so the two hooks now record the same kind of
|
|
fact in the same way.
|
|
*/
|
|
await store.enqueueSyncAsync({
|
|
cePipelineId: link.cePipelineId,
|
|
taskId: task.id,
|
|
reason: "task_completed",
|
|
toColumn: task.column,
|
|
});
|
|
if (!getReconcileOnHooks(ctx.settings)) return;
|
|
void reconcileCePipelines(ctx)
|
|
.catch((err) => ctx.logger.warn(`CE reconcile (onTaskCompleted) failed: ${String(err)}`));
|
|
},
|
|
// Install the bundled, pinned ce-* SKILL.md files into a plugin-local,
|
|
// discoverable directory on load. The engine ingests
|
|
// PluginSkillContribution only as a name; physical discovery requires the
|
|
// files to exist on a path it scans (U2 finding). Install is provenance-aware:
|
|
// current plugin-local copies are skipped, stale/unmarked copies are refreshed,
|
|
// and global ~/.claude/skills is never touched.
|
|
onLoad: async (ctx) => {
|
|
try {
|
|
const { targetRoot, results } = installBundledCeSkills();
|
|
const installed = results.filter((r) => r.outcome === "installed").length;
|
|
const refreshed = results.filter((r) => r.outcome === "refreshed").length;
|
|
const errored = results.filter((r) => r.outcome === "error");
|
|
if (errored.length > 0) {
|
|
ctx.logger.warn(
|
|
`Compound Engineering: ${errored.length} skill(s) failed to install: ${errored
|
|
.map((e) => `${e.skillId} (${e.reason})`)
|
|
.join(", ")}`,
|
|
);
|
|
}
|
|
ctx.logger.info(
|
|
`Compound Engineering skills ready — installed=${installed} refreshed=${refreshed} target=${targetRoot}`,
|
|
);
|
|
ctx.emitEvent("compound-engineering:skills-installed", { targetRoot, results });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
ctx.logger.error(`Compound Engineering skill install failed: ${message}`);
|
|
}
|
|
|
|
// Install the bundled ce-* persona definitions (same posture as skills:
|
|
// pinned, plugin-local, provenance-refreshed, never a global ~/.claude/agents).
|
|
// The CE skills read these and pass them to fn_spawn_agent.systemPromptOverride.
|
|
try {
|
|
const { targetRoot, results } = installBundledCeAgents();
|
|
const installed = results.filter((r) => r.outcome === "installed").length;
|
|
const refreshed = results.filter((r) => r.outcome === "refreshed").length;
|
|
const errored = results.filter((r) => r.outcome === "error");
|
|
if (errored.length > 0) {
|
|
ctx.logger.warn(
|
|
`Compound Engineering: ${errored.length} agent def(s) failed to install: ${errored
|
|
.map((e) => `${e.agentId} (${e.reason})`)
|
|
.join(", ")}`,
|
|
);
|
|
}
|
|
ctx.logger.info(
|
|
`Compound Engineering agent personas ready — installed=${installed} refreshed=${refreshed} target=${targetRoot}`,
|
|
);
|
|
ctx.emitEvent("compound-engineering:agents-installed", { targetRoot, results });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
ctx.logger.error(`Compound Engineering agent install failed: ${message}`);
|
|
}
|
|
|
|
await recoverStaleSessionsForContext(ctx, { reason: "load", force: true, emitEvent: true });
|
|
},
|
|
},
|
|
// Expose the plugin-local ce-* persona-definition directory to executor /
|
|
// workflow-step sessions via FUSION_CE_AGENTS_DIR. The CE skills read a persona
|
|
// def from here and pass its body to fn_spawn_agent's systemPromptOverride —
|
|
// the lightweight subagent path (no plugin agent-contribution channel exists).
|
|
// Defs are installed in onLoad.
|
|
executorRuntimeEnv: () => ({
|
|
env: {
|
|
FUSION_CE_AGENTS_DIR: resolveDefaultAgentsInstallTargetRoot(),
|
|
// Step sessions run with cwd=projectRoot, so a CE skill can't reach its
|
|
// bundled scripts via a relative `scripts/...` path. Expose the installed
|
|
// skills root so skill docs can invoke scripts by absolute path
|
|
// (`"$FUSION_CE_SKILLS_DIR/<skill-id>/scripts/<name>"`).
|
|
FUSION_CE_SKILLS_DIR: resolveDefaultInstallTargetRoot(),
|
|
},
|
|
description: "compound-engineering ce-* persona definitions and bundled skills directories",
|
|
}),
|
|
routes: [...createSessionRoutes(), ...createArtifactRoutes()],
|
|
dashboardViews: [
|
|
{
|
|
viewId: "compound-engineering",
|
|
label: "Compound Engineering",
|
|
componentPath: "./dashboard-view",
|
|
icon: "Boxes",
|
|
placement: "primary",
|
|
order: 36,
|
|
},
|
|
],
|
|
});
|
|
|
|
export default plugin;
|