@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,1272 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { dirname, extname, isAbsolute, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { premiumPersonaRoot, configureAndInstallPremiumPersonas } from '../checkin.mjs';
7
+ import { attachPersonaToIntent, detachPersonaFromIntent } from '../intents.mjs';
8
+ import { createPersona, listPersonas, personalPersonaRoot, projectPersonaRoot } from '../personas.mjs';
9
+ import { loadProjectConfig } from '../project.mjs';
10
+ import {readDashboardPreferences,saveDashboardPreferences,premiumPersonasActive} from '../dashboard-preferences.mjs';
11
+ import { readPortfolioWorkspace } from '../portfolio.mjs';
12
+ import { readCompanionGuidance } from '../companion-guidance.mjs';
13
+ import {
14
+ ROLLOUT_ADVISORY_NOTICE,
15
+ readRolloutWorkspace,
16
+ } from '../network-rollout.mjs';
17
+ import { readPersonaTestScenarioWorkspace } from '../test-scenarios.mjs';
18
+ import {
19
+ MEETING_EVIDENCE_DISCLAIMER,
20
+ prepareMeetingExtraction,
21
+ promoteMeetingEvidence,
22
+ readMeetingEvidenceWorkspace,
23
+ } from '../meeting-evidence.mjs';
24
+ import {
25
+ KNOWLEDGE_PROPOSALS_DISCLAIMER,
26
+ materialiseKnowledgeProposals,
27
+ prepareKnowledgeProposals,
28
+ readKnowledgeProposalWorkspace,
29
+ recordKnowledgeProposalBundle,
30
+ recordKnowledgeProposalReview,
31
+ recoverKnowledgeMaterialisation,
32
+ } from '../knowledge-proposals.mjs';
33
+ import { listRuntimeIntents, readRuntimeIntent, runtimeIntentSummary } from './intents.mjs';
34
+ import { initializeRuntime } from './database.mjs';
35
+ import { listKnowledge, readKnowledgeDocument } from './knowledge.mjs';
36
+ import { palaceTidiness, searchPalace } from './palace.mjs';
37
+ import { runtimePaths } from './paths.mjs';
38
+ import { dashboardRuntimeVersion } from './version.mjs';
39
+ import {
40
+ prepareProjectContext,
41
+ readStoredContextManifest,
42
+ safeContextManifest,
43
+ } from './context-assembly.mjs';
44
+ import {
45
+ approveBuildFromDashboard,
46
+ controlAfkFromDashboard,
47
+ dashboardAfkRuns,
48
+ enterBuildFromDashboard,
49
+ preflightAfkFromDashboard,
50
+ requestGuidedDashboardWork,
51
+ startAfkFromDashboard,
52
+ } from './dashboard-actions.mjs';
53
+ import { listDashboardHandoffs, resolveDashboardHandoff } from './dashboard-handoffs.mjs';
54
+ import {
55
+ approveGuidedDiscovery,
56
+ guidedDiscoveryOwnerEvidence,
57
+ readGuidedDiscovery,
58
+ saveGuidedDiscoveryDraft
59
+ } from './guided-discovery.mjs';
60
+ import {
61
+ compareEvidenceDepthWorkspaceRuns,
62
+ prepareEvidenceDepthWorkspace,
63
+ readEvidenceDepthWorkspace,
64
+ recordEvidenceDepthWorkspaceRun,
65
+ } from './evidence-depth-workspace.mjs';
66
+ import {
67
+ comparePrototypeWorkspaceReviews,
68
+ preparePrototypeCycleWorkspace,
69
+ preparePrototypePlanWorkspace,
70
+ readPrototypeIterationWorkspace,
71
+ recordPrototypeCycleWorkspaceReview,
72
+ recordPrototypePlanWorkspaceReview,
73
+ } from './prototype-iterations.mjs';
74
+ import {
75
+ approveGuidedIntent,
76
+ discardGuidedIntentDraft,
77
+ readGuidedIntent,
78
+ saveGuidedIntentDraft,
79
+ } from './guided-intents.mjs';
80
+ import {
81
+ confirmPhaseContribution,
82
+ discardPhaseContributionDraft,
83
+ handoffPhaseContribution,
84
+ readPhaseStudio,
85
+ requestPhaseContributionReview,
86
+ savePhaseContributionDraft,
87
+ } from './phase-contributions.mjs';
88
+ import {
89
+ confirmImpactAssessment,
90
+ previewImpactAssessment,
91
+ readImpactWorkspace
92
+ } from './impact-analysis.mjs';
93
+ import {
94
+ POLICY_DESIGN_AUTHORITY_NOTICE,
95
+ confirmPolicyFactsAction,
96
+ evaluatePolicyDesignAction,
97
+ readPolicyWorkspace,
98
+ recordPolicyExceptionAction,
99
+ recordPolicyReviewAction,
100
+ } from './policy-workspace.mjs';
101
+ import {
102
+ refreshRepositoryIndex,
103
+ repositorySourceMapCoverage,
104
+ repositorySourceMapFiles,
105
+ repositorySourceMapProfiles
106
+ } from './repository-index.mjs';
107
+ import {
108
+ disableLifecycleSubscription,
109
+ dispatchEligibleLifecycleDeliveries,
110
+ readLifecycleHookWorkspace,
111
+ reconcileCanonicalLifecycleEvents,
112
+ retryLifecycleDelivery,
113
+ } from './lifecycle-hooks.mjs';
114
+ import { ASSURANCE_NOTICE } from '../security-validation-config.mjs';
115
+ import {
116
+ cancelSecurityRun,
117
+ executeSecurityRun,
118
+ importSecurityArtifacts,
119
+ prepareSecurityArtifactImport,
120
+ prepareSecurityRun,
121
+ readSecurityWorkspace,
122
+ recordSecurityDisposition,
123
+ resolveSecurityRevision,
124
+ } from './security-validation.mjs';
125
+ import {
126
+ applyStarterMaterialisation,
127
+ prepareStarterMaterialisation,
128
+ readStarterMaterialisationWorkspace,
129
+ recoverStarterMaterialisation,
130
+ selectStarterPersonas,
131
+ } from './starter-materialisation.mjs';
132
+ import {
133
+ addActivityEvent,
134
+ addArtefact,
135
+ finishActiveSession,
136
+ listActiveSessions,
137
+ listActivityEvents,
138
+ listGroups,
139
+ listWorkItems,
140
+ readWorkItemView,
141
+ resolvePrototypeAsset,
142
+ startActiveSession,
143
+ updateWorkItem
144
+ } from './work.mjs';
145
+ import {
146
+ archiveErrorReport,
147
+ captureErrorReportFailure,
148
+ createLocalErrorReport,
149
+ deleteErrorReport,
150
+ finaliseLocalErrorReport,
151
+ listErrorReportProviders,
152
+ listErrorReportReceipts,
153
+ listLocalErrorReports,
154
+ prepareErrorReportEmail,
155
+ readLocalErrorReport,
156
+ submitErrorReportProvider,
157
+ updateErrorReportSettings,
158
+ updateLocalErrorReport,
159
+ } from './error-reporting.mjs';
160
+ import {
161
+ connectTeamHub,
162
+ disconnectTeamHub,
163
+ inspectConnectedTeamHubResource,
164
+ installConnectedTeamHubResource,
165
+ listConnectedTeamHubResources,
166
+ listTeamHubResourceReceipts,
167
+ readTeamHubWorkspace,
168
+ syncTeamHub,
169
+ } from './team-hub-client.mjs';
170
+
171
+ const installRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
172
+ const packageVersion = JSON.parse(readFileSync(resolve(installRoot, 'package.json'), 'utf8')).version;
173
+
174
+ function option(name, fallback = '') {
175
+ const index = process.argv.indexOf(name);
176
+ return index === -1 ? fallback : process.argv[index + 1] ?? fallback;
177
+ }
178
+
179
+ const projectRoot = resolve(option('--project', process.cwd()));
180
+ const requestedPort = Number(option('--port', '4787'));
181
+ const project = loadProjectConfig(projectRoot);
182
+ const paths = runtimePaths(projectRoot);
183
+ const startedAt = new Date().toISOString();
184
+ initializeRuntime(projectRoot);
185
+
186
+ let hookTickRunning = false;
187
+ async function lifecycleHookTick() {
188
+ if (hookTickRunning) return;
189
+ hookTickRunning = true;
190
+ try {
191
+ reconcileCanonicalLifecycleEvents(projectRoot);
192
+ await dispatchEligibleLifecycleDeliveries(projectRoot, { limit: 10 });
193
+ } catch {
194
+ // Hook delivery is observable through its own ledger and must not stop the dashboard.
195
+ } finally {
196
+ hookTickRunning = false;
197
+ }
198
+ }
199
+
200
+ const lifecycleHookTimer = setInterval(lifecycleHookTick, 1_500);
201
+ lifecycleHookTimer.unref();
202
+
203
+ function json(response, statusCode, body) {
204
+ response.writeHead(statusCode, {
205
+ 'content-type': 'application/json; charset=utf-8',
206
+ 'cache-control': 'no-store'
207
+ });
208
+ response.end(JSON.stringify(body, null, 2));
209
+ }
210
+
211
+ function rolloutError(response, statusCode, message) {
212
+ return json(response, statusCode, {
213
+ error: message,
214
+ notices: { advisory: ROLLOUT_ADVISORY_NOTICE, security: ASSURANCE_NOTICE },
215
+ });
216
+ }
217
+
218
+ async function readJson(request) {
219
+ const expectedOrigin = `http://${request.headers.host}`;
220
+ if (!request.headers.origin || request.headers.origin !== expectedOrigin) {
221
+ const error = new Error('Mutation requests require the matching dashboard origin');
222
+ error.statusCode = 403;
223
+ throw error;
224
+ }
225
+ if (!String(request.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
226
+ const error = new Error('Mutation requests require application/json');
227
+ error.statusCode = 415;
228
+ throw error;
229
+ }
230
+ const chunks = [];
231
+ let size = 0;
232
+ for await (const chunk of request) {
233
+ size += chunk.length;
234
+ if (size > 1024 * 1024) {
235
+ const error = new Error('Request body exceeds 1 MB');
236
+ error.statusCode = 413;
237
+ throw error;
238
+ }
239
+ chunks.push(chunk);
240
+ }
241
+ if (!chunks.length) return {};
242
+ try {
243
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
244
+ } catch {
245
+ const error = new Error('Request body is not valid JSON');
246
+ error.statusCode = 400;
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ function isWithinRoot(root, path) {
252
+ const rel = relative(root, path);
253
+ return rel === '' || (rel && !rel.startsWith('..') && !isAbsolute(rel));
254
+ }
255
+
256
+ function premiumSummary() {
257
+ const empty = {
258
+ provider: 'wordpress-edd', access: 'unknown', status: 'not-verified', installed: false,
259
+ verified: false, active: false, compatibility: null, revision: null, showUpgrade: false, upgradeUrl: null,
260
+ };
261
+ if (!existsSync(paths.checkinStatePath)) return empty;
262
+ try {
263
+ const state = JSON.parse(readFileSync(paths.checkinStatePath, 'utf8'));
264
+ const premium = state?.schema === 'ewai.checkin-state/v1' ? state.premium : null;
265
+ let upgradeUrl = null;
266
+ if (premium?.upgradeUrl) {
267
+ const candidate = new URL(premium.upgradeUrl);
268
+ if (candidate.protocol === 'https:') upgradeUrl = candidate.href;
269
+ }
270
+ return {
271
+ provider: 'wordpress-edd',
272
+ access: premium?.access ?? 'unknown',
273
+ status: premium?.status ?? 'not-verified',
274
+ installed: Boolean(premium?.installed),
275
+ verified: Boolean(premium?.verified),
276
+ active: premiumPersonasActive(premium),
277
+ compatibility: ['current', 'compatible-legacy'].includes(premium?.compatibility) ? premium.compatibility : null,
278
+ revision: /^[a-f0-9]{40,64}$/.test(String(premium?.revision ?? '')) ? premium.revision : null,
279
+ version: /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(premium?.version ?? '')) ? premium.version : null,
280
+ latestVersion: /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(premium?.latestVersion ?? '')) ? premium.latestVersion : null,
281
+ planType: ['individual', 'team'].includes(premium?.planType) ? premium.planType : null,
282
+ showUpgrade: (premium?.access === 'unavailable' || premium?.accessReason === 'licence-not-configured') && !premium?.installed && Boolean(upgradeUrl),
283
+ upgradeUrl
284
+ };
285
+ } catch {
286
+ return empty;
287
+ }
288
+ }
289
+
290
+ function guarded(operation) {
291
+ try {
292
+ return operation();
293
+ } catch (error) {
294
+ if (!error.statusCode) error.statusCode = 409;
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ function strictBody(input, allowed, label) {
300
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
301
+ const error = new Error(`${label} must be a JSON object.`);
302
+ error.statusCode = 400;
303
+ throw error;
304
+ }
305
+ for (const key of Object.keys(input)) {
306
+ if (!allowed.includes(key)) {
307
+ const error = new Error(`${label} contains unknown field: ${key}`);
308
+ error.statusCode = 400;
309
+ throw error;
310
+ }
311
+ }
312
+ return input;
313
+ }
314
+
315
+ function dashboardWorkItemView(reference) {
316
+ const view = readWorkItemView(projectRoot, reference);
317
+ if (!view) return null;
318
+ const personas = personaLibrary();
319
+ const sourceMapCoverage = repositorySourceMapCoverage(projectRoot);
320
+ const activePersonas = (view.intent?.personas ?? []).map((attachment) => {
321
+ const persona = personas.find((candidate) => candidate.id === attachment.ref);
322
+ return {
323
+ id: attachment.ref,
324
+ name: persona?.name ?? attachment.ref,
325
+ tier: persona?.tier ?? (attachment.ref.includes('.premium.') ? 'premium-unavailable' : 'unavailable'),
326
+ role: attachment.role,
327
+ depth: attachment.depth,
328
+ engagementReason: `Attached to ${view.item.title} for the current Source Map and impact context.`
329
+ };
330
+ });
331
+ return {
332
+ ...view,
333
+ impact: readImpactWorkspace(projectRoot, view.item.slug),
334
+ sourceMap: {
335
+ schema: 'ewai.dashboard-source-map/v1',
336
+ coverage: sourceMapCoverage,
337
+ profiles: repositorySourceMapProfiles(projectRoot, { limit: 40 }),
338
+ files: repositorySourceMapFiles(projectRoot, { attentionOnly: true, limit: 80 }),
339
+ activePersonas,
340
+ guidance: { advisory: true, humanReviewRequired: true, personasAreLenses: true }
341
+ },
342
+ testScenarios: readPersonaTestScenarioWorkspace(projectRoot, view.item.slug, { personas }),
343
+ afk: dashboardAfkRuns(projectRoot, view.item.slug),
344
+ dashboardHandoff: listDashboardHandoffs(projectRoot, { status: 'pending' })
345
+ .find((handoff) => handoff.intentId === view.item.id && ['begin', 'continue'].includes(handoff.action)) ?? null,
346
+ };
347
+ }
348
+
349
+ function personaLibrary(query = '') {
350
+ const ranks = { project: 4, personal: 3, premium: 2, core: 1 };
351
+ const personas = listPersonas([
352
+ resolve(installRoot, 'packs/personas/core/personas'),
353
+ premiumPersonaRoot(),
354
+ personalPersonaRoot(),
355
+ projectPersonaRoot(projectRoot)
356
+ ], query);
357
+ const unique = new Map();
358
+ for (const persona of personas) {
359
+ const current = unique.get(persona.id);
360
+ if (!current || (ranks[persona.tier] ?? 0) > (ranks[current.tier] ?? 0)) unique.set(persona.id, persona);
361
+ }
362
+ return [...unique.values()].map(({ path: _path, parseStatus: _parseStatus, ...persona }) => persona);
363
+ }
364
+
365
+ function starterPersonas(moment) {
366
+ return selectStarterPersonas(personaLibrary(), moment);
367
+ }
368
+
369
+ const mime = {
370
+ '.html': 'text/html; charset=utf-8',
371
+ '.css': 'text/css; charset=utf-8',
372
+ '.js': 'text/javascript; charset=utf-8',
373
+ '.json': 'application/json; charset=utf-8',
374
+ '.svg': 'image/svg+xml',
375
+ '.png': 'image/png',
376
+ '.jpg': 'image/jpeg',
377
+ '.jpeg': 'image/jpeg',
378
+ '.webp': 'image/webp',
379
+ '.gif': 'image/gif',
380
+ '.ico': 'image/x-icon',
381
+ '.woff': 'font/woff',
382
+ '.woff2': 'font/woff2'
383
+ };
384
+
385
+ function serveStatic(response, pathname) {
386
+ const relativePath = pathname === '/' ? 'index.html' : pathname.slice(1);
387
+ const path = resolve(paths.publicRoot, relativePath);
388
+ if (!isWithinRoot(paths.publicRoot, path) || !existsSync(path)) return false;
389
+ response.writeHead(200, {
390
+ 'content-type': mime[extname(path)] || 'application/octet-stream',
391
+ 'cache-control': 'no-store',
392
+ 'x-content-type-options': 'nosniff',
393
+ 'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'"
394
+ });
395
+ createReadStream(path).pipe(response);
396
+ return true;
397
+ }
398
+
399
+ function servePrototype(response, reference, artefactId, assetPath) {
400
+ const resolved = resolvePrototypeAsset(projectRoot, reference, artefactId, assetPath);
401
+ if (!resolved) return false;
402
+ response.writeHead(200, {
403
+ 'content-type': mime[extname(resolved.path).toLowerCase()] || 'application/octet-stream',
404
+ 'cache-control': 'no-store',
405
+ 'x-content-type-options': 'nosniff',
406
+ 'content-security-policy': "sandbox allow-scripts allow-forms; default-src 'self' data: blob:; connect-src 'none'; frame-ancestors 'self'; base-uri 'none'"
407
+ });
408
+ createReadStream(resolved.path).pipe(response);
409
+ return true;
410
+ }
411
+
412
+ const server = createServer(async (request, response) => {
413
+ const host = request.headers.host ?? '';
414
+ if (!/^(?:127\.0\.0\.1|localhost):\d+$/.test(host)) {
415
+ const securityRequest = String(request.url ?? '').startsWith('/api/security-validation');
416
+ return json(response, 403, securityRequest
417
+ ? { error: 'Loopback host required', assurance_notice: ASSURANCE_NOTICE }
418
+ : { error: 'Loopback host required' });
419
+ }
420
+
421
+ const url = new URL(request.url ?? '/', `http://${host}`);
422
+ try {
423
+ if (request.method === 'GET' && url.pathname === '/api/health') {
424
+ return json(response, 200, {
425
+ ok: true,
426
+ schema: 'ewai.dashboard-health/v1',
427
+ runtimeVersion: dashboardRuntimeVersion,
428
+ projectRoot,
429
+ projectName: project.config.project.name,
430
+ pid: process.pid,
431
+ port: requestedPort,
432
+ url: `http://127.0.0.1:${requestedPort}`,
433
+ startedAt,
434
+ intents: runtimeIntentSummary(projectRoot)
435
+ });
436
+ }
437
+ if (request.method === 'GET' && url.pathname === '/api/dashboard/preferences') {
438
+ return json(response,200,guarded(()=>readDashboardPreferences(projectRoot)));
439
+ }
440
+ if (request.method === 'POST' && url.pathname === '/api/dashboard/preferences') {
441
+ const input=strictBody(await readJson(request),['confirmed','expectedDigest','preferences'],'Dashboard preferences');
442
+ return json(response,200,guarded(()=>saveDashboardPreferences(projectRoot,input)));
443
+ }
444
+ if (request.method === 'GET' && url.pathname === '/api/project') {
445
+ const workItems = listWorkItems(projectRoot);
446
+ return json(response, 200, {
447
+ name: project.config.project.name,
448
+ root: projectRoot,
449
+ premium: premiumSummary(),
450
+ intents: runtimeIntentSummary(projectRoot),
451
+ work: {
452
+ count: workItems.length,
453
+ sprint: workItems.filter((item) => item.currentSprint).length,
454
+ active: workItems.filter((item) => item.lane === 'active').length,
455
+ blocked: workItems.filter((item) => item.lane === 'blocked').length
456
+ }
457
+ });
458
+ }
459
+ if (request.method === 'GET' && url.pathname === '/api/intents') {
460
+ return json(response, 200, {
461
+ intents: listRuntimeIntents(projectRoot, {
462
+ query: url.searchParams.get('q') ?? '',
463
+ status: url.searchParams.get('status') ?? ''
464
+ })
465
+ });
466
+ }
467
+ if (request.method === 'GET' && url.pathname === '/api/intent') {
468
+ const intent = readRuntimeIntent(projectRoot, url.searchParams.get('id') ?? '');
469
+ return intent ? json(response, 200, { intent }) : json(response, 404, { error: 'Intent not found' });
470
+ }
471
+ if (request.method === 'GET' && url.pathname === '/api/work-items') {
472
+ return json(response, 200, {
473
+ items: listWorkItems(projectRoot, {
474
+ query: url.searchParams.get('q') ?? '',
475
+ group: url.searchParams.get('group') ?? '',
476
+ lane: url.searchParams.get('lane') ?? '',
477
+ sprint: url.searchParams.get('sprint') ?? ''
478
+ })
479
+ });
480
+ }
481
+ if (request.method === 'GET' && url.pathname === '/api/groups') {
482
+ return json(response, 200, { groups: listGroups(projectRoot) });
483
+ }
484
+ if (request.method === 'GET' && url.pathname === '/api/personas') {
485
+ return json(response, 200, { personas: personaLibrary(url.searchParams.get('q') ?? '') });
486
+ }
487
+ if (request.method === 'POST' && url.pathname === '/api/personas/premium/setup') {
488
+ let input;
489
+ try {
490
+ input = strictBody(await readJson(request), ['licenceKey', 'machineName', 'confirmed'], 'Premium persona setup');
491
+ if (input.confirmed !== true || typeof input.licenceKey !== 'string' || !input.licenceKey.trim()
492
+ || input.licenceKey.length > 512 || /[\x00-\x1f\x7f]/.test(input.licenceKey)
493
+ || (input.machineName !== undefined && (typeof input.machineName !== 'string' || input.machineName.length > 120))) {
494
+ return json(response, 400, { error: 'Provide a valid key and confirm verify-and-install.' });
495
+ }
496
+ const result = await configureAndInstallPremiumPersonas(projectRoot, { licenceKey: input.licenceKey, machineName: input.machineName, confirmed: true });
497
+ return json(response, result.status === 'ready' ? 200 : 400, result.status === 'ready' ? result : { ...result, error: result.message });
498
+ } catch (error) {
499
+ return json(response, [403, 415, 413].includes(error.statusCode) ? error.statusCode : 400,
500
+ { error: error.statusCode === 403 ? 'Use the matching dashboard origin.' : error.statusCode === 415 ? 'Use application/json.' : error.statusCode === 413 ? 'Setup request is too large.' : 'Setup could not be completed. Check the key and retry when no other setup is running.' });
501
+ } finally { if (input) input.licenceKey = undefined; }
502
+ }
503
+ if (request.method === 'GET' && url.pathname === '/api/policies') {
504
+ const unknown = [...url.searchParams.keys()].filter((key) => !['intent', 'mode'].includes(key));
505
+ if (unknown.length) return json(response, 400, {
506
+ error: `Unsupported policy query: ${unknown[0]}`,
507
+ authority_notice: POLICY_DESIGN_AUTHORITY_NOTICE,
508
+ });
509
+ if (!['business', 'technical'].includes(url.searchParams.get('mode') ?? 'business')) return json(response, 400, {
510
+ error: 'Policy workspace mode must be business or technical',
511
+ authority_notice: POLICY_DESIGN_AUTHORITY_NOTICE,
512
+ });
513
+ return json(response, 200, readPolicyWorkspace(projectRoot, {
514
+ intentReference: url.searchParams.get('intent') ?? '',
515
+ mode: url.searchParams.get('mode') ?? 'business',
516
+ personas: personaLibrary(),
517
+ }));
518
+ }
519
+ if (request.method === 'POST' && url.pathname === '/api/policies/facts/confirm') {
520
+ const input = strictBody(await readJson(request), ['intentReference', 'expectedRevision', 'expectedProposalDigest', 'confirmed', 'authority', 'confirmedBy', 'decisions'], 'Policy fact confirmation request');
521
+ return json(response, 200, guarded(() => confirmPolicyFactsAction(projectRoot, input, { personas: personaLibrary() })));
522
+ }
523
+ if (request.method === 'POST' && url.pathname === '/api/policies/evaluate') {
524
+ const input = strictBody(await readJson(request), ['intentReference', 'expectedIntentRevision', 'expectedPolicyDigest', 'expectedFactsDigest'], 'Policy evaluation request');
525
+ return json(response, 200, guarded(() => evaluatePolicyDesignAction(projectRoot, input, { personas: personaLibrary() })));
526
+ }
527
+ if (request.method === 'POST' && url.pathname === '/api/policies/reviews') {
528
+ const input = strictBody(await readJson(request), ['intentReference', 'evaluationDigest', 'ruleId', 'authority', 'reviewedBy', 'reviewerRole', 'decision', 'rationale', 'evidence', 'controls'], 'Policy review request');
529
+ return json(response, 200, guarded(() => recordPolicyReviewAction(projectRoot, input, { personas: personaLibrary() })));
530
+ }
531
+ if (request.method === 'POST' && url.pathname === '/api/policies/exceptions') {
532
+ const input = strictBody(await readJson(request), ['intentReference', 'evaluationDigest', 'ruleId', 'authority', 'approvedBy', 'owner', 'reviewerRole', 'scope', 'rationale', 'compensatingControls', 'evidence', 'expiresAt'], 'Policy exception request');
533
+ return json(response, 200, guarded(() => recordPolicyExceptionAction(projectRoot, input, { personas: personaLibrary() })));
534
+ }
535
+ if (request.method === 'POST' && url.pathname === '/api/context-packs/prepare') {
536
+ const input = strictBody(
537
+ await readJson(request),
538
+ ['profile', 'slug', 'taskId', 'phase', 'focus', 'budgetTokens', 'previousDigest'],
539
+ 'Context preparation request',
540
+ );
541
+ const pack = guarded(() => prepareProjectContext(projectRoot, {
542
+ ...input,
543
+ personaCatalogue: personaLibrary(),
544
+ }));
545
+ return json(response, 200, safeContextManifest(pack));
546
+ }
547
+ const contextPack = url.pathname.match(/^\/api\/context-packs\/([^/]+)$/);
548
+ if (request.method === 'GET' && contextPack) {
549
+ try {
550
+ return json(response, 200, readStoredContextManifest(projectRoot, decodeURIComponent(contextPack[1])));
551
+ } catch (error) {
552
+ error.statusCode = /not found/i.test(error.message) ? 404 : 400;
553
+ throw error;
554
+ }
555
+ }
556
+ if (request.method === 'GET' && url.pathname === '/api/companion') {
557
+ const unknown = [...new Set(url.searchParams.keys())].filter((key) => key !== 'focus');
558
+ if (unknown.length) return json(response, 400, { error: `Unsupported companion query: ${unknown[0]}` });
559
+ const focus = url.searchParams.get('focus') ?? '';
560
+ if (focus.length > 500 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(focus)) {
561
+ return json(response, 400, { error: 'Companion focus exceeds 500 characters or contains unsafe text' });
562
+ }
563
+ return json(response, 200, readCompanionGuidance(projectRoot, { focus, personas: personaLibrary() }));
564
+ }
565
+ if (request.method === 'GET' && url.pathname === '/api/portfolio') {
566
+ const unknown = [...new Set(url.searchParams.keys())].filter((key) => key !== 'focus');
567
+ if (unknown.length) return json(response, 400, { error: `Unsupported portfolio query: ${unknown[0]}` });
568
+ const focus = url.searchParams.get('focus') ?? '';
569
+ if (focus.length > 500) return json(response, 400, { error: 'Portfolio focus exceeds 500 characters' });
570
+ return json(response, 200, readPortfolioWorkspace(projectRoot, { focus, personas: personaLibrary() }));
571
+ }
572
+ if (request.method === 'GET' && url.pathname === '/api/team-hub') {
573
+ const unknown = [...new Set(url.searchParams.keys())];
574
+ if (unknown.length) return json(response, 400, { error: `Unsupported Team Hub query: ${unknown[0]}` });
575
+ return json(response, 200, readTeamHubWorkspace(projectRoot));
576
+ }
577
+ if (request.method === 'POST' && url.pathname === '/api/team-hub/connect') {
578
+ const input = strictBody(
579
+ await readJson(request),
580
+ ['endpoint', 'projectId', 'tokenEnv', 'confirmed', 'disclosureAcknowledged'],
581
+ 'Team Hub connection request',
582
+ );
583
+ guarded(() => connectTeamHub(projectRoot, input));
584
+ return json(response, 200, readTeamHubWorkspace(projectRoot));
585
+ }
586
+ if (request.method === 'POST' && url.pathname === '/api/team-hub/sync') {
587
+ const input = strictBody(await readJson(request), ['confirmed'], 'Team Hub sync request');
588
+ return json(response, 200, await syncTeamHub(projectRoot, input));
589
+ }
590
+ if (request.method === 'POST' && url.pathname === '/api/team-hub/disconnect') {
591
+ const input = strictBody(await readJson(request), ['confirmed'], 'Team Hub disconnect request');
592
+ return json(response, 200, guarded(() => disconnectTeamHub(projectRoot, input)));
593
+ }
594
+ if (request.method === 'GET' && url.pathname === '/api/team-hub/resources') {
595
+ if ([...url.searchParams.keys()].length) return json(response, 400, { error: 'Team Hub resources do not accept query parameters' });
596
+ return json(response, 200, await listConnectedTeamHubResources(projectRoot));
597
+ }
598
+ const teamHubResource = url.pathname.match(/^\/api\/team-hub\/resources\/([^/]+)\/releases\/([^/]+)$/);
599
+ if (request.method === 'GET' && teamHubResource) {
600
+ if ([...url.searchParams.keys()].length) return json(response, 400, { error: 'Team Hub resource inspection does not accept query parameters' });
601
+ let id;
602
+ let version;
603
+ try { id = decodeURIComponent(teamHubResource[1]); version = decodeURIComponent(teamHubResource[2]); }
604
+ catch { return json(response, 400, { error: 'Team Hub resource identity is invalid' }); }
605
+ return json(response, 200, await inspectConnectedTeamHubResource(projectRoot, id, version));
606
+ }
607
+ if (request.method === 'POST' && url.pathname === '/api/team-hub/resources/install') {
608
+ const input = strictBody(await readJson(request), ['id', 'version', 'expectedDigest', 'approvedBy', 'confirmed'], 'Team Hub resource installation request');
609
+ return json(response, 200, await installConnectedTeamHubResource(projectRoot, input));
610
+ }
611
+ if (request.method === 'GET' && url.pathname === '/api/team-hub/resource-receipts') {
612
+ if ([...url.searchParams.keys()].length) return json(response, 400, { error: 'Team Hub resource receipts do not accept query parameters' });
613
+ return json(response, 200, { schema: 'ewai.team-hub-resource-receipts/v1', receipts: listTeamHubResourceReceipts(projectRoot) });
614
+ }
615
+ if (request.method === 'GET' && url.pathname === '/api/rollout') {
616
+ const unknown = [...new Set(url.searchParams.keys())].filter((key) => key !== 'focus');
617
+ if (unknown.length) return rolloutError(response, 400, `Unsupported rollout query: ${unknown[0]}`);
618
+ const focus = url.searchParams.get('focus') ?? '';
619
+ if (focus.length > 500 || focus.includes('\0')) return rolloutError(response, 400, 'Rollout focus exceeds 500 characters or contains unsafe text');
620
+ return json(response, 200, readRolloutWorkspace(projectRoot, { focus, personas: personaLibrary() }));
621
+ }
622
+ const rolloutAssurance = url.pathname.match(/^\/api\/rollout\/projects\/([^/]+)\/assurance$/);
623
+ if (request.method === 'GET' && rolloutAssurance) {
624
+ if ([...url.searchParams.keys()].length) return rolloutError(response, 400, 'Focused rollout assurance accepts no query parameters');
625
+ const projectId = decodeURIComponent(rolloutAssurance[1]);
626
+ return json(response, 200, readRolloutWorkspace(projectRoot, { projectId, personas: personaLibrary() }));
627
+ }
628
+ if (request.method === 'GET' && url.pathname === '/api/hooks') {
629
+ guarded(() => reconcileCanonicalLifecycleEvents(projectRoot));
630
+ return json(response, 200, readLifecycleHookWorkspace(projectRoot, {
631
+ status: url.searchParams.get('status') ?? '',
632
+ event: url.searchParams.get('event') ?? '',
633
+ handler: url.searchParams.get('handler') ?? '',
634
+ }));
635
+ }
636
+ if (request.method === 'GET' && url.pathname === '/api/security-validation') {
637
+ return json(response, 200, readSecurityWorkspace(projectRoot, { personas: personaLibrary() }));
638
+ }
639
+ if (request.method === 'GET' && url.pathname === '/api/error-reports') {
640
+ const unknown = [...url.searchParams.keys()].filter((key) => key !== 'status');
641
+ if (unknown.length) return json(response, 400, { error: `Unsupported error-report query: ${unknown[0]}` });
642
+ const status = url.searchParams.get('status') ?? '';
643
+ if (status && !['draft', 'finalised', 'archived'].includes(status)) return json(response, 400, { error: 'Error-report status filter is invalid.' });
644
+ return json(response, 200, {
645
+ ...listLocalErrorReports(projectRoot, { status }),
646
+ providers: listErrorReportProviders(projectRoot).providers,
647
+ });
648
+ }
649
+ if (request.method === 'POST' && url.pathname === '/api/error-reports') {
650
+ const input = strictBody(await readJson(request), ['capability', 'errorCode', 'command', 'title', 'expected', 'actual', 'reproductionSteps'], 'Error report creation request');
651
+ return json(response, 201, createLocalErrorReport(projectRoot, {
652
+ ...input,
653
+ ewaiVersion: packageVersion,
654
+ nodeVersion: process.versions.node,
655
+ osClass: ['darwin', 'linux', 'win32'].includes(process.platform) ? process.platform : 'other',
656
+ installationSource: 'npm',
657
+ }));
658
+ }
659
+ if (request.method === 'PATCH' && url.pathname === '/api/error-reports/settings') {
660
+ const input = strictBody(await readJson(request), ['automaticLocalDrafts', 'supportEmail'], 'Error report settings request');
661
+ return json(response, 200, updateErrorReportSettings(projectRoot, input));
662
+ }
663
+ const errorReport = url.pathname.match(/^\/api\/error-reports\/(report_[a-z0-9_]{3,80})$/);
664
+ if (request.method === 'GET' && errorReport) {
665
+ if ([...url.searchParams.keys()].length) return json(response, 400, { error: 'Focused error reports accept no query parameters.' });
666
+ const reportId = decodeURIComponent(errorReport[1]);
667
+ const report = readLocalErrorReport(projectRoot, reportId);
668
+ return report
669
+ ? json(response, 200, { report, ...listErrorReportReceipts(projectRoot, reportId) })
670
+ : json(response, 404, { error: 'Error report not found.' });
671
+ }
672
+ if (request.method === 'PATCH' && errorReport) {
673
+ const input = strictBody(await readJson(request), ['title', 'expected', 'actual', 'reproductionSteps'], 'Error report update request');
674
+ return json(response, 200, updateLocalErrorReport(projectRoot, decodeURIComponent(errorReport[1]), input));
675
+ }
676
+ if (request.method === 'DELETE' && errorReport) {
677
+ const input = strictBody(await readJson(request), ['confirmed'], 'Error report deletion request');
678
+ return json(response, 200, deleteErrorReport(projectRoot, decodeURIComponent(errorReport[1]), { confirmed: input.confirmed === true }));
679
+ }
680
+ const errorReportAction = url.pathname.match(/^\/api\/error-reports\/(report_[a-z0-9_]{3,80})\/(finalise|prepare-email|provider-attempts|archive)$/);
681
+ if (request.method === 'POST' && errorReportAction) {
682
+ const reportId = decodeURIComponent(errorReportAction[1]);
683
+ const action = errorReportAction[2];
684
+ if (action === 'finalise') {
685
+ strictBody(await readJson(request), [], 'Error report finalisation request');
686
+ return json(response, 200, finaliseLocalErrorReport(projectRoot, reportId));
687
+ }
688
+ if (action === 'prepare-email') {
689
+ const input = strictBody(await readJson(request), ['expectedDigest', 'recipient', 'launch'], 'Error report email preparation request');
690
+ return json(response, 200, prepareErrorReportEmail(projectRoot, reportId, input));
691
+ }
692
+ if (action === 'provider-attempts') {
693
+ const input = strictBody(await readJson(request), ['providerId', 'expectedDigest', 'confirmed'], 'Error report provider submission request');
694
+ return json(response, 200, await submitErrorReportProvider(projectRoot, reportId, input.providerId, {
695
+ confirmed: input.confirmed === true,
696
+ expectedDigest: input.expectedDigest,
697
+ }));
698
+ }
699
+ const input = strictBody(await readJson(request), ['confirmed'], 'Error report archive request');
700
+ return json(response, 200, archiveErrorReport(projectRoot, reportId, { confirmed: input.confirmed === true }));
701
+ }
702
+ if (request.method === 'GET' && url.pathname === '/api/starter-materialisation') {
703
+ return json(response, 200, readStarterMaterialisationWorkspace(projectRoot, {
704
+ personas: starterPersonas('review'),
705
+ }));
706
+ }
707
+ if (request.method === 'POST' && url.pathname === '/api/starter-materialisation/previews') {
708
+ const input = strictBody(
709
+ await readJson(request),
710
+ ['receiptId', 'adapterId', 'confirmed', 'timeoutMs'],
711
+ 'Starter preview request',
712
+ );
713
+ const preview = await prepareStarterMaterialisation(projectRoot, input.receiptId, input.adapterId, {
714
+ confirmed: input.confirmed === true,
715
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
716
+ });
717
+ return json(response, 200, preview);
718
+ }
719
+ const starterApply = url.pathname.match(/^\/api\/starter-materialisation\/previews\/([^/]+)\/apply$/);
720
+ if (request.method === 'POST' && starterApply) {
721
+ const input = strictBody(await readJson(request), ['confirmed', 'approvedBy'], 'Starter application request');
722
+ return json(response, 200, guarded(() => applyStarterMaterialisation(
723
+ projectRoot,
724
+ decodeURIComponent(starterApply[1]),
725
+ { confirmed: input.confirmed === true, approvedBy: input.approvedBy, personas: starterPersonas('apply') },
726
+ )));
727
+ }
728
+ const starterRecovery = url.pathname.match(/^\/api\/starter-materialisation\/attempts\/([^/]+)\/recover$/);
729
+ if (request.method === 'POST' && starterRecovery) {
730
+ const input = strictBody(await readJson(request), ['confirmed'], 'Starter recovery request');
731
+ return json(response, 200, guarded(() => recoverStarterMaterialisation(
732
+ projectRoot,
733
+ decodeURIComponent(starterRecovery[1]),
734
+ { confirmed: input.confirmed === true },
735
+ )));
736
+ }
737
+ const securityRun = url.pathname.match(/^\/api\/security-validation\/profiles\/([^/]+)\/runs$/);
738
+ if (request.method === 'POST' && securityRun) {
739
+ const input = strictBody(await readJson(request), ['confirmed', 'mode', 'confirmedRuntimeTarget'], 'Security run request');
740
+ const prepared = guarded(() => prepareSecurityRun(projectRoot, decodeURIComponent(securityRun[1]), {
741
+ confirmed: input.confirmed === true,
742
+ mode: input.mode,
743
+ confirmedRuntimeTarget: input.confirmedRuntimeTarget === true,
744
+ trustedRevision: resolveSecurityRevision(projectRoot),
745
+ }));
746
+ const result = prepared.run ? await executeSecurityRun(projectRoot, prepared.run.id) : prepared;
747
+ return json(response, 200, result);
748
+ }
749
+ const securityImport = url.pathname.match(/^\/api\/security-validation\/profiles\/([^/]+)\/imports$/);
750
+ if (request.method === 'POST' && securityImport) {
751
+ const input = strictBody(await readJson(request), ['confirmed', 'providerId', 'discoveryToken'], 'Security import request');
752
+ if (input.discoveryToken) {
753
+ return json(response, 200, guarded(() => importSecurityArtifacts(projectRoot, input.discoveryToken, { confirmed: input.confirmed === true })));
754
+ }
755
+ const prepared = guarded(() => prepareSecurityArtifactImport(
756
+ projectRoot,
757
+ decodeURIComponent(securityImport[1]),
758
+ input.providerId,
759
+ { trustedRevision: resolveSecurityRevision(projectRoot) },
760
+ ));
761
+ return json(response, 202, prepared);
762
+ }
763
+ const securityCancel = url.pathname.match(/^\/api\/security-validation\/runs\/([^/]+)\/cancel$/);
764
+ if (request.method === 'POST' && securityCancel) {
765
+ const input = strictBody(await readJson(request), ['confirmed'], 'Security cancellation request');
766
+ return json(response, 200, guarded(() => cancelSecurityRun(projectRoot, decodeURIComponent(securityCancel[1]), { confirmed: input.confirmed === true })));
767
+ }
768
+ const securityDisposition = url.pathname.match(/^\/api\/security-validation\/findings\/([^/]+)\/dispositions$/);
769
+ if (request.method === 'POST' && securityDisposition) {
770
+ const input = strictBody(
771
+ await readJson(request),
772
+ ['decision', 'reviewer', 'reason', 'evidence', 'riskOwner', 'expiresAt', 'nextRole'],
773
+ 'Security disposition request',
774
+ );
775
+ return json(response, 200, guarded(() => recordSecurityDisposition(projectRoot, decodeURIComponent(securityDisposition[1]), input)));
776
+ }
777
+ const hookRetry = url.pathname.match(/^\/api\/hooks\/deliveries\/([^/]+)\/retry$/);
778
+ if (request.method === 'POST' && hookRetry) {
779
+ const input = await readJson(request);
780
+ const result = guarded(() => retryLifecycleDelivery(projectRoot, decodeURIComponent(hookRetry[1]), input));
781
+ void lifecycleHookTick();
782
+ return json(response, 200, result);
783
+ }
784
+ const hookDisable = url.pathname.match(/^\/api\/hooks\/subscriptions\/([^/]+)\/disable$/);
785
+ if (request.method === 'POST' && hookDisable) {
786
+ const input = await readJson(request);
787
+ return json(response, 200, guarded(() => disableLifecycleSubscription(projectRoot, decodeURIComponent(hookDisable[1]), input)));
788
+ }
789
+ if (request.method === 'GET' && url.pathname === '/api/guided-discovery') {
790
+ return json(response, 200, readGuidedDiscovery(projectRoot, { personas: personaLibrary() }));
791
+ }
792
+ if (request.method === 'GET' && url.pathname === '/api/evidence-depth') {
793
+ const unknown = [...new Set(url.searchParams.keys())];
794
+ if (unknown.length) return json(response, 400, { error: `Unsupported evidence depth query: ${unknown[0]}` });
795
+ return json(response, 200, guarded(() => readEvidenceDepthWorkspace(projectRoot, { personas: personaLibrary() })));
796
+ }
797
+ if (request.method === 'POST' && url.pathname === '/api/evidence-depth/prepare') {
798
+ const input = strictBody(await readJson(request), ['focus', 'ownerEvidence'], 'Evidence depth preparation request');
799
+ return json(response, 200, guarded(() => prepareEvidenceDepthWorkspace(
800
+ projectRoot,
801
+ { focus: input.focus ?? '' },
802
+ { personas: personaLibrary(), ownerEvidence: input.ownerEvidence ?? guidedDiscoveryOwnerEvidence(projectRoot) },
803
+ )));
804
+ }
805
+ if (request.method === 'POST' && url.pathname === '/api/evidence-depth/record') {
806
+ const input = strictBody(await readJson(request), ['schema', 'expectedPreparationDigest', 'reviewedBy', 'dimensions', 'grouping'], 'Evidence depth review request');
807
+ return json(response, 200, guarded(() => recordEvidenceDepthWorkspaceRun(projectRoot, input)));
808
+ }
809
+ if (request.method === 'POST' && url.pathname === '/api/evidence-depth/compare') {
810
+ const input = strictBody(await readJson(request), ['leftRunId', 'rightRunId'], 'Evidence depth comparison request');
811
+ return json(response, 200, guarded(() => compareEvidenceDepthWorkspaceRuns(projectRoot, input)));
812
+ }
813
+ const prototypeReview = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)$/);
814
+ if (request.method === 'GET' && prototypeReview) {
815
+ const unknown = [...new Set(url.searchParams.keys())];
816
+ if (unknown.length) return json(response, 400, { error: `Unsupported prototype review query: ${unknown[0]}` });
817
+ return json(response, 200, guarded(() => readPrototypeIterationWorkspace(
818
+ projectRoot,
819
+ decodeURIComponent(prototypeReview[1]),
820
+ { personas: personaLibrary() },
821
+ )));
822
+ }
823
+ const prototypePlanPrepare = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)\/plan\/prepare$/);
824
+ if (request.method === 'POST' && prototypePlanPrepare) {
825
+ const input = strictBody(await readJson(request), ['intentDigest', 'designSystem', 'plan', 'signals', 'predecessorDigest'], 'Prototype plan preparation request');
826
+ return json(response, 200, guarded(() => preparePrototypePlanWorkspace(
827
+ projectRoot,
828
+ decodeURIComponent(prototypePlanPrepare[1]),
829
+ input,
830
+ { personas: personaLibrary() },
831
+ )));
832
+ }
833
+ const prototypePlanRecord = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)\/plan\/record$/);
834
+ if (request.method === 'POST' && prototypePlanRecord) {
835
+ const input = strictBody(await readJson(request), ['schema', 'expectedPreparationDigest', 'reviewedBy', 'findings', 'assessments'], 'Prototype plan review request');
836
+ return json(response, 201, guarded(() => recordPrototypePlanWorkspaceReview(projectRoot, decodeURIComponent(prototypePlanRecord[1]), input)));
837
+ }
838
+ const prototypeCyclePrepare = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)\/cycles\/prepare$/);
839
+ if (request.method === 'POST' && prototypeCyclePrepare) {
840
+ const input = strictBody(await readJson(request), ['planReviewDigest', 'cycleNumber', 'maxCycles', 'predecessorDigest', 'prototype', 'evidenceChannels', 'signals'], 'Prototype cycle preparation request');
841
+ return json(response, 200, guarded(() => preparePrototypeCycleWorkspace(
842
+ projectRoot,
843
+ decodeURIComponent(prototypeCyclePrepare[1]),
844
+ input,
845
+ { personas: personaLibrary() },
846
+ )));
847
+ }
848
+ const prototypeCycleRecord = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)\/cycles\/record$/);
849
+ if (request.method === 'POST' && prototypeCycleRecord) {
850
+ const input = strictBody(await readJson(request), ['schema', 'expectedPreparationDigest', 'reviewedBy', 'findings', 'assessments'], 'Prototype cycle review request');
851
+ return json(response, 201, guarded(() => recordPrototypeCycleWorkspaceReview(projectRoot, decodeURIComponent(prototypeCycleRecord[1]), input)));
852
+ }
853
+ const prototypeCompare = url.pathname.match(/^\/api\/prototype-reviews\/([^/]+)\/compare$/);
854
+ if (request.method === 'POST' && prototypeCompare) {
855
+ const input = strictBody(await readJson(request), ['leftDigest', 'rightDigest'], 'Prototype review comparison request');
856
+ return json(response, 200, guarded(() => comparePrototypeWorkspaceReviews(projectRoot, decodeURIComponent(prototypeCompare[1]), input)));
857
+ }
858
+ if (request.method === 'PUT' && url.pathname === '/api/guided-discovery/draft') {
859
+ const input = await readJson(request);
860
+ return json(response, 200, saveGuidedDiscoveryDraft(projectRoot, input, { personas: personaLibrary() }));
861
+ }
862
+ if (request.method === 'POST' && url.pathname === '/api/guided-discovery/approve') {
863
+ const input = await readJson(request);
864
+ return json(response, 200, approveGuidedDiscovery(projectRoot, input, { personas: personaLibrary() }));
865
+ }
866
+ if (request.method === 'GET' && url.pathname === '/api/guided-intent') {
867
+ const unknown = [...new Set(url.searchParams.keys())];
868
+ if (unknown.length) return json(response, 400, { error: `Unsupported guided intent query: ${unknown[0]}` });
869
+ return json(response, 200, readGuidedIntent(projectRoot, { personas: personaLibrary() }));
870
+ }
871
+ if (request.method === 'POST' && url.pathname === '/api/guided-intent/draft') {
872
+ const input = strictBody(await readJson(request), ['expectedRevision', 'currentSection', 'mode', 'sourceReference', 'intent'], 'Guided intent draft request');
873
+ return json(response, 200, guarded(() => saveGuidedIntentDraft(projectRoot, input, { personas: personaLibrary() })));
874
+ }
875
+ if (request.method === 'DELETE' && url.pathname === '/api/guided-intent/draft') {
876
+ const input = strictBody(await readJson(request), ['expectedRevision', 'confirmed'], 'Guided intent discard request');
877
+ return json(response, 200, guarded(() => discardGuidedIntentDraft(projectRoot, input)));
878
+ }
879
+ if (request.method === 'POST' && url.pathname === '/api/guided-intent/approve') {
880
+ const input = strictBody(await readJson(request), ['expectedRevision', 'confirmed', 'approvedBy'], 'Guided intent approval request');
881
+ return json(response, 200, guarded(() => approveGuidedIntent(projectRoot, input, { personas: personaLibrary() })));
882
+ }
883
+ if (request.method === 'POST' && url.pathname === '/api/personas') {
884
+ const input = await readJson(request);
885
+ const created = createPersona({
886
+ scope: 'project',
887
+ projectRoot,
888
+ slug: input.slug,
889
+ name: input.name,
890
+ category: input.category,
891
+ force: false
892
+ });
893
+ const persona = personaLibrary().find((candidate) => candidate.id === created.id);
894
+ return json(response, 201, { persona });
895
+ }
896
+ if (request.method === 'GET' && url.pathname === '/api/knowledge') {
897
+ const query = url.searchParams.get('q') ?? '';
898
+ const knowledge = listKnowledge(projectRoot);
899
+ if (!query.trim()) return json(response, 200, { ...knowledge, matches: [] });
900
+ const search = searchPalace(projectRoot, query, { limit: 100 });
901
+ const paths = new Set(search.results.map((result) => result.path));
902
+ return json(response, 200, {
903
+ ...knowledge,
904
+ documents: knowledge.documents.filter((document) => paths.has(document.path)),
905
+ count: paths.size,
906
+ matches: search.results
907
+ });
908
+ }
909
+ if (request.method === 'GET' && url.pathname === '/api/meeting-evidence') {
910
+ const unknown = [...new Set(url.searchParams.keys())].filter((key) => key !== 'source');
911
+ if (unknown.length) return json(response, 400, { error: `Unsupported meeting evidence query: ${unknown[0]}`, assurance_notice: MEETING_EVIDENCE_DISCLAIMER });
912
+ return json(response, 200, readMeetingEvidenceWorkspace(projectRoot, {
913
+ sourceId: url.searchParams.get('source') || undefined,
914
+ personaCatalogue: personaLibrary(),
915
+ }));
916
+ }
917
+ if (request.method === 'GET' && url.pathname === '/api/knowledge-proposals') {
918
+ const unknown = [...new Set(url.searchParams.keys())].filter((key) => key !== 'bundle');
919
+ if (unknown.length) return json(response, 400, { error: `Unsupported knowledge proposal query: ${unknown[0]}`, assurance_notice: KNOWLEDGE_PROPOSALS_DISCLAIMER });
920
+ return json(response, 200, readKnowledgeProposalWorkspace(projectRoot, {
921
+ bundleId: url.searchParams.get('bundle') || undefined,
922
+ personaCatalogue: personaLibrary(),
923
+ }));
924
+ }
925
+ if (request.method === 'POST' && url.pathname === '/api/knowledge-proposals/prepare') {
926
+ const input = strictBody(await readJson(request), ['sourceRef', 'focus'], 'Knowledge proposal preparation request');
927
+ const prepared = prepareKnowledgeProposals(projectRoot, input.sourceRef, {
928
+ focus: input.focus,
929
+ personaCatalogue: personaLibrary(),
930
+ });
931
+ const { modelContext, ...safePreparation } = prepared;
932
+ return json(response, 200, {
933
+ ...safePreparation,
934
+ modelContextAvailable: Boolean(modelContext),
935
+ hostDirection: 'Use the CLI, MCP tool or ewai-knowledge-proposals skill to draft against the private bounded model context.',
936
+ });
937
+ }
938
+ if (request.method === 'POST' && url.pathname === '/api/knowledge-proposals/record') {
939
+ const input = strictBody(await readJson(request), ['sourceRef', 'bundle', 'activePersonas'], 'Knowledge proposal recording request');
940
+ return json(response, 200, {
941
+ ...recordKnowledgeProposalBundle(projectRoot, input.sourceRef, { bundle: input.bundle, activePersonas: input.activePersonas }),
942
+ notices: [KNOWLEDGE_PROPOSALS_DISCLAIMER],
943
+ });
944
+ }
945
+ const knowledgeReview = url.pathname.match(/^\/api\/knowledge-proposals\/([^/]+)\/review$/);
946
+ if (request.method === 'POST' && knowledgeReview) {
947
+ const input = strictBody(await readJson(request), ['reviewedBy', 'dispositions'], 'Knowledge proposal review request');
948
+ return json(response, 200, {
949
+ ...recordKnowledgeProposalReview(projectRoot, decodeURIComponent(knowledgeReview[1]), input),
950
+ notices: [KNOWLEDGE_PROPOSALS_DISCLAIMER],
951
+ });
952
+ }
953
+ const knowledgeMaterialise = url.pathname.match(/^\/api\/knowledge-proposals\/([^/]+)\/materialise$/);
954
+ if (request.method === 'POST' && knowledgeMaterialise) {
955
+ const input = strictBody(await readJson(request), ['confirmed', 'approvedBy'], 'Knowledge proposal materialisation request');
956
+ return json(response, 200, {
957
+ ...materialiseKnowledgeProposals(projectRoot, decodeURIComponent(knowledgeMaterialise[1]), { confirmed: input.confirmed === true, approvedBy: input.approvedBy }),
958
+ notices: [KNOWLEDGE_PROPOSALS_DISCLAIMER],
959
+ });
960
+ }
961
+ const knowledgeRecovery = url.pathname.match(/^\/api\/knowledge-proposals\/([^/]+)\/recover$/);
962
+ if (request.method === 'POST' && knowledgeRecovery) {
963
+ const input = strictBody(await readJson(request), ['confirmed'], 'Knowledge proposal recovery request');
964
+ return json(response, 200, {
965
+ ...recoverKnowledgeMaterialisation(projectRoot, decodeURIComponent(knowledgeRecovery[1]), { confirmed: input.confirmed === true }),
966
+ notices: [KNOWLEDGE_PROPOSALS_DISCLAIMER],
967
+ });
968
+ }
969
+ const meetingPrepare = url.pathname.match(/^\/api\/meeting-evidence\/([^/]+)\/prepare$/);
970
+ if (request.method === 'POST' && meetingPrepare) {
971
+ const input = strictBody(await readJson(request), ['focus'], 'Meeting extraction request');
972
+ return json(response, 200, prepareMeetingExtraction(projectRoot, decodeURIComponent(meetingPrepare[1]), {
973
+ focus: input.focus,
974
+ personaCatalogue: personaLibrary(),
975
+ }));
976
+ }
977
+ const meetingPromote = url.pathname.match(/^\/api\/meeting-evidence\/([^/]+)\/promote$/);
978
+ if (request.method === 'POST' && meetingPromote) {
979
+ const input = strictBody(await readJson(request), ['confirmed', 'approvedBy'], 'Meeting promotion request');
980
+ return json(response, 200, promoteMeetingEvidence(projectRoot, decodeURIComponent(meetingPromote[1]), {
981
+ confirmed: input.confirmed === true,
982
+ approvedBy: input.approvedBy,
983
+ }));
984
+ }
985
+ if (request.method === 'GET' && url.pathname === '/api/palace/tidiness') {
986
+ return json(response, 200, palaceTidiness(projectRoot));
987
+ }
988
+ if (request.method === 'GET' && url.pathname === '/api/knowledge/document') {
989
+ const document = readKnowledgeDocument(projectRoot, url.searchParams.get('path') ?? '');
990
+ return document ? json(response, 200, { document }) : json(response, 404, { error: 'SPECS document not found' });
991
+ }
992
+ if (request.method === 'GET' && url.pathname === '/api/active') {
993
+ return json(response, 200, {
994
+ sessions: listActiveSessions(projectRoot, {
995
+ includeCompleted: url.searchParams.get('completed') === '1'
996
+ })
997
+ });
998
+ }
999
+ if (request.method === 'GET' && url.pathname === '/api/dashboard-handoffs') {
1000
+ return json(response, 200, { handoffs: listDashboardHandoffs(projectRoot, { status: 'pending' }) });
1001
+ }
1002
+ if (request.method === 'GET' && url.pathname === '/api/afk/runs') {
1003
+ return json(response, 200, { runs: dashboardAfkRuns(projectRoot, url.searchParams.get('slug') ?? '') });
1004
+ }
1005
+ const workItemViewer = url.pathname.match(/^\/api\/work-items\/([^/]+)\/viewer$/);
1006
+ if (request.method === 'GET' && workItemViewer) {
1007
+ const view = dashboardWorkItemView(decodeURIComponent(workItemViewer[1]));
1008
+ return view ? json(response, 200, view) : json(response, 404, { error: 'Work item not found' });
1009
+ }
1010
+ const phaseStudio = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phase-studio$/);
1011
+ if (request.method === 'GET' && phaseStudio) {
1012
+ const unknown = [...new Set(url.searchParams.keys())];
1013
+ if (unknown.length) return json(response, 400, { error: `Unsupported Phase Studio query: ${unknown[0]}` });
1014
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudio[1]));
1015
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1016
+ return json(response, 200, guarded(() => readPhaseStudio(projectRoot, intent.slug, { intent, personas: personaLibrary() })));
1017
+ }
1018
+ const phaseStudioDraft = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phase-studio\/draft$/);
1019
+ if (request.method === 'POST' && phaseStudioDraft) {
1020
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudioDraft[1]));
1021
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1022
+ const input = strictBody(
1023
+ await readJson(request),
1024
+ ['expectedRevision', 'ownerContext', 'ownerName', 'entries', 'questions', 'conflicts', 'resolutions', 'limitations'],
1025
+ 'Phase Studio draft request',
1026
+ );
1027
+ return json(response, 200, guarded(() => savePhaseContributionDraft(projectRoot, intent.slug, input, { intent, personas: personaLibrary() })));
1028
+ }
1029
+ if (request.method === 'DELETE' && phaseStudioDraft) {
1030
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudioDraft[1]));
1031
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1032
+ const input = strictBody(await readJson(request), ['expectedRevision', 'confirmed'], 'Phase Studio discard request');
1033
+ return json(response, 200, guarded(() => discardPhaseContributionDraft(projectRoot, intent.slug, input, { intent, personas: personaLibrary() })));
1034
+ }
1035
+ const phaseStudioHandoff = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phase-studio\/handoff$/);
1036
+ if (request.method === 'POST' && phaseStudioHandoff) {
1037
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudioHandoff[1]));
1038
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1039
+ const input = strictBody(
1040
+ await readJson(request), ['expectedRevision', 'currentDigest', 'destinationContext', 'toOwner', 'reason', 'questions'], 'Phase Studio hand-off request',
1041
+ );
1042
+ return json(response, 200, guarded(() => handoffPhaseContribution(projectRoot, intent.slug, input, { intent, personas: personaLibrary() })));
1043
+ }
1044
+ const phaseStudioReview = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phase-studio\/review$/);
1045
+ if (request.method === 'POST' && phaseStudioReview) {
1046
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudioReview[1]));
1047
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1048
+ const input = strictBody(await readJson(request), ['expectedRevision', 'currentDigest'], 'Phase Studio review request');
1049
+ return json(response, 202, guarded(() => requestPhaseContributionReview(projectRoot, intent.slug, input, { intent, personas: personaLibrary() })));
1050
+ }
1051
+ const phaseStudioConfirm = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phase-studio\/confirm$/);
1052
+ if (request.method === 'POST' && phaseStudioConfirm) {
1053
+ const intent = readRuntimeIntent(projectRoot, decodeURIComponent(phaseStudioConfirm[1]));
1054
+ if (!intent) return json(response, 404, { error: 'Work item not found' });
1055
+ const input = strictBody(await readJson(request), ['expectedRevision', 'currentDigest', 'confirmed', 'confirmedBy'], 'Phase Studio confirmation request');
1056
+ return json(response, 201, guarded(() => confirmPhaseContribution(projectRoot, intent.slug, input, { intent, personas: personaLibrary() })));
1057
+ }
1058
+ const impactPreview = url.pathname.match(/^\/api\/work-items\/([^/]+)\/impact\/preview$/);
1059
+ if (request.method === 'POST' && impactPreview) {
1060
+ const reference = decodeURIComponent(impactPreview[1]);
1061
+ const view = dashboardWorkItemView(reference);
1062
+ if (!view) return json(response, 404, { error: 'Work item not found' });
1063
+ const input = await readJson(request);
1064
+ return json(response, 200, guarded(() => previewImpactAssessment(projectRoot, view.item.slug, input, { personas: personaLibrary() })));
1065
+ }
1066
+ const impactConfirm = url.pathname.match(/^\/api\/work-items\/([^/]+)\/impact\/confirm$/);
1067
+ if (request.method === 'POST' && impactConfirm) {
1068
+ const reference = decodeURIComponent(impactConfirm[1]);
1069
+ const view = dashboardWorkItemView(reference);
1070
+ if (!view) return json(response, 404, { error: 'Work item not found' });
1071
+ const input = await readJson(request);
1072
+ return json(response, 200, guarded(() => confirmImpactAssessment(projectRoot, view.item.slug, input, { personas: personaLibrary() })));
1073
+ }
1074
+ const impactRefresh = url.pathname.match(/^\/api\/work-items\/([^/]+)\/impact\/refresh$/);
1075
+ if (request.method === 'POST' && impactRefresh) {
1076
+ const reference = decodeURIComponent(impactRefresh[1]);
1077
+ const view = dashboardWorkItemView(reference);
1078
+ if (!view) return json(response, 404, { error: 'Work item not found' });
1079
+ const input = await readJson(request);
1080
+ if (input.confirmed !== true) return json(response, 409, { error: 'Refreshing the repository map requires explicit confirmation.' });
1081
+ const index = guarded(() => refreshRepositoryIndex(projectRoot));
1082
+ return json(response, 200, { index, view: dashboardWorkItemView(reference) });
1083
+ }
1084
+ const guidedHandoff = url.pathname.match(/^\/api\/work-items\/([^/]+)\/handoff$/);
1085
+ if (request.method === 'POST' && guidedHandoff) {
1086
+ const reference = decodeURIComponent(guidedHandoff[1]);
1087
+ const input = await readJson(request);
1088
+ const handoff = guarded(() => requestGuidedDashboardWork(projectRoot, reference, input));
1089
+ return json(response, 201, { handoff, view: dashboardWorkItemView(reference) });
1090
+ }
1091
+ const buildApproval = url.pathname.match(/^\/api\/work-items\/([^/]+)\/approve-build$/);
1092
+ if (request.method === 'POST' && buildApproval) {
1093
+ const reference = decodeURIComponent(buildApproval[1]);
1094
+ const input = await readJson(request);
1095
+ const result = guarded(() => approveBuildFromDashboard(projectRoot, reference, input));
1096
+ return json(response, 200, { ...result, view: dashboardWorkItemView(reference) });
1097
+ }
1098
+ const enterBuild = url.pathname.match(/^\/api\/work-items\/([^/]+)\/enter-build$/);
1099
+ if (request.method === 'POST' && enterBuild) {
1100
+ const reference = decodeURIComponent(enterBuild[1]);
1101
+ const input = await readJson(request);
1102
+ const result = guarded(() => enterBuildFromDashboard(projectRoot, reference, input));
1103
+ return json(response, 200, { ...result, view: dashboardWorkItemView(reference) });
1104
+ }
1105
+ const afkPreflight = url.pathname.match(/^\/api\/work-items\/([^/]+)\/afk\/preflight$/);
1106
+ if (request.method === 'GET' && afkPreflight) {
1107
+ const reference = decodeURIComponent(afkPreflight[1]);
1108
+ const preflight = guarded(() => preflightAfkFromDashboard(projectRoot, reference, {
1109
+ provider: url.searchParams.get('provider') ?? 'auto',
1110
+ maxParallel: Number(url.searchParams.get('parallel') ?? '1'),
1111
+ }));
1112
+ return json(response, 200, { preflight });
1113
+ }
1114
+ const afkStart = url.pathname.match(/^\/api\/work-items\/([^/]+)\/afk\/start$/);
1115
+ if (request.method === 'POST' && afkStart) {
1116
+ const reference = decodeURIComponent(afkStart[1]);
1117
+ const input = await readJson(request);
1118
+ const run = guarded(() => startAfkFromDashboard(projectRoot, reference, input));
1119
+ return json(response, 201, { run, view: dashboardWorkItemView(reference) });
1120
+ }
1121
+ const afkControl = url.pathname.match(/^\/api\/afk\/runs\/([^/]+)\/(pause|resume|cancel)$/);
1122
+ if (request.method === 'POST' && afkControl) {
1123
+ const input = await readJson(request);
1124
+ const run = guarded(() => controlAfkFromDashboard(projectRoot, decodeURIComponent(afkControl[1]), {
1125
+ ...input,
1126
+ action: afkControl[2],
1127
+ }));
1128
+ return json(response, 200, { run });
1129
+ }
1130
+ const handoffControl = url.pathname.match(/^\/api\/dashboard-handoffs\/([^/]+)\/(cancel)$/);
1131
+ if (request.method === 'POST' && handoffControl) {
1132
+ const input = await readJson(request);
1133
+ if (input.confirmed !== true) return json(response, 409, { error: 'Cancelling a dashboard handoff requires confirmation.' });
1134
+ const handoff = guarded(() => resolveDashboardHandoff(projectRoot, decodeURIComponent(handoffControl[1]), {
1135
+ status: 'cancelled',
1136
+ resolvedBy: 'dashboard',
1137
+ }));
1138
+ return json(response, 200, { handoff });
1139
+ }
1140
+ const workItemPersonas = url.pathname.match(/^\/api\/work-items\/([^/]+)\/personas$/);
1141
+ if (request.method === 'POST' && workItemPersonas) {
1142
+ const reference = decodeURIComponent(workItemPersonas[1]);
1143
+ const input = await readJson(request);
1144
+ const persona = personaLibrary().find((candidate) => candidate.id === input.personaRef);
1145
+ if (!persona) return json(response, 400, { error: `Unknown persona: ${input.personaRef ?? ''}` });
1146
+ const attachment = await attachPersonaToIntent(projectRoot, reference, input);
1147
+ return json(response, 200, { attachment, view: dashboardWorkItemView(reference) });
1148
+ }
1149
+ const workItemPersona = url.pathname.match(/^\/api\/work-items\/([^/]+)\/personas\/([^/]+)$/);
1150
+ if (request.method === 'DELETE' && workItemPersona) {
1151
+ const reference = decodeURIComponent(workItemPersona[1]);
1152
+ await readJson(request);
1153
+ const attachment = await detachPersonaFromIntent(projectRoot, reference, decodeURIComponent(workItemPersona[2]));
1154
+ return json(response, 200, { attachment, view: dashboardWorkItemView(reference) });
1155
+ }
1156
+ const workItem = url.pathname.match(/^\/api\/work-items\/([^/]+)$/);
1157
+ if (request.method === 'PATCH' && workItem) {
1158
+ const requested = await readJson(request);
1159
+ const allowed = Object.fromEntries(Object.entries(requested).filter(([key]) => (
1160
+ ['priority', 'currentSprint', 'blockedBy', 'notes'].includes(key)
1161
+ )));
1162
+ if (Object.keys(allowed).length !== Object.keys(requested).length) {
1163
+ return json(response, 409, {
1164
+ error: 'Delivery phase, status, and completion are controlled by the guarded EWAI delivery harness.'
1165
+ });
1166
+ }
1167
+ return json(response, 200, {
1168
+ item: updateWorkItem(projectRoot, decodeURIComponent(workItem[1]), allowed)
1169
+ });
1170
+ }
1171
+ const workItemPhase = url.pathname.match(/^\/api\/work-items\/([^/]+)\/phases\/([^/]+)$/);
1172
+ if (request.method === 'POST' && workItemPhase) {
1173
+ await readJson(request);
1174
+ return json(response, 409, {
1175
+ error: 'Raw phase changes are disabled. Use the guarded EWAI delivery harness and its deterministic gate evidence.'
1176
+ });
1177
+ }
1178
+ const workItemArtefacts = url.pathname.match(/^\/api\/work-items\/([^/]+)\/artefacts$/);
1179
+ if (request.method === 'POST' && workItemArtefacts) {
1180
+ return json(response, 201, {
1181
+ artefact: addArtefact(projectRoot, decodeURIComponent(workItemArtefacts[1]), await readJson(request))
1182
+ });
1183
+ }
1184
+ const activeEvents = url.pathname.match(/^\/api\/active\/([^/]+)\/events$/);
1185
+ if (request.method === 'GET' && activeEvents) {
1186
+ return json(response, 200, {
1187
+ events: listActivityEvents(projectRoot, decodeURIComponent(activeEvents[1]))
1188
+ });
1189
+ }
1190
+ const activeAction = url.pathname.match(/^\/api\/active\/([^/]+)\/(start|event|finish)$/);
1191
+ if (request.method === 'POST' && activeAction) {
1192
+ const reference = decodeURIComponent(activeAction[1]);
1193
+ const input = await readJson(request);
1194
+ const handler = activeAction[2] === 'start'
1195
+ ? startActiveSession
1196
+ : activeAction[2] === 'finish' ? finishActiveSession : addActivityEvent;
1197
+ return json(response, 200, handler(projectRoot, reference, input));
1198
+ }
1199
+ const prototype = url.pathname.match(/^\/prototype\/([^/]+)\/(\d+)\/(.*)$/);
1200
+ if (request.method === 'GET' && prototype) {
1201
+ const assetPath = decodeURIComponent(prototype[3] || '');
1202
+ if (servePrototype(response, decodeURIComponent(prototype[1]), Number(prototype[2]), assetPath)) return;
1203
+ return json(response, 404, { error: 'Prototype asset not found' });
1204
+ }
1205
+ if (request.method === 'GET' && serveStatic(response, url.pathname)) return;
1206
+ return json(response, 404, { error: 'Not found' });
1207
+ } catch (error) {
1208
+ if (!url.pathname.startsWith('/api/error-reports')) {
1209
+ try {
1210
+ captureErrorReportFailure(projectRoot, {
1211
+ capability: 'dashboard',
1212
+ errorCode: 'EWAI-DASHBOARD-REQUEST-FAILED',
1213
+ command: 'ewai dashboard',
1214
+ ewaiVersion: packageVersion,
1215
+ nodeVersion: process.versions.node,
1216
+ osClass: ['darwin', 'linux', 'win32'].includes(process.platform) ? process.platform : 'other',
1217
+ installationSource: 'npm',
1218
+ title: 'An EWAI dashboard request did not complete',
1219
+ expected: 'The requested dashboard action completes.',
1220
+ actual: 'The action returned an error. The original error was shown separately and is not copied into this report.',
1221
+ reproductionSteps: ['Repeat the dashboard action'],
1222
+ });
1223
+ } catch {
1224
+ // Reporting must never replace or mask the original dashboard failure.
1225
+ }
1226
+ }
1227
+ if (url.pathname.startsWith('/api/rollout')) return rolloutError(response, error.statusCode ?? 400, error.message);
1228
+ return json(response, error.statusCode ?? (url.pathname.startsWith('/api/context-packs') || url.pathname.startsWith('/api/meeting-evidence') || url.pathname.startsWith('/api/knowledge-proposals') || url.pathname.startsWith('/api/error-reports') || url.pathname.startsWith('/api/team-hub') ? 400 : 500), url.pathname.startsWith('/api/security-validation')
1229
+ ? { error: error.message, assurance_notice: ASSURANCE_NOTICE }
1230
+ : url.pathname.startsWith('/api/meeting-evidence')
1231
+ ? { error: error.message, assurance_notice: MEETING_EVIDENCE_DISCLAIMER }
1232
+ : url.pathname.startsWith('/api/knowledge-proposals')
1233
+ ? { error: error.message, assurance_notice: KNOWLEDGE_PROPOSALS_DISCLAIMER }
1234
+ : url.pathname.startsWith('/api/policies')
1235
+ ? { error: error.message, authority_notice: POLICY_DESIGN_AUTHORITY_NOTICE }
1236
+ : { error: error.message });
1237
+ }
1238
+ });
1239
+
1240
+ function dashboardState(port) {
1241
+ return {
1242
+ schema: 'ewai.dashboard-state/v1',
1243
+ runtimeVersion: dashboardRuntimeVersion,
1244
+ projectRoot,
1245
+ pid: process.pid,
1246
+ port,
1247
+ url: `http://127.0.0.1:${port}`,
1248
+ startedAt
1249
+ };
1250
+ }
1251
+
1252
+ server.listen(requestedPort, '127.0.0.1', () => {
1253
+ mkdirSync(paths.runtimeStateRoot, { recursive: true });
1254
+ writeFileSync(paths.dashboardStatePath, `${JSON.stringify(dashboardState(requestedPort), null, 2)}\n`, 'utf8');
1255
+ });
1256
+
1257
+ function shutdown() {
1258
+ clearInterval(lifecycleHookTimer);
1259
+ server.close(() => {
1260
+ try {
1261
+ const state = JSON.parse(readFileSync(paths.dashboardStatePath, 'utf8'));
1262
+ if (state.pid === process.pid) rmSync(paths.dashboardStatePath, { force: true });
1263
+ } catch {
1264
+ // A replacement process may already own the state file.
1265
+ }
1266
+ process.exit(0);
1267
+ });
1268
+ setTimeout(() => process.exit(1), 2000).unref();
1269
+ }
1270
+
1271
+ process.on('SIGTERM', shutdown);
1272
+ process.on('SIGINT', shutdown);