@runfusion/fusion 0.38.1 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/bin.mjs +20 -0
  2. package/dist/automation-store-EKXXEE2S.js +10 -0
  3. package/dist/automation-store-PLNG3X72.js +10 -0
  4. package/dist/automation-store-RDDYZFUJ.js +10 -0
  5. package/dist/bin.js +72539 -40364
  6. package/dist/chunk-2JI3F6MG.js +948 -0
  7. package/dist/chunk-4L6CLWJC.js +8345 -0
  8. package/dist/chunk-4VNS5WPM.js +42 -0
  9. package/dist/chunk-5RGECGQL.js +1139 -0
  10. package/dist/chunk-6JDLN7IL.js +409 -0
  11. package/dist/chunk-AICJATVW.js +948 -0
  12. package/dist/chunk-AWKNRUOR.js +1148 -0
  13. package/dist/chunk-BHM3UYWS.js +4651 -0
  14. package/dist/chunk-BMUXDSBF.js +4647 -0
  15. package/dist/chunk-GETECT6V.js +353 -0
  16. package/dist/chunk-GUV4QQT3.js +1143 -0
  17. package/dist/chunk-JKQO7YYZ.js +8369 -0
  18. package/dist/chunk-LYZVU6UV.js +409 -0
  19. package/dist/chunk-MAN4WYHO.js +353 -0
  20. package/dist/chunk-MCUG2Z7M.js +4651 -0
  21. package/dist/chunk-PSDYB45U.js +948 -0
  22. package/dist/chunk-RR2HM46R.js +8345 -0
  23. package/dist/chunk-RXMQ5OMY.js +9226 -0
  24. package/dist/chunk-S2EVJUY3.js +353 -0
  25. package/dist/chunk-UXR5XZFZ.js +409 -0
  26. package/dist/client/.vite/manifest.json +457 -155
  27. package/dist/client/assets/AgentDetailView-BrETpTDj.js +18 -0
  28. package/dist/client/assets/AgentPermissionPolicyEditor-BP6d1LFG.js +6 -0
  29. package/dist/client/assets/AgentsView-COx3draV.js +29 -0
  30. package/dist/client/assets/{AgentsView-BI_CESkd.css → AgentsView-DfznJDek.css} +1 -1
  31. package/dist/client/assets/ChatView-CrVSqQK_.js +6 -0
  32. package/dist/client/assets/DevServerView-DXQ1LX4H.js +1 -0
  33. package/dist/client/assets/DirectoryPicker-B_8ICSPl.js +1 -0
  34. package/dist/client/assets/{DirectoryPicker-DzKVmxOf.css → DirectoryPicker-DTKtd8L2.css} +1 -1
  35. package/dist/client/assets/DocumentsView-DmNB9rJZ.js +1 -0
  36. package/dist/client/assets/DocumentsView-WfVoTZt8.css +1 -0
  37. package/dist/client/assets/EvalsView-72w3_AO6.js +1 -0
  38. package/dist/client/assets/{ExperimentalAgentOnboardingModal-lXM--LV-.js → ExperimentalAgentOnboardingModal-Be4oaohM.js} +41 -41
  39. package/dist/client/assets/GoalsView-DI66UsjT.css +1 -0
  40. package/dist/client/assets/GoalsView-DkEXO_mx.js +2 -0
  41. package/dist/client/assets/InsightsView-_-wd4BOJ.js +11 -0
  42. package/dist/client/assets/MemoryView-BOCIc8HT.js +2 -0
  43. package/dist/client/assets/NodesView-7VInxyf_.css +1 -0
  44. package/dist/client/assets/NodesView-DTQP1j4H.js +14 -0
  45. package/dist/client/assets/PiExtensionsManager-BqaI9GKX.js +11 -0
  46. package/dist/client/assets/PluginManager-DYaVwsXd.js +1 -0
  47. package/dist/client/assets/{PluginManager-BJQXdUmV.css → PluginManager-UHVLvWWl.css} +1 -1
  48. package/dist/client/assets/PullRequestView-Dk9a7jRZ.js +6 -0
  49. package/dist/client/assets/PullRequestView-WQIS0zUC.css +1 -0
  50. package/dist/client/assets/ReliabilityView-B-i5SOoD.js +1 -0
  51. package/dist/client/assets/ReliabilityView-eoKBhBc4.css +1 -0
  52. package/dist/client/assets/ResearchView-C2Gx8jC9.js +1 -0
  53. package/dist/client/assets/SecretsView-Db2HAGi2.js +1 -0
  54. package/dist/client/assets/SessionTerminal-BiigPP2l.css +1 -0
  55. package/dist/client/assets/SessionTerminal-DwlNZLlr.js +2 -0
  56. package/dist/client/assets/{SettingsModal-Cy42vswI.js → SettingsModal-B-d30TVy.js} +1 -1
  57. package/dist/client/assets/SettingsModal-Dje7N0hv.js +31 -0
  58. package/dist/client/assets/SettingsModal-bJvc9aHl.css +1 -0
  59. package/dist/client/assets/SettingsModal-nF_GaeeK.css +1 -0
  60. package/dist/client/assets/SetupWizardModal-CIqFbkrD.css +1 -0
  61. package/dist/client/assets/SetupWizardModal-Cw3RdN83.js +1 -0
  62. package/dist/client/assets/SkillsView-3bhGyWs1.css +1 -0
  63. package/dist/client/assets/SkillsView-BGyynaI4.js +1 -0
  64. package/dist/client/assets/StashRecoveryView-CFaU-hym.js +1 -0
  65. package/dist/client/assets/{TodoView-Cx9cVhq7.css → TodoView-7xIh-Fmi.css} +1 -1
  66. package/dist/client/assets/TodoView-Djjwcb49.js +6 -0
  67. package/dist/client/assets/WorkflowNodeEditor-Po5uv_dN.css +1 -0
  68. package/dist/client/assets/WorkflowNodeEditor-vnXnKvAr.js +8 -0
  69. package/dist/client/assets/{addon-fit-C7jRorKK.js → addon-fit-B4jkcKk4.js} +1 -1
  70. package/dist/client/assets/addon-unicode11-Cdne2Sj4.js +1 -0
  71. package/dist/client/assets/{addon-web-links-CkJwd4cQ.js → addon-web-links-CRNLzj7m.js} +1 -1
  72. package/dist/client/assets/{addon-webgl-BjvOw43o.js → addon-webgl-CeCSULF1.js} +1 -1
  73. package/dist/client/assets/app-CTRT_hQh.js +13 -0
  74. package/dist/client/assets/app-CVg6W-HU.js +13 -0
  75. package/dist/client/assets/app-CY3pQXOr.js +13 -0
  76. package/dist/client/assets/app-DNh04p5I.js +13 -0
  77. package/dist/client/assets/app-DhHgtB9j.js +13 -0
  78. package/dist/client/assets/app-NL_hPGDS.js +13 -0
  79. package/dist/client/assets/common-BRyX4VxB.js +1 -0
  80. package/dist/client/assets/common-CN9QPI2d.js +1 -0
  81. package/dist/client/assets/common-CNbjPELi.js +1 -0
  82. package/dist/client/assets/common-DFDqu_n0.js +1 -0
  83. package/dist/client/assets/common-DgWWKG8Z.js +1 -0
  84. package/dist/client/assets/common-X6PzBnVA.js +1 -0
  85. package/dist/client/assets/{createLucideIcon-BazL2hk5.js → createLucideIcon-BYvOGRiO.js} +2 -2
  86. package/dist/client/assets/dashboard-view-BgiLoG1G.css +1 -0
  87. package/dist/client/assets/{dashboard-view-DqWnXEbq.css → dashboard-view-CuUPQZzi.css} +1 -1
  88. package/dist/client/assets/dashboard-view-DOEa6gWS.js +1 -0
  89. package/dist/client/assets/dashboard-view-DlKsVySD.js +3 -0
  90. package/dist/client/assets/dashboard-view-O5j3PSGY.js +8101 -0
  91. package/dist/client/assets/errors-07IrmOYM.js +1 -0
  92. package/dist/client/assets/errors-BPsCufin.js +1 -0
  93. package/dist/client/assets/errors-CcVDgqlA.js +1 -0
  94. package/dist/client/assets/errors-D-i_Bxc6.js +1 -0
  95. package/dist/client/assets/errors-DDdWAK-U.js +1 -0
  96. package/dist/client/assets/errors-DOF5Td3F.js +1 -0
  97. package/dist/client/assets/{folder-open-Doam5g-y.js → folder-open-BtEAEp7E.js} +1 -1
  98. package/dist/client/assets/index-BHGM5W8a.css +1 -0
  99. package/dist/client/assets/index-CW4OzecJ.js +793 -0
  100. package/dist/client/assets/{upload-BUfrZl69.js → upload-Y9NiLQ9o.js} +1 -1
  101. package/dist/client/assets/{users-XNGnYBJb.js → users-B13HD4Tp.js} +1 -1
  102. package/dist/client/assets/vendor-i18n-CV0mwX_-.js +9 -0
  103. package/dist/client/assets/{vendor-react-K0fH_qHe.js → vendor-react-D37HfzIf.js} +1 -1
  104. package/dist/client/assets/vendor-reactflow-C5ap-Sga.css +1 -0
  105. package/dist/client/assets/vendor-reactflow-CRZcPCXF.js +15 -0
  106. package/dist/client/assets/{vendor-xterm-DzcZoU0P.js → vendor-xterm-tcedd2Jt.js} +1 -1
  107. package/dist/client/assets/x-CqNX_9E2.js +61 -0
  108. package/dist/client/assets/zoom-out-CSTHbJgD.js +21 -0
  109. package/dist/client/index.html +7 -4
  110. package/dist/client/sw.js +36 -1
  111. package/dist/client/version.json +1 -1
  112. package/dist/droid-cli/package.json +2 -2
  113. package/dist/extension.js +67616 -44149
  114. package/dist/extract-zip-WL55Z3AU.js +2497 -0
  115. package/dist/index.min-CHQ3ABYJ.js +2926 -0
  116. package/dist/memory-dreams-7USISKVE.js +32 -0
  117. package/dist/memory-dreams-PBPISUDD.js +32 -0
  118. package/dist/memory-dreams-XNO2B2QF.js +32 -0
  119. package/dist/migration-EH2YOCYQ.js +15 -0
  120. package/dist/migration-PLGTTKL5.js +15 -0
  121. package/dist/migration-WZEZK22R.js +15 -0
  122. package/dist/pi-claude-cli/package.json +2 -2
  123. package/dist/plugin-sdk/index.d.ts +97 -0
  124. package/dist/plugin-sdk/index.js +55 -0
  125. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +17286 -0
  126. package/dist/plugins/fusion-plugin-acp-runtime/manifest.json +13 -0
  127. package/dist/plugins/fusion-plugin-acp-runtime/package.json +11 -0
  128. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +2 -3
  129. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-interop.d.ts +19 -0
  130. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +0 -3
  131. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +1984 -0
  132. package/dist/plugins/fusion-plugin-compound-engineering/manifest.json +56 -0
  133. package/dist/plugins/fusion-plugin-compound-engineering/package.json +11 -0
  134. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +5 -0
  135. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  136. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +5 -0
  137. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  138. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +43 -4
  139. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  140. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +5 -0
  141. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  142. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +5 -0
  143. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  144. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +5 -0
  145. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  146. package/dist/plugins/fusion-plugin-reports/package.json +2 -2
  147. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +3 -3
  148. package/dist/plugins/fusion-plugin-reports/src/index.ts +0 -1
  149. package/dist/plugins/fusion-plugin-roadmap/bundled.js +5 -0
  150. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  151. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +2 -2
  152. package/dist/routine-store-GIJ6MXWY.js +10 -0
  153. package/dist/routine-store-HTVC2PUQ.js +10 -0
  154. package/dist/routine-store-KGHZBPDF.js +10 -0
  155. package/package.json +24 -10
  156. package/skill/fusion/SKILL.md +1 -1
  157. package/skill/fusion/references/engine-tools.md +79 -3
  158. package/skill/fusion/references/extension-tools.md +79 -4
  159. package/skill/fusion/references/fusion-capabilities.md +12 -4
  160. package/dist/client/assets/AgentDetailView-DuRYmHK3.js +0 -18
  161. package/dist/client/assets/AgentPermissionPolicyEditor-ClY32kYN.js +0 -6
  162. package/dist/client/assets/AgentsView-UbCAHs_i.js +0 -29
  163. package/dist/client/assets/ChatView-DDDjYRKc.js +0 -6
  164. package/dist/client/assets/DevServerView-DY5GlJcD.js +0 -1
  165. package/dist/client/assets/DirectoryPicker-B4VRL2BH.js +0 -1
  166. package/dist/client/assets/DocumentsView-gv4zG3aT.css +0 -1
  167. package/dist/client/assets/DocumentsView-tqpIZ9t9.js +0 -1
  168. package/dist/client/assets/EvalsView-C_Br3ry8.js +0 -1
  169. package/dist/client/assets/GoalsView-B65urz9k.css +0 -1
  170. package/dist/client/assets/GoalsView-CI3Bzs1b.js +0 -1
  171. package/dist/client/assets/InsightsView-BYfsVobi.js +0 -11
  172. package/dist/client/assets/MemoryView-DT4vIhaW.js +0 -2
  173. package/dist/client/assets/NodesView-DT4pXowv.css +0 -1
  174. package/dist/client/assets/NodesView-Dx6HFqjI.js +0 -14
  175. package/dist/client/assets/PiExtensionsManager-BcGBnNiC.js +0 -6
  176. package/dist/client/assets/PluginManager-T_XZ7iNI.js +0 -1
  177. package/dist/client/assets/ReliabilityView-C1C1j1SD.js +0 -1
  178. package/dist/client/assets/ReliabilityView-D9AsVv5l.css +0 -1
  179. package/dist/client/assets/ResearchView-Bv4igpnv.js +0 -1
  180. package/dist/client/assets/SecretsView-C4VX2ZTa.js +0 -1
  181. package/dist/client/assets/SettingsModal-CLEkhLK_.js +0 -31
  182. package/dist/client/assets/SettingsModal-DZrJ5PFH.css +0 -1
  183. package/dist/client/assets/SettingsModal-D_uIvf6n.css +0 -1
  184. package/dist/client/assets/SetupWizardModal-CGYGKurR.css +0 -1
  185. package/dist/client/assets/SetupWizardModal-LqloAL4J.js +0 -1
  186. package/dist/client/assets/SkillsView-CUbSBeZ2.css +0 -1
  187. package/dist/client/assets/SkillsView-I1SDKaRz.js +0 -1
  188. package/dist/client/assets/StashRecoveryView-DMUUR3cy.js +0 -1
  189. package/dist/client/assets/TodoView-PKvJKjtV.js +0 -6
  190. package/dist/client/assets/dashboard-view-BWGH_fAq.js +0 -63
  191. package/dist/client/assets/dashboard-view-D4XVI1XP.js +0 -21
  192. package/dist/client/assets/index-BS-jSWPW.js +0 -742
  193. package/dist/client/assets/index-ktpD_W3I.css +0 -1
