@guanlin4924/devflow-pi 0.1.94 → 0.1.96

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 (245) hide show
  1. package/README.md +34 -4
  2. package/node_modules/@playwright/test/LICENSE +202 -0
  3. package/node_modules/@playwright/test/NOTICE +5 -0
  4. package/node_modules/@playwright/test/README.md +318 -0
  5. package/node_modules/@playwright/test/cli.js +19 -0
  6. package/node_modules/@playwright/test/index.d.ts +18 -0
  7. package/node_modules/@playwright/test/index.js +17 -0
  8. package/node_modules/@playwright/test/index.mjs +18 -0
  9. package/node_modules/@playwright/test/package.json +35 -0
  10. package/node_modules/@playwright/test/reporter.d.ts +17 -0
  11. package/node_modules/@playwright/test/reporter.js +17 -0
  12. package/node_modules/@playwright/test/reporter.mjs +17 -0
  13. package/node_modules/playwright/LICENSE +202 -0
  14. package/node_modules/playwright/NOTICE +5 -0
  15. package/node_modules/playwright/README.md +318 -0
  16. package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
  17. package/node_modules/playwright/cli.js +19 -0
  18. package/node_modules/playwright/index.d.ts +17 -0
  19. package/node_modules/playwright/index.js +17 -0
  20. package/node_modules/playwright/index.mjs +18 -0
  21. package/node_modules/playwright/jsx-runtime.js +42 -0
  22. package/node_modules/playwright/jsx-runtime.mjs +21 -0
  23. package/node_modules/playwright/lib/agents/agentParser.js +89 -0
  24. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
  25. package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
  26. package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
  27. package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
  28. package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
  29. package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
  30. package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
  31. package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
  32. package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
  33. package/node_modules/playwright/lib/cli/reportActions.js +78 -0
  34. package/node_modules/playwright/lib/cli/testActions.js +214 -0
  35. package/node_modules/playwright/lib/common/index.js +2923 -0
  36. package/node_modules/playwright/lib/common/index.js.txt +34 -0
  37. package/node_modules/playwright/lib/errorContext.js +129 -0
  38. package/node_modules/playwright/lib/globals.js +58 -0
  39. package/node_modules/playwright/lib/index.js +815 -0
  40. package/node_modules/playwright/lib/isomorphic.js +260 -0
  41. package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
  42. package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
  43. package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
  44. package/node_modules/playwright/lib/matchers/expect.js +13490 -0
  45. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
  46. package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
  47. package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
  48. package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
  49. package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
  50. package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
  51. package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
  52. package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
  53. package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
  54. package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
  55. package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
  56. package/node_modules/playwright/lib/package.js +47 -0
  57. package/node_modules/playwright/lib/program.js +241 -0
  58. package/node_modules/playwright/lib/runner/index.js +8527 -0
  59. package/node_modules/playwright/lib/runner/index.js.txt +61 -0
  60. package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
  61. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
  62. package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
  63. package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
  64. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
  65. package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
  66. package/node_modules/playwright/lib/util.js +373 -0
  67. package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
  68. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
  69. package/node_modules/playwright/package.json +55 -0
  70. package/node_modules/playwright/test.d.ts +18 -0
  71. package/node_modules/playwright/test.js +24 -0
  72. package/node_modules/playwright/test.mjs +35 -0
  73. package/node_modules/playwright/types/test.d.ts +11074 -0
  74. package/node_modules/playwright/types/testReporter.d.ts +950 -0
  75. package/node_modules/playwright-core/LICENSE +202 -0
  76. package/node_modules/playwright-core/NOTICE +5 -0
  77. package/node_modules/playwright-core/README.md +3 -0
  78. package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  79. package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  80. package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  81. package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  82. package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  83. package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  84. package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  85. package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  86. package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  87. package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  88. package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  89. package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  90. package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  91. package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  92. package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  93. package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  94. package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  95. package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  96. package/node_modules/playwright-core/browsers.json +52 -0
  97. package/node_modules/playwright-core/cli.js +21 -0
  98. package/node_modules/playwright-core/index.d.ts +17 -0
  99. package/node_modules/playwright-core/index.js +17 -0
  100. package/node_modules/playwright-core/index.mjs +28 -0
  101. package/node_modules/playwright-core/lib/bootstrap.js +88 -0
  102. package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
  103. package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  104. package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  105. package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  106. package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  107. package/node_modules/playwright-core/lib/package.js +50 -0
  108. package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  109. package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
  110. package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
  111. package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
  112. package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  113. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
  114. package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
  115. package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  116. package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
  117. package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
  118. package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
  119. package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
  120. package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  121. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
  122. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
  123. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
  124. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
  125. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
  126. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
  127. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
  128. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
  129. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
  130. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
  131. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
  132. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  133. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
  134. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
  135. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
  136. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
  137. package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
  138. package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
  139. package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  140. package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
  141. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
  142. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
  143. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
  144. package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  145. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  146. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  147. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  148. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
  149. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
  150. package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  151. package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  152. package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  153. package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  154. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  155. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  156. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  157. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
  158. package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  159. package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
  160. package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
  161. package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  162. package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  163. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
  164. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
  165. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  166. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
  167. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  168. package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  169. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
  170. package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
  171. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  172. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  173. package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  174. package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  175. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  176. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  177. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  178. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
  179. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
  180. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  181. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  182. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  183. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  184. package/node_modules/playwright-core/lib/xdg-open +1267 -0
  185. package/node_modules/playwright-core/package.json +34 -0
  186. package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
  187. package/node_modules/playwright-core/types/structs.d.ts +52 -0
  188. package/node_modules/playwright-core/types/types.d.ts +26632 -0
  189. package/package.json +13 -2
  190. package/plugins/devflow-core/extensions/devflow-core.ts +432 -7
  191. package/plugins/devflow-core/runtime/hooks/hook_common.py +514 -0
  192. package/plugins/devflow-core/runtime/hooks/index_gate.py +47 -16
  193. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +488 -71
  194. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +51 -0
  195. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
  196. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
  197. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
  198. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py +53 -0
  199. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
  200. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
  201. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -0
  202. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
  203. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
  204. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
  205. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +102 -8
  206. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +13 -0
  207. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +13 -34
  208. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
  209. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
  210. package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
  211. package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +3 -1
  212. package/plugins/devflow-intel/extensions/devflow-intel.ts +1 -1
  213. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +23 -4
  214. package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
  215. package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
  216. package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
  217. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
  218. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
  219. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -0
  220. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +71 -58
  221. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
  222. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +3 -1
  223. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
  224. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
  225. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
  226. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
  227. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
  228. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
  229. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
  230. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
  231. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +63 -0
  232. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +4 -4
  233. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +17 -9
  234. package/src/approval-store.ts +340 -0
  235. package/src/cli.ts +10 -0
  236. package/src/constants.ts +1 -1
  237. package/src/doctor.ts +268 -2
  238. package/src/initialization-store.ts +48 -1
  239. package/src/mcp/auth.ts +103 -0
  240. package/src/mcp/core.ts +102 -0
  241. package/src/mcp/server.ts +193 -0
  242. package/src/mcp/types.ts +5 -0
  243. package/src/paths.ts +12 -1
  244. package/src/project-context.ts +24 -1
  245. package/templates/omp/hook-config.json +6 -0
package/src/doctor.ts CHANGED
@@ -1,10 +1,22 @@
1
- import { access, lstat, readFile } from "node:fs/promises";
1
+ import { access, lstat, readFile, readdir } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
2
3
  import path from "node:path";
3
4
 
4
5
  import { DEVFLOW_PLUGIN_NAMES, OMP_RUNTIME_VERSION, PRODUCT_VERSION } from "./constants.ts";
5
6
  import { devflowAgentDir, devflowHome, ompSdkManifestPath, pluginRoots, projectSessionDir } from "./paths.ts";
6
7
  import type { ProjectContext } from "./project-context.ts";
7
8
 
9
+ const RUNTIME_FILES = ["runner.spec.mjs", "playwright.config.mjs", "reporter.mjs", "devflow-test-dsl-v1.schema.json"] as const;
10
+ const SHA256 = /^[0-9a-f]{64}$/;
11
+ const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
12
+ const PROFILE_FIELDS = new Set(["profile", "base_url", "api_base_url", "navigation_origins", "api_origins", "subresources", "secret_env", "trace", "screenshots"]);
13
+ const RESERVED_ENV = new Set([
14
+ "DEVFLOW_DSL_FILE", "DEVFLOW_DSL_SHA256", "DEVFLOW_TC_ID", "DEVFLOW_TC_SHA256", "DEVFLOW_EXECUTION_ID",
15
+ "DEVFLOW_EXECUTION_NONCE", "DEVFLOW_RESULT_FILE", "DEVFLOW_ARTIFACT_DIR", "DEVFLOW_ENVIRONMENT_FILE",
16
+ "PATH", "HOME", "TMPDIR", "LANG", "PLAYWRIGHT_BROWSERS_PATH", "SHELL", "USER", "PWD",
17
+ ]);
18
+ const RESERVED_ENV_PREFIXES = ["NODE_", "LD_", "DYLD_", "PLAYWRIGHT_", "PYTHON", "BUN_", "NPM_", "DEVFLOW_HOME", "DEVFLOW_KIT", "DEVFLOW_SOFT"];
19
+
8
20
  export interface DoctorCheck {
9
21
  id: string;
10
22
  ok: boolean;
@@ -30,11 +42,200 @@ function bunVersionOk(version: string): boolean {
30
42
  return major > 1 || (major === 1 && (minor > 3 || (minor === 3 && patch >= 14)));
31
43
  }
32
44
 
33
- export async function runHostDoctor(project: ProjectContext): Promise<HostDoctorReport> {
45
+ function majorVersion(version: string): number {
46
+ return Number((version.match(/v?(\d+)/) || [])[1] || 0);
47
+ }
48
+
49
+ export interface ProbeResult {
50
+ ok: boolean;
51
+ timedOut: boolean;
52
+ exitCode: number | null;
53
+ stdout: string;
54
+ stderr: string;
55
+ /** Human-readable reason when ok is false. */
56
+ error?: string;
57
+ }
58
+
59
+ export interface ProbeOptions {
60
+ timeoutMs: number;
61
+ cwd?: string;
62
+ }
63
+
64
+ /** Runs one external command; must never throw and must always settle. */
65
+ export type ProbeRunner = (argv: string[], options: ProbeOptions) => Promise<ProbeResult>;
66
+
67
+ export interface DoctorOptions {
68
+ probe?: ProbeRunner;
69
+ /** Hard limits. A probe that exceeds them is reported as a failed check, never a pass. */
70
+ timeouts?: { browserLaunchMs?: number; versionMs?: number };
71
+ }
72
+
73
+ const DEFAULT_BROWSER_LAUNCH_TIMEOUT_MS = 20_000;
74
+ const DEFAULT_VERSION_TIMEOUT_MS = 10_000;
75
+ const PIPE_DRAIN_MS = 500;
76
+
77
+ function settleWithin<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
78
+ return new Promise(resolve => {
79
+ const timer = setTimeout(() => resolve(fallback), ms);
80
+ promise.then(value => { clearTimeout(timer); resolve(value); }, () => { clearTimeout(timer); resolve(fallback); });
81
+ });
82
+ }
83
+
84
+ /** Async spawn with a hard timeout: the child is SIGKILLed on expiry and the result is marked failed. */
85
+ export const spawnProbe: ProbeRunner = async (argv, options) => {
86
+ let child: ReturnType<typeof Bun.spawn>;
87
+ try {
88
+ child = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe", stdin: "ignore", cwd: options.cwd });
89
+ } catch (error) {
90
+ return { ok: false, timedOut: false, exitCode: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) };
91
+ }
92
+ let timedOut = false;
93
+ const timer = setTimeout(() => {
94
+ timedOut = true;
95
+ try { child.kill("SIGKILL"); } catch { /* already gone */ }
96
+ }, options.timeoutMs);
97
+ const stdoutText = new Response(child.stdout as ReadableStream).text();
98
+ const stderrText = new Response(child.stderr as ReadableStream).text();
99
+ try {
100
+ const exitCode = await child.exited;
101
+ // A grandchild may keep the pipes open after the kill; never wait on them indefinitely.
102
+ const [stdout, stderr] = await Promise.all([
103
+ settleWithin(stdoutText, timedOut ? PIPE_DRAIN_MS : 5_000, ""),
104
+ settleWithin(stderrText, timedOut ? PIPE_DRAIN_MS : 5_000, ""),
105
+ ]);
106
+ if (timedOut) {
107
+ return { ok: false, timedOut: true, exitCode: null, stdout: stdout.trim(), stderr: stderr.trim(), error: `timed out after ${options.timeoutMs} ms and was killed` };
108
+ }
109
+ return { ok: exitCode === 0, timedOut: false, exitCode, stdout: stdout.trim(), stderr: stderr.trim(), error: exitCode === 0 ? undefined : `exit code ${exitCode}` };
110
+ } catch (error) {
111
+ return { ok: false, timedOut, exitCode: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) };
112
+ } finally {
113
+ clearTimeout(timer);
114
+ }
115
+ };
116
+
117
+ /** Wraps a probe so that even a misbehaving runner cannot block the caller past its deadline. */
118
+ async function guardedProbe(probe: ProbeRunner, argv: string[], options: ProbeOptions): Promise<ProbeResult> {
119
+ const deadline = options.timeoutMs + Math.min(2_000, Math.max(50, options.timeoutMs));
120
+ const fallback: ProbeResult = { ok: false, timedOut: true, exitCode: null, stdout: "", stderr: "", error: `timed out after ${options.timeoutMs} ms` };
121
+ try {
122
+ const result = await settleWithin(probe(argv, options), deadline, fallback);
123
+ // A timed-out probe can never count as a pass, whatever the runner claims.
124
+ return result.timedOut ? { ...result, ok: false } : result;
125
+ } catch (error) {
126
+ return { ...fallback, timedOut: false, error: error instanceof Error ? error.message : String(error) };
127
+ }
128
+ }
129
+
130
+ function probeFailure(result: ProbeResult): string {
131
+ const detail = (result.stderr || result.stdout).split("\n")[0]?.trim();
132
+ return [result.error, detail].filter(Boolean).join(": ") || "probe failed";
133
+ }
134
+
135
+ export async function verifyTestRuntime(runtimeRoot: string): Promise<{ ok: boolean; message: string; playwrightVersion?: string }> {
136
+ const manifestPath = path.join(runtimeRoot, "runtime-manifest.json");
137
+ try {
138
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
139
+ const files = manifest.files && typeof manifest.files === "object" ? manifest.files as Record<string, unknown> : {};
140
+ const errors: string[] = [];
141
+ if (manifest.schema_version !== 1) errors.push("schema_version must be 1");
142
+ if (typeof manifest.runtime_version !== "string" || !manifest.runtime_version) errors.push("runtime_version is missing");
143
+ if (typeof manifest.playwright_version !== "string" || !/^\d+\.\d+\.\d+$/.test(manifest.playwright_version)) errors.push("playwright_version is invalid");
144
+ for (const name of RUNTIME_FILES) {
145
+ const expected = files[name];
146
+ const target = path.join(runtimeRoot, name);
147
+ const stat = await lstat(target).catch(() => null);
148
+ if (typeof expected !== "string" || !SHA256.test(expected)) errors.push(`manifest does not pin ${name}`);
149
+ else if (!stat?.isFile() || stat.isSymbolicLink()) errors.push(`runtime file is missing or unsafe: ${name}`);
150
+ else if (createHash("sha256").update(await readFile(target)).digest("hex") !== expected) errors.push(`runtime file hash mismatch: ${name}`);
151
+ }
152
+ if (!errors.length) {
153
+ const reporter = await readFile(path.join(runtimeRoot, "reporter.mjs"), "utf8");
154
+ const embedded = /RUNTIME_VERSION\s*=\s*["']([^"']+)["']/.exec(reporter)?.[1];
155
+ if (embedded !== manifest.runtime_version) errors.push(`runtime_version ${String(manifest.runtime_version)} does not match reporter ${String(embedded)}`);
156
+ }
157
+ return { ok: !errors.length, message: errors.length ? errors.join("; ") : manifestPath, playwrightVersion: typeof manifest.playwright_version === "string" ? manifest.playwright_version : undefined };
158
+ } catch (error) {
159
+ return { ok: false, message: `invalid manifest: ${error instanceof Error ? error.message : String(error)}` };
160
+ }
161
+ }
162
+
163
+ export function playwrightVersionMatches(installed: string, manifest: string | undefined): boolean {
164
+ return /^\d+\.\d+\.\d+$/.test(installed) && installed === manifest;
165
+ }
166
+
167
+ function exactOrigin(value: unknown): string | null {
168
+ if (typeof value !== "string") return null;
169
+ try {
170
+ const url = new URL(value);
171
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null;
172
+ return `${url.protocol}//${url.host}` === value ? value : null;
173
+ } catch { return null; }
174
+ }
175
+
176
+ export async function validateEnvironmentProfile(file: string): Promise<string[]> {
177
+ try {
178
+ const stat = await lstat(file);
179
+ if (!stat.isFile() || stat.isSymbolicLink()) return ["profile must be a regular non-symlink file"];
180
+ const profile = JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>;
181
+ const name = path.basename(file, ".json");
182
+ const errors: string[] = [];
183
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) return ["profile root must be an object"];
184
+ if (!PROFILE_NAME.test(name)) errors.push(`invalid environment profile name: ${name}`);
185
+ for (const key of Object.keys(profile)) if (!PROFILE_FIELDS.has(key)) errors.push(`profile field is not allowed: ${key}`);
186
+ if (profile.profile !== name) errors.push("profile field must equal file name");
187
+ for (const key of ["navigation_origins", "api_origins"] as const) {
188
+ const values = key === "api_origins" && !(key in profile) ? [] : profile[key];
189
+ if (!Array.isArray(values) || (key === "navigation_origins" && !values.length)) errors.push(`${key} must be ${key === "navigation_origins" ? "a non-empty " : "an "}array`);
190
+ else {
191
+ if (values.some(value => !exactOrigin(value))) errors.push(`${key} contains an invalid origin`);
192
+ if (new Set(values).size !== values.length) errors.push(`${key} contains duplicate origins`);
193
+ }
194
+ }
195
+ const navigation = Array.isArray(profile.navigation_origins) ? profile.navigation_origins : [];
196
+ const api = Array.isArray(profile.api_origins) ? profile.api_origins : [];
197
+ for (const [key, allowed] of [["base_url", navigation], ["api_base_url", api]] as const) {
198
+ if (key === "api_base_url" && !(key in profile)) continue;
199
+ if (typeof profile[key] !== "string") errors.push(`${key} must be an absolute http(s) URL`);
200
+ else {
201
+ try { if (!allowed.includes(new URL(profile[key]).origin)) errors.push(`${key} origin is not declared`); }
202
+ catch { errors.push(`${key} must be an absolute http(s) URL`); }
203
+ }
204
+ }
205
+ const subresources = profile.subresources ?? { policy: "same-site-plus-declared", allowed_origins: [] };
206
+ if (!subresources || typeof subresources !== "object" || Array.isArray(subresources)) errors.push("subresources must be an object");
207
+ else {
208
+ const value = subresources as Record<string, unknown>;
209
+ if (Object.keys(value).some(key => !["policy", "allowed_origins"].includes(key))) errors.push("subresources contains an unsupported field");
210
+ if (!["same-site-plus-declared", "declared-only"].includes(String(value.policy))) errors.push("subresources.policy is invalid");
211
+ const allowed = value.allowed_origins;
212
+ if (!Array.isArray(allowed) || allowed.some(origin => !exactOrigin(origin))) errors.push("subresources.allowed_origins contains an invalid origin");
213
+ }
214
+ const secrets = profile.secret_env ?? {};
215
+ if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) errors.push("secret_env must be an object");
216
+ else for (const [key, value] of Object.entries(secrets)) {
217
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(key)) errors.push(`secret_env key is invalid: ${key}`);
218
+ if (typeof value !== "string" || !/^[A-Z_][A-Z0-9_]{0,127}$/.test(value)) errors.push(`secret_env.${key} must name an upper-case environment variable`);
219
+ else if (RESERVED_ENV.has(value) || RESERVED_ENV_PREFIXES.some(prefix => value.startsWith(prefix))) errors.push(`secret_env.${key} uses a reserved environment variable`);
220
+ }
221
+ for (const key of ["trace", "screenshots"] as const) if (key in profile && !["on_failure", "off"].includes(String(profile[key]))) errors.push(`${key} is invalid`);
222
+ return errors;
223
+ } catch (error) {
224
+ return [`invalid profile: ${error instanceof Error ? error.message : String(error)}`];
225
+ }
226
+ }
227
+
228
+ export async function runHostDoctor(project: ProjectContext, options: DoctorOptions = {}): Promise<HostDoctorReport> {
34
229
  const checks: DoctorCheck[] = [];
35
230
  const add = (id: string, ok: boolean, message: string) => checks.push({ id, ok, message });
231
+ const probe = options.probe ?? spawnProbe;
232
+ const versionMs = options.timeouts?.versionMs ?? DEFAULT_VERSION_TIMEOUT_MS;
233
+ const browserMs = options.timeouts?.browserLaunchMs ?? DEFAULT_BROWSER_LAUNCH_TIMEOUT_MS;
36
234
  add("branch-runtime", true, `devflow-pi ${PRODUCT_VERSION}; OMP SDK ${OMP_RUNTIME_VERSION}`);
37
235
  add("bun", bunVersionOk(Bun.version), bunVersionOk(Bun.version) ? Bun.version : `${Bun.version}; requires >=1.3.14`);
236
+ const node = await guardedProbe(probe, ["node", "--version"], { timeoutMs: versionMs });
237
+ const nodeOk = node.ok && majorVersion(node.stdout) >= 20;
238
+ add("node", nodeOk, node.ok ? `${node.stdout}; requires >=20` : `missing or unusable: ${probeFailure(node)}`);
38
239
  add("project-root", path.isAbsolute(project.root), project.root);
39
240
  add("project-id", Boolean(project.manifest.projectId), project.manifest.projectId);
40
241
  const planningStat = await lstat(project.planningRoot).catch(() => null);
@@ -45,6 +246,62 @@ export async function runHostDoctor(project: ProjectContext): Promise<HostDoctor
45
246
  const root = roots[index];
46
247
  add(`plugin:${plugin}`, await exists(path.join(root, "skills")), root);
47
248
  }