@@ -0,0 +1,1984 @@
1
+ // ../plugin-sdk/src/index.ts
2
+ import {
3
+ WORKFLOW_EXTENSION_SCHEMA_VERSION,
4
+ workflowExtensionRegistryId,
5
+ createBoardActionServices
6
+ } from "@fusion/core";
7
+ function definePlugin(plugin2) {
8
+ return plugin2;
9
+ }
10
+
11
+ // ../../plugins/fusion-plugin-compound-engineering/src/skills.ts
12
+ var COMPOUND_ENGINEERING_SKILLS = [
13
+ {
14
+ skillId: "ce-strategy",
15
+ name: "ce-strategy",
16
+ description: "Create or maintain STRATEGY.md \u2014 the product's target problem, approach, users, key metrics, and tracks of work.",
17
+ skillFiles: ["skills/ce-strategy/SKILL.md"],
18
+ enabled: true,
19
+ triggerPatterns: ["strategy", "roadmap", "what are we working on", "set up the strategy doc"]
20
+ },
21
+ {
22
+ skillId: "ce-ideate",
23
+ name: "ce-ideate",
24
+ description: "Generate and critically evaluate grounded ideas about a topic before committing to one direction.",
25
+ skillFiles: ["skills/ce-ideate/SKILL.md"],
26
+ enabled: true,
27
+ triggerPatterns: ["ideate", "give me ideas", "what should I improve", "surprise me"]
28
+ },
29
+ {
30
+ skillId: "ce-brainstorm",
31
+ name: "ce-brainstorm",
32
+ description: "Explore requirements and approaches through collaborative dialogue, then write a right-sized requirements document.",
33
+ skillFiles: ["skills/ce-brainstorm/SKILL.md"],
34
+ enabled: true,
35
+ triggerPatterns: ["brainstorm", "what should we build", "help me think through"]
36
+ },
37
+ {
38
+ skillId: "ce-plan",
39
+ name: "ce-plan",
40
+ description: "Create structured plans for multi-step tasks and optionally deepen existing plans via sub-agent review.",
41
+ skillFiles: ["skills/ce-plan/SKILL.md"],
42
+ enabled: true,
43
+ triggerPatterns: ["plan this", "create a plan", "break this down", "deepen the plan"]
44
+ },
45
+ {
46
+ skillId: "ce-work",
47
+ name: "ce-work",
48
+ description: "Execute work efficiently while maintaining quality and finishing features.",
49
+ skillFiles: ["skills/ce-work/SKILL.md"],
50
+ enabled: true,
51
+ triggerPatterns: ["do the work", "implement", "execute the plan", "finish this feature"]
52
+ },
53
+ {
54
+ skillId: "ce-code-review",
55
+ name: "ce-code-review",
56
+ description: "Structured code review using tiered persona agents, confidence-gated findings, and a merge/dedup pipeline.",
57
+ skillFiles: ["skills/ce-code-review/SKILL.md"],
58
+ enabled: true,
59
+ triggerPatterns: ["code review", "review this change", "review before PR"]
60
+ },
61
+ {
62
+ skillId: "ce-compound",
63
+ name: "ce-compound",
64
+ description: "Document a recently solved problem to compound the team's knowledge or the project's shared CONCEPTS.md vocabulary.",
65
+ skillFiles: ["skills/ce-compound/SKILL.md"],
66
+ enabled: true,
67
+ triggerPatterns: ["compound this", "document this learning", "capture this solution"]
68
+ }
69
+ ];
70
+
71
+ // ../../plugins/fusion-plugin-compound-engineering/src/skill-installation.ts
72
+ import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
73
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
74
+ import { fileURLToPath } from "node:url";
75
+ function resolveBundledSkillsRoot() {
76
+ const here = fileURLToPath(import.meta.url);
77
+ const dir = dirname(here);
78
+ const local = resolve(dir, "skills");
79
+ if (existsSync(local)) return local;
80
+ return resolve(dir, "..", "src", "skills");
81
+ }
82
+ function resolveDefaultInstallTargetRoot() {
83
+ const here = fileURLToPath(import.meta.url);
84
+ return resolve(dirname(here), "..", ".fusion-ce-skills");
85
+ }
86
+ var GLOBAL_SKILL_DIR_PATTERN = /[\\/]\.(claude|codex|gemini)[\\/]skills([\\/]|$)/;
87
+ function assertPluginLocalTarget(targetRoot) {
88
+ const normalized = resolve(targetRoot);
89
+ if (GLOBAL_SKILL_DIR_PATTERN.test(normalized + sep)) {
90
+ throw new Error(
91
+ `Refusing to install Compound Engineering skills into a global client skills directory: ${normalized}. Install target MUST be plugin-local (never <home>/.claude|.codex|.gemini/skills).`
92
+ );
93
+ }
94
+ }
95
+ function assertValidSkillSource(skillId, sourceDir) {
96
+ if (!existsSync(sourceDir)) {
97
+ throw new Error(`Bundled skill source directory missing for '${skillId}': ${sourceDir}`);
98
+ }
99
+ const skillMd = join(sourceDir, "SKILL.md");
100
+ if (!existsSync(skillMd)) {
101
+ throw new Error(`Bundled skill '${skillId}' has no SKILL.md at ${skillMd}`);
102
+ }
103
+ const content = readFileSync(skillMd, "utf-8");
104
+ if (!/^---[\s\S]*?\bname\s*:\s*\S/m.test(content)) {
105
+ throw new Error(
106
+ `Bundled skill '${skillId}' SKILL.md at ${skillMd} is missing a frontmatter 'name:' field`
107
+ );
108
+ }
109
+ }
110
+ function installBundledCeSkills(options = {}) {
111
+ const targetRoot = options.targetRoot ? resolve(options.targetRoot) : resolveDefaultInstallTargetRoot();
112
+ assertPluginLocalTarget(targetRoot);
113
+ const sourceRoot = options.sourceRoot ? resolve(options.sourceRoot) : resolveBundledSkillsRoot();
114
+ const results = COMPOUND_ENGINEERING_SKILLS.map((skill) => {
115
+ const sourceDir = join(sourceRoot, skill.skillId);
116
+ const targetDir = join(targetRoot, skill.skillId);
117
+ try {
118
+ assertValidSkillSource(skill.skillId, sourceDir);
119
+ if (existsSync(targetDir)) {
120
+ return { skillId: skill.skillId, sourceDir, targetDir, outcome: "skipped", reason: "existing install preserved" };
121
+ }
122
+ mkdirSync(targetRoot, { recursive: true });
123
+ cpSync(sourceDir, targetDir, { recursive: true });
124
+ return { skillId: skill.skillId, sourceDir, targetDir, outcome: "installed" };
125
+ } catch (error) {
126
+ return {
127
+ skillId: skill.skillId,
128
+ sourceDir,
129
+ targetDir,
130
+ outcome: "error",
131
+ reason: error instanceof Error ? error.message : String(error)
132
+ };
133
+ }
134
+ });
135
+ return { targetRoot, results };
136
+ }
137
+ function isPluginLocalPath(p) {
138
+ return isAbsolute(p) && !GLOBAL_SKILL_DIR_PATTERN.test(resolve(p) + sep);
139
+ }
140
+
141
+ // ../../plugins/fusion-plugin-compound-engineering/src/schema.ts
142
+ function ensureCeSchema(db) {
143
+ db.exec(`
144
+ CREATE TABLE IF NOT EXISTS ce_sessions (
145
+ id TEXT PRIMARY KEY,
146
+ stage TEXT NOT NULL,
147
+ status TEXT NOT NULL CHECK (status IN (
148
+ 'launching','active','awaiting_input','completed','error','interrupted'
149
+ )),
150
+ currentQuestion TEXT,
151
+ conversationHistory TEXT NOT NULL DEFAULT '[]',
152
+ projectId TEXT,
153
+ artifactPath TEXT,
154
+ error TEXT,
155
+ turnIntervalMs INTEGER NOT NULL DEFAULT 120000,
156
+ lastActivityAt INTEGER NOT NULL,
157
+ createdAt TEXT NOT NULL,
158
+ updatedAt TEXT NOT NULL
159
+ );
160
+
161
+ CREATE INDEX IF NOT EXISTS idxCeSessionsStatusUpdated
162
+ ON ce_sessions(status, updatedAt DESC, id);
163
+
164
+ CREATE INDEX IF NOT EXISTS idxCeSessionsStageCreated
165
+ ON ce_sessions(stage, createdAt DESC, id);
166
+
167
+ CREATE INDEX IF NOT EXISTS idxCeSessionsProject
168
+ ON ce_sessions(projectId, updatedAt DESC, id);
169
+
170
+ CREATE TABLE IF NOT EXISTS ce_pipeline_links (
171
+ id TEXT PRIMARY KEY,
172
+ taskId TEXT NOT NULL,
173
+ cePipelineId TEXT NOT NULL,
174
+ ceStageId TEXT NOT NULL,
175
+ ceArtifactPath TEXT,
176
+ createdAt TEXT NOT NULL
177
+ );
178
+
179
+ CREATE INDEX IF NOT EXISTS idxCePipelineLinksPipeline
180
+ ON ce_pipeline_links(cePipelineId, createdAt DESC, id);
181
+
182
+ CREATE UNIQUE INDEX IF NOT EXISTS idxCePipelineLinksTask
183
+ ON ce_pipeline_links(taskId);
184
+
185
+ CREATE TABLE IF NOT EXISTS ce_pipeline_state (
186
+ cePipelineId TEXT PRIMARY KEY,
187
+ currentStage TEXT NOT NULL,
188
+ status TEXT NOT NULL CHECK (status IN (
189
+ 'running','advancing','awaiting_board','completed'
190
+ )),
191
+ lastArtifactPath TEXT,
192
+ createdAt TEXT NOT NULL,
193
+ updatedAt TEXT NOT NULL
194
+ );
195
+
196
+ CREATE INDEX IF NOT EXISTS idxCePipelineStateStatus
197
+ ON ce_pipeline_state(status, updatedAt DESC, cePipelineId);
198
+
199
+ CREATE TABLE IF NOT EXISTS ce_pipeline_sync_queue (
200
+ id TEXT PRIMARY KEY,
201
+ cePipelineId TEXT NOT NULL,
202
+ taskId TEXT NOT NULL,
203
+ reason TEXT NOT NULL,
204
+ fromColumn TEXT,
205
+ toColumn TEXT,
206
+ enqueuedAt TEXT NOT NULL,
207
+ processedAt TEXT
208
+ );
209
+
210
+ CREATE INDEX IF NOT EXISTS idxCePipelineSyncQueuePending
211
+ ON ce_pipeline_sync_queue(processedAt, enqueuedAt, id);
212
+
213
+ CREATE INDEX IF NOT EXISTS idxCePipelineSyncQueuePipeline
214
+ ON ce_pipeline_sync_queue(cePipelineId, enqueuedAt, id);
215
+ `);
216
+ }
217
+
218
+ // ../../plugins/fusion-plugin-compound-engineering/src/session/orchestrator.ts
219
+ import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
220
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2 } from "node:path";
221
+
222
+ // ../../plugins/fusion-plugin-compound-engineering/src/sync/pipeline-store.ts
223
+ import { randomUUID } from "node:crypto";
224
+ function rowToState(row) {
225
+ return {
226
+ cePipelineId: row.cePipelineId,
227
+ currentStage: row.currentStage,
228
+ status: row.status,
229
+ lastArtifactPath: row.lastArtifactPath,
230
+ createdAt: row.createdAt,
231
+ updatedAt: row.updatedAt
232
+ };
233
+ }
234
+ function rowToQueueEntry(row) {
235
+ return {
236
+ id: row.id,
237
+ cePipelineId: row.cePipelineId,
238
+ taskId: row.taskId,
239
+ reason: row.reason,
240
+ fromColumn: row.fromColumn,
241
+ toColumn: row.toColumn,
242
+ enqueuedAt: row.enqueuedAt,
243
+ processedAt: row.processedAt
244
+ };
245
+ }
246
+ function rowToLink(row) {
247
+ return {
248
+ id: row.id,
249
+ taskId: row.taskId,
250
+ cePipelineId: row.cePipelineId,
251
+ ceStageId: row.ceStageId,
252
+ ceArtifactPath: row.ceArtifactPath,
253
+ createdAt: row.createdAt
254
+ };
255
+ }
256
+ var CePipelineStore = class {
257
+ db;
258
+ constructor(db) {
259
+ this.db = db;
260
+ ensureCeSchema(db);
261
+ }
262
+ /** Record a task→pipeline/artifact link. */
263
+ createLink(input) {
264
+ const link = {
265
+ id: input.id ?? randomUUID(),
266
+ taskId: input.taskId,
267
+ cePipelineId: input.cePipelineId,
268
+ ceStageId: input.ceStageId,
269
+ ceArtifactPath: input.ceArtifactPath ?? null,
270
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
271
+ };
272
+ this.db.prepare(
273
+ `INSERT INTO ce_pipeline_links
274
+ (id, taskId, cePipelineId, ceStageId, ceArtifactPath, createdAt)
275
+ VALUES (?, ?, ?, ?, ?, ?)`
276
+ ).run(link.id, link.taskId, link.cePipelineId, link.ceStageId, link.ceArtifactPath, link.createdAt);
277
+ return link;
278
+ }
279
+ /** All links produced by a given CE pipeline, newest first. */
280
+ listByPipeline(cePipelineId) {
281
+ const rows = this.db.prepare(`SELECT * FROM ce_pipeline_links WHERE cePipelineId = ? ORDER BY createdAt DESC, id`).all(cePipelineId);
282
+ return rows.map(rowToLink);
283
+ }
284
+ /** Resolve a board task back to its CE link (the back-reference). */
285
+ findByTaskId(taskId) {
286
+ const row = this.db.prepare(`SELECT * FROM ce_pipeline_links WHERE taskId = ?`).get(taskId);
287
+ return row ? rowToLink(row) : void 0;
288
+ }
289
+ // ── CE-pipeline STATE machine (U8) ───────────────────────────────────
290
+ // Separate from board-task columns: this table is the pipeline's OWN state.
291
+ /** Read a pipeline's own state record. */
292
+ getState(cePipelineId) {
293
+ const row = this.db.prepare(`SELECT * FROM ce_pipeline_state WHERE cePipelineId = ?`).get(cePipelineId);
294
+ return row ? rowToState(row) : void 0;
295
+ }
296
+ /** All pipeline state records (the reconciler sweeps every one). */
297
+ listAllState() {
298
+ const rows = this.db.prepare(`SELECT * FROM ce_pipeline_state ORDER BY updatedAt DESC, cePipelineId`).all();
299
+ return rows.map(rowToState);
300
+ }
301
+ /**
302
+ * Create or update a pipeline's state. Idempotent on `cePipelineId`. `status`
303
+ * defaults to `running` on first write and is preserved on update unless given.
304
+ */
305
+ upsertState(input) {
306
+ const now = (/* @__PURE__ */ new Date()).toISOString();
307
+ const existing = this.getState(input.cePipelineId);
308
+ const status = input.status ?? existing?.status ?? "running";
309
+ const lastArtifactPath = input.lastArtifactPath !== void 0 ? input.lastArtifactPath : existing?.lastArtifactPath ?? null;
310
+ if (existing) {
311
+ this.db.prepare(
312
+ `UPDATE ce_pipeline_state
313
+ SET currentStage = ?, status = ?, lastArtifactPath = ?, updatedAt = ?
314
+ WHERE cePipelineId = ?`
315
+ ).run(input.currentStage, status, lastArtifactPath, now, input.cePipelineId);
316
+ } else {
317
+ this.db.prepare(
318
+ `INSERT INTO ce_pipeline_state
319
+ (cePipelineId, currentStage, status, lastArtifactPath, createdAt, updatedAt)
320
+ VALUES (?, ?, ?, ?, ?, ?)`
321
+ ).run(input.cePipelineId, input.currentStage, status, lastArtifactPath, now, now);
322
+ }
323
+ return this.getState(input.cePipelineId);
324
+ }
325
+ /**
326
+ * Transition a pipeline to a new stage/status. Returns the updated state, or
327
+ * `undefined` if the pipeline has no state row yet (caller should seed first).
328
+ */
329
+ transitionState(cePipelineId, next) {
330
+ const existing = this.getState(cePipelineId);
331
+ if (!existing) return void 0;
332
+ return this.upsertState({
333
+ cePipelineId,
334
+ currentStage: next.currentStage ?? existing.currentStage,
335
+ status: next.status ?? existing.status,
336
+ lastArtifactPath: next.lastArtifactPath !== void 0 ? next.lastArtifactPath : existing.lastArtifactPath
337
+ });
338
+ }
339
+ // ── Event-enqueue seam (U8 / FN-5719) ────────────────────────────────
340
+ // Hooks write here FAST and return; the reconciler drains. A missed enqueue is
341
+ // still recovered because reconcile() re-derives from board state too.
342
+ /** Append a pending board→pipeline sync signal. Fast, append-only. */
343
+ enqueueSync(input) {
344
+ const entry = {
345
+ id: input.id ?? randomUUID(),
346
+ cePipelineId: input.cePipelineId,
347
+ taskId: input.taskId,
348
+ reason: input.reason,
349
+ fromColumn: input.fromColumn ?? null,
350
+ toColumn: input.toColumn ?? null,
351
+ enqueuedAt: (/* @__PURE__ */ new Date()).toISOString(),
352
+ processedAt: null
353
+ };
354
+ this.db.prepare(
355
+ `INSERT INTO ce_pipeline_sync_queue
356
+ (id, cePipelineId, taskId, reason, fromColumn, toColumn, enqueuedAt, processedAt)
357
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`
358
+ ).run(entry.id, entry.cePipelineId, entry.taskId, entry.reason, entry.fromColumn, entry.toColumn, entry.enqueuedAt);
359
+ return entry;
360
+ }
361
+ /** All pending (un-drained) queue entries, oldest first. */
362
+ listPendingSync() {
363
+ const rows = this.db.prepare(`SELECT * FROM ce_pipeline_sync_queue WHERE processedAt IS NULL ORDER BY enqueuedAt, id`).all();
364
+ return rows.map(rowToQueueEntry);
365
+ }
366
+ /** Mark a queue entry drained (idempotent). */
367
+ markSyncProcessed(id) {
368
+ this.db.prepare(`UPDATE ce_pipeline_sync_queue SET processedAt = ? WHERE id = ? AND processedAt IS NULL`).run((/* @__PURE__ */ new Date()).toISOString(), id);
369
+ }
370
+ };
371
+ var storeCache = /* @__PURE__ */ new WeakMap();
372
+ function getCePipelineStore(ctx) {
373
+ const key = ctx.taskStore;
374
+ const cached = storeCache.get(key);
375
+ if (cached) return cached;
376
+ const store = new CePipelineStore(ctx.taskStore.getDatabase());
377
+ storeCache.set(key, store);
378
+ return store;
379
+ }
380
+
381
+ // ../../plugins/fusion-plugin-compound-engineering/src/sync/ce-task.ts
382
+ var CE_PLUGIN_ID = "fusion-plugin-compound-engineering";
383
+ var CE_WORK_SOURCE_TYPE = "workflow_step";
384
+ async function createCeTaskWithLink(taskStore, pipelineStore, spec) {
385
+ const task = await taskStore.createTask({
386
+ title: spec.title,
387
+ description: spec.description,
388
+ column: spec.column,
389
+ source: {
390
+ sourceType: CE_WORK_SOURCE_TYPE,
391
+ sourceSessionId: spec.cePipelineId,
392
+ sourceMetadata: {
393
+ pluginId: CE_PLUGIN_ID,
394
+ cePipelineId: spec.cePipelineId,
395
+ ceStageId: spec.ceStageId,
396
+ ceArtifactPath: spec.ceArtifactPath
397
+ }
398
+ }
399
+ });
400
+ pipelineStore.createLink({
401
+ taskId: task.id,
402
+ cePipelineId: spec.cePipelineId,
403
+ ceStageId: spec.ceStageId,
404
+ ceArtifactPath: spec.ceArtifactPath
405
+ });
406
+ return task;
407
+ }
408
+
409
+ // ../../plugins/fusion-plugin-compound-engineering/src/session/stage-registry.ts
410
+ var STAGE_DEFINITIONS = [
411
+ {
412
+ stageId: "strategy",
413
+ order: 100,
414
+ skillId: "ce-strategy",
415
+ artifactLocation: "STRATEGY.md",
416
+ icon: "Compass",
417
+ label: "Strategy",
418
+ artifactGlob: "STRATEGY.md"
419
+ },
420
+ {
421
+ stageId: "ideate",
422
+ order: 200,
423
+ skillId: "ce-ideate",
424
+ artifactLocation: "docs/ideation/",
425
+ icon: "Lightbulb",
426
+ label: "Ideate",
427
+ artifactGlob: "docs/ideation/**/*.md"
428
+ },
429
+ {
430
+ stageId: "brainstorm",
431
+ order: 300,
432
+ skillId: "ce-brainstorm",
433
+ artifactLocation: "docs/brainstorms/",
434
+ icon: "Sparkles",
435
+ label: "Brainstorm",
436
+ artifactGlob: "docs/brainstorms/**/*.md"
437
+ },
438
+ {
439
+ stageId: "plan",
440
+ order: 400,
441
+ skillId: "ce-plan",
442
+ artifactLocation: "docs/plans/",
443
+ icon: "ListChecks",
444
+ label: "Plan",
445
+ artifactGlob: "docs/plans/**/*.md"
446
+ },
447
+ {
448
+ // The work stage (U7). Its `ce-work` skill drives execution and, on
449
+ // `complete`, carries a derived task list that the orchestrator lands on the
450
+ // board (tagged CE-originated + recorded as pipeline links). The artifact is
451
+ // the work log / summary for this stage.
452
+ stageId: "work",
453
+ order: 500,
454
+ skillId: "ce-work",
455
+ artifactLocation: "docs/work/",
456
+ icon: "Hammer",
457
+ label: "Work",
458
+ artifactGlob: "docs/work/**/*.md"
459
+ }
460
+ ];
461
+ var REGISTRY = new Map(STAGE_DEFINITIONS.map((s) => [s.stageId, s]));
462
+ function getStage(stageId) {
463
+ return REGISTRY.get(stageId);
464
+ }
465
+ function listStages() {
466
+ return [...REGISTRY.values()].sort((a, b) => a.order - b.order || a.stageId.localeCompare(b.stageId));
467
+ }
468
+ function registerStage(def) {
469
+ REGISTRY.set(def.stageId, def);
470
+ }
471
+
472
+ // ../../plugins/fusion-plugin-compound-engineering/src/settings.ts
473
+ var DEFAULT_PROVIDER = "";
474
+ var DEFAULT_MODEL_ID = "";
475
+ var DEFAULT_ENABLED_STAGES = listStages().map((s) => s.stageId);
476
+ var DEFAULT_RECONCILE_ON_HOOKS = true;
477
+ var DEFAULT_RECONCILE_INTERVAL_MINUTES = 15;
478
+ var settingsSchema = {
479
+ defaultProvider: {
480
+ type: "string",
481
+ label: "Default Session Provider",
482
+ description: "Model provider used for CE interactive sessions (for example anthropic). Leave blank to use the host default.",
483
+ group: "Sessions",
484
+ defaultValue: DEFAULT_PROVIDER
485
+ },
486
+ defaultModelId: {
487
+ type: "string",
488
+ label: "Default Session Model",
489
+ description: "Model ID within the provider used for CE interactive sessions. Leave blank to use the host default.",
490
+ group: "Sessions",
491
+ defaultValue: DEFAULT_MODEL_ID
492
+ },
493
+ enabledStages: {
494
+ type: "array",
495
+ itemType: "string",
496
+ label: "Enabled Stages",
497
+ description: "Stage IDs that may be launched from the Compound Engineering view (for example strategy, ideate, brainstorm, plan, work).",
498
+ group: "Sessions",
499
+ defaultValue: DEFAULT_ENABLED_STAGES
500
+ },
501
+ reconcileOnHooks: {
502
+ type: "boolean",
503
+ label: "Reconcile on Board Changes",
504
+ description: "Run the board\u2192pipeline reconcile sweep automatically after task move/complete hooks. Disable to only reconcile on demand.",
505
+ group: "Sync",
506
+ defaultValue: DEFAULT_RECONCILE_ON_HOOKS
507
+ },
508
+ reconcileIntervalMinutes: {
509
+ type: "number",
510
+ label: "Reconcile Cadence (minutes)",
511
+ description: "Cadence hint for how often an on-demand refresh surface sweeps the reconciler. Not a continuous poll loop.",
512
+ group: "Sync",
513
+ defaultValue: DEFAULT_RECONCILE_INTERVAL_MINUTES
514
+ }
515
+ };
516
+ function asString(settings, key) {
517
+ const value = settings[key];
518
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
519
+ }
520
+ function asBoolean(settings, key, fallback) {
521
+ const value = settings[key];
522
+ return typeof value === "boolean" ? value : fallback;
523
+ }
524
+ function asNumber(settings, key, fallback) {
525
+ const value = settings[key];
526
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
527
+ }
528
+ function asStringArray(settings, key, fallback) {
529
+ const value = settings[key];
530
+ if (!Array.isArray(value)) return [...fallback];
531
+ const normalized = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
532
+ return normalized.length > 0 ? normalized : [...fallback];
533
+ }
534
+ function getDefaultProvider(settings) {
535
+ return asString(settings, "defaultProvider");
536
+ }
537
+ function getDefaultModelId(settings) {
538
+ return asString(settings, "defaultModelId");
539
+ }
540
+ function getEnabledStages(settings) {
541
+ return asStringArray(settings, "enabledStages", listStages().map((s) => s.stageId));
542
+ }
543
+ function getReconcileOnHooks(settings) {
544
+ return asBoolean(settings, "reconcileOnHooks", DEFAULT_RECONCILE_ON_HOOKS);
545
+ }
546
+ function getReconcileIntervalMinutes(settings) {
547
+ return Math.max(1, Math.floor(asNumber(settings, "reconcileIntervalMinutes", DEFAULT_RECONCILE_INTERVAL_MINUTES)));
548
+ }
549
+
550
+ // ../../plugins/fusion-plugin-compound-engineering/src/session/session-store.ts
551
+ import { randomUUID as randomUUID2 } from "node:crypto";
552
+ var CE_SESSION_STATUSES = [
553
+ "launching",
554
+ "active",
555
+ "awaiting_input",
556
+ "completed",
557
+ "error",
558
+ "interrupted"
559
+ ];
560
+ function asCeSessionStatus(value) {
561
+ return value && CE_SESSION_STATUSES.includes(value) ? value : void 0;
562
+ }
563
+ var STALE_INTERVAL_MULTIPLE = 3;
564
+ var DEFAULT_TURN_INTERVAL_MS = 12e4;
565
+ function safeParse(raw, fallback, isValid) {
566
+ if (!raw) return fallback;
567
+ try {
568
+ const parsed = JSON.parse(raw);
569
+ return isValid(parsed) ? parsed : fallback;
570
+ } catch {
571
+ return fallback;
572
+ }
573
+ }
574
+ function isConversationHistory(value) {
575
+ return Array.isArray(value) && value.every((turn) => {
576
+ if (typeof turn !== "object" || turn === null) return false;
577
+ const t = turn;
578
+ return (t.role === "user" || t.role === "agent") && typeof t.text === "string" && typeof t.at === "string";
579
+ });
580
+ }
581
+ function isPlanningQuestionOrNull(value) {
582
+ if (value === null) return true;
583
+ if (typeof value !== "object") return false;
584
+ const q = value;
585
+ return typeof q.id === "string" && typeof q.type === "string" && typeof q.question === "string";
586
+ }
587
+ function rowToSession(row) {
588
+ return {
589
+ id: row.id,
590
+ stage: row.stage,
591
+ status: row.status,
592
+ currentQuestion: safeParse(row.currentQuestion, null, isPlanningQuestionOrNull),
593
+ conversationHistory: safeParse(row.conversationHistory, [], isConversationHistory),
594
+ projectId: row.projectId,
595
+ artifactPath: row.artifactPath,
596
+ error: row.error,
597
+ turnIntervalMs: row.turnIntervalMs,
598
+ lastActivityAt: row.lastActivityAt,
599
+ createdAt: row.createdAt,
600
+ updatedAt: row.updatedAt
601
+ };
602
+ }
603
+ var CeSessionStore = class {
604
+ db;
605
+ constructor(db) {
606
+ this.db = db;
607
+ ensureCeSchema(db);
608
+ }
609
+ create(input) {
610
+ const now = (/* @__PURE__ */ new Date()).toISOString();
611
+ const session = {
612
+ id: input.id ?? randomUUID2(),
613
+ stage: input.stage,
614
+ status: "launching",
615
+ currentQuestion: null,
616
+ conversationHistory: [],
617
+ projectId: input.projectId ?? null,
618
+ artifactPath: input.artifactPath ?? null,
619
+ error: null,
620
+ turnIntervalMs: input.turnIntervalMs ?? DEFAULT_TURN_INTERVAL_MS,
621
+ lastActivityAt: Date.now(),
622
+ createdAt: now,
623
+ updatedAt: now
624
+ };
625
+ this.db.prepare(
626
+ `INSERT INTO ce_sessions
627
+ (id, stage, status, currentQuestion, conversationHistory, projectId, artifactPath, error, turnIntervalMs, lastActivityAt, createdAt, updatedAt)
628
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
629
+ ).run(
630
+ session.id,
631
+ session.stage,
632
+ session.status,
633
+ null,
634
+ JSON.stringify(session.conversationHistory),
635
+ session.projectId,
636
+ session.artifactPath,
637
+ null,
638
+ session.turnIntervalMs,
639
+ session.lastActivityAt,
640
+ session.createdAt,
641
+ session.updatedAt
642
+ );
643
+ return session;
644
+ }
645
+ get(id) {
646
+ const row = this.db.prepare(`SELECT * FROM ce_sessions WHERE id = ?`).get(id);
647
+ return row ? rowToSession(row) : void 0;
648
+ }
649
+ list(filter = {}) {
650
+ const clauses = [];
651
+ const params = [];
652
+ if (filter.status) {
653
+ clauses.push("status = ?");
654
+ params.push(filter.status);
655
+ }
656
+ if (filter.stage) {
657
+ clauses.push("stage = ?");
658
+ params.push(filter.stage);
659
+ }
660
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
661
+ const rows = this.db.prepare(`SELECT * FROM ce_sessions ${where} ORDER BY updatedAt DESC, id`).all(...params);
662
+ return rows.map(rowToSession);
663
+ }
664
+ /**
665
+ * Patch a session. Always bumps `updatedAt`; bumps `lastActivityAt` unless the
666
+ * caller explicitly overrides it (used by liveness tests to simulate age).
667
+ */
668
+ update(id, patch) {
669
+ const existing = this.get(id);
670
+ if (!existing) return void 0;
671
+ const next = {
672
+ ...existing,
673
+ ...patch,
674
+ lastActivityAt: patch.lastActivityAt ?? Date.now(),
675
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
676
+ };
677
+ this.db.prepare(
678
+ `UPDATE ce_sessions SET
679
+ status = ?, currentQuestion = ?, conversationHistory = ?, projectId = ?,
680
+ artifactPath = ?, error = ?, lastActivityAt = ?, updatedAt = ?
681
+ WHERE id = ?`
682
+ ).run(
683
+ next.status,
684
+ next.currentQuestion ? JSON.stringify(next.currentQuestion) : null,
685
+ JSON.stringify(next.conversationHistory),
686
+ next.projectId,
687
+ next.artifactPath,
688
+ next.error,
689
+ next.lastActivityAt,
690
+ next.updatedAt,
691
+ id
692
+ );
693
+ return next;
694
+ }
695
+ /** Delete a session row. Returns true when a row was removed. */
696
+ delete(id) {
697
+ const result = this.db.prepare(`DELETE FROM ce_sessions WHERE id = ?`).run(id);
698
+ return Number(result.changes ?? 0) > 0;
699
+ }
700
+ /** Append a turn to the conversation history (no other field touched). */
701
+ appendHistory(id, turn) {
702
+ const existing = this.get(id);
703
+ if (!existing) return void 0;
704
+ return this.update(id, { conversationHistory: [...existing.conversationHistory, turn] });
705
+ }
706
+ /**
707
+ * Interval-relative staleness: a non-terminal session is stale only when its
708
+ * last activity is older than `multiple × turnIntervalMs`. A healthy-but-slow
709
+ * session (within the interval band) is NOT stale. Terminal sessions
710
+ * (completed/error/interrupted) are never "stale" — they are already settled.
711
+ */
712
+ isStale(session, now = Date.now(), multiple = STALE_INTERVAL_MULTIPLE) {
713
+ if (session.status === "completed" || session.status === "error" || session.status === "interrupted") {
714
+ return false;
715
+ }
716
+ return now - session.lastActivityAt > multiple * session.turnIntervalMs;
717
+ }
718
+ /**
719
+ * Recover sessions left non-terminal by a crash/restart. A session with a
720
+ * persisted `currentQuestion` is restored to `awaiting_input` (resumable);
721
+ * one without is marked `interrupted` with its progress preserved — never
722
+ * silently dropped. Returns the ids transitioned.
723
+ */
724
+ recoverStaleSessions(now = Date.now(), multiple = STALE_INTERVAL_MULTIPLE) {
725
+ const candidates = this.list().filter(
726
+ (s) => (s.status === "active" || s.status === "launching") && this.isStale(s, now, multiple)
727
+ );
728
+ const recovered = [];
729
+ for (const s of candidates) {
730
+ if (s.currentQuestion) {
731
+ this.update(s.id, { status: "awaiting_input" });
732
+ } else {
733
+ this.update(s.id, { status: "interrupted", error: s.error ?? "Session interrupted \u2014 progress preserved, resume to continue" });
734
+ }
735
+ recovered.push(s.id);
736
+ }
737
+ return recovered;
738
+ }
739
+ };
740
+ var storeCache2 = /* @__PURE__ */ new WeakMap();
741
+ function getCeSessionStore(ctx) {
742
+ const key = ctx.taskStore;
743
+ const cached = storeCache2.get(key);
744
+ if (cached) return cached;
745
+ const store = new CeSessionStore(ctx.taskStore.getDatabase());
746
+ storeCache2.set(key, store);
747
+ return store;
748
+ }
749
+
750
+ // ../../plugins/fusion-plugin-compound-engineering/src/session/orchestrator.ts
751
+ var WORK_STAGE_ID = "work";
752
+ var DEFAULT_TURN_TIMEOUT_MS = 12e4;
753
+ var PROGRESS_EMIT_INTERVAL_MS = 500;
754
+ var MAX_ACTIVITY_TURNS = 200;
755
+ var MAX_ACTIVITY_TURN_CHARS = 16e3;
756
+ var MAX_PERSISTED_ACTIVITY_TURNS = 50;
757
+ var MAX_PERSISTED_ACTIVITY_TURN_CHARS = 4e3;
758
+ var CE_EVENTS = {
759
+ turn: "compound-engineering:session-turn",
760
+ question: "compound-engineering:session-question",
761
+ completed: "compound-engineering:session-completed",
762
+ error: "compound-engineering:session-error",
763
+ interrupted: "compound-engineering:session-interrupted"
764
+ };
765
+ var CeTurnTimeoutError = class extends Error {
766
+ constructor(ms) {
767
+ super(`CE session turn stalled: no agent activity for ${ms}ms`);
768
+ this.name = "CeTurnTimeoutError";
769
+ }
770
+ };
771
+ function resolveStageSkillPaths() {
772
+ return [resolveDefaultInstallTargetRoot()];
773
+ }
774
+ function buildStageSystemPrompt(stage) {
775
+ return [
776
+ `You are running the Compound Engineering "${stage.stageId}" stage.`,
777
+ `Apply the bundled skill "${stage.skillId}" (it has been loaded into this session).`,
778
+ "",
779
+ "Drive the stage as an interactive question/answer flow. On every turn respond with ONLY a JSON object:",
780
+ ' - To ask the user something: {"type":"question","data":{"id":"<unique>","type":"single_select|multi_select|text|confirm","question":"...","options":[{"id":"..","label":".."}]}}',
781
+ ' - When the stage is finished: {"type":"complete","data":{"artifact":"<full markdown document>", ...}}',
782
+ "No markdown fences, no prose outside the JSON object.",
783
+ "",
784
+ `The user's reply arrives as {"type":"answer","questionId":"...","response":...}. The response takes one of three shapes:`,
785
+ " - a direct answer to your question (an option id, array of option ids, text, or boolean),",
786
+ ' - {"value": <direct answer>, "comment": "<guidance>"} \u2014 apply the answer AND incorporate the guidance into how you proceed,',
787
+ ' - {"feedback": "<guidance only>"} \u2014 the user is steering rather than answering. Incorporate the feedback, adjust course, and either re-ask the question (possibly revised) or continue if the feedback resolves it.',
788
+ "Steering feedback is first-class input: never ignore it, and acknowledge course corrections in your next question or output."
789
+ ].join("\n");
790
+ }
791
+ var CeOrchestrator = class {
792
+ ctx;
793
+ store;
794
+ pipelineStore;
795
+ factory;
796
+ projectRoot;
797
+ turnTimeoutMs;
798
+ executor;
799
+ /** Live in-memory session handles keyed by ce_session id. */
800
+ live = /* @__PURE__ */ new Map();
801
+ /** Mid-turn working output per session (transient; flushed to history on settle). */
802
+ activity = /* @__PURE__ */ new Map();
803
+ /** Last progress timestamp per session (drives the inactivity watchdog). */
804
+ lastProgressAt = /* @__PURE__ */ new Map();
805
+ /** Last progress-driven emit per session (throttling). */
806
+ lastProgressEmitAt = /* @__PURE__ */ new Map();
807
+ /** Sessions currently REPLAYING history (rehydrate) — progress suppressed. */
808
+ replaying = /* @__PURE__ */ new Set();
809
+ constructor(deps) {
810
+ this.ctx = deps.ctx;
811
+ this.store = getCeSessionStore(deps.ctx);
812
+ this.pipelineStore = getCePipelineStore(deps.ctx);
813
+ this.factory = deps.createInteractiveAiSession ?? deps.ctx.createInteractiveAiSession;
814
+ this.projectRoot = deps.projectRoot ?? deps.ctx.taskStore.getRootDir();
815
+ this.turnTimeoutMs = deps.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
816
+ this.executor = deps.executor ?? { kind: "model" };
817
+ }
818
+ /**
819
+ * Resolve the session backend for a CE stage (U9 seam). Threaded from
820
+ * `OrchestratorDeps.executor`; defaults to the model-backed interactive
821
+ * session. Exposed so the seam contract is directly assertable in tests and so
822
+ * future stage-loop wiring has a single resolution point.
823
+ */
824
+ resolveExecutor() {
825
+ return this.executor;
826
+ }
827
+ /**
828
+ * Build the interactive-session options for a stage. The session runs with
829
+ * `cwd` on the real project root (where it reads context and writes the stage
830
+ * artifact) and is handed BOTH the stage's `ce-*` skill id and the plugin-local
831
+ * install directory to discover it from — so the live agent actually loads the
832
+ * bundled skill (closing the U2/U5 skill-discovery carry-forward). Model
833
+ * provider/model are setting-gated (U9); omitted keys let the host pick defaults.
834
+ */
835
+ buildSessionOptions(stage, sessionId) {
836
+ const defaultProvider = getDefaultProvider(this.ctx.settings);
837
+ const defaultModelId = getDefaultModelId(this.ctx.settings);
838
+ return {
839
+ cwd: this.projectRoot,
840
+ systemPrompt: buildStageSystemPrompt(stage),
841
+ tools: "coding",
842
+ requestedSkillNames: [stage.skillId],
843
+ additionalSkillPaths: resolveStageSkillPaths(),
844
+ onProgress: (event) => this.handleProgress(sessionId, event),
845
+ ...defaultProvider ? { defaultProvider } : {},
846
+ ...defaultModelId ? { defaultModelId } : {}
847
+ };
848
+ }
849
+ /**
850
+ * Live mid-turn visibility. Accumulates streamed deltas into the session's
851
+ * activity buffer (consecutive deltas of one kind merge into one turn; tool
852
+ * markers are discrete), pokes the inactivity watchdog, and — throttled —
853
+ * bumps the persisted liveness anchor and emits an observable turn event so
854
+ * push clients refetch. Replay (rehydrate) progress is fully suppressed:
855
+ * it reconstructs context, it is not new work.
856
+ */
857
+ handleProgress(sessionId, event) {
858
+ if (this.replaying.has(sessionId)) return;
859
+ this.lastProgressAt.set(sessionId, Date.now());
860
+ const turns = this.activity.get(sessionId) ?? [];
861
+ if (!this.activity.has(sessionId)) this.activity.set(sessionId, turns);
862
+ const now = (/* @__PURE__ */ new Date()).toISOString();
863
+ if (event.type === "tool") {
864
+ if (event.phase === "start") {
865
+ turns.push({ kind: "tool", text: event.name, at: now, done: false });
866
+ } else {
867
+ for (let i = turns.length - 1; i >= 0; i--) {
868
+ const t = turns[i];
869
+ if (t.kind === "tool" && t.text === event.name && !t.done) {
870
+ t.done = true;
871
+ if (event.isError) t.isError = true;
872
+ break;
873
+ }
874
+ }
875
+ }
876
+ } else {
877
+ const last = turns[turns.length - 1];
878
+ if (last && last.kind === event.type) {
879
+ if (last.text.length < MAX_ACTIVITY_TURN_CHARS) {
880
+ last.text = (last.text + event.delta).slice(0, MAX_ACTIVITY_TURN_CHARS);
881
+ }
882
+ } else {
883
+ turns.push({ kind: event.type, text: event.delta.slice(0, MAX_ACTIVITY_TURN_CHARS), at: now });
884
+ }
885
+ }
886
+ if (turns.length > MAX_ACTIVITY_TURNS) turns.splice(0, turns.length - MAX_ACTIVITY_TURNS);
887
+ const nowMs = Date.now();
888
+ if (nowMs - (this.lastProgressEmitAt.get(sessionId) ?? 0) >= PROGRESS_EMIT_INTERVAL_MS) {
889
+ this.lastProgressEmitAt.set(sessionId, nowMs);
890
+ this.store.update(sessionId, {});
891
+ this.ctx.emitEvent(CE_EVENTS.turn, { sessionId, kind: "progress" });
892
+ }
893
+ }
894
+ /** Read the in-flight working output for a session (route accessor). */
895
+ getLiveActivity(sessionId) {
896
+ return this.activity.get(sessionId) ?? [];
897
+ }
898
+ /**
899
+ * Persist a condensed copy of the live activity buffer into history (so the
900
+ * transcript keeps the working trace after the turn settles), then clear it.
901
+ */
902
+ flushActivity(sessionId) {
903
+ const turns = this.activity.get(sessionId);
904
+ this.activity.delete(sessionId);
905
+ if (!turns || turns.length === 0) return;
906
+ const condensed = turns.slice(-MAX_PERSISTED_ACTIVITY_TURNS).map((t) => ({
907
+ ...t,
908
+ text: t.text.slice(0, MAX_PERSISTED_ACTIVITY_TURN_CHARS)
909
+ }));
910
+ this.store.appendHistory(sessionId, {
911
+ role: "agent",
912
+ text: JSON.stringify({ activity: { turns: condensed } }),
913
+ at: (/* @__PURE__ */ new Date()).toISOString()
914
+ });
915
+ }
916
+ /**
917
+ * Inactivity watchdog: rejects only after `turnTimeoutMs` with NO progress.
918
+ * Every progress event re-arms it, so a long actively-working turn survives;
919
+ * with a non-streaming factory it degrades to a fixed per-turn timeout.
920
+ */
921
+ createWatchdog(sessionId) {
922
+ let cancelled = false;
923
+ let timer;
924
+ this.lastProgressAt.set(sessionId, Date.now());
925
+ const promise = new Promise((_, reject) => {
926
+ const check = () => {
927
+ if (cancelled) return;
928
+ const elapsed = Date.now() - (this.lastProgressAt.get(sessionId) ?? 0);
929
+ if (elapsed >= this.turnTimeoutMs) {
930
+ reject(new CeTurnTimeoutError(this.turnTimeoutMs));
931
+ return;
932
+ }
933
+ timer = globalThis.setTimeout(check, this.turnTimeoutMs - elapsed);
934
+ timer.unref?.();
935
+ };
936
+ check();
937
+ });
938
+ return {
939
+ promise,
940
+ cancel: () => {
941
+ cancelled = true;
942
+ if (timer) clearTimeout(timer);
943
+ }
944
+ };
945
+ }
946
+ /**
947
+ * Start a fresh session for a registered stage and run the opening turn.
948
+ *
949
+ * `detach: true` (the route posture) returns as soon as the session row
950
+ * exists with the turn running in the background — the client converges via
951
+ * push/poll and can watch the live working output. Errors during a detached
952
+ * turn surface through session state (failSession/interruptSession), never
953
+ * as an unhandled rejection. Validation errors still throw synchronously.
954
+ */
955
+ async start(stageId, opts) {
956
+ const stage = getStage(stageId);
957
+ if (!stage) throw new Error(`Unknown CE stage: ${stageId}`);
958
+ if (!getEnabledStages(this.ctx.settings).includes(stageId)) {
959
+ throw new Error(`CE stage is not enabled: ${stageId}`);
960
+ }
961
+ if (!this.factory) {
962
+ throw new Error(
963
+ "Interactive AI sessions are not available (createInteractiveAiSession is only injected on route contexts with the engine loaded)."
964
+ );
965
+ }
966
+ const session = this.store.create({
967
+ stage: stageId,
968
+ projectId: opts.projectId ?? null,
969
+ turnIntervalMs: this.turnTimeoutMs
970
+ });
971
+ this.store.appendHistory(session.id, { role: "user", text: opts.openingMessage, at: (/* @__PURE__ */ new Date()).toISOString() });
972
+ const turn = this.runOpeningTurn(session.id, stage, opts.openingMessage);
973
+ if (opts.detach) {
974
+ void turn;
975
+ return { session: this.requireSession(session.id) };
976
+ }
977
+ return turn;
978
+ }
979
+ /** Create the live handle and run the opening turn. Never rejects. */
980
+ async runOpeningTurn(sessionId, stage, openingMessage) {
981
+ let interactive;
982
+ try {
983
+ interactive = await this.factory(this.buildSessionOptions(stage, sessionId));
984
+ } catch (err) {
985
+ return { session: this.failSession(sessionId, err), event: void 0 };
986
+ }
987
+ this.live.set(sessionId, interactive.session);
988
+ this.store.update(sessionId, { status: "active" });
989
+ return this.runTurn(sessionId, () => interactive.session.prompt(openingMessage), interactive.session);
990
+ }
991
+ /** Answer the awaiting question and continue the loop (detachable like start). */
992
+ async answer(sessionId, questionId, response, opts = {}) {
993
+ const session = this.requireSession(sessionId);
994
+ if (session.status !== "awaiting_input") {
995
+ throw new Error(`Session ${sessionId} is not awaiting input (status=${session.status}).`);
996
+ }
997
+ if (questionId !== session.currentQuestion?.id) {
998
+ throw new Error(
999
+ `Session ${sessionId} is awaiting question "${session.currentQuestion?.id ?? "(none)"}", not "${questionId}".`
1000
+ );
1001
+ }
1002
+ const live = this.live.get(sessionId);
1003
+ if (!live) {
1004
+ throw new Error(`Session ${sessionId} has no live handle in this process; call resume() first.`);
1005
+ }
1006
+ this.store.appendHistory(sessionId, {
1007
+ role: "user",
1008
+ text: JSON.stringify({ answer: response, questionId }),
1009
+ at: (/* @__PURE__ */ new Date()).toISOString()
1010
+ });
1011
+ this.store.update(sessionId, { status: "active", currentQuestion: null });
1012
+ const turn = this.runTurn(sessionId, () => live.answer(questionId, response), live);
1013
+ if (opts.detach) {
1014
+ void turn;
1015
+ return { session: this.requireSession(sessionId) };
1016
+ }
1017
+ return turn;
1018
+ }
1019
+ /**
1020
+ * Resume an `awaiting_input`, `interrupted`, or `error` session — and, crucially,
1021
+ * RE-ESTABLISH a live interactive handle so the resumed session can actually be
1022
+ * answered (Bug 5). After an interrupt/timeout the live handle was disposed and
1023
+ * removed from `this.live`; flipping persisted status back to `awaiting_input`
1024
+ * without a live handle was a dead end (resume → answer → "call resume() first").
1025
+ *
1026
+ * When a question is still pending and no live handle exists, we rehydrate via
1027
+ * the factory and REPLAY the persisted conversation history (opening message +
1028
+ * prior answers) to prime the fresh agent back to the current awaiting question,
1029
+ * repopulating `this.live`. Replay is side-effect-suppressed: it reconstructs
1030
+ * the agent's context only — no artifact writes, no event emits, no history
1031
+ * re-append (the DB already reflects the final state).
1032
+ *
1033
+ * If no factory is available (no live handle can ever be created in this
1034
+ * process), we DO NOT advertise a misleading answerable status: the session is
1035
+ * left `interrupted` with a clear error explaining it can't be continued here.
1036
+ */
1037
+ async resume(sessionId, opts = {}) {
1038
+ const session = this.requireSession(sessionId);
1039
+ if (session.status === "completed") return { session };
1040
+ if (session.status === "awaiting_input" && this.live.has(sessionId)) {
1041
+ return { session };
1042
+ }
1043
+ if (!session.currentQuestion) {
1044
+ const next = this.store.update(sessionId, { status: "active", error: null }) ?? session;
1045
+ return { session: next };
1046
+ }
1047
+ if (this.live.has(sessionId)) {
1048
+ const next = this.store.update(sessionId, { status: "awaiting_input", error: null }) ?? session;
1049
+ return { session: next };
1050
+ }
1051
+ if (!this.factory) {
1052
+ const next = this.store.update(sessionId, {
1053
+ status: "interrupted",
1054
+ error: "Session cannot be continued in this process: interactive AI sessions are unavailable (no factory on this context). Resume from a route context with the engine loaded."
1055
+ }) ?? session;
1056
+ return { session: next };
1057
+ }
1058
+ const rehydration = (async () => {
1059
+ try {
1060
+ await this.rehydrate(session);
1061
+ } catch (err) {
1062
+ return { session: this.interruptSession(sessionId, err) };
1063
+ }
1064
+ const next = this.store.update(sessionId, { status: "awaiting_input", error: null }) ?? session;
1065
+ return { session: next };
1066
+ })();
1067
+ if (opts.detach) {
1068
+ const next = this.store.update(sessionId, { status: "active", error: null }) ?? session;
1069
+ void rehydration;
1070
+ return { session: next };
1071
+ }
1072
+ return rehydration;
1073
+ }
1074
+ /**
1075
+ * Re-create a live interactive session and REPLAY the persisted conversation so
1076
+ * the fresh agent is primed back to the current awaiting question. Side effects
1077
+ * are suppressed: we drain the seam's events to advance the agent's context but
1078
+ * do NOT persist/emit/write — the DB already holds the authoritative final
1079
+ * state. Populates `this.live[session.id]` on success.
1080
+ */
1081
+ async rehydrate(session) {
1082
+ const stage = getStage(session.stage);
1083
+ if (!stage) throw new Error(`Unknown CE stage: ${session.stage}`);
1084
+ this.replaying.add(session.id);
1085
+ try {
1086
+ await this.rehydrateReplay(session, stage);
1087
+ } finally {
1088
+ this.replaying.delete(session.id);
1089
+ }
1090
+ }
1091
+ async rehydrateReplay(session, stage) {
1092
+ const interactive = await this.factory(this.buildSessionOptions(stage, session.id));
1093
+ const live = interactive.session;
1094
+ const userTurns = session.conversationHistory.filter((t) => t.role === "user");
1095
+ try {
1096
+ for (let i = 0; i < userTurns.length; i++) {
1097
+ const turn = userTurns[i];
1098
+ if (i === 0) {
1099
+ await live.prompt(turn.text);
1100
+ } else {
1101
+ const parsed = this.parseAnswerTurn(turn.text);
1102
+ if (!parsed) continue;
1103
+ await live.answer(parsed.questionId, parsed.answer);
1104
+ }
1105
+ await live.nextEvent();
1106
+ }
1107
+ } catch (err) {
1108
+ try {
1109
+ live.dispose();
1110
+ } catch {
1111
+ }
1112
+ throw err;
1113
+ }
1114
+ this.live.set(session.id, live);
1115
+ }
1116
+ /** Parse a serialized `{ answer, questionId }` user turn produced by answer(). */
1117
+ parseAnswerTurn(text) {
1118
+ try {
1119
+ const obj = JSON.parse(text);
1120
+ if (typeof obj?.questionId === "string") {
1121
+ return { questionId: obj.questionId, answer: obj.answer };
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ return void 0;
1126
+ }
1127
+ /** Read-through accessor for routes. */
1128
+ getState(sessionId) {
1129
+ return this.store.get(sessionId);
1130
+ }
1131
+ /**
1132
+ * Discard a session: dispose any live in-process handle (so an in-flight
1133
+ * agent doesn't keep running unobserved) and delete the persisted row.
1134
+ * Returns false when the session doesn't exist. Pipeline-link rows are NOT
1135
+ * touched — board tasks the session landed keep their provenance records.
1136
+ */
1137
+ discard(sessionId) {
1138
+ this.disposeLive(sessionId);
1139
+ return this.store.delete(sessionId);
1140
+ }
1141
+ /**
1142
+ * Run one turn behind a timeout race, persist the resulting event, and on a
1143
+ * turn-level failure auto-save + emit. The `driver` performs the prompt/answer
1144
+ * against the live session; we then pull exactly one event.
1145
+ */
1146
+ async runTurn(sessionId, driver, live) {
1147
+ let event;
1148
+ const watchdog = this.createWatchdog(sessionId);
1149
+ try {
1150
+ event = await Promise.race([
1151
+ (async () => {
1152
+ await driver();
1153
+ return live.nextEvent();
1154
+ })(),
1155
+ watchdog.promise
1156
+ ]);
1157
+ } catch (err) {
1158
+ watchdog.cancel();
1159
+ const session2 = this.interruptSession(sessionId, err);
1160
+ this.disposeLive(sessionId);
1161
+ return { session: session2, event: { type: "error", data: { message: session2.error ?? "interrupted", cause: err } } };
1162
+ }
1163
+ watchdog.cancel();
1164
+ const session = this.applyEvent(sessionId, event);
1165
+ if (event.type === "complete" || event.type === "error") {
1166
+ this.disposeLive(sessionId);
1167
+ }
1168
+ if (event.type === "complete" && session.stage === WORK_STAGE_ID) {
1169
+ await this.landWorkTasks(session, event.data);
1170
+ }
1171
+ return { session, event };
1172
+ }
1173
+ /** Persist a seam event onto the session row + emit the matching observable event. */
1174
+ applyEvent(sessionId, event) {
1175
+ if (event.type === "question" || event.type === "complete" || event.type === "error") {
1176
+ this.flushActivity(sessionId);
1177
+ }
1178
+ switch (event.type) {
1179
+ case "thinking":
1180
+ case "text": {
1181
+ this.store.appendHistory(sessionId, { role: "agent", text: event.data, at: (/* @__PURE__ */ new Date()).toISOString() });
1182
+ const s = this.store.update(sessionId, { status: "active" }) ?? this.requireSession(sessionId);
1183
+ this.ctx.emitEvent(CE_EVENTS.turn, { sessionId, kind: event.type });
1184
+ return s;
1185
+ }
1186
+ case "question": {
1187
+ const q = event.data;
1188
+ this.store.appendHistory(sessionId, {
1189
+ role: "agent",
1190
+ text: JSON.stringify({ question: q }),
1191
+ at: (/* @__PURE__ */ new Date()).toISOString()
1192
+ });
1193
+ const s = this.store.update(sessionId, { status: "awaiting_input", currentQuestion: q }) ?? this.requireSession(sessionId);
1194
+ this.ctx.emitEvent(CE_EVENTS.question, { sessionId, questionId: q.id });
1195
+ return s;
1196
+ }
1197
+ case "complete": {
1198
+ const artifactPath = this.writeArtifact(sessionId, event.data);
1199
+ this.store.appendHistory(sessionId, {
1200
+ role: "agent",
1201
+ text: JSON.stringify({ complete: true }),
1202
+ at: (/* @__PURE__ */ new Date()).toISOString()
1203
+ });
1204
+ const s = this.store.update(sessionId, { status: "completed", currentQuestion: null, artifactPath }) ?? this.requireSession(sessionId);
1205
+ this.ctx.emitEvent(CE_EVENTS.completed, { sessionId, artifactPath });
1206
+ return s;
1207
+ }
1208
+ case "error": {
1209
+ const message = event.data.message;
1210
+ const s = this.store.update(sessionId, { status: "error", error: message }) ?? this.requireSession(sessionId);
1211
+ this.ctx.emitEvent(CE_EVENTS.error, { sessionId, message });
1212
+ return s;
1213
+ }
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Work bridge (U7). Read the derived task list from the work stage's
1218
+ * completion payload, create each as a board task tagged CE-originated, and
1219
+ * record a pipeline-link row resolving task→pipeline/stage/artifact. Zero
1220
+ * derived tasks is a clean no-op (no board tasks, no orphan link rows). The
1221
+ * created tasks then run the normal lifecycle — no hooks attached here (U8).
1222
+ */
1223
+ async landWorkTasks(session, data) {
1224
+ const specs = this.extractTaskSpecs(data);
1225
+ if (specs.length === 0) return;
1226
+ const cePipelineId = session.id;
1227
+ const ceStageId = session.stage;
1228
+ const ceArtifactPath = session.artifactPath ?? null;
1229
+ this.pipelineStore.upsertState({
1230
+ cePipelineId,
1231
+ currentStage: ceStageId,
1232
+ status: "running",
1233
+ lastArtifactPath: ceArtifactPath
1234
+ });
1235
+ for (const spec of specs) {
1236
+ const description = spec.description.trim();
1237
+ if (!description) continue;
1238
+ await createCeTaskWithLink(this.ctx.taskStore, this.pipelineStore, {
1239
+ title: spec.title,
1240
+ description,
1241
+ column: spec.column,
1242
+ cePipelineId,
1243
+ ceStageId,
1244
+ ceArtifactPath
1245
+ });
1246
+ }
1247
+ }
1248
+ /** Parse the `{ tasks: [...] }` completion-payload contract; tolerant of shape. */
1249
+ extractTaskSpecs(data) {
1250
+ if (!data || typeof data !== "object") return [];
1251
+ const raw = data.tasks;
1252
+ if (!Array.isArray(raw)) return [];
1253
+ const specs = [];
1254
+ for (const entry of raw) {
1255
+ if (!entry || typeof entry !== "object") continue;
1256
+ const e = entry;
1257
+ const description = typeof e.description === "string" ? e.description : "";
1258
+ if (!description.trim()) continue;
1259
+ specs.push({
1260
+ description,
1261
+ title: typeof e.title === "string" ? e.title : void 0,
1262
+ column: typeof e.column === "string" ? e.column : void 0
1263
+ });
1264
+ }
1265
+ return specs;
1266
+ }
1267
+ /** Persist `interrupted` with progress preserved and emit. */
1268
+ interruptSession(sessionId, cause) {
1269
+ this.flushActivity(sessionId);
1270
+ const message = cause instanceof Error ? cause.message : String(cause);
1271
+ const s = this.store.update(sessionId, { status: "interrupted", error: message }) ?? this.requireSession(sessionId);
1272
+ this.ctx.emitEvent(CE_EVENTS.interrupted, { sessionId, message });
1273
+ return s;
1274
+ }
1275
+ /** Persist `error` (session-create failure path) and emit. */
1276
+ failSession(sessionId, cause) {
1277
+ const message = cause instanceof Error ? cause.message : String(cause);
1278
+ const s = this.store.update(sessionId, { status: "error", error: message }) ?? this.requireSession(sessionId);
1279
+ this.ctx.emitEvent(CE_EVENTS.error, { sessionId, message });
1280
+ return s;
1281
+ }
1282
+ /**
1283
+ * Write the stage artifact to its conventional location (R10). Accepts either
1284
+ * a `{ artifact: string }` payload or a raw string. Returns the absolute path.
1285
+ */
1286
+ writeArtifact(sessionId, data) {
1287
+ const session = this.requireSession(sessionId);
1288
+ const stage = getStage(session.stage);
1289
+ const location = stage?.artifactLocation ?? `docs/ce/${session.stage}/`;
1290
+ const content = this.extractArtifactContent(data);
1291
+ const target = location.endsWith("/") ? join2(location, `${session.stage}-${session.id}.md`) : location;
1292
+ const abs = isAbsolute2(target) ? target : join2(this.projectRoot, target);
1293
+ mkdirSync2(dirname2(abs), { recursive: true });
1294
+ writeFileSync(abs, content, "utf-8");
1295
+ return abs;
1296
+ }
1297
+ extractArtifactContent(data) {
1298
+ if (typeof data === "string") return data;
1299
+ if (data && typeof data === "object" && "artifact" in data) {
1300
+ const a = data.artifact;
1301
+ if (typeof a === "string") return a;
1302
+ }
1303
+ return JSON.stringify(data, null, 2);
1304
+ }
1305
+ requireSession(sessionId) {
1306
+ const s = this.store.get(sessionId);
1307
+ if (!s) throw new Error(`CE session not found: ${sessionId}`);
1308
+ return s;
1309
+ }
1310
+ disposeLive(sessionId) {
1311
+ const live = this.live.get(sessionId);
1312
+ if (live) {
1313
+ try {
1314
+ live.dispose();
1315
+ } catch {
1316
+ }
1317
+ this.live.delete(sessionId);
1318
+ }
1319
+ this.activity.delete(sessionId);
1320
+ this.lastProgressAt.delete(sessionId);
1321
+ this.lastProgressEmitAt.delete(sessionId);
1322
+ }
1323
+ };
1324
+
1325
+ // ../../plugins/fusion-plugin-compound-engineering/src/routes/route-helpers.ts
1326
+ function asString2(v) {
1327
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1328
+ }
1329
+
1330
+ // ../../plugins/fusion-plugin-compound-engineering/src/routes/session-routes.ts
1331
+ var orchestratorCache = /* @__PURE__ */ new WeakMap();
1332
+ function getOrchestrator(ctx) {
1333
+ const key = ctx.taskStore;
1334
+ const cached = orchestratorCache.get(key);
1335
+ if (cached) return cached;
1336
+ const orch = new CeOrchestrator({ ctx });
1337
+ orchestratorCache.set(key, orch);
1338
+ return orch;
1339
+ }
1340
+ function badRequest(message) {
1341
+ return { status: 400, body: { error: message } };
1342
+ }
1343
+ function createSessionRoutes() {
1344
+ return [
1345
+ {
1346
+ method: "POST",
1347
+ path: "/sessions",
1348
+ description: "Start an interactive CE stage session.",
1349
+ handler: async (req, ctx) => {
1350
+ const body = req.body;
1351
+ const stageId = asString2(body?.stage);
1352
+ const openingMessage = asString2(body?.message) ?? "";
1353
+ if (!stageId) return badRequest("`stage` is required");
1354
+ const orch = getOrchestrator(ctx);
1355
+ try {
1356
+ const result = await orch.start(stageId, {
1357
+ openingMessage,
1358
+ projectId: asString2(body?.projectId) ?? null,
1359
+ detach: true
1360
+ });
1361
+ return { status: 201, body: { session: result.session } };
1362
+ } catch (err) {
1363
+ return { status: 400, body: { error: err instanceof Error ? err.message : String(err) } };
1364
+ }
1365
+ }
1366
+ },
1367
+ {
1368
+ method: "POST",
1369
+ path: "/sessions/:id/answer",
1370
+ description: "Answer the awaiting question and continue the session.",
1371
+ handler: async (req, ctx) => {
1372
+ const request = req;
1373
+ const id = request.params.id;
1374
+ const body = request.body;
1375
+ const questionId = asString2(body?.questionId);
1376
+ if (!questionId) return badRequest("`questionId` is required");
1377
+ if (!("response" in (body ?? {}))) return badRequest("`response` is required");
1378
+ const orch = getOrchestrator(ctx);
1379
+ try {
1380
+ const result = await orch.answer(id, questionId, body.response, {
1381
+ detach: true
1382
+ });
1383
+ return { status: 200, body: { session: result.session } };
1384
+ } catch (err) {
1385
+ return { status: 409, body: { error: err instanceof Error ? err.message : String(err) } };
1386
+ }
1387
+ }
1388
+ },
1389
+ {
1390
+ method: "POST",
1391
+ path: "/sessions/:id/resume",
1392
+ description: "Resume an awaiting_input or interrupted session to its current question.",
1393
+ handler: async (req, ctx) => {
1394
+ const id = req.params.id;
1395
+ const orch = getOrchestrator(ctx);
1396
+ try {
1397
+ const result = await orch.resume(id, { detach: true });
1398
+ return { status: 200, body: { session: result.session } };
1399
+ } catch (err) {
1400
+ return { status: 404, body: { error: err instanceof Error ? err.message : String(err) } };
1401
+ }
1402
+ }
1403
+ },
1404
+ {
1405
+ method: "GET",
1406
+ path: "/sessions/:id",
1407
+ description: "Get current session state, including in-flight working output (liveActivity).",
1408
+ handler: async (req, ctx) => {
1409
+ const id = req.params.id;
1410
+ const session = getCeSessionStore(ctx).get(id);
1411
+ if (!session) return { status: 404, body: { error: `Session ${id} not found` } };
1412
+ const liveActivity = getOrchestrator(ctx).getLiveActivity(id);
1413
+ return {
1414
+ status: 200,
1415
+ body: { session: liveActivity.length > 0 ? { ...session, liveActivity } : session }
1416
+ };
1417
+ }
1418
+ },
1419
+ {
1420
+ method: "GET",
1421
+ path: "/sessions",
1422
+ description: "List CE sessions (optionally filtered by status/stage).",
1423
+ handler: async (req, ctx) => {
1424
+ const query = req.query ?? {};
1425
+ const status = asCeSessionStatus(typeof query.status === "string" ? query.status : void 0);
1426
+ const stage = typeof query.stage === "string" ? query.stage : void 0;
1427
+ const sessions = getCeSessionStore(ctx).list({ status, stage });
1428
+ return { status: 200, body: { sessions } };
1429
+ }
1430
+ },
1431
+ {
1432
+ method: "DELETE",
1433
+ path: "/sessions/:id",
1434
+ description: "Discard a CE session (disposes any live handle, deletes the row).",
1435
+ handler: async (req, ctx) => {
1436
+ const id = req.params.id;
1437
+ const removed = getOrchestrator(ctx).discard(id);
1438
+ if (!removed) return { status: 404, body: { error: `Session ${id} not found` } };
1439
+ return { status: 200, body: { deleted: true } };
1440
+ }
1441
+ },
1442
+ {
1443
+ // U7 work bridge: observe the board tasks a CE pipeline (session) landed,
1444
+ // via their link records (the addressable back-reference, FN-5719). The
1445
+ // session id IS the pipeline id. Outbound-only in U7; U8 layers state.
1446
+ method: "GET",
1447
+ path: "/sessions/:id/links",
1448
+ description: "List the CE pipeline-link records (work\u2192board) for a session/pipeline.",
1449
+ handler: async (req, ctx) => {
1450
+ const id = req.params.id;
1451
+ const links = getCePipelineStore(ctx).listByPipeline(id);
1452
+ return { status: 200, body: { links } };
1453
+ }
1454
+ }
1455
+ ];
1456
+ }
1457
+
1458
+ // ../../plugins/fusion-plugin-compound-engineering/src/artifacts/discovery.ts
1459
+ import { accessSync, constants, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
1460
+ import { isAbsolute as isAbsolute3, join as join3, relative, sep as sep2 } from "node:path";
1461
+ var CONVENTIONAL_LOCATIONS = [
1462
+ { stage: "strategy", label: "Strategy", path: "STRATEGY.md", kind: "file" },
1463
+ { stage: "ideation", label: "Ideation", path: "docs/ideation", kind: "directory" },
1464
+ { stage: "brainstorm", label: "Brainstorms", path: "docs/brainstorms", kind: "directory" },
1465
+ { stage: "plan", label: "Plans", path: "docs/plans", kind: "directory" },
1466
+ { stage: "solution", label: "Solutions", path: "docs/solutions", kind: "directory" },
1467
+ { stage: "concepts", label: "Concepts", path: "CONCEPTS.md", kind: "file" }
1468
+ ];
1469
+ var MAX_ARTIFACT_BYTES = 2e6;
1470
+ function toPosix(p) {
1471
+ return p.split(sep2).join("/");
1472
+ }
1473
+ function isWithin(root, locationAbs, candidate) {
1474
+ const relToLocation = relative(locationAbs, candidate);
1475
+ if (relToLocation.startsWith("..") || isAbsolute3(relToLocation)) return false;
1476
+ const relToRoot = relative(root, candidate);
1477
+ if (relToRoot.startsWith("..") || isAbsolute3(relToRoot)) return false;
1478
+ return true;
1479
+ }
1480
+ function makeId(stage, relPath) {
1481
+ return `${stage}:${relPath}`;
1482
+ }
1483
+ function makeError(stage, relPath, message) {
1484
+ return {
1485
+ id: makeId(stage, relPath),
1486
+ stage,
1487
+ path: relPath,
1488
+ name: relPath.split("/").pop() ?? relPath,
1489
+ kind: "error",
1490
+ error: message
1491
+ };
1492
+ }
1493
+ function readArtifactEntry(stage, root, locationAbs, abs, relPath) {
1494
+ const name = relPath.split("/").pop() ?? relPath;
1495
+ if (!isWithin(root, locationAbs, abs)) {
1496
+ return makeError(stage, relPath, "Path is outside its conventional location");
1497
+ }
1498
+ try {
1499
+ const st = statSync(abs);
1500
+ if (st.size > MAX_ARTIFACT_BYTES) {
1501
+ return makeError(stage, relPath, `Artifact too large to read (${st.size} bytes)`);
1502
+ }
1503
+ accessSync(abs, constants.R_OK);
1504
+ return {
1505
+ id: makeId(stage, relPath),
1506
+ stage,
1507
+ path: relPath,
1508
+ name,
1509
+ size: st.size,
1510
+ updatedAt: st.mtimeMs,
1511
+ kind: "artifact"
1512
+ };
1513
+ } catch (err) {
1514
+ return makeError(stage, relPath, err instanceof Error ? err.message : String(err));
1515
+ }
1516
+ }
1517
+ function sortEntries(entries) {
1518
+ return [...entries].sort((a, b) => {
1519
+ if (a.kind !== b.kind) return a.kind === "artifact" ? -1 : 1;
1520
+ if (a.kind === "artifact" && b.kind === "artifact") return b.updatedAt - a.updatedAt;
1521
+ return a.name.localeCompare(b.name);
1522
+ });
1523
+ }
1524
+ function discoverLocation(root, loc) {
1525
+ const locationAbs = join3(root, loc.path);
1526
+ const entries = [];
1527
+ let present = false;
1528
+ let st;
1529
+ try {
1530
+ st = statSync(locationAbs);
1531
+ present = true;
1532
+ } catch {
1533
+ return { stage: loc.stage, label: loc.label, present: false, entries: [] };
1534
+ }
1535
+ if (loc.kind === "file") {
1536
+ if (st.isFile()) {
1537
+ entries.push(readArtifactEntry(loc.stage, root, locationAbs, locationAbs, toPosix(loc.path)));
1538
+ } else {
1539
+ entries.push(
1540
+ makeError(
1541
+ loc.stage,
1542
+ toPosix(loc.path),
1543
+ "Expected a file at the conventional location but found a directory"
1544
+ )
1545
+ );
1546
+ }
1547
+ return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
1548
+ }
1549
+ let names = [];
1550
+ try {
1551
+ if (!st.isDirectory()) {
1552
+ entries.push(
1553
+ makeError(
1554
+ loc.stage,
1555
+ toPosix(loc.path),
1556
+ "Expected a directory at the conventional location but found a file"
1557
+ )
1558
+ );
1559
+ return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
1560
+ }
1561
+ names = readdirSync(locationAbs);
1562
+ } catch (err) {
1563
+ entries.push(makeError(loc.stage, toPosix(loc.path), err instanceof Error ? err.message : String(err)));
1564
+ return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
1565
+ }
1566
+ for (const childName of names) {
1567
+ if (childName.startsWith(".")) continue;
1568
+ if (!childName.toLowerCase().endsWith(".md")) continue;
1569
+ const abs = join3(locationAbs, childName);
1570
+ const relPath = toPosix(join3(loc.path, childName));
1571
+ let childStat;
1572
+ try {
1573
+ childStat = statSync(abs);
1574
+ } catch (err) {
1575
+ entries.push(makeError(loc.stage, relPath, err instanceof Error ? err.message : String(err)));
1576
+ continue;
1577
+ }
1578
+ if (!childStat.isFile()) continue;
1579
+ entries.push(readArtifactEntry(loc.stage, root, locationAbs, abs, relPath));
1580
+ }
1581
+ return { stage: loc.stage, label: loc.label, present, entries: sortEntries(entries) };
1582
+ }
1583
+ function discoverArtifacts(projectRoot) {
1584
+ const root = projectRoot;
1585
+ const groups = CONVENTIONAL_LOCATIONS.map((loc) => discoverLocation(root, loc));
1586
+ let totalArtifacts = 0;
1587
+ let totalErrors = 0;
1588
+ for (const g of groups) {
1589
+ for (const e of g.entries) {
1590
+ if (e.kind === "artifact") totalArtifacts += 1;
1591
+ else totalErrors += 1;
1592
+ }
1593
+ }
1594
+ return { groups, totalArtifacts, totalErrors };
1595
+ }
1596
+ function readArtifactById(projectRoot, id) {
1597
+ const sepIdx = id.indexOf(":");
1598
+ if (sepIdx <= 0) return void 0;
1599
+ const stage = id.slice(0, sepIdx);
1600
+ const relPath = id.slice(sepIdx + 1);
1601
+ const loc = CONVENTIONAL_LOCATIONS.find((l) => l.stage === stage);
1602
+ if (!loc) return void 0;
1603
+ const locationAbs = join3(projectRoot, loc.path);
1604
+ const abs = join3(projectRoot, relPath);
1605
+ if (loc.kind === "file") {
1606
+ if (toPosix(relPath) !== toPosix(loc.path)) return void 0;
1607
+ } else if (!isWithin(projectRoot, locationAbs, abs)) {
1608
+ return void 0;
1609
+ }
1610
+ if (loc.kind === "directory") {
1611
+ const rel = relative(locationAbs, abs);
1612
+ if (rel.includes(sep2) || rel.startsWith("..") || !rel.toLowerCase().endsWith(".md")) {
1613
+ return void 0;
1614
+ }
1615
+ }
1616
+ let content;
1617
+ let mtimeMs;
1618
+ let size;
1619
+ try {
1620
+ const st = statSync(abs);
1621
+ if (!st.isFile()) return { error: "Artifact is not a readable file" };
1622
+ if (st.size > MAX_ARTIFACT_BYTES) return { error: `Artifact too large to read (${st.size} bytes)` };
1623
+ mtimeMs = st.mtimeMs;
1624
+ size = st.size;
1625
+ } catch (err) {
1626
+ if (err?.code === "ENOENT") return void 0;
1627
+ return { error: err instanceof Error ? err.message : String(err) };
1628
+ }
1629
+ try {
1630
+ content = readFileSync2(abs, "utf8");
1631
+ } catch (err) {
1632
+ return { error: err instanceof Error ? err.message : String(err) };
1633
+ }
1634
+ const name = relPath.split("/").pop() ?? relPath;
1635
+ return {
1636
+ artifact: {
1637
+ id,
1638
+ stage,
1639
+ path: toPosix(relPath),
1640
+ name,
1641
+ size,
1642
+ updatedAt: mtimeMs,
1643
+ kind: "artifact"
1644
+ },
1645
+ content
1646
+ };
1647
+ }
1648
+
1649
+ // ../../plugins/fusion-plugin-compound-engineering/src/routes/artifact-routes.ts
1650
+ async function resolveProjectRoot(ctx, projectId) {
1651
+ if (projectId && ctx.resolveProjectTaskStore) {
1652
+ try {
1653
+ const store = await ctx.resolveProjectTaskStore(projectId);
1654
+ return store.getRootDir();
1655
+ } catch {
1656
+ }
1657
+ }
1658
+ return ctx.taskStore.getRootDir();
1659
+ }
1660
+ function escapeHtml(input) {
1661
+ return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1662
+ }
1663
+ function renderArtifactDocument(name, content) {
1664
+ const escaped = escapeHtml(content);
1665
+ return `<!DOCTYPE html>
1666
+ <html lang="en">
1667
+ <head>
1668
+ <meta charset="utf-8" />
1669
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1670
+ <title>${escapeHtml(name)}</title>
1671
+ <style>
1672
+ :root { color-scheme: light dark; }
1673
+ * { box-sizing: border-box; }
1674
+ body { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.5; background: #ffffff; color: #1a1a1a; }
1675
+ @media (prefers-color-scheme: dark) { body { background: #16181d; color: #e6e6e6; } }
1676
+ .artifact-doc { padding: 1.25rem 1.5rem; }
1677
+ .artifact-doc h1 { font-size: 1.1rem; margin: 0 0 1rem; font-family: ui-sans-serif, system-ui, sans-serif; }
1678
+ .artifact-body { white-space: pre-wrap; word-break: break-word; margin: 0; font-size: 0.85rem; }
1679
+ </style>
1680
+ </head>
1681
+ <body>
1682
+ <main class="artifact-doc" data-section="artifact">
1683
+ <h1 data-section="title">${escapeHtml(name)}</h1>
1684
+ <pre class="artifact-body" data-section="content">${escaped}</pre>
1685
+ </main>
1686
+ </body>
1687
+ </html>`;
1688
+ }
1689
+ function createArtifactRoutes() {
1690
+ return [
1691
+ {
1692
+ method: "GET",
1693
+ path: "/artifacts",
1694
+ description: "List discovered CE artifacts grouped by stage.",
1695
+ handler: async (req, ctx) => {
1696
+ const query = req.query ?? {};
1697
+ const projectId = asString2(query.projectId);
1698
+ const root = await resolveProjectRoot(ctx, projectId);
1699
+ const result = discoverArtifacts(root);
1700
+ return { status: 200, body: result };
1701
+ }
1702
+ },
1703
+ {
1704
+ method: "GET",
1705
+ path: "/artifacts/:id",
1706
+ description: "Read a single CE artifact's raw content (JSON).",
1707
+ handler: async (req, ctx) => {
1708
+ const request = req;
1709
+ const id = decodeURIComponent(request.params.id);
1710
+ const projectId = asString2(request.query?.projectId);
1711
+ const root = await resolveProjectRoot(ctx, projectId);
1712
+ const result = readArtifactById(root, id);
1713
+ if (!result) return { status: 404, body: { error: `Artifact ${id} not found` } };
1714
+ if ("error" in result) return { status: 422, body: { error: result.error } };
1715
+ return { status: 200, body: { artifact: result.artifact, content: result.content } };
1716
+ }
1717
+ },
1718
+ {
1719
+ method: "GET",
1720
+ path: "/artifacts/:id/preview.html",
1721
+ description: "Read a single CE artifact rendered as a self-contained HTML document.",
1722
+ handler: async (req, ctx) => {
1723
+ const request = req;
1724
+ const id = decodeURIComponent(request.params.id);
1725
+ const projectId = asString2(request.query?.projectId);
1726
+ const root = await resolveProjectRoot(ctx, projectId);
1727
+ const result = readArtifactById(root, id);
1728
+ if (!result) return { status: 404, body: { error: `Artifact ${id} not found` } };
1729
+ if ("error" in result) return { status: 422, body: { error: result.error } };
1730
+ return {
1731
+ status: 200,
1732
+ contentType: "text/html; charset=utf-8",
1733
+ body: renderArtifactDocument(result.artifact.name, result.content)
1734
+ };
1735
+ }
1736
+ }
1737
+ ];
1738
+ }
1739
+
1740
+ // ../../plugins/fusion-plugin-compound-engineering/src/sync/reconciler.ts
1741
+ var TERMINAL_COLUMNS = /* @__PURE__ */ new Set(["in-review", "done"]);
1742
+ function stageOrder() {
1743
+ return listStages().map((s) => s.stageId);
1744
+ }
1745
+ function nextStageAfter(stageId) {
1746
+ const order = stageOrder();
1747
+ const idx = order.indexOf(stageId);
1748
+ if (idx < 0 || idx >= order.length - 1) return void 0;
1749
+ return order[idx + 1];
1750
+ }
1751
+ var CeReconciler = class {
1752
+ ctx;
1753
+ store;
1754
+ constructor(ctx) {
1755
+ this.ctx = ctx;
1756
+ this.store = getCePipelineStore(ctx);
1757
+ }
1758
+ /**
1759
+ * Drain the queue AND re-derive missed transitions from live board state, then
1760
+ * apply any pipeline advancement (with outbound board propagation). Idempotent:
1761
+ * running it twice is a no-op once everything has converged.
1762
+ */
1763
+ async reconcile() {
1764
+ const result = { drained: 0, advanced: 0, tasksCreated: 0, inspected: 0 };
1765
+ const pending = this.store.listPendingSync();
1766
+ for (const entry of pending) {
1767
+ this.store.markSyncProcessed(entry.id);
1768
+ result.drained++;
1769
+ }
1770
+ const states = this.store.listAllState();
1771
+ for (const state of states) {
1772
+ result.inspected++;
1773
+ const advanced = await this.reconcileOne(state);
1774
+ if (advanced) {
1775
+ result.advanced++;
1776
+ if (advanced.created) result.tasksCreated++;
1777
+ }
1778
+ }
1779
+ return result;
1780
+ }
1781
+ /**
1782
+ * Re-derive whether ONE pipeline should advance by reading the live board
1783
+ * column of its current-stage task(s). Board is authoritative for task state;
1784
+ * we never write the task column from here for the current stage.
1785
+ */
1786
+ async reconcileOne(state) {
1787
+ if (state.status === "completed") return void 0;
1788
+ const links = this.store.listByPipeline(state.cePipelineId);
1789
+ const currentStageLinks = links.filter((l) => l.ceStageId === state.currentStage);
1790
+ if (currentStageLinks.length === 0) return void 0;
1791
+ const tasks = await this.loadTasks(currentStageLinks);
1792
+ if (tasks.length === 0) return void 0;
1793
+ const existing = tasks.filter((t) => t != null);
1794
+ if (existing.length === 0) {
1795
+ return void 0;
1796
+ }
1797
+ const allTerminal = existing.every((t) => TERMINAL_COLUMNS.has(t.column));
1798
+ if (!allTerminal) {
1799
+ if (state.status !== "running") {
1800
+ this.store.transitionState(state.cePipelineId, { status: "running" });
1801
+ }
1802
+ return void 0;
1803
+ }
1804
+ const next = nextStageAfter(state.currentStage);
1805
+ if (!next) {
1806
+ this.store.transitionState(state.cePipelineId, { status: "completed" });
1807
+ this.ctx.emitEvent("compound-engineering:pipeline-completed", {
1808
+ cePipelineId: state.cePipelineId,
1809
+ stage: state.currentStage
1810
+ });
1811
+ return { created: false };
1812
+ }
1813
+ const created = await this.advance(state, next, links);
1814
+ return { created };
1815
+ }
1816
+ /**
1817
+ * Advance the pipeline to `nextStage` (CE-owned write) and propagate OUTBOUND
1818
+ * by creating the next-stage board task (board-owned write on a NEW row).
1819
+ * Idempotent: if a link for the next stage already exists, we don't duplicate.
1820
+ */
1821
+ async advance(state, nextStage, links) {
1822
+ const already = links.some((l) => l.ceStageId === nextStage);
1823
+ if (already) {
1824
+ this.store.transitionState(state.cePipelineId, {
1825
+ currentStage: nextStage,
1826
+ status: "running"
1827
+ });
1828
+ return false;
1829
+ }
1830
+ const task = await createCeTaskWithLink(this.ctx.taskStore, this.store, {
1831
+ title: `CE ${nextStage}: continue pipeline`,
1832
+ description: `Continue the compound-engineering pipeline at the "${nextStage}" stage.`,
1833
+ cePipelineId: state.cePipelineId,
1834
+ ceStageId: nextStage,
1835
+ ceArtifactPath: state.lastArtifactPath
1836
+ });
1837
+ this.store.transitionState(state.cePipelineId, {
1838
+ currentStage: nextStage,
1839
+ status: "awaiting_board"
1840
+ });
1841
+ this.ctx.emitEvent("compound-engineering:pipeline-advanced", {
1842
+ cePipelineId: state.cePipelineId,
1843
+ fromStage: state.currentStage,
1844
+ toStage: nextStage,
1845
+ taskId: task.id
1846
+ });
1847
+ return true;
1848
+ }
1849
+ /** Load the live board tasks for a set of links (board-authoritative read). */
1850
+ async loadTasks(links) {
1851
+ const out = [];
1852
+ for (const link of links) {
1853
+ try {
1854
+ const task = await this.ctx.taskStore.getTask(link.taskId);
1855
+ out.push(task ?? void 0);
1856
+ } catch {
1857
+ out.push(void 0);
1858
+ }
1859
+ }
1860
+ return out;
1861
+ }
1862
+ };
1863
+ async function reconcileCePipelines(ctx) {
1864
+ return new CeReconciler(ctx).reconcile();
1865
+ }
1866
+
1867
+ // ../../plugins/fusion-plugin-compound-engineering/src/index.ts
1868
+ var plugin = definePlugin({
1869
+ manifest: {
1870
+ id: "fusion-plugin-compound-engineering",
1871
+ name: "Compound Engineering",
1872
+ version: "0.1.0",
1873
+ description: "A dedicated dashboard surface for compound-engineering artifacts and interactive ce-* sessions.",
1874
+ author: "Fusion Team",
1875
+ fusionVersion: ">=0.1.0",
1876
+ skills: COMPOUND_ENGINEERING_SKILLS.map((s) => ({ skillId: s.skillId, name: s.name })),
1877
+ settingsSchema
1878
+ },
1879
+ state: "installed",
1880
+ skills: COMPOUND_ENGINEERING_SKILLS,
1881
+ hooks: {
1882
+ // Idempotent DDL for the plugin-local CE tables (ce_sessions). Runs against
1883
+ // the same DB route handlers reach via ctx.taskStore.getDatabase() (U5).
1884
+ onSchemaInit: ensureCeSchema,
1885
+ // INBOUND board→pipeline sync (U8 / FN-5719). The 5s hook budget
1886
+ // (plugin-runner invokeHookSafe) means these MUST be fast: resolve the link,
1887
+ // ENQUEUE a sync signal, and return. Heavy advancement (board reads, outbound
1888
+ // task creation) happens in the reconciler, NOT inline here. A reconcile
1889
+ // drain is fired-and-forgotten (never awaited) so a slow sweep cannot blow
1890
+ // the hook budget; correctness does not depend on it firing because the next
1891
+ // reconcile() sweep re-derives the transition from board truth.
1892
+ onTaskMoved: (task, fromColumn, toColumn, ctx) => {
1893
+ const store = getCePipelineStore(ctx);
1894
+ const link = store.findByTaskId(task.id);
1895
+ if (!link) return;
1896
+ store.enqueueSync({
1897
+ cePipelineId: link.cePipelineId,
1898
+ taskId: task.id,
1899
+ reason: "task_moved",
1900
+ fromColumn,
1901
+ toColumn
1902
+ });
1903
+ if (!getReconcileOnHooks(ctx.settings)) return;
1904
+ void Promise.resolve().then(() => reconcileCePipelines(ctx)).catch((err) => ctx.logger.warn(`CE reconcile (onTaskMoved) failed: ${String(err)}`));
1905
+ },
1906
+ onTaskCompleted: (task, ctx) => {
1907
+ const store = getCePipelineStore(ctx);
1908
+ const link = store.findByTaskId(task.id);
1909
+ if (!link) return;
1910
+ store.enqueueSync({
1911
+ cePipelineId: link.cePipelineId,
1912
+ taskId: task.id,
1913
+ reason: "task_completed",
1914
+ toColumn: "done"
1915
+ });
1916
+ if (!getReconcileOnHooks(ctx.settings)) return;
1917
+ void Promise.resolve().then(() => reconcileCePipelines(ctx)).catch((err) => ctx.logger.warn(`CE reconcile (onTaskCompleted) failed: ${String(err)}`));
1918
+ },
1919
+ // Install the bundled, pinned ce-* SKILL.md files into a plugin-local,
1920
+ // discoverable directory on load. The engine ingests
1921
+ // PluginSkillContribution only as a name; physical discovery requires the
1922
+ // files to exist on a path it scans (U2 finding). Install is idempotent
1923
+ // (skip-if-exists) and guarded to never touch a global ~/.claude/skills.
1924
+ onLoad: async (ctx) => {
1925
+ try {
1926
+ const { targetRoot, results } = installBundledCeSkills();
1927
+ const installed = results.filter((r) => r.outcome === "installed").length;
1928
+ const errored = results.filter((r) => r.outcome === "error");
1929
+ if (errored.length > 0) {
1930
+ ctx.logger.warn(
1931
+ `Compound Engineering: ${errored.length} skill(s) failed to install: ${errored.map((e) => `${e.skillId} (${e.reason})`).join(", ")}`
1932
+ );
1933
+ }
1934
+ ctx.logger.info(
1935
+ `Compound Engineering skills ready \u2014 installed=${installed} target=${targetRoot}`
1936
+ );
1937
+ ctx.emitEvent("compound-engineering:skills-installed", { targetRoot, results });
1938
+ } catch (error) {
1939
+ const message = error instanceof Error ? error.message : String(error);
1940
+ ctx.logger.error(`Compound Engineering skill install failed: ${message}`);
1941
+ }
1942
+ }
1943
+ },
1944
+ routes: [...createSessionRoutes(), ...createArtifactRoutes()],
1945
+ dashboardViews: [
1946
+ {
1947
+ viewId: "compound-engineering",
1948
+ label: "Compound Engineering",
1949
+ componentPath: "./dashboard-view",
1950
+ icon: "Sparkles",
1951
+ placement: "primary",
1952
+ order: 36
1953
+ }
1954
+ ]
1955
+ });
1956
+ var index_default = plugin;
1957
+ export {
1958
+ CE_PLUGIN_ID,
1959
+ CE_WORK_SOURCE_TYPE,
1960
+ COMPOUND_ENGINEERING_SKILLS,
1961
+ CeOrchestrator,
1962
+ CePipelineStore,
1963
+ CeReconciler,
1964
+ CeSessionStore,
1965
+ WORK_STAGE_ID,
1966
+ index_default as default,
1967
+ ensureCeSchema,
1968
+ getCePipelineStore,
1969
+ getCeSessionStore,
1970
+ getDefaultModelId,
1971
+ getDefaultProvider,
1972
+ getEnabledStages,
1973
+ getReconcileIntervalMinutes,
1974
+ getReconcileOnHooks,
1975
+ getStage,
1976
+ installBundledCeSkills,
1977
+ isPluginLocalPath,
1978
+ listStages,
1979
+ reconcileCePipelines,
1980
+ registerStage,
1981
+ resolveBundledSkillsRoot,
1982
+ resolveDefaultInstallTargetRoot,
1983
+ settingsSchema
1984
+ };