249
+ const qualityRoot = roots[DEVFLOW_PLUGIN_NAMES.indexOf("devflow-quality")];
250
+ // Never fall back to a relative path: verifyTestRuntime("") would read ./runtime-manifest.json from the cwd.
251
+ const runtimeRoot = qualityRoot ? path.join(qualityRoot, "runtime", "playwright") : null;
252
+ const runtime = runtimeRoot
253
+ ? await verifyTestRuntime(runtimeRoot)
254
+ : { ok: false, message: "devflow-quality plugin root is unknown", playwrightVersion: undefined };
255
+ add("test-runtime-manifest", runtime.ok, runtime.message);
256
+ let installedPlaywright: ProbeResult | null = null;
257
+ if (runtime.ok && runtimeRoot) {
258
+ installedPlaywright = await guardedProbe(
259
+ probe,
260
+ ["node", "-e", "process.stdout.write(require(require.resolve('@playwright/test/package.json',{paths:[process.argv[1]]})).version)", runtimeRoot],
261
+ { timeoutMs: versionMs },
262
+ );
263
+ }
264
+ const versionOk = Boolean(installedPlaywright?.ok && playwrightVersionMatches(installedPlaywright.stdout, runtime.playwrightVersion));
265
+ add("test-runtime-playwright-version", versionOk,
266
+ !installedPlaywright ? "skipped: Runtime manifest is invalid"
267
+ : !installedPlaywright.ok ? `Playwright version probe failed: ${probeFailure(installedPlaywright)}`
268
+ : `${installedPlaywright.stdout}; manifest ${runtime.playwrightVersion}${versionOk ? "" : " (mismatch)"}`);
269
+ let chromiumOk = false;
270
+ let chromiumMessage = "not checked: Node >=20 and a valid Runtime manifest are required";
271
+ if (nodeOk && runtime.ok && runtimeRoot) {
272
+ const launch = await guardedProbe(
273
+ probe,
274
+ ["node", "-e", `const {chromium}=require(require.resolve('@playwright/test',{paths:[process.argv[1]]}));chromium.launch({timeout:${Math.max(1000, browserMs - 2000)}}).then(b=>b.close()).then(()=>process.exit(0),e=>{console.error(e.message);process.exit(1)})`, runtimeRoot],
275
+ { timeoutMs: browserMs },
276
+ );
277
+ chromiumOk = launch.ok && !launch.timedOut;
278
+ chromiumMessage = chromiumOk ? "launchable"
279
+ : launch.timedOut ? `Chromium launch timed out after ${browserMs} ms and was killed; the browser is not launchable`
280
+ : `Chromium is not launchable: ${probeFailure(launch)}`;
281
+ }
282
+ add("test-runtime-chromium", chromiumOk, chromiumMessage);
283
+ const environmentRoot = path.join(project.planningRoot, "test-environments");
284
+ const environmentStat = await lstat(environmentRoot).catch(() => null);
285
+ let profiles: string[] = [];
286
+ if (environmentStat?.isDirectory() && !environmentStat.isSymbolicLink()) {
287
+ profiles = (await readdir(environmentRoot)).filter(name => name.endsWith(".json") && !name.endsWith(".example.json"));
288
+ }
289
+ const profileErrors: string[] = [];
290
+ for (const profile of profiles) for (const error of await validateEnvironmentProfile(path.join(environmentRoot, profile))) profileErrors.push(`${profile}: ${error}`);
291
+ add("test-environments", profiles.length > 0 && !profileErrors.length,
292
+ !profiles.length ? `${environmentRoot}: configure a profile from local.example.json` : profileErrors.length ? profileErrors.join("; ") : `${environmentRoot}: ${profiles.join(", ")}`);
293
+ const runsRoot = path.join(project.root, ".devflow", "runs");
294
+ const runsStat = await lstat(runsRoot).catch(() => null);
295
+ if (!runsStat?.isDirectory() || runsStat.isSymbolicLink()) {
296
+ add("test-runs-root", false, `${runsRoot}: missing, not a directory, or a symlink`);
297
+ } else {
298
+ // Run directories hold traces, screenshots and result files and must never enter source control.
299
+ const ignored = await guardedProbe(probe, ["git", "-C", project.root, "check-ignore", "-q", "--no-index", ".devflow/runs/doctor-probe/x"], { timeoutMs: versionMs });
300
+ if (ignored.ok) add("test-runs-root", true, `${runsRoot} (git-ignored)`);
301
+ else if (ignored.exitCode === 1) add("test-runs-root", false, `${runsRoot} is not git-ignored; add .devflow/runs/ to .gitignore (run artifacts may contain secrets)`);
302
+ else if (ignored.exitCode === 128 && /not a git repository/i.test(ignored.stderr)) add("test-runs-root", true, `${runsRoot} (project is not a git repository; ignore rule not applicable)`);
303
+ else add("test-runs-root", false, `cannot verify that ${runsRoot} is git-ignored: ${probeFailure(ignored)}`);
304
+ }
48
305
  const sdkManifest = ompSdkManifestPath();
49
306
  let installedVersion = "missing";
50
307
  if (await exists(sdkManifest)) {
@@ -59,6 +316,15 @@ export async function runHostDoctor(project: ProjectContext): Promise<HostDoctor
59
316
  add("agent-dir", true, devflowAgentDir());
60
317
  add("session-isolation", true, projectSessionDir(project.manifest.projectId));
61
318
  add("devflow-home", true, devflowHome());
319
+ const mcpConfig = path.join(devflowHome(), "mcp.json");
320
+ if (!(await exists(mcpConfig))) add("mcp", true, "MCP 未配置");
321
+ else {
322
+ try {
323
+ const config = JSON.parse(await readFile(mcpConfig, "utf8")) as Record<string, unknown>;
324
+ const valid = config.schema_version === 1 && config.clients !== null && typeof config.clients === "object" && !Array.isArray(config.clients);
325
+ add("mcp", valid, valid ? `${mcpConfig} (schema_version 1)` : `${mcpConfig}: schema_version must be 1 and clients must be an object`);
326
+ } catch (error) { add("mcp", false, `${mcpConfig}: invalid JSON: ${error instanceof Error ? error.message : String(error)}`); }
327
+ }
62
328
  return { ok: checks.every(check => check.ok), checks };
63
329
  }
64
330
 
@@ -7,7 +7,7 @@ import { Database } from "bun:sqlite";
7
7
  import { projectControlDatabasePath, projectDataDir } from "./paths.ts";
8
8
  import { DEVFLOW_SCAN_EXCLUDES, PLANNING_DIRECTORY } from "./project-layout.ts";
9
9
 
10
- export const INITIALIZATION_SCHEMA_VERSION = 2;
10
+ export const INITIALIZATION_SCHEMA_VERSION = 3;
11
11
  export const FACT_KINDS = [
12
12
  "asset", "technology", "api", "page", "data_model", "integration",
13
13
  "rule", "toolchain", "evidence", "capability",
@@ -199,6 +199,48 @@ WHEN EXISTS(SELECT 1 FROM initialization_runs r WHERE r.id=OLD.run_id AND r.stat
199
199
  BEGIN SELECT RAISE(ABORT, 'published facts are immutable'); END;
200
200
  `;
201
201
 
202
+ // Human approval records (schema v3). The column contract is shared with the
203
+ // Python reader side; the triggers below make the records append-only except
204
+ // for the Core-owned proposal lifecycle and approval supersession.
205
+ export const APPROVAL_SCHEMA = `
206
+ CREATE TABLE IF NOT EXISTS approval_proposals(
207
+ id TEXT PRIMARY KEY, project_id TEXT NOT NULL, feature TEXT NOT NULL,
208
+ kind TEXT NOT NULL CHECK(kind IN ('execution_plan','environment_profile','mode_override','human_result')),
209
+ subject_key TEXT NOT NULL, payload_json TEXT NOT NULL, pins_json TEXT NOT NULL,
210
+ state_revision INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('proposed','consumed','rejected','expired')),
211
+ created_at TEXT NOT NULL, resolved_at TEXT);
212
+ CREATE TABLE IF NOT EXISTS approvals(
213
+ id TEXT PRIMARY KEY, proposal_id TEXT NOT NULL UNIQUE REFERENCES approval_proposals(id),
214
+ project_id TEXT NOT NULL, feature TEXT NOT NULL, kind TEXT NOT NULL, subject_key TEXT NOT NULL,
215
+ pins_json TEXT NOT NULL, decision_json TEXT NOT NULL, approved_via TEXT NOT NULL CHECK(approved_via='runtime_ui_confirm'),
216
+ approved_at TEXT NOT NULL, superseded_at TEXT);
217
+ CREATE INDEX IF NOT EXISTS idx_approval_proposals_subject ON approval_proposals(project_id, feature, kind, subject_key, status);
218
+ CREATE INDEX IF NOT EXISTS idx_approvals_subject ON approvals(project_id, feature, kind, subject_key, superseded_at);
219
+ CREATE TRIGGER IF NOT EXISTS approval_proposal_lifecycle BEFORE UPDATE ON approval_proposals
220
+ WHEN OLD.status!='proposed' OR NEW.status='proposed' OR NEW.resolved_at IS NULL
221
+ OR NEW.id!=OLD.id OR NEW.project_id!=OLD.project_id OR NEW.feature!=OLD.feature OR NEW.kind!=OLD.kind
222
+ OR NEW.subject_key!=OLD.subject_key OR NEW.payload_json!=OLD.payload_json OR NEW.pins_json!=OLD.pins_json
223
+ OR NEW.state_revision!=OLD.state_revision OR NEW.created_at!=OLD.created_at
224
+ BEGIN SELECT RAISE(ABORT, 'approval proposals only transition once from proposed'); END;
225
+ CREATE TRIGGER IF NOT EXISTS approval_proposal_delete BEFORE DELETE ON approval_proposals
226
+ BEGIN SELECT RAISE(ABORT, 'approval proposals are append-only'); END;
227
+ CREATE TRIGGER IF NOT EXISTS approval_requires_open_proposal BEFORE INSERT ON approvals
228
+ WHEN NOT EXISTS(
229
+ SELECT 1 FROM approval_proposals p
230
+ WHERE p.id=NEW.proposal_id AND p.status='proposed' AND p.project_id=NEW.project_id AND p.feature=NEW.feature
231
+ AND p.kind=NEW.kind AND p.subject_key=NEW.subject_key AND p.pins_json=NEW.pins_json
232
+ )
233
+ BEGIN SELECT RAISE(ABORT, 'approval does not match an open proposal'); END;
234
+ CREATE TRIGGER IF NOT EXISTS approval_supersede_only BEFORE UPDATE ON approvals
235
+ WHEN OLD.superseded_at IS NOT NULL OR NEW.superseded_at IS NULL
236
+ OR NEW.id!=OLD.id OR NEW.proposal_id!=OLD.proposal_id OR NEW.project_id!=OLD.project_id OR NEW.feature!=OLD.feature
237
+ OR NEW.kind!=OLD.kind OR NEW.subject_key!=OLD.subject_key OR NEW.pins_json!=OLD.pins_json
238
+ OR NEW.decision_json!=OLD.decision_json OR NEW.approved_via!=OLD.approved_via OR NEW.approved_at!=OLD.approved_at
239
+ BEGIN SELECT RAISE(ABORT, 'approvals are immutable except supersession'); END;
240
+ CREATE TRIGGER IF NOT EXISTS approval_delete BEFORE DELETE ON approvals
241
+ BEGIN SELECT RAISE(ABORT, 'approvals are append-only'); END;
242
+ `;
243
+
202
244
  function now(): string { return new Date().toISOString(); }
203
245
  function canonicalJson(value: unknown): string {
204
246
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -268,6 +310,7 @@ export class InitializationStore {
268
310
  }
269
311
  const migrate = db.transaction(() => {
270
312
  db.exec(SCHEMA);
313
+ db.exec(APPROVAL_SCHEMA);
271
314
  const ensureColumn = (table: string, column: string, definition: string) => {
272
315
  const columns = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
273
316
  if (!columns.some(item => item.name === column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
@@ -287,6 +330,10 @@ export class InitializationStore {
287
330
  db.exec("CREATE INDEX IF NOT EXISTS idx_document_jobs_status ON document_jobs(status, updated_at)");
288
331
  version = 2;
289
332
  }
333
+ if (version < 3) {
334
+ // v3 only adds the approval tables created above; existing rows are untouched.
335
+ version = 3;
336
+ }
290
337
  db.query("UPDATE schema_meta SET version=?").run(version);
291
338
  }
292
339
  const verified = db.query("SELECT version FROM schema_meta LIMIT 1").get() as { version: number };
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { normalizeDevflowHome } from "../paths.ts";
6
+ import type { McpToolName } from "./types.ts";
7
+
8
+ export const FEATURE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
9
+ export const TOOLS = ["validate_feature", "run_feature", "get_feature_report", "dsl_draft_validate"] as const;
10
+ export const MAX_DSL_BYTES = 2 * 1024 * 1024;
11
+ const PERMISSIONS = new Set(["validate", "report", "run", "dsl_draft"]);
12
+
13
+ export type ProjectGrant = { features: string[] | "*"; permissions: string[]; environment_profiles: string[] };
14
+ export type McpConfig = { schema_version: 1; clients: Record<string, { projects: Record<string, ProjectGrant> }> };
15
+
16
+ export class McpError extends Error {
17
+ constructor(public readonly code: string, message: string) { super(message); }
18
+ }
19
+
20
+ function invalidConfig(message: string): never {
21
+ throw new McpError("mcp_permission_denied", `invalid mcp.json: ${message}`);
22
+ }
23
+
24
+ function stringArray(value: unknown, label: string): string[] {
25
+ if (!Array.isArray(value) || value.some(item => typeof item !== "string")) invalidConfig(`${label} must be an array of strings`);
26
+ return value as string[];
27
+ }
28
+
29
+ function validateGrant(value: unknown, label: string): ProjectGrant {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalidConfig(`${label} must be an object`);
31
+ const grant = value as Record<string, unknown>;
32
+ const features = grant.features === "*" ? "*" : stringArray(grant.features, `${label}.features`);
33
+ const permissions = stringArray(grant.permissions, `${label}.permissions`);
34
+ if (permissions.some(permission => !PERMISSIONS.has(permission))) invalidConfig(`${label}.permissions contains an unsupported permission`);
35
+ const environmentProfiles = grant.environment_profiles === undefined ? [] : stringArray(grant.environment_profiles, `${label}.environment_profiles`);
36
+ return { features, permissions, environment_profiles: environmentProfiles };
37
+ }
38
+
39
+ function validateConfig(value: unknown): McpConfig {
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalidConfig("root must be an object");
41
+ const root = value as Record<string, unknown>;
42
+ if (root.schema_version !== 1) invalidConfig("schema_version must be 1");
43
+ if (!root.clients || typeof root.clients !== "object" || Array.isArray(root.clients)) invalidConfig("clients must be an object");
44
+ const clients: McpConfig["clients"] = {};
45
+ for (const [clientName, clientValue] of Object.entries(root.clients as Record<string, unknown>)) {
46
+ if (!clientValue || typeof clientValue !== "object" || Array.isArray(clientValue)) invalidConfig(`clients.${clientName} must be an object`);
47
+ const projectsValue = (clientValue as Record<string, unknown>).projects;
48
+ if (!projectsValue || typeof projectsValue !== "object" || Array.isArray(projectsValue)) invalidConfig(`clients.${clientName}.projects must be an object`);
49
+ const projects: Record<string, ProjectGrant> = {};
50
+ for (const [project, grant] of Object.entries(projectsValue as Record<string, unknown>)) {
51
+ if (!path.isAbsolute(project)) invalidConfig(`project key must be absolute: ${project}`);
52
+ projects[project] = validateGrant(grant, `clients.${clientName}.projects.${project}`);
53
+ }
54
+ clients[clientName] = { projects };
55
+ }
56
+ return { schema_version: 1, clients };
57
+ }
58
+
59
+ export async function loadAuthorization(client: string, home = normalizeDevflowHome(process.env.DEVFLOW_HOME)): Promise<McpConfig> {
60
+ const file = path.join(home, "mcp.json");
61
+ let value: unknown;
62
+ try {
63
+ value = JSON.parse(await readFile(file, "utf8"));
64
+ } catch (error) {
65
+ throw new McpError("mcp_permission_denied", `MCP authorization is unavailable: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ const config = validateConfig(value);
68
+ if (!Object.prototype.hasOwnProperty.call(config.clients, client)) throw new McpError("mcp_permission_denied", `unknown MCP client: ${client}`);
69
+ return config;
70
+ }
71
+
72
+ function permissionFor(tool: McpToolName): string {
73
+ if (tool === "dsl_draft_validate") return "dsl_draft";
74
+ if (tool === "get_feature_report") return "report";
75
+ if (tool === "validate_feature") return "validate";
76
+ return "run";
77
+ }
78
+
79
+ export async function authorize(config: McpConfig, client: string, tool: McpToolName, project: unknown, feature?: unknown): Promise<{ project: string; grant: ProjectGrant }> {
80
+ if (typeof project !== "string" || !path.isAbsolute(project)) throw new McpError("mcp_input_invalid", "project must be an absolute path");
81
+ let resolved: string;
82
+ try { resolved = await realpath(project); } catch { throw new McpError("mcp_project_not_allowed", "project does not exist"); }
83
+ const grants = config.clients[client]?.projects ?? {};
84
+ let grant: ProjectGrant | undefined;
85
+ for (const [key, candidate] of Object.entries(grants)) {
86
+ try {
87
+ if (await realpath(key) === resolved) { grant = candidate; break; }
88
+ } catch {
89
+ // A missing configured project cannot authorize a request.
90
+ }
91
+ }
92
+ if (!grant) throw new McpError("mcp_project_not_allowed", "project is not authorized");
93
+ if (tool !== "dsl_draft_validate") {
94
+ if (typeof feature !== "string" || !FEATURE_RE.test(feature)) throw new McpError("mcp_input_invalid", "feature is invalid");
95
+ if (grant.features !== "*" && !grant.features.includes(feature)) throw new McpError("mcp_feature_not_allowed", "feature is not authorized");
96
+ }
97
+ if (!grant.permissions.includes(permissionFor(tool))) throw new McpError("mcp_permission_denied", "permission is not authorized");
98
+ return { project: resolved, grant };
99
+ }
100
+
101
+ export function requestHash(value: unknown): string {
102
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
103
+ }
@@ -0,0 +1,102 @@
1
+ import path from "node:path";
2
+
3
+ import { packageRoot } from "../paths.ts";
4
+ import type { CoreRunner, CoreResult } from "./types.ts";
5
+
6
+ export const runnerScript = path.join(packageRoot(), "plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py");
7
+ export const dslScript = path.join(packageRoot(), "plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py");
8
+
9
+ const DEFAULT_TIMEOUT_MS = 7_200_000;
10
+ const TERMINATION_GRACE_MS = 5_000;
11
+ const PROGRESS_INTERVAL_MS = 10_000;
12
+
13
+ function terminateProcessGroup(pid: number | undefined, signal: "SIGTERM" | "SIGKILL"): void {
14
+ if (pid === undefined) return;
15
+ try { process.kill(-pid, signal); } catch { /* process group may already be gone */ }
16
+ }
17
+
18
+ async function settleText(value: Promise<string>, timeoutMs: number): Promise<string> {
19
+ return Promise.race([
20
+ value,
21
+ new Promise<string>(resolve => setTimeout(() => resolve(""), timeoutMs)),
22
+ ]);
23
+ }
24
+
25
+ export const runPython: CoreRunner = async (script, args, cwd, stdin, timeoutMs = DEFAULT_TIMEOUT_MS, onProgress) => {
26
+ // -E/-s ignore PYTHON* variables and user site-packages. Not -I: isolated mode also drops the
27
+ // script directory from sys.path, and the Core scripts import their sibling modules.
28
+ const child = Bun.spawn(["python3", "-E", "-s", script, ...args], {
29
+ cwd,
30
+ detached: true,
31
+ stdin: "pipe",
32
+ stdout: "pipe",
33
+ stderr: "pipe",
34
+ });
35
+ const stdoutPromise = new Response(child.stdout).text();
36
+ const stderrPromise = new Response(child.stderr).text();
37
+ if (stdin !== undefined) child.stdin.write(stdin);
38
+ child.stdin.end();
39
+ const progressTimer = onProgress === undefined ? undefined : setInterval(() => {
40
+ void Promise.resolve(onProgress()).catch(() => undefined);
41
+ }, PROGRESS_INTERVAL_MS);
42
+
43
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
44
+ let timedOut = false;
45
+ const timeoutPromise = new Promise<"timeout">(resolve => {
46
+ timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
47
+ });
48
+ const exitPromise = child.exited.then(exitCode => ({ exitCode }));
49
+ const winner = await Promise.race([exitPromise, timeoutPromise]);
50
+ if (winner === "timeout") {
51
+ timedOut = true;
52
+ terminateProcessGroup(child.pid, "SIGTERM");
53
+ try { child.kill("SIGTERM"); } catch { /* already exited */ }
54
+ await new Promise(resolve => setTimeout(resolve, TERMINATION_GRACE_MS));
55
+ terminateProcessGroup(child.pid, "SIGKILL");
56
+ try { child.kill("SIGKILL"); } catch { /* already exited */ }
57
+ if (progressTimer !== undefined) clearInterval(progressTimer);
58
+ clearTimeout(timeoutHandle);
59
+ return {
60
+ exitCode: null,
61
+ stdout: await settleText(stdoutPromise, 100),
62
+ stderr: await settleText(stderrPromise, 100),
63
+ timedOut,
64
+ };
65
+ }
66
+
67
+ if (progressTimer !== undefined) clearInterval(progressTimer);
68
+ clearTimeout(timeoutHandle);
69
+ return {
70
+ exitCode: winner.exitCode,
71
+ stdout: await stdoutPromise,
72
+ stderr: await stderrPromise,
73
+ timedOut,
74
+ };
75
+ };
76
+
77
+ export async function invokeCore(
78
+ runner: CoreRunner,
79
+ action: string,
80
+ project: string,
81
+ feature: string,
82
+ timeoutMs?: number,
83
+ onProgress?: () => void | Promise<void>,
84
+ ): Promise<CoreResult> {
85
+ const result = await runner(
86
+ runnerScript,
87
+ [action, "--feature", feature, "--json", ...(action === "run" ? ["--write"] : [])],
88
+ project,
89
+ undefined,
90
+ timeoutMs,
91
+ onProgress,
92
+ );
93
+ if (result.timedOut) return { ok: false, reason: "mcp_run_timeout", error_code: "mcp_run_timeout" };
94
+ try {
95
+ return JSON.parse(result.stdout) as CoreResult;
96
+ } catch {
97
+ // Keep the tail of the diagnostic (where Python tracebacks end), without project paths.
98
+ const detail = (result.stderr || result.stdout || `Core exited ${result.exitCode}`).slice(-1000)
99
+ .replace(/(?:[A-Za-z]:)?[^\s,;()"']+[\/][^\s,;()"']+/g, "<path>");
100
+ return { ok: false, reason: "core_failed", error_code: "core_failed", errors: [detail] };
101
+ }
102
+ }