devflow-kit 1.8.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (867) hide show
  1. package/CHANGELOG.md +182 -7
  2. package/LICENSE +1 -1
  3. package/README.md +139 -219
  4. package/dist/cli/agents-view/index.js +9 -0
  5. package/dist/cli/agents-view/render.js +310 -0
  6. package/dist/cli/agents-view/state.js +423 -0
  7. package/dist/cli/agents-view/terminal.js +179 -0
  8. package/dist/cli/commands/agents.js +585 -0
  9. package/dist/cli/commands/ambient.js +231 -0
  10. package/dist/cli/commands/capture.js +133 -0
  11. package/dist/cli/commands/compliance-prompts.js +174 -0
  12. package/dist/cli/commands/compliance.js +337 -0
  13. package/dist/cli/commands/context.js +66 -0
  14. package/dist/cli/commands/debug.js +122 -0
  15. package/dist/cli/commands/flags.js +119 -0
  16. package/dist/cli/commands/hud.js +229 -0
  17. package/dist/cli/commands/init-seed.js +226 -0
  18. package/dist/cli/commands/init.js +1786 -0
  19. package/dist/cli/commands/knowledge/index.js +25 -0
  20. package/dist/cli/commands/knowledge/list.js +115 -0
  21. package/dist/cli/commands/knowledge/toggle.js +84 -0
  22. package/dist/cli/commands/learning.js +311 -0
  23. package/dist/cli/commands/legacy-hooks.js +40 -0
  24. package/dist/cli/commands/memory.js +349 -0
  25. package/dist/cli/commands/proxy.js +1435 -0
  26. package/dist/cli/commands/rules.js +334 -0
  27. package/dist/cli/commands/safe-delete.js +140 -0
  28. package/dist/cli/commands/security.js +179 -0
  29. package/dist/cli/commands/skills.js +140 -0
  30. package/dist/cli/commands/uninstall.js +1214 -0
  31. package/dist/cli.js +35 -10
  32. package/dist/commands/bug-analysis.md +400 -0
  33. package/dist/commands/code-review.md +403 -0
  34. package/dist/commands/debug.md +282 -0
  35. package/dist/commands/dynamic-build.md +972 -0
  36. package/dist/commands/dynamic-plan.md +383 -0
  37. package/dist/commands/dynamic-profile.md +206 -0
  38. package/dist/commands/dynamic-tickets.md +583 -0
  39. package/dist/commands/explore.md +208 -0
  40. package/dist/commands/implement.md +590 -0
  41. package/dist/commands/plan.md +570 -0
  42. package/dist/commands/release.md +206 -0
  43. package/dist/commands/research.md +212 -0
  44. package/dist/commands/resolve.md +731 -0
  45. package/dist/commands/self-review.md +251 -0
  46. package/dist/core/agent-frontmatter.js +209 -0
  47. package/dist/core/agent-models.js +556 -0
  48. package/dist/core/agent-state.js +44 -0
  49. package/dist/core/assets.js +39 -0
  50. package/dist/core/cache.js +205 -0
  51. package/dist/core/codex-auth-inspect.js +128 -0
  52. package/dist/core/compliance-compose.js +458 -0
  53. package/dist/core/compliance.js +165 -0
  54. package/dist/core/external-models.js +76 -0
  55. package/dist/core/feature-config.js +125 -0
  56. package/dist/core/flags.js +309 -0
  57. package/dist/core/fs-atomic.js +74 -0
  58. package/dist/core/git.js +61 -0
  59. package/dist/core/learning-queue-cleanup.js +92 -0
  60. package/dist/core/learning-tuning-config.js +78 -0
  61. package/dist/core/manifest.js +171 -0
  62. package/dist/core/migrations.js +330 -0
  63. package/dist/core/model-discovery.js +655 -0
  64. package/dist/core/observation-io.js +50 -0
  65. package/dist/core/observations.js +109 -0
  66. package/dist/core/orphan-sweep.js +92 -0
  67. package/dist/core/paths.js +48 -0
  68. package/dist/core/plugins.js +525 -0
  69. package/dist/core/project-paths.js +159 -0
  70. package/dist/core/proxy-log.js +165 -0
  71. package/dist/core/proxy-state.js +305 -0
  72. package/dist/core/safe-delete-install.js +191 -0
  73. package/dist/core/teammate-mode-cleanup.js +33 -0
  74. package/dist/hud/colors.js +13 -2
  75. package/dist/hud/components/config-counts.js +2 -2
  76. package/dist/hud/components/context-usage.js +2 -2
  77. package/dist/hud/components/learning-counts.js +92 -0
  78. package/dist/hud/components/session-cost.js +10 -2
  79. package/dist/hud/components/usage-quota.js +43 -8
  80. package/dist/hud/components/version-badge.js +14 -4
  81. package/dist/hud/config.js +4 -2
  82. package/dist/hud/cost-history.js +281 -0
  83. package/dist/hud/git.js +112 -71
  84. package/dist/hud/index.js +47 -4
  85. package/dist/hud/render.js +8 -20
  86. package/dist/hud/transcript.js +5 -6
  87. package/dist/targets/claude-code/claude-paths.js +106 -0
  88. package/dist/targets/claude-code/compliance-install.js +305 -0
  89. package/dist/targets/claude-code/hooks.js +9 -0
  90. package/dist/targets/claude-code/installer.js +458 -0
  91. package/dist/targets/claude-code/legacy.js +239 -0
  92. package/dist/targets/claude-code/post-install.js +971 -0
  93. package/package.json +23 -17
  94. package/src/assets/agents/code.md +244 -0
  95. package/src/assets/agents/design.md +113 -0
  96. package/src/assets/agents/diagnose.md +210 -0
  97. package/src/assets/agents/evaluate.md +117 -0
  98. package/src/assets/agents/git.md +938 -0
  99. package/src/assets/agents/knowledge.md +89 -0
  100. package/src/assets/agents/learning.md +203 -0
  101. package/src/assets/agents/research.md +134 -0
  102. package/src/assets/agents/review.md +220 -0
  103. package/src/assets/agents/scrutinize.md +95 -0
  104. package/src/assets/agents/simplify.md +108 -0
  105. package/src/assets/agents/skim.md +136 -0
  106. package/src/assets/agents/synthesize.md +402 -0
  107. package/src/assets/agents/test.md +155 -0
  108. package/src/assets/agents/triage.md +141 -0
  109. package/src/assets/agents/validate.md +103 -0
  110. package/src/assets/commands/_partials/_compliance.mds +5 -0
  111. package/src/assets/commands/_partials/_decisions.mds +20 -0
  112. package/src/assets/commands/_partials/_engine.mds +244 -0
  113. package/src/assets/commands/_partials/_factory.mds +216 -0
  114. package/src/assets/commands/_partials/_knowledge.mds +95 -0
  115. package/src/assets/commands/_partials/_plan_contract.mds +51 -0
  116. package/src/assets/commands/_partials/_preamble.mds +82 -0
  117. package/src/assets/commands/_partials/_publication.mds +7 -0
  118. package/src/assets/commands/_partials/_roster.mds +31 -0
  119. package/src/assets/commands/_partials/_ticket_template.mds +59 -0
  120. package/src/assets/commands/_partials/_wave.mds +123 -0
  121. package/src/assets/commands/bug-analysis.mds +353 -0
  122. package/src/assets/commands/code-review.mds +355 -0
  123. package/src/assets/commands/debug.mds +224 -0
  124. package/src/assets/commands/dynamic-build.mds +534 -0
  125. package/src/assets/commands/dynamic-plan.mds +244 -0
  126. package/src/assets/commands/dynamic-profile.mds +132 -0
  127. package/src/assets/commands/dynamic-tickets.mds +226 -0
  128. package/src/assets/commands/explore.mds +150 -0
  129. package/src/assets/commands/implement.mds +496 -0
  130. package/src/assets/commands/plan.mds +523 -0
  131. package/src/assets/commands/release.md +206 -0
  132. package/src/assets/commands/research.mds +164 -0
  133. package/src/assets/commands/resolve.mds +636 -0
  134. package/src/assets/commands/self-review.mds +156 -0
  135. package/src/assets/rules/accessibility.md +11 -0
  136. package/src/assets/rules/compliance.md +15 -0
  137. package/src/assets/rules/engineering.md +13 -0
  138. package/src/assets/rules/go.md +12 -0
  139. package/src/assets/rules/java.md +12 -0
  140. package/src/assets/rules/python.md +12 -0
  141. package/src/assets/rules/quality.md +14 -0
  142. package/src/assets/rules/react.md +11 -0
  143. package/src/assets/rules/reliability.md +12 -0
  144. package/src/assets/rules/rust.md +12 -0
  145. package/src/assets/rules/security.md +12 -0
  146. package/src/assets/rules/typescript.md +12 -0
  147. package/src/assets/rules/ui-design.md +11 -0
  148. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +21 -0
  149. package/src/assets/scripts/hooks/background-memory-update +460 -0
  150. package/src/assets/scripts/hooks/capture-prompt +87 -0
  151. package/src/assets/scripts/hooks/capture-question +153 -0
  152. package/src/assets/scripts/hooks/capture-turn +105 -0
  153. package/src/assets/scripts/hooks/debug-trace +75 -0
  154. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +131 -0
  155. package/src/assets/scripts/hooks/ensure-devflow-init +39 -0
  156. package/src/assets/scripts/hooks/ensure-proxy +294 -0
  157. package/src/assets/scripts/hooks/ensure-root-gitignore +116 -0
  158. package/src/assets/scripts/hooks/get-mtime +23 -0
  159. package/src/assets/scripts/hooks/git-marker +16 -0
  160. package/src/assets/scripts/hooks/hook-bootstrap +25 -0
  161. package/src/assets/scripts/hooks/hook-log-init +41 -0
  162. package/src/assets/scripts/hooks/json-helper.cjs +725 -0
  163. package/src/assets/scripts/hooks/json-parse +243 -0
  164. package/src/assets/scripts/hooks/learning-lock +44 -0
  165. package/src/assets/scripts/hooks/lib/decisions-format.cjs +301 -0
  166. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +106 -0
  167. package/src/assets/scripts/hooks/lib/project-paths.cjs +230 -0
  168. package/src/assets/scripts/hooks/lib/render-decisions.cjs +398 -0
  169. package/src/assets/scripts/hooks/lib/safe-path.cjs +29 -0
  170. package/src/assets/scripts/hooks/log-paths +24 -0
  171. package/src/assets/scripts/hooks/memory-worker +101 -0
  172. package/src/assets/scripts/hooks/pre-compact-memory +126 -0
  173. package/src/assets/scripts/hooks/preamble +77 -0
  174. package/src/assets/scripts/hooks/queue-append +120 -0
  175. package/src/assets/scripts/hooks/resolve-project-root +37 -0
  176. package/src/assets/scripts/hooks/run-hook +31 -0
  177. package/src/assets/scripts/hooks/session-start-context +180 -0
  178. package/src/assets/scripts/hooks/session-start-memory +318 -0
  179. package/src/assets/scripts/hooks/session-start-orchestrator +54 -0
  180. package/src/assets/scripts/hud.sh +5 -0
  181. package/src/assets/scripts/redact-secrets.cjs +410 -0
  182. package/src/assets/skills/accessibility/SKILL.md +145 -0
  183. package/src/assets/skills/accessibility/references/patterns.md +398 -0
  184. package/src/assets/skills/accessibility/references/sources.md +26 -0
  185. package/src/assets/skills/accessibility/references/violations.md +358 -0
  186. package/src/assets/skills/apply-decisions/SKILL.md +100 -0
  187. package/src/assets/skills/apply-feature-knowledge/SKILL.md +69 -0
  188. package/src/assets/skills/architecture/SKILL.md +138 -0
  189. package/src/assets/skills/architecture/references/patterns.md +378 -0
  190. package/src/assets/skills/architecture/references/sources.md +41 -0
  191. package/src/assets/skills/architecture/references/violations.md +314 -0
  192. package/src/assets/skills/boundary-validation/SKILL.md +119 -0
  193. package/src/assets/skills/boundary-validation/references/detection.md +283 -0
  194. package/src/assets/skills/boundary-validation/references/patterns.md +283 -0
  195. package/src/assets/skills/boundary-validation/references/sources.md +41 -0
  196. package/src/assets/skills/boundary-validation/references/violations.md +211 -0
  197. package/src/assets/skills/complexity/SKILL.md +150 -0
  198. package/src/assets/skills/compliance/SKILL.md +121 -0
  199. package/src/assets/skills/compliance/frameworks/gdpr/fragment.md +11 -0
  200. package/src/assets/skills/compliance/frameworks/gdpr/reference.md +50 -0
  201. package/src/assets/skills/compliance/frameworks/hipaa/fragment.md +11 -0
  202. package/src/assets/skills/compliance/frameworks/hipaa/reference.md +44 -0
  203. package/src/assets/skills/compliance/frameworks/iso-27001/fragment.md +10 -0
  204. package/src/assets/skills/compliance/frameworks/iso-27001/reference.md +45 -0
  205. package/src/assets/skills/compliance/frameworks/pci-dss/fragment.md +11 -0
  206. package/src/assets/skills/compliance/frameworks/pci-dss/reference.md +55 -0
  207. package/src/assets/skills/compliance/frameworks/soc2/fragment.md +10 -0
  208. package/src/assets/skills/compliance/frameworks/soc2/reference.md +53 -0
  209. package/src/assets/skills/compliance/frameworks/sox/fragment.md +10 -0
  210. package/src/assets/skills/compliance/frameworks/sox/reference.md +67 -0
  211. package/src/assets/skills/compliance/references/detection.md +108 -0
  212. package/src/assets/skills/compliance/references/sources.md +28 -0
  213. package/src/assets/skills/consistency/SKILL.md +140 -0
  214. package/src/assets/skills/database/SKILL.md +134 -0
  215. package/src/assets/skills/dependencies/SKILL.md +141 -0
  216. package/src/assets/skills/dependency-research/SKILL.md +133 -0
  217. package/src/assets/skills/dependency-research/references/evaluation-criteria.md +101 -0
  218. package/src/assets/skills/design-review/SKILL.md +155 -0
  219. package/src/assets/skills/design-review/references/anti-patterns.md +207 -0
  220. package/src/assets/skills/docs-framework/SKILL.md +190 -0
  221. package/src/assets/skills/docs-framework/references/patterns.md +381 -0
  222. package/src/assets/skills/docs-framework/references/violations.md +221 -0
  223. package/src/assets/skills/documentation/SKILL.md +126 -0
  224. package/src/assets/skills/feature-knowledge/SKILL.md +341 -0
  225. package/src/assets/skills/gap-analysis/SKILL.md +151 -0
  226. package/src/assets/skills/gap-analysis/references/check-layers.md +125 -0
  227. package/src/assets/skills/git/SKILL.md +287 -0
  228. package/src/assets/skills/git/references/github-api.md +666 -0
  229. package/src/assets/skills/git/references/patterns.md +324 -0
  230. package/src/assets/skills/git/references/sources.md +16 -0
  231. package/src/assets/skills/git/references/violations.md +128 -0
  232. package/src/assets/skills/go/SKILL.md +147 -0
  233. package/src/assets/skills/go/references/concurrency.md +335 -0
  234. package/src/assets/skills/go/references/patterns.md +278 -0
  235. package/src/assets/skills/go/references/sources.md +41 -0
  236. package/src/assets/skills/go/references/violations.md +262 -0
  237. package/src/assets/skills/java/SKILL.md +148 -0
  238. package/src/assets/skills/java/references/modern-java.md +335 -0
  239. package/src/assets/skills/java/references/patterns.md +311 -0
  240. package/src/assets/skills/java/references/sources.md +44 -0
  241. package/src/assets/skills/java/references/violations.md +257 -0
  242. package/src/assets/skills/patterns/SKILL.md +162 -0
  243. package/src/assets/skills/performance/SKILL.md +135 -0
  244. package/src/assets/skills/performance/references/patterns.md +516 -0
  245. package/src/assets/skills/performance/references/sources.md +41 -0
  246. package/src/assets/skills/performance/references/violations.md +417 -0
  247. package/src/assets/skills/python/SKILL.md +156 -0
  248. package/src/assets/skills/python/references/async.md +255 -0
  249. package/src/assets/skills/python/references/patterns.md +280 -0
  250. package/src/assets/skills/python/references/sources.md +41 -0
  251. package/src/assets/skills/python/references/violations.md +272 -0
  252. package/src/assets/skills/qa/SKILL.md +136 -0
  253. package/src/assets/skills/qa/references/browser-testing.md +97 -0
  254. package/src/assets/skills/qa/references/patterns.md +118 -0
  255. package/src/assets/skills/qa/references/scenario-templates.md +102 -0
  256. package/src/assets/skills/qa/references/sources.md +30 -0
  257. package/src/assets/skills/qa/references/violations.md +98 -0
  258. package/src/assets/skills/quality-gates/SKILL.md +152 -0
  259. package/src/assets/skills/quality-gates/references/stub-detection.md +135 -0
  260. package/src/assets/skills/react/SKILL.md +143 -0
  261. package/src/assets/skills/react/references/error-handling.md +179 -0
  262. package/src/assets/skills/react/references/forms.md +214 -0
  263. package/src/assets/skills/react/references/hooks.md +193 -0
  264. package/src/assets/skills/react/references/patterns.md +1336 -0
  265. package/src/assets/skills/react/references/sources.md +35 -0
  266. package/src/assets/skills/react/references/violations.md +570 -0
  267. package/src/assets/skills/regression/SKILL.md +146 -0
  268. package/src/assets/skills/reliability/SKILL.md +153 -0
  269. package/src/assets/skills/reliability/references/detection.md +66 -0
  270. package/src/assets/skills/reliability/references/patterns.md +151 -0
  271. package/src/assets/skills/reliability/references/sources.md +27 -0
  272. package/src/assets/skills/reliability/references/violations.md +125 -0
  273. package/src/assets/skills/research-codebase/SKILL.md +135 -0
  274. package/src/assets/skills/research-competitor/SKILL.md +145 -0
  275. package/src/assets/skills/research-external/SKILL.md +141 -0
  276. package/src/assets/skills/research-market/SKILL.md +145 -0
  277. package/src/assets/skills/research-technology/SKILL.md +180 -0
  278. package/src/assets/skills/review-methodology/SKILL.md +121 -0
  279. package/src/assets/skills/review-methodology/references/patterns.md +186 -0
  280. package/src/assets/skills/review-methodology/references/report-template.md +142 -0
  281. package/src/assets/skills/review-methodology/references/violations.md +125 -0
  282. package/src/assets/skills/rust/SKILL.md +148 -0
  283. package/src/assets/skills/rust/references/ownership.md +278 -0
  284. package/src/assets/skills/rust/references/patterns.md +247 -0
  285. package/src/assets/skills/rust/references/sources.md +46 -0
  286. package/src/assets/skills/rust/references/violations.md +252 -0
  287. package/src/assets/skills/security/SKILL.md +121 -0
  288. package/src/assets/skills/security/references/patterns.md +271 -0
  289. package/src/assets/skills/security/references/sources.md +41 -0
  290. package/src/assets/skills/security/references/violations.md +241 -0
  291. package/src/assets/skills/software-design/SKILL.md +143 -0
  292. package/src/assets/skills/software-design/references/checklist.md +259 -0
  293. package/src/assets/skills/software-design/references/code-smell-violations.md +189 -0
  294. package/src/assets/skills/software-design/references/patterns.md +359 -0
  295. package/src/assets/skills/software-design/references/sources.md +41 -0
  296. package/src/assets/skills/software-design/references/violations.md +341 -0
  297. package/src/assets/skills/test-driven-development/SKILL.md +197 -0
  298. package/src/assets/skills/testing/SKILL.md +139 -0
  299. package/src/assets/skills/testing/references/patterns.md +346 -0
  300. package/src/assets/skills/testing/references/sources.md +53 -0
  301. package/src/assets/skills/testing/references/violations.md +322 -0
  302. package/src/assets/skills/typescript/SKILL.md +150 -0
  303. package/src/assets/skills/typescript/references/async.md +333 -0
  304. package/src/assets/skills/typescript/references/patterns.md +440 -0
  305. package/src/assets/skills/typescript/references/sources.md +41 -0
  306. package/src/assets/skills/typescript/references/type-guards.md +333 -0
  307. package/src/assets/skills/typescript/references/utility-types.md +255 -0
  308. package/src/assets/skills/typescript/references/violations.md +285 -0
  309. package/src/assets/skills/ui-design/SKILL.md +144 -0
  310. package/src/assets/skills/ui-design/references/patterns.md +341 -0
  311. package/src/assets/skills/ui-design/references/sources.md +41 -0
  312. package/src/assets/skills/ui-design/references/violations.md +312 -0
  313. package/src/assets/skills/worktree-support/SKILL.md +92 -0
  314. package/src/targets/claude-code/templates/claudeignore.template +188 -0
  315. package/src/targets/claude-code/templates/settings.json +86 -0
  316. package/dist/cli.d.ts +0 -3
  317. package/dist/commands/ambient.d.ts +0 -18
  318. package/dist/commands/ambient.js +0 -137
  319. package/dist/commands/hud.d.ts +0 -22
  320. package/dist/commands/hud.js +0 -180
  321. package/dist/commands/init.d.ts +0 -16
  322. package/dist/commands/init.js +0 -738
  323. package/dist/commands/list.d.ts +0 -24
  324. package/dist/commands/list.js +0 -89
  325. package/dist/commands/memory.d.ts +0 -22
  326. package/dist/commands/memory.js +0 -175
  327. package/dist/commands/skills.d.ts +0 -11
  328. package/dist/commands/skills.js +0 -116
  329. package/dist/commands/uninstall.d.ts +0 -22
  330. package/dist/commands/uninstall.js +0 -558
  331. package/dist/hud/cache.d.ts +0 -14
  332. package/dist/hud/cache.js +0 -54
  333. package/dist/hud/colors.d.ts +0 -23
  334. package/dist/hud/components/config-counts.d.ts +0 -8
  335. package/dist/hud/components/context-usage.d.ts +0 -8
  336. package/dist/hud/components/diff-stats.d.ts +0 -3
  337. package/dist/hud/components/directory.d.ts +0 -3
  338. package/dist/hud/components/git-ahead-behind.d.ts +0 -3
  339. package/dist/hud/components/git-branch.d.ts +0 -3
  340. package/dist/hud/components/model.d.ts +0 -3
  341. package/dist/hud/components/release-info.d.ts +0 -3
  342. package/dist/hud/components/session-cost.d.ts +0 -3
  343. package/dist/hud/components/session-duration.d.ts +0 -3
  344. package/dist/hud/components/todo-progress.d.ts +0 -3
  345. package/dist/hud/components/usage-quota.d.ts +0 -3
  346. package/dist/hud/components/version-badge.d.ts +0 -3
  347. package/dist/hud/components/worktree-count.d.ts +0 -3
  348. package/dist/hud/config.d.ts +0 -10
  349. package/dist/hud/credentials.d.ts +0 -17
  350. package/dist/hud/credentials.js +0 -99
  351. package/dist/hud/git.d.ts +0 -7
  352. package/dist/hud/index.d.ts +0 -2
  353. package/dist/hud/render.d.ts +0 -7
  354. package/dist/hud/stdin.d.ts +0 -6
  355. package/dist/hud/transcript.d.ts +0 -7
  356. package/dist/hud/types.d.ts +0 -116
  357. package/dist/hud/usage-api.d.ts +0 -7
  358. package/dist/hud/usage-api.js +0 -84
  359. package/dist/plugins.d.ts +0 -46
  360. package/dist/plugins.js +0 -233
  361. package/dist/utils/cli.d.ts +0 -5
  362. package/dist/utils/cli.js +0 -14
  363. package/dist/utils/git.d.ts +0 -11
  364. package/dist/utils/git.js +0 -36
  365. package/dist/utils/installer.d.ts +0 -41
  366. package/dist/utils/installer.js +0 -195
  367. package/dist/utils/manifest.d.ts +0 -46
  368. package/dist/utils/manifest.js +0 -100
  369. package/dist/utils/paths.d.ts +0 -42
  370. package/dist/utils/paths.js +0 -106
  371. package/dist/utils/post-install.d.ts +0 -87
  372. package/dist/utils/post-install.js +0 -546
  373. package/dist/utils/safe-delete-install.d.ts +0 -29
  374. package/dist/utils/safe-delete-install.js +0 -191
  375. package/dist/utils/safe-delete.d.ts +0 -12
  376. package/plugins/devflow-accessibility/.claude-plugin/plugin.json +0 -21
  377. package/plugins/devflow-accessibility/skills/accessibility/SKILL.md +0 -229
  378. package/plugins/devflow-accessibility/skills/accessibility/references/patterns.md +0 -670
  379. package/plugins/devflow-accessibility/skills/accessibility/references/violations.md +0 -419
  380. package/plugins/devflow-ambient/.claude-plugin/plugin.json +0 -33
  381. package/plugins/devflow-ambient/README.md +0 -68
  382. package/plugins/devflow-ambient/agents/coder.md +0 -135
  383. package/plugins/devflow-ambient/agents/reviewer.md +0 -165
  384. package/plugins/devflow-ambient/agents/scrutinizer.md +0 -82
  385. package/plugins/devflow-ambient/agents/shepherd.md +0 -108
  386. package/plugins/devflow-ambient/agents/simplifier.md +0 -102
  387. package/plugins/devflow-ambient/agents/skimmer.md +0 -143
  388. package/plugins/devflow-ambient/agents/validator.md +0 -86
  389. package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +0 -145
  390. package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +0 -74
  391. package/plugins/devflow-ambient/skills/debug-orchestration/SKILL.md +0 -69
  392. package/plugins/devflow-ambient/skills/implementation-orchestration/SKILL.md +0 -92
  393. package/plugins/devflow-ambient/skills/plan-orchestration/SKILL.md +0 -71
  394. package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +0 -19
  395. package/plugins/devflow-audit-claude/README.md +0 -46
  396. package/plugins/devflow-audit-claude/agents/claude-md-auditor.md +0 -134
  397. package/plugins/devflow-audit-claude/commands/audit-claude.md +0 -89
  398. package/plugins/devflow-code-review/.claude-plugin/plugin.json +0 -38
  399. package/plugins/devflow-code-review/README.md +0 -73
  400. package/plugins/devflow-code-review/agents/git.md +0 -277
  401. package/plugins/devflow-code-review/agents/reviewer.md +0 -165
  402. package/plugins/devflow-code-review/agents/synthesizer.md +0 -211
  403. package/plugins/devflow-code-review/commands/code-review-teams.md +0 -285
  404. package/plugins/devflow-code-review/commands/code-review.md +0 -163
  405. package/plugins/devflow-code-review/skills/agent-teams/SKILL.md +0 -124
  406. package/plugins/devflow-code-review/skills/agent-teams/references/cleanup.md +0 -104
  407. package/plugins/devflow-code-review/skills/agent-teams/references/communication.md +0 -122
  408. package/plugins/devflow-code-review/skills/agent-teams/references/team-patterns.md +0 -217
  409. package/plugins/devflow-code-review/skills/architecture-patterns/SKILL.md +0 -153
  410. package/plugins/devflow-code-review/skills/architecture-patterns/references/patterns.md +0 -873
  411. package/plugins/devflow-code-review/skills/architecture-patterns/references/violations.md +0 -575
  412. package/plugins/devflow-code-review/skills/complexity-patterns/SKILL.md +0 -143
  413. package/plugins/devflow-code-review/skills/consistency-patterns/SKILL.md +0 -140
  414. package/plugins/devflow-code-review/skills/database-patterns/SKILL.md +0 -134
  415. package/plugins/devflow-code-review/skills/dependencies-patterns/SKILL.md +0 -141
  416. package/plugins/devflow-code-review/skills/documentation-patterns/SKILL.md +0 -125
  417. package/plugins/devflow-code-review/skills/knowledge-persistence/SKILL.md +0 -128
  418. package/plugins/devflow-code-review/skills/knowledge-persistence/references/examples.md +0 -44
  419. package/plugins/devflow-code-review/skills/performance-patterns/SKILL.md +0 -154
  420. package/plugins/devflow-code-review/skills/performance-patterns/references/patterns.md +0 -503
  421. package/plugins/devflow-code-review/skills/performance-patterns/references/violations.md +0 -354
  422. package/plugins/devflow-code-review/skills/regression-patterns/SKILL.md +0 -146
  423. package/plugins/devflow-code-review/skills/review-methodology/SKILL.md +0 -119
  424. package/plugins/devflow-code-review/skills/review-methodology/references/patterns.md +0 -186
  425. package/plugins/devflow-code-review/skills/review-methodology/references/report-template.md +0 -142
  426. package/plugins/devflow-code-review/skills/review-methodology/references/violations.md +0 -125
  427. package/plugins/devflow-code-review/skills/security-patterns/SKILL.md +0 -156
  428. package/plugins/devflow-code-review/skills/security-patterns/references/patterns.md +0 -507
  429. package/plugins/devflow-code-review/skills/security-patterns/references/violations.md +0 -237
  430. package/plugins/devflow-code-review/skills/test-patterns/SKILL.md +0 -183
  431. package/plugins/devflow-code-review/skills/test-patterns/references/patterns.md +0 -220
  432. package/plugins/devflow-code-review/skills/test-patterns/references/violations.md +0 -221
  433. package/plugins/devflow-core-skills/.claude-plugin/plugin.json +0 -31
  434. package/plugins/devflow-core-skills/README.md +0 -50
  435. package/plugins/devflow-core-skills/skills/core-patterns/SKILL.md +0 -162
  436. package/plugins/devflow-core-skills/skills/core-patterns/references/checklist.md +0 -276
  437. package/plugins/devflow-core-skills/skills/core-patterns/references/code-smell-violations.md +0 -144
  438. package/plugins/devflow-core-skills/skills/core-patterns/references/patterns.md +0 -576
  439. package/plugins/devflow-core-skills/skills/core-patterns/references/violations.md +0 -369
  440. package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +0 -143
  441. package/plugins/devflow-core-skills/skills/docs-framework/references/patterns.md +0 -346
  442. package/plugins/devflow-core-skills/skills/docs-framework/references/violations.md +0 -221
  443. package/plugins/devflow-core-skills/skills/git-safety/SKILL.md +0 -122
  444. package/plugins/devflow-core-skills/skills/git-safety/references/patterns.md +0 -289
  445. package/plugins/devflow-core-skills/skills/git-safety/references/violations.md +0 -18
  446. package/plugins/devflow-core-skills/skills/git-workflow/SKILL.md +0 -158
  447. package/plugins/devflow-core-skills/skills/git-workflow/references/commit-patterns.md +0 -115
  448. package/plugins/devflow-core-skills/skills/git-workflow/references/commit-violations.md +0 -77
  449. package/plugins/devflow-core-skills/skills/git-workflow/references/pr-patterns.md +0 -127
  450. package/plugins/devflow-core-skills/skills/git-workflow/references/pr-violations.md +0 -96
  451. package/plugins/devflow-core-skills/skills/github-patterns/SKILL.md +0 -153
  452. package/plugins/devflow-core-skills/skills/github-patterns/references/patterns.md +0 -572
  453. package/plugins/devflow-core-skills/skills/github-patterns/references/violations.md +0 -298
  454. package/plugins/devflow-core-skills/skills/input-validation/SKILL.md +0 -148
  455. package/plugins/devflow-core-skills/skills/input-validation/references/detection.md +0 -283
  456. package/plugins/devflow-core-skills/skills/input-validation/references/patterns.md +0 -361
  457. package/plugins/devflow-core-skills/skills/input-validation/references/violations.md +0 -224
  458. package/plugins/devflow-core-skills/skills/search-first/SKILL.md +0 -133
  459. package/plugins/devflow-core-skills/skills/search-first/references/evaluation-criteria.md +0 -101
  460. package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +0 -137
  461. package/plugins/devflow-core-skills/skills/test-patterns/SKILL.md +0 -183
  462. package/plugins/devflow-core-skills/skills/test-patterns/references/detection.md +0 -149
  463. package/plugins/devflow-core-skills/skills/test-patterns/references/patterns.md +0 -220
  464. package/plugins/devflow-core-skills/skills/test-patterns/references/report-template.md +0 -108
  465. package/plugins/devflow-core-skills/skills/test-patterns/references/violations.md +0 -221
  466. package/plugins/devflow-debug/.claude-plugin/plugin.json +0 -26
  467. package/plugins/devflow-debug/README.md +0 -65
  468. package/plugins/devflow-debug/agents/git.md +0 -277
  469. package/plugins/devflow-debug/agents/synthesizer.md +0 -211
  470. package/plugins/devflow-debug/commands/debug-teams.md +0 -245
  471. package/plugins/devflow-debug/commands/debug.md +0 -174
  472. package/plugins/devflow-debug/skills/agent-teams/SKILL.md +0 -124
  473. package/plugins/devflow-debug/skills/agent-teams/references/cleanup.md +0 -104
  474. package/plugins/devflow-debug/skills/agent-teams/references/communication.md +0 -122
  475. package/plugins/devflow-debug/skills/agent-teams/references/team-patterns.md +0 -217
  476. package/plugins/devflow-debug/skills/git-safety/SKILL.md +0 -122
  477. package/plugins/devflow-debug/skills/git-safety/references/detection.md +0 -290
  478. package/plugins/devflow-debug/skills/git-safety/references/patterns.md +0 -289
  479. package/plugins/devflow-debug/skills/git-safety/references/violations.md +0 -18
  480. package/plugins/devflow-debug/skills/knowledge-persistence/SKILL.md +0 -128
  481. package/plugins/devflow-debug/skills/knowledge-persistence/references/examples.md +0 -44
  482. package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +0 -21
  483. package/plugins/devflow-frontend-design/skills/frontend-design/SKILL.md +0 -254
  484. package/plugins/devflow-frontend-design/skills/frontend-design/references/patterns.md +0 -511
  485. package/plugins/devflow-frontend-design/skills/frontend-design/references/violations.md +0 -453
  486. package/plugins/devflow-go/.claude-plugin/plugin.json +0 -21
  487. package/plugins/devflow-go/skills/go/SKILL.md +0 -187
  488. package/plugins/devflow-go/skills/go/references/concurrency.md +0 -312
  489. package/plugins/devflow-go/skills/go/references/patterns.md +0 -232
  490. package/plugins/devflow-go/skills/go/references/violations.md +0 -205
  491. package/plugins/devflow-implement/.claude-plugin/plugin.json +0 -35
  492. package/plugins/devflow-implement/README.md +0 -71
  493. package/plugins/devflow-implement/agents/coder.md +0 -135
  494. package/plugins/devflow-implement/agents/git.md +0 -277
  495. package/plugins/devflow-implement/agents/scrutinizer.md +0 -82
  496. package/plugins/devflow-implement/agents/shepherd.md +0 -108
  497. package/plugins/devflow-implement/agents/simplifier.md +0 -102
  498. package/plugins/devflow-implement/agents/skimmer.md +0 -143
  499. package/plugins/devflow-implement/agents/synthesizer.md +0 -211
  500. package/plugins/devflow-implement/agents/validator.md +0 -86
  501. package/plugins/devflow-implement/commands/implement-teams.md +0 -620
  502. package/plugins/devflow-implement/commands/implement.md +0 -430
  503. package/plugins/devflow-implement/skills/agent-teams/SKILL.md +0 -124
  504. package/plugins/devflow-implement/skills/agent-teams/references/cleanup.md +0 -104
  505. package/plugins/devflow-implement/skills/agent-teams/references/communication.md +0 -122
  506. package/plugins/devflow-implement/skills/agent-teams/references/team-patterns.md +0 -217
  507. package/plugins/devflow-implement/skills/implementation-patterns/SKILL.md +0 -162
  508. package/plugins/devflow-implement/skills/knowledge-persistence/SKILL.md +0 -128
  509. package/plugins/devflow-implement/skills/knowledge-persistence/references/examples.md +0 -44
  510. package/plugins/devflow-implement/skills/self-review/SKILL.md +0 -149
  511. package/plugins/devflow-implement/skills/self-review/references/stub-detection.md +0 -135
  512. package/plugins/devflow-java/.claude-plugin/plugin.json +0 -21
  513. package/plugins/devflow-java/skills/java/SKILL.md +0 -183
  514. package/plugins/devflow-java/skills/java/references/modern-java.md +0 -270
  515. package/plugins/devflow-java/skills/java/references/patterns.md +0 -235
  516. package/plugins/devflow-java/skills/java/references/violations.md +0 -213
  517. package/plugins/devflow-python/.claude-plugin/plugin.json +0 -21
  518. package/plugins/devflow-python/skills/python/SKILL.md +0 -188
  519. package/plugins/devflow-python/skills/python/references/async.md +0 -220
  520. package/plugins/devflow-python/skills/python/references/patterns.md +0 -226
  521. package/plugins/devflow-python/skills/python/references/violations.md +0 -204
  522. package/plugins/devflow-react/.claude-plugin/plugin.json +0 -21
  523. package/plugins/devflow-react/skills/react/SKILL.md +0 -276
  524. package/plugins/devflow-react/skills/react/references/patterns.md +0 -1331
  525. package/plugins/devflow-react/skills/react/references/violations.md +0 -565
  526. package/plugins/devflow-resolve/.claude-plugin/plugin.json +0 -29
  527. package/plugins/devflow-resolve/README.md +0 -65
  528. package/plugins/devflow-resolve/agents/git.md +0 -277
  529. package/plugins/devflow-resolve/agents/resolver.md +0 -131
  530. package/plugins/devflow-resolve/agents/simplifier.md +0 -102
  531. package/plugins/devflow-resolve/commands/resolve-teams.md +0 -307
  532. package/plugins/devflow-resolve/commands/resolve.md +0 -246
  533. package/plugins/devflow-resolve/skills/agent-teams/SKILL.md +0 -124
  534. package/plugins/devflow-resolve/skills/agent-teams/references/cleanup.md +0 -104
  535. package/plugins/devflow-resolve/skills/agent-teams/references/communication.md +0 -122
  536. package/plugins/devflow-resolve/skills/agent-teams/references/team-patterns.md +0 -217
  537. package/plugins/devflow-resolve/skills/implementation-patterns/SKILL.md +0 -162
  538. package/plugins/devflow-resolve/skills/implementation-patterns/references/patterns.md +0 -1063
  539. package/plugins/devflow-resolve/skills/implementation-patterns/references/violations.md +0 -483
  540. package/plugins/devflow-resolve/skills/knowledge-persistence/SKILL.md +0 -128
  541. package/plugins/devflow-resolve/skills/knowledge-persistence/references/examples.md +0 -44
  542. package/plugins/devflow-resolve/skills/security-patterns/SKILL.md +0 -156
  543. package/plugins/devflow-resolve/skills/security-patterns/references/detection.md +0 -287
  544. package/plugins/devflow-resolve/skills/security-patterns/references/patterns.md +0 -507
  545. package/plugins/devflow-resolve/skills/security-patterns/references/violations.md +0 -237
  546. package/plugins/devflow-rust/.claude-plugin/plugin.json +0 -21
  547. package/plugins/devflow-rust/skills/rust/SKILL.md +0 -193
  548. package/plugins/devflow-rust/skills/rust/references/ownership.md +0 -242
  549. package/plugins/devflow-rust/skills/rust/references/patterns.md +0 -210
  550. package/plugins/devflow-rust/skills/rust/references/violations.md +0 -191
  551. package/plugins/devflow-self-review/.claude-plugin/plugin.json +0 -26
  552. package/plugins/devflow-self-review/README.md +0 -38
  553. package/plugins/devflow-self-review/agents/scrutinizer.md +0 -82
  554. package/plugins/devflow-self-review/agents/simplifier.md +0 -102
  555. package/plugins/devflow-self-review/agents/validator.md +0 -86
  556. package/plugins/devflow-self-review/commands/self-review.md +0 -132
  557. package/plugins/devflow-self-review/skills/core-patterns/SKILL.md +0 -162
  558. package/plugins/devflow-self-review/skills/core-patterns/references/checklist.md +0 -276
  559. package/plugins/devflow-self-review/skills/core-patterns/references/code-smell-violations.md +0 -144
  560. package/plugins/devflow-self-review/skills/core-patterns/references/detection.md +0 -303
  561. package/plugins/devflow-self-review/skills/core-patterns/references/patterns.md +0 -576
  562. package/plugins/devflow-self-review/skills/core-patterns/references/violations.md +0 -369
  563. package/plugins/devflow-self-review/skills/self-review/SKILL.md +0 -149
  564. package/plugins/devflow-self-review/skills/self-review/references/patterns.md +0 -405
  565. package/plugins/devflow-self-review/skills/self-review/references/report-template.md +0 -253
  566. package/plugins/devflow-self-review/skills/self-review/references/stub-detection.md +0 -135
  567. package/plugins/devflow-self-review/skills/self-review/references/violations.md +0 -308
  568. package/plugins/devflow-specify/.claude-plugin/plugin.json +0 -25
  569. package/plugins/devflow-specify/README.md +0 -46
  570. package/plugins/devflow-specify/agents/skimmer.md +0 -143
  571. package/plugins/devflow-specify/agents/synthesizer.md +0 -211
  572. package/plugins/devflow-specify/commands/specify-teams.md +0 -321
  573. package/plugins/devflow-specify/commands/specify.md +0 -186
  574. package/plugins/devflow-specify/skills/agent-teams/SKILL.md +0 -124
  575. package/plugins/devflow-specify/skills/agent-teams/references/cleanup.md +0 -104
  576. package/plugins/devflow-specify/skills/agent-teams/references/communication.md +0 -122
  577. package/plugins/devflow-specify/skills/agent-teams/references/team-patterns.md +0 -217
  578. package/plugins/devflow-typescript/.claude-plugin/plugin.json +0 -21
  579. package/plugins/devflow-typescript/skills/typescript/SKILL.md +0 -176
  580. package/plugins/devflow-typescript/skills/typescript/references/patterns.md +0 -1105
  581. package/plugins/devflow-typescript/skills/typescript/references/violations.md +0 -433
  582. package/scripts/hooks/ambient-prompt +0 -50
  583. package/scripts/hooks/background-memory-update +0 -257
  584. package/scripts/hooks/ensure-memory-gitignore +0 -17
  585. package/scripts/hooks/pre-compact-memory +0 -87
  586. package/scripts/hooks/run-hook +0 -23
  587. package/scripts/hooks/session-start-memory +0 -165
  588. package/scripts/hooks/stop-update-memory +0 -85
  589. package/scripts/hud/cache.d.ts +0 -14
  590. package/scripts/hud/cache.d.ts.map +0 -1
  591. package/scripts/hud/cache.js +0 -54
  592. package/scripts/hud/cache.js.map +0 -1
  593. package/scripts/hud/colors.d.ts +0 -23
  594. package/scripts/hud/colors.d.ts.map +0 -1
  595. package/scripts/hud/colors.js +0 -62
  596. package/scripts/hud/colors.js.map +0 -1
  597. package/scripts/hud/components/config-counts.d.ts +0 -8
  598. package/scripts/hud/components/config-counts.d.ts.map +0 -1
  599. package/scripts/hud/components/config-counts.js +0 -91
  600. package/scripts/hud/components/config-counts.js.map +0 -1
  601. package/scripts/hud/components/context-usage.d.ts +0 -8
  602. package/scripts/hud/components/context-usage.d.ts.map +0 -1
  603. package/scripts/hud/components/context-usage.js +0 -49
  604. package/scripts/hud/components/context-usage.js.map +0 -1
  605. package/scripts/hud/components/diff-stats.d.ts +0 -3
  606. package/scripts/hud/components/diff-stats.d.ts.map +0 -1
  607. package/scripts/hud/components/diff-stats.js +0 -36
  608. package/scripts/hud/components/diff-stats.js.map +0 -1
  609. package/scripts/hud/components/directory.d.ts +0 -3
  610. package/scripts/hud/components/directory.d.ts.map +0 -1
  611. package/scripts/hud/components/directory.js +0 -10
  612. package/scripts/hud/components/directory.js.map +0 -1
  613. package/scripts/hud/components/git-ahead-behind.d.ts +0 -3
  614. package/scripts/hud/components/git-ahead-behind.d.ts.map +0 -1
  615. package/scripts/hud/components/git-ahead-behind.js +0 -16
  616. package/scripts/hud/components/git-ahead-behind.js.map +0 -1
  617. package/scripts/hud/components/git-branch.d.ts +0 -3
  618. package/scripts/hud/components/git-branch.d.ts.map +0 -1
  619. package/scripts/hud/components/git-branch.js +0 -14
  620. package/scripts/hud/components/git-branch.js.map +0 -1
  621. package/scripts/hud/components/model.d.ts +0 -3
  622. package/scripts/hud/components/model.d.ts.map +0 -1
  623. package/scripts/hud/components/model.js +0 -21
  624. package/scripts/hud/components/model.js.map +0 -1
  625. package/scripts/hud/components/release-info.d.ts +0 -3
  626. package/scripts/hud/components/release-info.d.ts.map +0 -1
  627. package/scripts/hud/components/release-info.js +0 -9
  628. package/scripts/hud/components/release-info.js.map +0 -1
  629. package/scripts/hud/components/session-cost.d.ts +0 -3
  630. package/scripts/hud/components/session-cost.d.ts.map +0 -1
  631. package/scripts/hud/components/session-cost.js +0 -9
  632. package/scripts/hud/components/session-cost.js.map +0 -1
  633. package/scripts/hud/components/session-duration.d.ts +0 -3
  634. package/scripts/hud/components/session-duration.d.ts.map +0 -1
  635. package/scripts/hud/components/session-duration.js +0 -18
  636. package/scripts/hud/components/session-duration.js.map +0 -1
  637. package/scripts/hud/components/todo-progress.d.ts +0 -3
  638. package/scripts/hud/components/todo-progress.d.ts.map +0 -1
  639. package/scripts/hud/components/todo-progress.js +0 -11
  640. package/scripts/hud/components/todo-progress.js.map +0 -1
  641. package/scripts/hud/components/usage-quota.d.ts +0 -3
  642. package/scripts/hud/components/usage-quota.d.ts.map +0 -1
  643. package/scripts/hud/components/usage-quota.js +0 -45
  644. package/scripts/hud/components/usage-quota.js.map +0 -1
  645. package/scripts/hud/components/version-badge.d.ts +0 -3
  646. package/scripts/hud/components/version-badge.d.ts.map +0 -1
  647. package/scripts/hud/components/version-badge.js +0 -80
  648. package/scripts/hud/components/version-badge.js.map +0 -1
  649. package/scripts/hud/components/worktree-count.d.ts +0 -3
  650. package/scripts/hud/components/worktree-count.d.ts.map +0 -1
  651. package/scripts/hud/components/worktree-count.js +0 -8
  652. package/scripts/hud/components/worktree-count.js.map +0 -1
  653. package/scripts/hud/config.d.ts +0 -10
  654. package/scripts/hud/config.d.ts.map +0 -1
  655. package/scripts/hud/config.js +0 -55
  656. package/scripts/hud/config.js.map +0 -1
  657. package/scripts/hud/credentials.d.ts +0 -17
  658. package/scripts/hud/credentials.d.ts.map +0 -1
  659. package/scripts/hud/credentials.js +0 -99
  660. package/scripts/hud/credentials.js.map +0 -1
  661. package/scripts/hud/git.d.ts +0 -7
  662. package/scripts/hud/git.d.ts.map +0 -1
  663. package/scripts/hud/git.js +0 -153
  664. package/scripts/hud/git.js.map +0 -1
  665. package/scripts/hud/index.d.ts +0 -2
  666. package/scripts/hud/index.d.ts.map +0 -1
  667. package/scripts/hud/index.js +0 -81
  668. package/scripts/hud/index.js.map +0 -1
  669. package/scripts/hud/render.d.ts +0 -7
  670. package/scripts/hud/render.d.ts.map +0 -1
  671. package/scripts/hud/render.js +0 -111
  672. package/scripts/hud/render.js.map +0 -1
  673. package/scripts/hud/stdin.d.ts +0 -6
  674. package/scripts/hud/stdin.d.ts.map +0 -1
  675. package/scripts/hud/stdin.js +0 -25
  676. package/scripts/hud/stdin.js.map +0 -1
  677. package/scripts/hud/transcript.d.ts +0 -7
  678. package/scripts/hud/transcript.d.ts.map +0 -1
  679. package/scripts/hud/transcript.js +0 -130
  680. package/scripts/hud/transcript.js.map +0 -1
  681. package/scripts/hud/types.d.ts +0 -116
  682. package/scripts/hud/types.d.ts.map +0 -1
  683. package/scripts/hud/types.js +0 -2
  684. package/scripts/hud/types.js.map +0 -1
  685. package/scripts/hud/usage-api.d.ts +0 -7
  686. package/scripts/hud/usage-api.d.ts.map +0 -1
  687. package/scripts/hud/usage-api.js +0 -84
  688. package/scripts/hud/usage-api.js.map +0 -1
  689. package/scripts/hud.sh +0 -5
  690. package/shared/agents/coder.md +0 -135
  691. package/shared/agents/git.md +0 -277
  692. package/shared/agents/resolver.md +0 -131
  693. package/shared/agents/reviewer.md +0 -165
  694. package/shared/agents/scrutinizer.md +0 -82
  695. package/shared/agents/shepherd.md +0 -108
  696. package/shared/agents/simplifier.md +0 -102
  697. package/shared/agents/skimmer.md +0 -143
  698. package/shared/agents/synthesizer.md +0 -211
  699. package/shared/agents/validator.md +0 -86
  700. package/shared/skills/accessibility/SKILL.md +0 -229
  701. package/shared/skills/accessibility/references/detection.md +0 -171
  702. package/shared/skills/accessibility/references/patterns.md +0 -670
  703. package/shared/skills/accessibility/references/violations.md +0 -419
  704. package/shared/skills/agent-teams/SKILL.md +0 -124
  705. package/shared/skills/agent-teams/references/cleanup.md +0 -104
  706. package/shared/skills/agent-teams/references/communication.md +0 -122
  707. package/shared/skills/agent-teams/references/team-patterns.md +0 -217
  708. package/shared/skills/ambient-router/SKILL.md +0 -145
  709. package/shared/skills/ambient-router/references/skill-catalog.md +0 -74
  710. package/shared/skills/architecture-patterns/SKILL.md +0 -153
  711. package/shared/skills/architecture-patterns/references/detection.md +0 -337
  712. package/shared/skills/architecture-patterns/references/patterns.md +0 -873
  713. package/shared/skills/architecture-patterns/references/violations.md +0 -575
  714. package/shared/skills/complexity-patterns/SKILL.md +0 -143
  715. package/shared/skills/complexity-patterns/references/detection.md +0 -264
  716. package/shared/skills/complexity-patterns/references/patterns.md +0 -487
  717. package/shared/skills/complexity-patterns/references/violations.md +0 -361
  718. package/shared/skills/consistency-patterns/SKILL.md +0 -140
  719. package/shared/skills/consistency-patterns/references/detection.md +0 -207
  720. package/shared/skills/consistency-patterns/references/patterns.md +0 -202
  721. package/shared/skills/consistency-patterns/references/violations.md +0 -213
  722. package/shared/skills/core-patterns/SKILL.md +0 -162
  723. package/shared/skills/core-patterns/references/checklist.md +0 -276
  724. package/shared/skills/core-patterns/references/code-smell-violations.md +0 -144
  725. package/shared/skills/core-patterns/references/detection.md +0 -303
  726. package/shared/skills/core-patterns/references/patterns.md +0 -576
  727. package/shared/skills/core-patterns/references/violations.md +0 -369
  728. package/shared/skills/database-patterns/SKILL.md +0 -134
  729. package/shared/skills/database-patterns/references/detection.md +0 -208
  730. package/shared/skills/database-patterns/references/patterns.md +0 -394
  731. package/shared/skills/database-patterns/references/violations.md +0 -332
  732. package/shared/skills/debug-orchestration/SKILL.md +0 -69
  733. package/shared/skills/dependencies-patterns/SKILL.md +0 -141
  734. package/shared/skills/dependencies-patterns/references/detection.md +0 -181
  735. package/shared/skills/dependencies-patterns/references/patterns.md +0 -225
  736. package/shared/skills/dependencies-patterns/references/violations.md +0 -247
  737. package/shared/skills/docs-framework/SKILL.md +0 -143
  738. package/shared/skills/docs-framework/references/patterns.md +0 -346
  739. package/shared/skills/docs-framework/references/violations.md +0 -221
  740. package/shared/skills/documentation-patterns/SKILL.md +0 -125
  741. package/shared/skills/documentation-patterns/references/detection.md +0 -190
  742. package/shared/skills/documentation-patterns/references/patterns.md +0 -189
  743. package/shared/skills/documentation-patterns/references/violations.md +0 -163
  744. package/shared/skills/frontend-design/SKILL.md +0 -254
  745. package/shared/skills/frontend-design/references/detection.md +0 -184
  746. package/shared/skills/frontend-design/references/patterns.md +0 -511
  747. package/shared/skills/frontend-design/references/violations.md +0 -453
  748. package/shared/skills/git-safety/SKILL.md +0 -122
  749. package/shared/skills/git-safety/references/detection.md +0 -290
  750. package/shared/skills/git-safety/references/patterns.md +0 -289
  751. package/shared/skills/git-safety/references/violations.md +0 -18
  752. package/shared/skills/git-workflow/SKILL.md +0 -158
  753. package/shared/skills/git-workflow/references/commit-patterns.md +0 -115
  754. package/shared/skills/git-workflow/references/commit-violations.md +0 -77
  755. package/shared/skills/git-workflow/references/pr-patterns.md +0 -127
  756. package/shared/skills/git-workflow/references/pr-violations.md +0 -96
  757. package/shared/skills/github-patterns/SKILL.md +0 -153
  758. package/shared/skills/github-patterns/references/patterns.md +0 -572
  759. package/shared/skills/github-patterns/references/violations.md +0 -298
  760. package/shared/skills/go/SKILL.md +0 -187
  761. package/shared/skills/go/references/concurrency.md +0 -312
  762. package/shared/skills/go/references/detection.md +0 -129
  763. package/shared/skills/go/references/patterns.md +0 -232
  764. package/shared/skills/go/references/violations.md +0 -205
  765. package/shared/skills/implementation-orchestration/SKILL.md +0 -92
  766. package/shared/skills/implementation-patterns/SKILL.md +0 -162
  767. package/shared/skills/implementation-patterns/references/patterns.md +0 -1063
  768. package/shared/skills/implementation-patterns/references/violations.md +0 -483
  769. package/shared/skills/input-validation/SKILL.md +0 -148
  770. package/shared/skills/input-validation/references/detection.md +0 -283
  771. package/shared/skills/input-validation/references/patterns.md +0 -361
  772. package/shared/skills/input-validation/references/violations.md +0 -224
  773. package/shared/skills/java/SKILL.md +0 -183
  774. package/shared/skills/java/references/detection.md +0 -120
  775. package/shared/skills/java/references/modern-java.md +0 -270
  776. package/shared/skills/java/references/patterns.md +0 -235
  777. package/shared/skills/java/references/violations.md +0 -213
  778. package/shared/skills/knowledge-persistence/SKILL.md +0 -128
  779. package/shared/skills/knowledge-persistence/references/examples.md +0 -44
  780. package/shared/skills/performance-patterns/SKILL.md +0 -154
  781. package/shared/skills/performance-patterns/references/detection.md +0 -351
  782. package/shared/skills/performance-patterns/references/patterns.md +0 -503
  783. package/shared/skills/performance-patterns/references/violations.md +0 -354
  784. package/shared/skills/plan-orchestration/SKILL.md +0 -71
  785. package/shared/skills/python/SKILL.md +0 -188
  786. package/shared/skills/python/references/async.md +0 -220
  787. package/shared/skills/python/references/detection.md +0 -128
  788. package/shared/skills/python/references/patterns.md +0 -226
  789. package/shared/skills/python/references/violations.md +0 -204
  790. package/shared/skills/react/SKILL.md +0 -276
  791. package/shared/skills/react/references/patterns.md +0 -1331
  792. package/shared/skills/react/references/violations.md +0 -565
  793. package/shared/skills/regression-patterns/SKILL.md +0 -146
  794. package/shared/skills/regression-patterns/references/detection.md +0 -237
  795. package/shared/skills/regression-patterns/references/patterns.md +0 -226
  796. package/shared/skills/regression-patterns/references/violations.md +0 -225
  797. package/shared/skills/review-methodology/SKILL.md +0 -119
  798. package/shared/skills/review-methodology/references/patterns.md +0 -186
  799. package/shared/skills/review-methodology/references/report-template.md +0 -142
  800. package/shared/skills/review-methodology/references/violations.md +0 -125
  801. package/shared/skills/rust/SKILL.md +0 -193
  802. package/shared/skills/rust/references/detection.md +0 -131
  803. package/shared/skills/rust/references/ownership.md +0 -242
  804. package/shared/skills/rust/references/patterns.md +0 -210
  805. package/shared/skills/rust/references/violations.md +0 -191
  806. package/shared/skills/search-first/SKILL.md +0 -133
  807. package/shared/skills/search-first/references/evaluation-criteria.md +0 -101
  808. package/shared/skills/security-patterns/SKILL.md +0 -156
  809. package/shared/skills/security-patterns/references/detection.md +0 -287
  810. package/shared/skills/security-patterns/references/patterns.md +0 -507
  811. package/shared/skills/security-patterns/references/violations.md +0 -237
  812. package/shared/skills/self-review/SKILL.md +0 -149
  813. package/shared/skills/self-review/references/patterns.md +0 -405
  814. package/shared/skills/self-review/references/report-template.md +0 -253
  815. package/shared/skills/self-review/references/stub-detection.md +0 -135
  816. package/shared/skills/self-review/references/violations.md +0 -308
  817. package/shared/skills/test-driven-development/SKILL.md +0 -137
  818. package/shared/skills/test-driven-development/references/rationalization-prevention.md +0 -111
  819. package/shared/skills/test-patterns/SKILL.md +0 -183
  820. package/shared/skills/test-patterns/references/detection.md +0 -149
  821. package/shared/skills/test-patterns/references/patterns.md +0 -220
  822. package/shared/skills/test-patterns/references/report-template.md +0 -108
  823. package/shared/skills/test-patterns/references/violations.md +0 -221
  824. package/shared/skills/typescript/SKILL.md +0 -176
  825. package/shared/skills/typescript/references/patterns.md +0 -1105
  826. package/shared/skills/typescript/references/violations.md +0 -433
  827. package/src/templates/claudeignore.template +0 -188
  828. package/src/templates/settings.json +0 -59
  829. /package/dist/{utils → core}/safe-delete.js +0 -0
  830. /package/{plugins/devflow-accessibility → src/assets}/skills/accessibility/references/detection.md +0 -0
  831. /package/{plugins/devflow-code-review/skills/architecture-patterns → src/assets/skills/architecture}/references/detection.md +0 -0
  832. /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/detection.md +0 -0
  833. /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/patterns.md +0 -0
  834. /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/violations.md +0 -0
  835. /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/detection.md +0 -0
  836. /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/patterns.md +0 -0
  837. /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/violations.md +0 -0
  838. /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/detection.md +0 -0
  839. /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/patterns.md +0 -0
  840. /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/violations.md +0 -0
  841. /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/detection.md +0 -0
  842. /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/patterns.md +0 -0
  843. /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/violations.md +0 -0
  844. /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/detection.md +0 -0
  845. /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/patterns.md +0 -0
  846. /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/violations.md +0 -0
  847. /package/{plugins/devflow-core-skills/skills/git-safety → src/assets/skills/git}/references/detection.md +0 -0
  848. /package/{plugins/devflow-go → src/assets}/skills/go/references/detection.md +0 -0
  849. /package/{plugins/devflow-java → src/assets}/skills/java/references/detection.md +0 -0
  850. /package/{plugins/devflow-implement/skills/implementation-patterns → src/assets/skills/patterns}/references/patterns.md +0 -0
  851. /package/{plugins/devflow-implement/skills/implementation-patterns → src/assets/skills/patterns}/references/violations.md +0 -0
  852. /package/{plugins/devflow-code-review/skills/performance-patterns → src/assets/skills/performance}/references/detection.md +0 -0
  853. /package/{plugins/devflow-python → src/assets}/skills/python/references/detection.md +0 -0
  854. /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/patterns.md +0 -0
  855. /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/report-template.md +0 -0
  856. /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/violations.md +0 -0
  857. /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/detection.md +0 -0
  858. /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/patterns.md +0 -0
  859. /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/violations.md +0 -0
  860. /package/{plugins/devflow-rust → src/assets}/skills/rust/references/detection.md +0 -0
  861. /package/{plugins/devflow-code-review/skills/security-patterns → src/assets/skills/security}/references/detection.md +0 -0
  862. /package/{plugins/devflow-core-skills/skills/core-patterns → src/assets/skills/software-design}/references/detection.md +0 -0
  863. /package/{plugins/devflow-core-skills → src/assets}/skills/test-driven-development/references/rationalization-prevention.md +0 -0
  864. /package/{plugins/devflow-code-review/skills/test-patterns → src/assets/skills/testing}/references/detection.md +0 -0
  865. /package/{plugins/devflow-code-review/skills/test-patterns → src/assets/skills/testing}/references/report-template.md +0 -0
  866. /package/{plugins/devflow-frontend-design/skills/frontend-design → src/assets/skills/ui-design}/references/detection.md +0 -0
  867. /package/src/{templates → targets/claude-code/templates}/managed-settings.json +0 -0
@@ -0,0 +1,1435 @@
1
+ /**
2
+ * devflow proxy — Enable, disable, and check status of external model routing
3
+ * (GPT models via your OpenAI/Codex subscription).
4
+ *
5
+ * applies ADR-013: CLI-layer module; all core logic lives in src/core/proxy-state.ts
6
+ * and src/core/agent-models.ts.
7
+ * avoids PF-014: never process.exit() inside a finally-guarded scope; use return
8
+ * from the async action handler for all early-exit paths.
9
+ * avoids PF-001: hook output strings use fixed templates; port number interpolation
10
+ * is acceptable (digit-validated integer, not user-controlled content).
11
+ *
12
+ * Branding note: "subswitch" must NEVER appear in user-visible strings. Internal
13
+ * identifiers (SUBSWITCH_CONFIG env var, health body checks) are fine.
14
+ */
15
+ import { Command } from 'commander';
16
+ import { promises as fs, readFileSync } from 'fs';
17
+ import { fileURLToPath } from 'url';
18
+ import * as path from 'path';
19
+ import * as net from 'net';
20
+ import * as http from 'http';
21
+ import * as https from 'https';
22
+ import { spawn as cpSpawn } from 'child_process';
23
+ import * as p from '@clack/prompts';
24
+ import color from 'picocolors';
25
+ import { readProxyState, writeProxyState, buildProxyState, buildRoutingConfigJson, proxyBaseUrl, resolveProxyBin, DEFAULT_PROXY_PORT, } from '../../core/proxy-state.js';
26
+ import { syncManifestFeature, readManifest } from '../../core/manifest.js';
27
+ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
28
+ import { scrubChildEnv, openProxyLog, rotateProxyLogIfLarge } from '../../core/proxy-log.js';
29
+ import { inspectCodexAuth, classifyCodexAuthReadError } from '../../core/codex-auth-inspect.js';
30
+ import { stripAnsi } from '../../hud/colors.js';
31
+ import { reapplyAgentMapping, revertExternalAgents, countExternalMappedAgents, readAgentMapping, } from '../../core/agent-models.js';
32
+ import { getExternalModelsCached, discoverExternalModels, } from '../../core/model-discovery.js';
33
+ import { modelCacheDir } from '../../core/cache.js';
34
+ import { getClaudeDirectory, getDevFlowDirectory, getHomeDirectory, } from '../../targets/claude-code/claude-paths.js';
35
+ function Ok(value) {
36
+ return { ok: true, value };
37
+ }
38
+ function Err(error) {
39
+ return { ok: false, error };
40
+ }
41
+ // ─── Constants ────────────────────────────────────────────────────────────────
42
+ /** Marker used to identify ensure-proxy hook entries. */
43
+ const PROXY_HOOK_MARKER = 'ensure-proxy';
44
+ /** Pattern matching our relay's ANTHROPIC_BASE_URL value. */
45
+ const OUR_BASE_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d+$/;
46
+ /** Timeout for individual TCP connect probes and HTTP health checks (ms). */
47
+ const PROBE_TIMEOUT_MS = 2000;
48
+ /**
49
+ * Maximum length of the authMode field rendered by formatCodexAuthLine.
50
+ *
51
+ * Known values are 'chatgpt', 'apikey', 'unknown' (≤7 chars). A generous cap
52
+ * of 32 accommodates any future mode strings while bounding hostile input from
53
+ * the third-party ~/.codex/auth.json file. Matches the screenshot/bug-report
54
+ * hygiene rationale applied to accountSuffix (6-char cap).
55
+ */
56
+ const AUTH_MODE_MAX_LEN = 32;
57
+ /**
58
+ * Maximum response body accepted by realHttpGet before aborting (64 KB).
59
+ * Prevents unbounded memory growth on slow-trickle or misconfigured endpoints
60
+ * (e.g. an SSE stream on the same port that --disable probes).
61
+ */
62
+ const BODY_CAP_BYTES = 64 * 1024;
63
+ /** Timeout for the relay doctor subprocess (ms). */
64
+ const DOCTOR_TIMEOUT_MS = 10_000;
65
+ /**
66
+ * Maximum number of relay port probes during the CLI spawn wait (5s at 100ms each).
67
+ *
68
+ * The ensure-proxy hook uses a larger budget (80×0.1s = 8s) — this difference is
69
+ * intentional:
70
+ * - CLI (--enable): the user is waiting at an interactive terminal. 5s is the
71
+ * maximum comfortable wait; a cold relay start typically completes in <2s.
72
+ * - Hook (ensure-proxy): the hook fires inside a 15-second platform timeout. The
73
+ * relay may be starting from a cold OS state (first session after reboot) where
74
+ * 5s is too short. 8s gives a wider revival window while leaving 7s of headroom
75
+ * before the platform kills the hook.
76
+ */
77
+ const RELAY_SPAWN_MAX_PROBES = 50;
78
+ /** Interval between relay port probes during the CLI spawn wait (ms). */
79
+ const RELAY_SPAWN_PROBE_INTERVAL_MS = 100;
80
+ /** TCP connect timeout for each relay port probe during the CLI spawn wait (ms). */
81
+ const RELAY_SPAWN_PER_PROBE_TIMEOUT_MS = 500;
82
+ // ─── Version helper ───────────────────────────────────────────────────────────
83
+ const __filename = fileURLToPath(import.meta.url);
84
+ const __dirname = path.dirname(__filename);
85
+ let _cachedVersion;
86
+ function getDevflowVersion() {
87
+ if (_cachedVersion !== undefined)
88
+ return _cachedVersion;
89
+ try {
90
+ // dist/cli/commands/ → ../../.. → repo root
91
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf-8'));
92
+ _cachedVersion = typeof pkg.version === 'string' ? pkg.version : null;
93
+ }
94
+ catch {
95
+ _cachedVersion = null;
96
+ }
97
+ return _cachedVersion;
98
+ }
99
+ // ─── Internal object helpers (used for single-pass atomic settings write) ────
100
+ /**
101
+ * Mutate a parsed Settings object in place: set ANTHROPIC_BASE_URL to our relay.
102
+ * Returns true when the object was changed (used to detect if a write is needed).
103
+ */
104
+ function _applyProxyEnvToObject(settings, port) {
105
+ const s = settings;
106
+ s.env = s.env ?? {};
107
+ const env = s.env;
108
+ const newUrl = proxyBaseUrl(port);
109
+ if (env.ANTHROPIC_BASE_URL === newUrl)
110
+ return false;
111
+ env.ANTHROPIC_BASE_URL = newUrl;
112
+ return true;
113
+ }
114
+ /**
115
+ * Mutate a parsed Settings object in place: remove ANTHROPIC_BASE_URL only when
116
+ * its value exactly matches our relay on the given managed port.
117
+ *
118
+ * Scoped to `managedPort` so a user's own localhost gateway (LiteLLM,
119
+ * local Ollama proxy, etc.) on ANY other port is never clobbered.
120
+ * The caller is responsible for passing the port Devflow currently owns:
121
+ * - disable path → proxy.json.port (or DEFAULT_PROXY_PORT)
122
+ * - init path → proxy.json.port after the preflight block
123
+ * - enable path → the new port being applied (followed immediately by _applyProxyEnvToObject)
124
+ * - uninstall → proxy.json.port (or DEFAULT_PROXY_PORT)
125
+ *
126
+ * Returns true when the object was changed.
127
+ */
128
+ function _stripProxyEnvFromObject(settings, managedPort) {
129
+ const s = settings;
130
+ const env = s.env;
131
+ if (typeof env?.ANTHROPIC_BASE_URL !== 'string')
132
+ return false;
133
+ if (env.ANTHROPIC_BASE_URL !== proxyBaseUrl(managedPort))
134
+ return false;
135
+ delete env.ANTHROPIC_BASE_URL;
136
+ if (Object.keys(env).length === 0)
137
+ delete s.env;
138
+ return true;
139
+ }
140
+ /** Internal: add ensure-proxy hook to one event. Returns true when added. */
141
+ function _ensureProxyHook(settings, eventName, hookCmd) {
142
+ const existing = settings.hooks?.[eventName];
143
+ if (existing?.some((m) => m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER)))) {
144
+ return false;
145
+ }
146
+ settings.hooks ??= {};
147
+ settings.hooks[eventName] ??= [];
148
+ const entry = {
149
+ hooks: [{ type: 'command', command: hookCmd, timeout: 15 }],
150
+ };
151
+ settings.hooks[eventName].push(entry);
152
+ return true;
153
+ }
154
+ /** Internal: remove ensure-proxy hooks from one event. Returns true when removed. */
155
+ function _filterProxyHooks(settings, eventName) {
156
+ if (!settings.hooks?.[eventName])
157
+ return false;
158
+ const before = settings.hooks[eventName].length;
159
+ settings.hooks[eventName] = settings.hooks[eventName].filter((m) => !m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER)));
160
+ if (settings.hooks[eventName].length === before)
161
+ return false;
162
+ if (settings.hooks[eventName].length === 0)
163
+ delete settings.hooks[eventName];
164
+ if (Object.keys(settings.hooks).length === 0)
165
+ delete settings.hooks;
166
+ return true;
167
+ }
168
+ // ─── Pure env functions (exported for testing and cross-module reuse) ─────────
169
+ /**
170
+ * Apply ANTHROPIC_BASE_URL=http://127.0.0.1:<port> to settings JSON.
171
+ * Returns new serialized settings string. Does not mutate input.
172
+ * Idempotent — calling twice with the same port produces the same result.
173
+ */
174
+ export function applyProxyEnv(settingsJson, port) {
175
+ const settings = JSON.parse(settingsJson);
176
+ _applyProxyEnvToObject(settings, port);
177
+ return JSON.stringify(settings, null, 2) + '\n';
178
+ }
179
+ /**
180
+ * Remove ANTHROPIC_BASE_URL from settings JSON, but ONLY when its value exactly
181
+ * matches our relay on `managedPort`.
182
+ *
183
+ * `managedPort` scopes the strip to the port Devflow owns — a user's own
184
+ * localhost gateway on any other port is never touched. Pass `proxy.json.port`
185
+ * (or `DEFAULT_PROXY_PORT` when the file is absent) at every call site.
186
+ *
187
+ * Returns new serialized settings string. Does not mutate input.
188
+ * Cleans up an emptied env object.
189
+ */
190
+ export function stripProxyEnv(settingsJson, managedPort) {
191
+ const settings = JSON.parse(settingsJson);
192
+ _stripProxyEnvFromObject(settings, managedPort);
193
+ return JSON.stringify(settings, null, 2) + '\n';
194
+ }
195
+ /**
196
+ * Read the proxy env state from a settings JSON string.
197
+ *
198
+ * Returns:
199
+ * 'ours' — ANTHROPIC_BASE_URL = our relay on the given port
200
+ * 'ours-other-port'— ANTHROPIC_BASE_URL = our relay on a different port
201
+ * 'foreign' — ANTHROPIC_BASE_URL set but not our relay
202
+ * 'absent' — ANTHROPIC_BASE_URL not set
203
+ */
204
+ export function readProxyEnvState(settingsJson, port) {
205
+ const settings = JSON.parse(settingsJson);
206
+ const s = settings;
207
+ const env = s.env;
208
+ const url = env?.ANTHROPIC_BASE_URL;
209
+ if (typeof url !== 'string')
210
+ return 'absent';
211
+ if (url === proxyBaseUrl(port))
212
+ return 'ours';
213
+ if (OUR_BASE_URL_PATTERN.test(url))
214
+ return 'ours-other-port';
215
+ return 'foreign';
216
+ }
217
+ // ─── Pure hook helpers ──────────────────────────────────────────────────────────
218
+ /**
219
+ * Add ensure-proxy hooks to BOTH SessionStart and UserPromptSubmit events.
220
+ * Idempotent — skips events that already have the hook.
221
+ * Repairs partial state (one event present, other missing) by adding the missing one.
222
+ * Mutates settings in place. Returns true when any hook was added.
223
+ */
224
+ export function addProxyHooks(settings, devflowDir) {
225
+ const hookCmd = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ' ' + PROXY_HOOK_MARKER;
226
+ const addedSession = _ensureProxyHook(settings, 'SessionStart', hookCmd);
227
+ const addedPrompt = _ensureProxyHook(settings, 'UserPromptSubmit', hookCmd);
228
+ return addedSession || addedPrompt;
229
+ }
230
+ /**
231
+ * Remove ensure-proxy hooks from all events.
232
+ * Idempotent — no-op when hooks are not present.
233
+ * Preserves other hooks. Cleans empty arrays/objects.
234
+ * Mutates settings in place. Returns true when any hook was removed.
235
+ */
236
+ export function removeProxyHooks(settings) {
237
+ const removedSession = _filterProxyHooks(settings, 'SessionStart');
238
+ const removedPrompt = _filterProxyHooks(settings, 'UserPromptSubmit');
239
+ return removedSession || removedPrompt;
240
+ }
241
+ /**
242
+ * Apply the disable settings-pass: remove proxy hooks AND strip ANTHROPIC_BASE_URL.
243
+ *
244
+ * Both operations always run unconditionally — never short-circuited with ||.
245
+ * A full enabled state (hooks present AND ANTHROPIC_BASE_URL set) requires
246
+ * both calls to be evaluated; short-circuiting left ANTHROPIC_BASE_URL in
247
+ * settings when hooks were present, keeping new sessions pointed at a disabled
248
+ * relay.
249
+ *
250
+ * `managedPort` scopes the URL strip to the port Devflow owns — pass
251
+ * `proxy.json.port` (or `DEFAULT_PROXY_PORT`) at the call site.
252
+ *
253
+ * Mutates settings in place. Returns true when any change was made.
254
+ */
255
+ export function applyDisableToSettings(settings, managedPort) {
256
+ const removedHooks = removeProxyHooks(settings);
257
+ const strippedEnv = _stripProxyEnvFromObject(settings, managedPort);
258
+ return removedHooks || strippedEnv;
259
+ }
260
+ /**
261
+ * Check whether the ensure-proxy hook is registered on at least one event.
262
+ * Returns true if present on either SessionStart or UserPromptSubmit.
263
+ * Partial state (one event present, other missing) returns true —
264
+ * addProxyHooks will repair it on next enable.
265
+ */
266
+ export function hasProxyHooks(input) {
267
+ const settings = typeof input === 'string' ? JSON.parse(input) : input;
268
+ const check = (eventName) => settings.hooks?.[eventName]?.some((m) => m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER))) === true;
269
+ return check('SessionStart') || check('UserPromptSubmit');
270
+ }
271
+ // ─── Health-check identity helper ────────────────────────────────────────────
272
+ /**
273
+ * Return true when the raw health-check response body identifies our relay.
274
+ *
275
+ * The internal check `parsed['name'] === 'subswitch'` is correct here — 'subswitch'
276
+ * is the internal package name (fine in code, not in user-facing output — see branding
277
+ * note at the top of this file). Returns false on JSON parse failure, empty body, or a
278
+ * mismatched name field.
279
+ *
280
+ * Note: the ensure-proxy shell hook has its own inline copy of this check — it is a
281
+ * different language and cannot share the TypeScript module.
282
+ */
283
+ export function isOurRelayBody(body) {
284
+ try {
285
+ const parsed = JSON.parse(body);
286
+ return parsed['name'] === 'subswitch';
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ }
292
+ /**
293
+ * Run preflight checks before enabling the Devflow proxy.
294
+ *
295
+ * Checks (in order):
296
+ * ① Routing runtime bin resolvable.
297
+ * ② ~/.codex/auth.json exists.
298
+ * ③ Port probe: free → OK; accepting → health check → adopt or fail.
299
+ * ④ settings.json parseable; ANTHROPIC_BASE_URL not pointing elsewhere; API key warn.
300
+ *
301
+ * Doctor is deliberately excluded from preflight: the relay's doctor subcommand
302
+ * probes the relay port — a not-yet-started relay makes that probe fail (exit 1). A
303
+ * pre-spawn gate is therefore always unsatisfiable on a cold path. Doctor now runs in
304
+ * runPostSpawnVerification, after the relay is confirmed up. See D-EFR-2.
305
+ *
306
+ * Returns Ok(PreflightResult) on success, Err(message) on any check failure.
307
+ */
308
+ export async function runProxyPreflight(port, codexAuthPath, configPath, logPath, deps) {
309
+ // ① Routing runtime bin
310
+ const binResult = await deps.resolveProxyBin();
311
+ if (!binResult.ok)
312
+ return Err(binResult.error);
313
+ const { binPath, npxWarning } = binResult.value;
314
+ // ② Codex auth
315
+ const codexAuthExists = await deps.fileExists(codexAuthPath);
316
+ if (!codexAuthExists) {
317
+ return Err('Sign in to the Codex CLI first (codex login)');
318
+ }
319
+ // ③ Port probe
320
+ const portAccepting = await deps.tcpConnectable(port, PROBE_TIMEOUT_MS);
321
+ if (portAccepting) {
322
+ // Port is up — check health identity
323
+ const healthResult = await deps.httpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
324
+ if (healthResult.ok && isOurRelayBody(healthResult.value)) {
325
+ return Ok({ binPath, npxWarning, adopted: true });
326
+ }
327
+ // Port accepting but health timed out, failed, or not our relay
328
+ return Err(`port ${port} is in use by another application — pick a different port with \`devflow proxy --enable --port <n>\``);
329
+ }
330
+ // Port refused — free to proceed
331
+ // ④ Settings.json check
332
+ let settingsJson;
333
+ try {
334
+ settingsJson = await deps.readSettingsJson();
335
+ }
336
+ catch {
337
+ return Err('Could not read settings.json — check file permissions');
338
+ }
339
+ let parsedSettings;
340
+ try {
341
+ parsedSettings = JSON.parse(settingsJson);
342
+ }
343
+ catch {
344
+ return Err('settings.json is malformed — fix it before enabling the proxy');
345
+ }
346
+ const envState = readProxyEnvState(settingsJson, port);
347
+ if (envState === 'foreign') {
348
+ return Err('An existing ANTHROPIC_BASE_URL in settings.json points to a different gateway — Devflow will not overwrite it');
349
+ }
350
+ // API key warning (non-fatal)
351
+ const envBlock = parsedSettings.env;
352
+ if (typeof envBlock === 'object' &&
353
+ envBlock !== null &&
354
+ !Array.isArray(envBlock) &&
355
+ typeof envBlock.ANTHROPIC_API_KEY === 'string') {
356
+ deps.onWarn?.('ANTHROPIC_API_KEY is set in settings.json — requests will use that key through the local relay');
357
+ }
358
+ return Ok({ binPath, npxWarning, adopted: false });
359
+ }
360
+ // ─── Production dependency implementations ────────────────────────────────────
361
+ /** Production TCP connect implementation. */
362
+ async function realTcpConnectable(port, timeoutMs) {
363
+ return new Promise((resolve) => {
364
+ const socket = net.createConnection({ host: '127.0.0.1', port, timeout: timeoutMs });
365
+ socket.on('connect', () => {
366
+ socket.destroy();
367
+ resolve(true);
368
+ });
369
+ socket.on('error', () => {
370
+ socket.destroy();
371
+ resolve(false);
372
+ });
373
+ socket.on('timeout', () => {
374
+ socket.destroy();
375
+ resolve(false);
376
+ });
377
+ });
378
+ }
379
+ /**
380
+ * Production HTTP/HTTPS GET implementation — selects module from URL scheme.
381
+ *
382
+ * Three resource bounds:
383
+ * • Wall-clock total deadline: fires when 'end' never arrives (SSE / trickle server).
384
+ * `--disable` probes precisely the port Devflow no longer owns, so any local
385
+ * service that trickles bytes could pin the CLI forever without this bound.
386
+ * • 64 KB body cap: aborts before accumulating unbounded memory.
387
+ * • res.on('error') listener: response-stream errors are caught, not ignored.
388
+ *
389
+ * Exported for reliability tests.
390
+ */
391
+ export async function realHttpGet(url, timeoutMs) {
392
+ const mod = url.startsWith('https://') ? https : http;
393
+ return new Promise((resolve) => {
394
+ let settled = false;
395
+ const settle = (r) => {
396
+ if (settled)
397
+ return;
398
+ settled = true;
399
+ resolve(r);
400
+ };
401
+ // Wall-clock deadline — independent of the per-chunk socket idle timeout below.
402
+ const wallClock = setTimeout(() => {
403
+ req.destroy();
404
+ settle(Err('deadline exceeded'));
405
+ }, timeoutMs);
406
+ wallClock.unref();
407
+ const req = mod.get(url, { timeout: timeoutMs }, (res) => {
408
+ let bodyBytes = 0;
409
+ let body = '';
410
+ res.on('data', (chunk) => {
411
+ bodyBytes += chunk.length;
412
+ if (bodyBytes > BODY_CAP_BYTES) {
413
+ res.destroy();
414
+ req.destroy();
415
+ clearTimeout(wallClock);
416
+ settle(Err('response too large'));
417
+ return;
418
+ }
419
+ body += chunk.toString();
420
+ });
421
+ res.on('end', () => {
422
+ clearTimeout(wallClock);
423
+ settle(Ok(body));
424
+ });
425
+ res.on('error', (err) => {
426
+ clearTimeout(wallClock);
427
+ settle(Err(err.message));
428
+ });
429
+ });
430
+ req.on('error', (err) => {
431
+ clearTimeout(wallClock);
432
+ settle(Err(err.message));
433
+ });
434
+ req.on('timeout', () => {
435
+ req.destroy();
436
+ clearTimeout(wallClock);
437
+ settle(Err('timeout'));
438
+ });
439
+ });
440
+ }
441
+ /** Production doctor subprocess implementation. */
442
+ async function realSpawnDoctor(binPath, env, timeoutMs, logFile) {
443
+ // openProxyLog: 0700 parent dir + 0600 file creation + best-effort chmod for
444
+ // pre-existing wider modes (SEC-2). Non-fatal on chmod failure per PF-009.
445
+ const logFd = await openProxyLog(logFile);
446
+ try {
447
+ return await new Promise((resolve) => {
448
+ const proc = cpSpawn(process.execPath, [binPath, 'doctor'], {
449
+ env,
450
+ stdio: ['ignore', logFd.fd, logFd.fd],
451
+ });
452
+ let resolved = false;
453
+ const timer = setTimeout(() => {
454
+ if (!resolved) {
455
+ resolved = true;
456
+ proc.kill(); // SIGTERM — ask the process to terminate gracefully
457
+ // A SIGTERM-trapping child keeps the event loop alive (no unref on
458
+ // proc here, since we are awaiting the promise). Schedule a SIGKILL escalation
459
+ // after a short grace period. The escalation timer is unref()'d so it never
460
+ // prevents the CLI from exiting on its own if the process exits first.
461
+ const sigkillTimer = setTimeout(() => {
462
+ try {
463
+ proc.kill('SIGKILL');
464
+ }
465
+ catch { /* already dead — ignore */ }
466
+ }, 2000);
467
+ sigkillTimer.unref();
468
+ resolve(1);
469
+ }
470
+ }, timeoutMs);
471
+ proc.on('close', (code) => {
472
+ if (!resolved) {
473
+ resolved = true;
474
+ clearTimeout(timer);
475
+ resolve(code ?? 1);
476
+ }
477
+ });
478
+ // OS-level spawn failure (EMFILE, ENOMEM, EAGAIN) must be handled — an
479
+ // unhandled 'error' event becomes an uncaught exception. Resolve(1) so the
480
+ // finally block closes logFd and callers get a clean failure path.
481
+ proc.on('error', () => {
482
+ if (!resolved) {
483
+ resolved = true;
484
+ clearTimeout(timer);
485
+ resolve(1);
486
+ }
487
+ });
488
+ });
489
+ }
490
+ finally {
491
+ await logFd.close();
492
+ }
493
+ }
494
+ /**
495
+ * Build the real (production) ProxyPreflightDeps from the given options.
496
+ * Centralises the three private implementations so both runEnable and init.ts
497
+ * can consume them without byte-identical inline copies.
498
+ *
499
+ * applies ADR-013: pure configuration factory; I/O implementations factored once.
500
+ */
501
+ export function buildRealPreflightDeps(opts) {
502
+ const { settingsPath, onWarn, swallowSettingsReadError = false } = opts;
503
+ return {
504
+ resolveProxyBin,
505
+ fileExists: async (filePath) => {
506
+ try {
507
+ await fs.access(filePath);
508
+ return true;
509
+ }
510
+ catch {
511
+ return false;
512
+ }
513
+ },
514
+ tcpConnectable: realTcpConnectable,
515
+ httpGet: realHttpGet,
516
+ readSettingsJson: swallowSettingsReadError
517
+ ? async () => { try {
518
+ return await fs.readFile(settingsPath, 'utf-8');
519
+ }
520
+ catch {
521
+ return '{}';
522
+ } }
523
+ : () => fs.readFile(settingsPath, 'utf-8'),
524
+ spawnDoctor: realSpawnDoctor,
525
+ onWarn,
526
+ };
527
+ }
528
+ /**
529
+ * Spawn the relay (unless adopted) and wait up to 50×100ms for TCP accept.
530
+ *
531
+ * When `adopted` is true, the relay is already running — skip spawn entirely.
532
+ *
533
+ * Returns `{ ok: true }` when the port accepts connections.
534
+ * Returns `{ ok: false }` when:
535
+ * - relay never accepted after 50 probes (caller should rollback proxy.json)
536
+ * - relay process died before the port came up
537
+ * - OS-level spawn error (EMFILE, ENOMEM, EAGAIN) — always handled via the
538
+ * injected onError callback, never an uncaught exception
539
+ *
540
+ * avoids PF-014: no process.exit() — returns Result; caller decides error handling.
541
+ */
542
+ export async function spawnRelayAndWaitForPort(port, binPath, configPath, logPath, pidPath, adopted, deps) {
543
+ if (adopted) {
544
+ // Port already hosting our relay — skip spawn entirely, proceed to settings pass.
545
+ return { ok: true };
546
+ }
547
+ const logHandle = await deps.openLog(logPath);
548
+ let spawnError;
549
+ // SEC-2: allowlist env for the relay — only PATH/HOME/TMPDIR/LANG/LC_ALL are
550
+ // inherited; SUBSWITCH_CONFIG is the only process-specific addition here.
551
+ const env = { ...scrubChildEnv(), SUBSWITCH_CONFIG: configPath };
552
+ const { pid } = deps.spawnProcess({
553
+ execPath: process.execPath,
554
+ args: [binPath, 'serve'],
555
+ env,
556
+ stdioFd: logHandle.fd,
557
+ // Captured here; breaks the wait loop on the next iteration
558
+ onError: (err) => { spawnError = err; },
559
+ });
560
+ // Parent closes its copy; the spawned child retains the fd through the OS
561
+ await logHandle.close();
562
+ if (pid !== undefined) {
563
+ // Non-fatal: best-effort pid record for devflow proxy --status
564
+ await deps.writePid(pidPath, pid);
565
+ }
566
+ // Bounded wait: ≤RELAY_SPAWN_MAX_PROBES×100ms (5s max) for TCP accept
567
+ let portUp = false;
568
+ for (let i = 0; i < RELAY_SPAWN_MAX_PROBES; i++) {
569
+ await deps.sleep(RELAY_SPAWN_PROBE_INTERVAL_MS);
570
+ if (spawnError !== undefined) {
571
+ // OS-level error fired — no point waiting; relay will never start
572
+ break;
573
+ }
574
+ if (pid !== undefined && !deps.isProcessAlive(pid)) {
575
+ // Process died before port came up — check for EADDRINUSE race
576
+ // (another session may have started and already owns the port)
577
+ if (await deps.tcpConnectable(port, RELAY_SPAWN_PER_PROBE_TIMEOUT_MS)) {
578
+ portUp = true;
579
+ }
580
+ break;
581
+ }
582
+ if (await deps.tcpConnectable(port, RELAY_SPAWN_PER_PROBE_TIMEOUT_MS)) {
583
+ portUp = true;
584
+ break;
585
+ }
586
+ }
587
+ if (!portUp) {
588
+ return { ok: false, reason: 'relay-not-started' };
589
+ }
590
+ return { ok: true, spawnedPid: pid };
591
+ }
592
+ /** Build the real (production) SpawnAndWaitDeps. Not exported — internal to runEnable. */
593
+ function buildRealSpawnAndWaitDeps() {
594
+ return {
595
+ openLog: async (logPath) => {
596
+ // openProxyLog: 0700 parent dir + 0600 file creation + best-effort chmod (SEC-2).
597
+ const handle = await openProxyLog(logPath);
598
+ return { fd: handle.fd, close: () => handle.close() };
599
+ },
600
+ spawnProcess: ({ execPath, args, env, stdioFd, onError }) => {
601
+ const proc = cpSpawn(execPath, args, {
602
+ detached: true,
603
+ stdio: ['ignore', stdioFd, stdioFd],
604
+ env,
605
+ });
606
+ // Attach error handler before unref so OS-level failures are caught
607
+ proc.on('error', onError);
608
+ proc.unref();
609
+ return { pid: proc.pid };
610
+ },
611
+ writePid: async (pidPath, pid) => {
612
+ // Non-fatal: failure is inconvenient (--status loses pid) but not blocking
613
+ try {
614
+ await fs.writeFile(pidPath, String(pid), 'utf-8');
615
+ }
616
+ catch { /* non-fatal */ }
617
+ },
618
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
619
+ isProcessAlive: (pid) => {
620
+ try {
621
+ process.kill(pid, 0);
622
+ return true;
623
+ }
624
+ catch {
625
+ return false;
626
+ }
627
+ },
628
+ tcpConnectable: realTcpConnectable,
629
+ };
630
+ }
631
+ /**
632
+ * Run `node <binPath> doctor` against the live relay and handle failure.
633
+ *
634
+ * @D-EFR-2 Doctor gates post-spawn, not pre-spawn: the relay's doctor subcommand
635
+ * probes the relay port to confirm it is running — a not-yet-started relay makes
636
+ * that probe fail (exit 1), so a pre-spawn gate is always unsatisfiable on a cold
637
+ * path (the port is down by definition before spawn). Moving the gate post-spawn
638
+ * means doctor runs against an already-live relay, turning "running: NO" into
639
+ * "running: YES" on the healthy path, validating codex auth and TLS reachability.
640
+ *
641
+ * Kill-on-rollback is restricted to self-spawned relays (spawnedPid defined): an
642
+ * adopted relay may be serving other live Claude Code sessions, so we must never
643
+ * kill it. A just-spawned relay predates the settings pass, so no session is
644
+ * routing through it — SIGTERM is safe (with a SIGKILL escalation in the caller).
645
+ *
646
+ * @param spawnedPid Pid of the relay we spawned; undefined when the relay was adopted.
647
+ * @returns Ok(undefined) when doctor exits 0; Err(message) on failure. On failure the
648
+ * rollback (writeDisabledProxyState) and conditional kill are applied before
649
+ * returning — the caller does not need to do additional rollback.
650
+ */
651
+ export async function runPostSpawnVerification(binPath, configPath, logPath, spawnedPid, deps) {
652
+ // SEC-2: allowlist env for doctor — matches the relay spawn composition.
653
+ const doctorEnv = { ...scrubChildEnv(), SUBSWITCH_CONFIG: configPath };
654
+ const doctorExit = await deps.spawnDoctor(binPath, doctorEnv, DOCTOR_TIMEOUT_MS, logPath);
655
+ if (doctorExit === 0)
656
+ return Ok(undefined);
657
+ // Doctor failed — rollback proxy state and conditionally kill the relay.
658
+ // Both are best-effort: write failure is secondary; kill failure means process already exited.
659
+ await deps.writeDisabledProxyState();
660
+ if (spawnedPid !== undefined) {
661
+ deps.killProcess(spawnedPid);
662
+ }
663
+ return Err(`Routing verification failed — see ${logPath}`);
664
+ }
665
+ // ─── Atomic settings mutation for enable ─────────────────────────────────────
666
+ /**
667
+ * Perform the single atomic settings.json pass for enable:
668
+ * strip old hooks + env, then apply new hooks + env in one write.
669
+ *
670
+ * The writeFileAtomicExclusive call is guarded — ENOSPC/EACCES returns Err
671
+ * instead of crashing with an unhandled rejection.
672
+ *
673
+ * Returns Ok(undefined) on success, Err(reason) on hard failure.
674
+ */
675
+ async function applyEnableSettingsPass(settingsPath, devflowDir, port) {
676
+ let settingsContent;
677
+ try {
678
+ settingsContent = await fs.readFile(settingsPath, 'utf-8');
679
+ }
680
+ catch {
681
+ // Missing settings.json is fine — start from an empty object
682
+ settingsContent = '{}';
683
+ }
684
+ let parsedSettings;
685
+ try {
686
+ parsedSettings = JSON.parse(settingsContent);
687
+ }
688
+ catch {
689
+ return Err('settings.json is malformed — fix it before enabling the proxy');
690
+ }
691
+ // Atomic 4-call settings mutation: strip stale entries, then apply fresh ones.
692
+ // Strip is scoped to `port` (the new port being applied). The apply
693
+ // call below always overwrites ANTHROPIC_BASE_URL regardless, so the strip
694
+ // here primarily removes any exact-port match before the write-set cycle.
695
+ removeProxyHooks(parsedSettings);
696
+ _stripProxyEnvFromObject(parsedSettings, port);
697
+ addProxyHooks(parsedSettings, devflowDir);
698
+ _applyProxyEnvToObject(parsedSettings, port);
699
+ try {
700
+ await writeFileAtomicExclusive(settingsPath, JSON.stringify(parsedSettings, null, 2) + '\n');
701
+ }
702
+ catch (err) {
703
+ return Err(`Could not write settings.json: ${err instanceof Error ? err.message : String(err)}`);
704
+ }
705
+ return Ok(undefined);
706
+ }
707
+ // ─── PID-to-port identity binding ─────────────────────────────────────────────
708
+ /**
709
+ * Resolve the PID of the process currently listening on the given TCP port.
710
+ *
711
+ * @D-EFR-5 PID-port identity gate: signal 0 proves a PID exists; a health
712
+ * check proves some process on the port is our relay. Neither alone proves
713
+ * the pid-file PID *is* the listening process. A recycled PID can fool both
714
+ * checks simultaneously. The fix: use lsof to resolve the listening PID
715
+ * directly and require it to equal the pid-file PID before any signal.
716
+ *
717
+ * lsof is the canonical tool on darwin and is commonly available on linux.
718
+ * On unsupported platforms or when lsof is absent/restricted, returns null
719
+ * so the caller fails safe (identity-unconfirmed) rather than throwing.
720
+ *
721
+ * On darwin and linux: `lsof -nP -iTCP:<port> -sTCP:LISTEN -t`
722
+ * -n no host-name resolution -P no port-name resolution
723
+ * -sTCP:LISTEN only LISTEN-state sockets -t PIDs only
724
+ *
725
+ * avoids PF-001: fixed argument array — port is validated as a safe integer
726
+ * by the caller before this function is invoked; it is never interpolated
727
+ * into a shell string.
728
+ *
729
+ * Returns null when:
730
+ * • Platform is not darwin or linux
731
+ * • lsof is not on PATH or returns a non-zero exit code (EPERM, etc.)
732
+ * • Zero processes are listening on the port
733
+ * • More than one PID is returned (ambiguous — fail safe)
734
+ * • The single returned value is not a valid positive integer
735
+ */
736
+ async function realResolveListeningPid(port) {
737
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
738
+ return null;
739
+ }
740
+ return new Promise((resolve) => {
741
+ // SEC: fixed argument list — port is a validated integer (1-65535).
742
+ const proc = cpSpawn('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
743
+ stdio: ['ignore', 'pipe', 'ignore'],
744
+ });
745
+ let stdout = '';
746
+ proc.stdout.on('data', (chunk) => {
747
+ stdout += chunk.toString();
748
+ });
749
+ // Spawn failure (ENOENT = lsof absent, EPERM = restricted) → degrade to null
750
+ proc.on('error', () => resolve(null));
751
+ proc.on('close', () => {
752
+ const lines = stdout.trim().split('\n').filter(Boolean);
753
+ // Zero or more-than-one result: ambiguous → fail safe
754
+ if (lines.length !== 1) {
755
+ resolve(null);
756
+ return;
757
+ }
758
+ const parsed = parseInt(lines[0].trim(), 10);
759
+ resolve(Number.isInteger(parsed) && parsed > 0 ? parsed : null);
760
+ });
761
+ });
762
+ }
763
+ /**
764
+ * Read and parse the relay PID file. Returns null on ENOENT, parse failure, or
765
+ * invalid value — callers treat null as "no pid available".
766
+ */
767
+ async function readPidFile(pidPath) {
768
+ try {
769
+ const pidStr = await fs.readFile(pidPath, 'utf-8');
770
+ const pid = parseInt(pidStr.trim(), 10);
771
+ return !isNaN(pid) && pid > 0 ? pid : null;
772
+ }
773
+ catch {
774
+ return null;
775
+ }
776
+ }
777
+ /**
778
+ * Terminate the relay process recorded in the pid file, with identity confirmation.
779
+ *
780
+ * Protocol:
781
+ * 1. Read the pid file — skip silently if absent.
782
+ * 2. Check liveness (signal 0).
783
+ * 3. Confirm identity — three signals, ALL must pass before signalling:
784
+ * a. TCP probe: port is accepting connections.
785
+ * b. Health check: the process on the port is our relay (`isOurRelayBody`).
786
+ * c. PID-port binding (D-EFR-5): lsof confirms the pid-file PID is the
787
+ * process listening on the port. If lsof is unavailable or ambiguous,
788
+ * fail safe — return 'identity-unconfirmed' without signalling.
789
+ * 4. SIGTERM → poll for death every 250 ms for up to 2 s (8 checks).
790
+ * 5. SIGKILL → poll for death every 250 ms for up to 2 s (8 checks).
791
+ * 6. Remove the pid file when the process is gone.
792
+ * 7. Best-effort rmdir of .proxy-spawn.lock when stale.
793
+ *
794
+ * applies D-EFR-5: signal 0 and health check are independent facts; lsof
795
+ * binds the pid-file PID to the port owner before any signal is sent.
796
+ * avoids PF-009: all sub-operations are non-fatal; a kill failure never aborts disable.
797
+ * avoids PF-014: never process.exit() inside a finally-guarded scope.
798
+ *
799
+ * @param deps Optional injectable dependencies (default: real lsof implementation).
800
+ * Provide a stub in tests to exercise every branch without a real binary.
801
+ *
802
+ * @returns 'killed' — relay stopped successfully.
803
+ * 'not-running' — pid file absent or process already dead.
804
+ * 'identity-unconfirmed' — pid alive but identity not positively established
805
+ * (health or lsof mismatch); no signal sent.
806
+ * 'kill-failed' — SIGTERM + SIGKILL sent but process still alive after grace.
807
+ */
808
+ export async function terminateRelay(pidPath, spawnLockPath, port, deps = { resolveListeningPid: realResolveListeningPid }) {
809
+ // Step 1: read pid file
810
+ const pid = await readPidFile(pidPath);
811
+ if (pid === null)
812
+ return 'not-running';
813
+ // Step 2: check liveness
814
+ try {
815
+ process.kill(pid, 0);
816
+ }
817
+ catch {
818
+ // Process already dead
819
+ try {
820
+ await fs.unlink(pidPath);
821
+ }
822
+ catch { /* best-effort */ }
823
+ return 'not-running';
824
+ }
825
+ // Step 3: confirm identity — all three signals must pass (D-EFR-5)
826
+ //
827
+ // Signal (a): TCP probe — confirms the port is accepting connections at all.
828
+ const portUp = await realTcpConnectable(port, PROBE_TIMEOUT_MS);
829
+ let identityConfirmed = false;
830
+ if (portUp) {
831
+ // Signal (b): health check — confirms the process on the port is our relay.
832
+ const healthResult = await realHttpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
833
+ const healthOk = healthResult.ok && isOurRelayBody(healthResult.value);
834
+ if (healthOk) {
835
+ // Signal (c): PID-port binding — lsof confirms the pid-file PID owns the port.
836
+ // If lsof is absent, returns null → null !== pid → identityConfirmed stays false.
837
+ // Failing safe is strictly better than signalling the wrong process.
838
+ const listeningPid = await deps.resolveListeningPid(port);
839
+ identityConfirmed = listeningPid === pid;
840
+ }
841
+ }
842
+ if (!identityConfirmed) {
843
+ return 'identity-unconfirmed';
844
+ }
845
+ // Bounded poll helper: check if PID is alive every 250 ms, up to maxChecks iterations.
846
+ // Returns true when the process is gone within the polling window.
847
+ const pollDead = async (maxChecks) => {
848
+ for (let i = 0; i < maxChecks; i++) {
849
+ await new Promise((resolve) => setTimeout(resolve, 250));
850
+ try {
851
+ process.kill(pid, 0);
852
+ }
853
+ catch {
854
+ return true;
855
+ }
856
+ }
857
+ return false;
858
+ };
859
+ // Step 4: SIGTERM → bounded 2 s wait (8 × 250 ms)
860
+ try {
861
+ process.kill(pid, 'SIGTERM');
862
+ }
863
+ catch { /* already exited — race */ }
864
+ const deadAfterTerm = await pollDead(8);
865
+ if (!deadAfterTerm) {
866
+ // Step 5: SIGKILL → bounded 2 s wait (8 × 250 ms)
867
+ try {
868
+ process.kill(pid, 'SIGKILL');
869
+ }
870
+ catch { /* already exited — race */ }
871
+ const deadAfterKill = await pollDead(8);
872
+ if (!deadAfterKill) {
873
+ return 'kill-failed';
874
+ }
875
+ }
876
+ // Step 6: remove pid file
877
+ try {
878
+ await fs.unlink(pidPath);
879
+ }
880
+ catch { /* best-effort */ }
881
+ // Step 7: best-effort rmdir of spawn lock when stale
882
+ try {
883
+ await fs.rmdir(spawnLockPath);
884
+ }
885
+ catch { /* held by another session or absent — ignore */ }
886
+ return 'killed';
887
+ }
888
+ /**
889
+ * Probe relay process state via TCP connect + health identity check.
890
+ * Always probes — does not short-circuit when the feature is disabled.
891
+ * This ensures `--status` surfaces the divergence case where the feature is
892
+ * disabled but the relay process is still listening (the relay is not killed on
893
+ * `--disable` for live-session continuity; it may outlive the disable operation).
894
+ */
895
+ async function resolveProcessState(port) {
896
+ const portUp = await realTcpConnectable(port, PROBE_TIMEOUT_MS);
897
+ if (!portUp)
898
+ return 'down';
899
+ const healthResult = await realHttpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
900
+ // Uses shared isOurRelayBody helper (same logic as runProxyPreflight)
901
+ if (healthResult.ok && isOurRelayBody(healthResult.value)) {
902
+ return 'running-ours';
903
+ }
904
+ // Port accepting but health timed out, failed, or not our relay
905
+ return 'port-squatted';
906
+ }
907
+ /**
908
+ * Pure: compute the process log line from resolved state.
909
+ * Returns null when no line should be emitted (dead pid + non-down processState).
910
+ */
911
+ function formatProcessLine(processState, pidAlive, pidFromFile, port) {
912
+ if (pidFromFile !== null) {
913
+ if (pidAlive) {
914
+ if (processState === 'running-ours') {
915
+ return {
916
+ level: 'info',
917
+ msg: `Process: ${color.green('running')} (pid ${pidFromFile}) — stop manually with: kill ${pidFromFile}`,
918
+ };
919
+ }
920
+ else if (processState === 'port-squatted') {
921
+ return {
922
+ level: 'warn',
923
+ msg: `Process: ${color.yellow('port squatted by another app')} (pid ${pidFromFile} alive but port ${port} is not our relay)`,
924
+ };
925
+ }
926
+ else {
927
+ return {
928
+ level: 'info',
929
+ msg: `Process: ${color.yellow('pid alive but port not responding')} (pid ${pidFromFile})`,
930
+ };
931
+ }
932
+ }
933
+ // Pid dead
934
+ if (processState === 'down') {
935
+ return {
936
+ level: 'info',
937
+ msg: `Process: ${color.dim('down')} (last pid ${pidFromFile}, no longer running)`,
938
+ };
939
+ }
940
+ return null; // pid dead but port squatted/running — unusual, no line
941
+ }
942
+ // No pid file
943
+ if (processState === 'running-ours') {
944
+ return { level: 'info', msg: `Process: ${color.green('running')} (no pid file)` };
945
+ }
946
+ else if (processState === 'port-squatted') {
947
+ return {
948
+ level: 'warn',
949
+ msg: `Process: ${color.yellow('port squatted')} — port ${port} is in use by another application`,
950
+ };
951
+ }
952
+ else {
953
+ return { level: 'info', msg: `Process: ${color.dim('down')}` };
954
+ }
955
+ }
956
+ // ─── Status formatters ────────────────────────────────────────────────────────
957
+ /**
958
+ * Pure formatter for the external models status line shown by `devflow proxy --status`.
959
+ *
960
+ * Extracted to enable isolated testing without clack I/O. (AC-F6)
961
+ */
962
+ export function formatExternalModelsLine(catalog, logPath) {
963
+ if (catalog.known) {
964
+ return `External models: ${color.cyan(catalog.selectableNames.join(', '))}`;
965
+ }
966
+ return `External models: ${color.dim('unavailable')} — see ${logPath}`;
967
+ }
968
+ /**
969
+ * Pure formatter for the Codex auth status line shown by `devflow proxy --status`.
970
+ *
971
+ * Returns `{level, msg}` — level encodes the warn-on-unreadable rule so callers
972
+ * need not re-derive it from `state.kind`. `unreadable` is the only state the user
973
+ * must act on (the relay rejects requests at that point while a bare file-existence
974
+ * check still reported success). Expiry is informational: the relay refreshes the
975
+ * access token on its own, so an expired stamp on a healthy install is normal.
976
+ *
977
+ * @param now Injected for deterministic tests; pass `new Date()` in production.
978
+ */
979
+ export function formatCodexAuthLine(state, authPath, now) {
980
+ switch (state.kind) {
981
+ case 'absent':
982
+ return { level: 'info', msg: `Codex auth: ${color.dim('absent')} — run codex login` };
983
+ case 'unreadable':
984
+ return {
985
+ level: 'warn',
986
+ msg: `Codex auth: ${color.red(`unreadable (${state.reason})`)} at ${authPath} — run codex login`,
987
+ };
988
+ case 'present': {
989
+ // authMode comes from the third-party ~/.codex/auth.json; strip
990
+ // ANSI escapes and cap length before embedding in terminal output.
991
+ const safeAuthMode = stripAnsi(state.authMode).slice(0, AUTH_MODE_MAX_LEN);
992
+ const identity = `${safeAuthMode}, ${state.accountSuffix}`;
993
+ const present = `Codex auth: ${color.green('present')} (${identity})`;
994
+ if (state.expiresAt === undefined)
995
+ return { level: 'info', msg: present };
996
+ const stamp = state.expiresAt.toISOString().slice(0, 10);
997
+ const msg = state.expiresAt.getTime() <= now.getTime()
998
+ ? `${present} — access token expired ${stamp}, refreshes on next request`
999
+ : `${present} — access token valid through ${stamp}`;
1000
+ return { level: 'info', msg };
1001
+ }
1002
+ default: {
1003
+ const exhaustive = state;
1004
+ void exhaustive;
1005
+ return { level: 'info', msg: '' };
1006
+ }
1007
+ }
1008
+ }
1009
+ // ─── Port resolution ──────────────────────────────────────────────────────────
1010
+ /**
1011
+ * Resolve the effective port for enable.
1012
+ *
1013
+ * When portOption is undefined (--port flag not provided by the user), falls back to
1014
+ * priorPort from proxy.json. This is the remembered-port path: a user who enabled on
1015
+ * port 5000, disabled, then re-enables without --port correctly reuses 5000.
1016
+ *
1017
+ * @param portOption Commander --port value; undefined when flag not provided
1018
+ * @param priorPort Last-used port from proxy.json (or DEFAULT_PROXY_PORT)
1019
+ */
1020
+ export function resolvePort(portOption, priorPort) {
1021
+ if (portOption === undefined)
1022
+ return Ok(priorPort);
1023
+ const parsed = parseInt(portOption, 10);
1024
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
1025
+ return Err(`Invalid port: ${portOption}`);
1026
+ }
1027
+ return Ok(parsed);
1028
+ }
1029
+ export const proxyCommand = new Command('proxy')
1030
+ .description('Enable or disable external model routing (GPT models via your OpenAI/Codex subscription)')
1031
+ .option('--enable', 'Enable external model routing via the Devflow proxy')
1032
+ .option('--disable', 'Disable external model routing')
1033
+ .option('--status', 'Show proxy status')
1034
+ .option('--port <n>', 'Port for the local relay (default: remembered or 4141)')
1035
+ .action(async (options) => {
1036
+ // No flag → show status
1037
+ const hasFlag = options.enable || options.disable || options.status;
1038
+ if (!hasFlag) {
1039
+ await runStatus();
1040
+ return;
1041
+ }
1042
+ if (options.status) {
1043
+ await runStatus();
1044
+ return;
1045
+ }
1046
+ if (options.enable) {
1047
+ await runEnable(options.port);
1048
+ return;
1049
+ }
1050
+ if (options.disable) {
1051
+ await runDisable();
1052
+ return;
1053
+ }
1054
+ });
1055
+ // ─── Status ───────────────────────────────────────────────────────────────────
1056
+ async function runStatus() {
1057
+ const devflowDir = getDevFlowDirectory();
1058
+ const claudeDir = getClaudeDirectory();
1059
+ const settingsPath = path.join(claudeDir, 'settings.json');
1060
+ const home = getHomeDirectory();
1061
+ const logPath = path.join(devflowDir, 'logs', 'proxy.log');
1062
+ const pidPath = path.join(devflowDir, 'proxy.pid');
1063
+ const codexAuthPath = path.join(home, '.codex', 'auth.json');
1064
+ p.intro(color.bgBlue(color.white(' Devflow Proxy Status ')));
1065
+ // Feature state: manifest + proxy.json
1066
+ const manifest = await readManifest(devflowDir);
1067
+ const proxyStateResult = await readProxyState(devflowDir);
1068
+ const proxyState = proxyStateResult.ok ? proxyStateResult.value : null;
1069
+ const manifestEnabled = manifest?.features.proxy ?? false;
1070
+ const stateEnabled = proxyState?.enabled ?? false;
1071
+ if (manifestEnabled !== stateEnabled) {
1072
+ p.log.warn(`Feature state drift: manifest says ${manifestEnabled ? color.green('enabled') : color.dim('disabled')}, ` +
1073
+ `proxy.json says ${stateEnabled ? color.green('enabled') : color.dim('disabled')} — run devflow proxy --enable or --disable to repair`);
1074
+ }
1075
+ const featureEnabled = manifestEnabled && stateEnabled;
1076
+ p.log.info(`Feature: ${featureEnabled ? color.green('enabled') : color.dim('disabled')}` +
1077
+ (proxyState?.port ? ` (port ${proxyState.port})` : ''));
1078
+ // Process state — always probe regardless of featureEnabled so that the
1079
+ // divergence case (feature disabled but relay still running) is visible.
1080
+ const port = proxyState?.port ?? DEFAULT_PROXY_PORT;
1081
+ const processState = await resolveProcessState(port);
1082
+ // Divergence: feature is disabled but the relay is still responding.
1083
+ // This is the expected state immediately after `--disable` — the relay
1084
+ // keeps serving any live Claude Code sessions whose env captured the URL.
1085
+ if (!stateEnabled && processState === 'running-ours') {
1086
+ p.log.warn(`Relay is still running on port ${port} even though external model routing is disabled. ` +
1087
+ `Live Claude Code sessions whose env captured the relay URL continue routing through it. ` +
1088
+ `Run ${color.cyan('devflow proxy --disable')} again to stop the relay, ` +
1089
+ `or restart Claude Code to clear the routed session.`);
1090
+ }
1091
+ const pidFromFile = await readPidFile(pidPath);
1092
+ let pidAlive = false;
1093
+ if (pidFromFile !== null) {
1094
+ try {
1095
+ process.kill(pidFromFile, 0);
1096
+ pidAlive = true;
1097
+ }
1098
+ catch { /* dead */ }
1099
+ }
1100
+ const processLine = formatProcessLine(processState, pidAlive, pidFromFile, port);
1101
+ if (processLine !== null) {
1102
+ if (processLine.level === 'warn') {
1103
+ p.log.warn(processLine.msg);
1104
+ }
1105
+ else {
1106
+ p.log.info(processLine.msg);
1107
+ }
1108
+ }
1109
+ // Env state
1110
+ let envState = 'absent';
1111
+ try {
1112
+ const settingsJson = await fs.readFile(settingsPath, 'utf-8');
1113
+ envState = readProxyEnvState(settingsJson, port);
1114
+ }
1115
+ catch { /* settings missing */ }
1116
+ const envStateLabels = {
1117
+ 'ours': color.green('set (our relay)'),
1118
+ 'ours-other-port': color.yellow('set (our relay, different port)'),
1119
+ 'foreign': color.red('set (foreign gateway — Devflow will not overwrite)'),
1120
+ 'absent': color.dim('not set'),
1121
+ };
1122
+ p.log.info(`ANTHROPIC_BASE_URL: ${envStateLabels[envState]}`);
1123
+ // Codex auth — inspect contents, not just existence. A file the relay would
1124
+ // reject (bad JSON, no account id) 401s every request while an existence check
1125
+ // still reports "present"; that gap is what this check closes.
1126
+ let codexAuth;
1127
+ try {
1128
+ codexAuth = inspectCodexAuth(await fs.readFile(codexAuthPath, 'utf-8'));
1129
+ }
1130
+ catch (err) {
1131
+ codexAuth = classifyCodexAuthReadError(err);
1132
+ }
1133
+ const codexAuthLine = formatCodexAuthLine(codexAuth, codexAuthPath, new Date());
1134
+ if (codexAuthLine.level === 'warn') {
1135
+ p.log.warn(codexAuthLine.msg);
1136
+ }
1137
+ else {
1138
+ p.log.info(codexAuthLine.msg);
1139
+ }
1140
+ // Agent mapping count
1141
+ const mappingResult = await readAgentMapping(devflowDir);
1142
+ if (mappingResult.ok) {
1143
+ const count = countExternalMappedAgents(mappingResult.value);
1144
+ if (count > 0) {
1145
+ p.log.info(`External-mapped agents: ${color.cyan(String(count))}`);
1146
+ }
1147
+ else {
1148
+ p.log.info('External-mapped agents: none — use devflow agents to configure');
1149
+ }
1150
+ }
1151
+ // External models registry (cache-only, zero spawns — avoids multi-second silent pause in --status)
1152
+ const cacheDir = modelCacheDir(devflowDir); // authoritative path from cache.ts (avoids PF-013)
1153
+ const catalog = getExternalModelsCached(cacheDir);
1154
+ p.log.info(formatExternalModelsLine(catalog, logPath));
1155
+ // Log path
1156
+ p.log.info(`Proxy log: ${color.dim(logPath)}`);
1157
+ if (featureEnabled && processState === 'running-ours') {
1158
+ p.note(`${color.cyan('devflow proxy --disable')} Disable external model routing\n` +
1159
+ `${color.cyan('devflow proxy --status')} Refresh status`, 'Management');
1160
+ }
1161
+ else {
1162
+ p.note(`${color.cyan('devflow proxy --enable')} Enable external model routing\n` +
1163
+ `${color.cyan('devflow proxy --status')} Refresh status`, 'Management');
1164
+ }
1165
+ }
1166
+ // ─── Enable ───────────────────────────────────────────────────────────────────
1167
+ async function runEnable(portOption) {
1168
+ const devflowDir = getDevFlowDirectory();
1169
+ const claudeDir = getClaudeDirectory();
1170
+ const settingsPath = path.join(claudeDir, 'settings.json');
1171
+ const installDir = path.join(claudeDir, 'agents', 'devflow');
1172
+ const home = getHomeDirectory();
1173
+ const codexAuthPath = path.join(home, '.codex', 'auth.json');
1174
+ const configPath = path.join(devflowDir, 'proxy-routing.json');
1175
+ const logPath = path.join(devflowDir, 'logs', 'proxy.log');
1176
+ const pidPath = path.join(devflowDir, 'proxy.pid');
1177
+ const cacheDir = modelCacheDir(devflowDir); // authoritative path from cache.ts (avoids PF-013)
1178
+ // Step 1: Read prior proxy.json (remembered port); --port flag overrides
1179
+ const priorStateResult = await readProxyState(devflowDir);
1180
+ const priorPort = priorStateResult.ok ? priorStateResult.value.port : DEFAULT_PROXY_PORT;
1181
+ const portResult = resolvePort(portOption, priorPort);
1182
+ if (!portResult.ok) {
1183
+ p.log.error(portResult.error);
1184
+ process.exitCode = 1;
1185
+ return;
1186
+ }
1187
+ const port = portResult.value;
1188
+ const s = p.spinner();
1189
+ s.start('Running preflight checks...');
1190
+ // Step 2: Write routing config
1191
+ await fs.mkdir(devflowDir, { recursive: true });
1192
+ // SEC-2: mode 0o700 for the logs directory (new directories only; pre-existing
1193
+ // dirs are unaffected by mkdir). openProxyLog handles individual file mode.
1194
+ await fs.mkdir(path.join(devflowDir, 'logs'), { recursive: true, mode: 0o700 });
1195
+ // SEC-2 pre-spawn rotation: rotate proxy.log before the relay opens an fd on it.
1196
+ // MUST run before spawnRelayAndWaitForPort (Step 5) — a rename while the relay
1197
+ // holds an append fd would orphan that fd. At this point no relay is running.
1198
+ await rotateProxyLogIfLarge(logPath);
1199
+ // Read existing routing config to preserve user-added anthropic/limits/logLevel/providers
1200
+ // blocks. A missing or malformed file falls back to clean defaults inside
1201
+ // buildRoutingConfigJson (non-fatal; avoids PF-009).
1202
+ let existingRoutingContent;
1203
+ try {
1204
+ existingRoutingContent = await fs.readFile(configPath, 'utf-8');
1205
+ }
1206
+ catch { /* ENOENT or EACCES — fall through to clean defaults */ }
1207
+ try {
1208
+ await fs.writeFile(configPath, buildRoutingConfigJson(port, existingRoutingContent), 'utf-8');
1209
+ }
1210
+ catch (err) {
1211
+ s.stop(color.red('Failed to write routing config'));
1212
+ p.log.error(`Could not write routing config: ${err instanceof Error ? err.message : String(err)}`);
1213
+ process.exitCode = 1;
1214
+ return;
1215
+ }
1216
+ // Step 3: Preflight checks (①–④: bin, codex auth, port probe, settings)
1217
+ // preflightDeps is saved so spawnDoctor can be reused in step 6 (post-spawn verification).
1218
+ const preflightDeps = buildRealPreflightDeps({
1219
+ settingsPath,
1220
+ // runEnable propagates settings read errors (init.ts swallows — see swallowSettingsReadError)
1221
+ swallowSettingsReadError: false,
1222
+ onWarn: (msg) => { s.stop(''); p.log.warn(msg); s.start(''); },
1223
+ });
1224
+ const preflightResult = await runProxyPreflight(port, codexAuthPath, configPath, logPath, preflightDeps);
1225
+ if (!preflightResult.ok) {
1226
+ s.stop(color.red('Preflight failed'));
1227
+ p.log.error(preflightResult.error);
1228
+ process.exitCode = 1;
1229
+ return;
1230
+ }
1231
+ const { binPath, npxWarning, adopted } = preflightResult.value;
1232
+ // Shared best-effort rollback for steps 5–7: write proxy.json enabled:false so a
1233
+ // subsequent --enable attempt does not believe the relay is running. A write failure
1234
+ // here is always secondary to the primary failure — never surfaced to the user.
1235
+ // All captured variables (binPath, configPath, port, devflowDir) are resolved by
1236
+ // this point.
1237
+ const rollbackProxyState = async () => {
1238
+ await writeProxyState(devflowDir, buildProxyState({
1239
+ enabled: false,
1240
+ port,
1241
+ binPath,
1242
+ configPath,
1243
+ devflowVersion: getDevflowVersion(),
1244
+ }));
1245
+ };
1246
+ s.message('Writing proxy state...');
1247
+ // Step 4: Write proxy.json enabled:true
1248
+ const newState = buildProxyState({
1249
+ enabled: true,
1250
+ port,
1251
+ binPath,
1252
+ configPath,
1253
+ devflowVersion: getDevflowVersion(),
1254
+ });
1255
+ const writeStateResult = await writeProxyState(devflowDir, newState);
1256
+ if (!writeStateResult.ok) {
1257
+ s.stop(color.red('Failed to write proxy state'));
1258
+ p.log.error(writeStateResult.error);
1259
+ process.exitCode = 1;
1260
+ return;
1261
+ }
1262
+ // Step 5: Spawn relay and wait for port
1263
+ if (!adopted) {
1264
+ s.message('Starting relay...');
1265
+ }
1266
+ const spawnResult = await spawnRelayAndWaitForPort(port, binPath, configPath, logPath, pidPath, adopted, buildRealSpawnAndWaitDeps());
1267
+ if (!spawnResult.ok) {
1268
+ await rollbackProxyState();
1269
+ s.stop(color.red('Relay failed to start'));
1270
+ p.log.error(`Proxy failed to start — check ${logPath}`);
1271
+ process.exitCode = 1;
1272
+ return;
1273
+ }
1274
+ const { spawnedPid } = spawnResult;
1275
+ // Step 6: Post-spawn doctor verification — runs against the live relay (see D-EFR-2).
1276
+ // Relay is confirmed up before this runs, so "running: YES" is the expected outcome.
1277
+ s.message('Verifying routing connection...');
1278
+ const verifyResult = await runPostSpawnVerification(binPath, configPath, logPath, spawnedPid, {
1279
+ spawnDoctor: preflightDeps.spawnDoctor,
1280
+ // Best-effort SIGTERM + SIGKILL escalation after 2s grace period.
1281
+ // Only called when we spawned the relay (spawnedPid defined) — see D-EFR-2.
1282
+ killProcess: (pid) => {
1283
+ try {
1284
+ process.kill(pid, 'SIGTERM');
1285
+ }
1286
+ catch { /* already exited — ignore */ }
1287
+ const sigkillTimer = setTimeout(() => {
1288
+ try {
1289
+ process.kill(pid, 'SIGKILL');
1290
+ }
1291
+ catch { /* already exited — ignore */ }
1292
+ }, 2000);
1293
+ sigkillTimer.unref();
1294
+ },
1295
+ writeDisabledProxyState: rollbackProxyState,
1296
+ });
1297
+ if (!verifyResult.ok) {
1298
+ // Rollback and conditional kill already applied inside runPostSpawnVerification
1299
+ s.stop(color.red('Routing verification failed'));
1300
+ p.log.error(verifyResult.error);
1301
+ process.exitCode = 1;
1302
+ return;
1303
+ }
1304
+ s.message('Updating settings...');
1305
+ // Step 7: Atomic settings mutation
1306
+ const settingsResult = await applyEnableSettingsPass(settingsPath, devflowDir, port);
1307
+ if (!settingsResult.ok) {
1308
+ await rollbackProxyState();
1309
+ s.stop(color.red('Cannot update settings'));
1310
+ p.log.error(settingsResult.error);
1311
+ process.exitCode = 1;
1312
+ return;
1313
+ }
1314
+ // Step 8: Sync manifest
1315
+ await syncManifestFeature(devflowDir, 'proxy', true);
1316
+ // Step 9: Reapply agent mapping
1317
+ const reapplyResult = await reapplyAgentMapping({
1318
+ proxyEnabled: true,
1319
+ installDir,
1320
+ devflowDir,
1321
+ });
1322
+ const mappedCount = reapplyResult.updated.length;
1323
+ // Warm the model cache UNDER the spinner so the wait is visible and
1324
+ // attributed. Strictly non-fatal — a cache failure must never affect the
1325
+ // enable result. The promise is awaited so Node can exit cleanly after
1326
+ // s.stop, instead of being held open ~200-600ms.
1327
+ s.message('Warming model cache...');
1328
+ await discoverExternalModels(cacheDir, logPath).catch(() => { });
1329
+ s.stop(color.green('External model routing enabled'));
1330
+ if (adopted) {
1331
+ p.log.info(`Relay already running on port ${port} — adopted`);
1332
+ }
1333
+ else {
1334
+ p.log.success(`Relay started on port ${port}`);
1335
+ }
1336
+ if (npxWarning) {
1337
+ p.log.warn('Relay binary resolved from npx cache — may not persist across reboots; run devflow init to reinstall');
1338
+ }
1339
+ if (mappedCount > 0) {
1340
+ p.log.info(`Restored external model mapping for ${mappedCount} agent(s)`);
1341
+ }
1342
+ p.log.info(color.dim('Takes effect in new Claude Code sessions. Use devflow agents to configure per-agent models.'));
1343
+ }
1344
+ // ─── Disable ──────────────────────────────────────────────────────────────────
1345
+ async function runDisable() {
1346
+ const devflowDir = getDevFlowDirectory();
1347
+ const claudeDir = getClaudeDirectory();
1348
+ const settingsPath = path.join(claudeDir, 'settings.json');
1349
+ const installDir = path.join(claudeDir, 'agents', 'devflow');
1350
+ const pidPath = path.join(devflowDir, 'proxy.pid');
1351
+ // Read prior state FIRST (before settings pass) to determine managed port.
1352
+ // applyDisableToSettings strips ANTHROPIC_BASE_URL only when the URL
1353
+ // port matches the port Devflow manages — callers must supply it. Reading
1354
+ // proxy.json here also consolidates state for Step 2 below.
1355
+ const priorStateResult = await readProxyState(devflowDir);
1356
+ const priorState = priorStateResult.ok ? priorStateResult.value : null;
1357
+ const managedPort = priorState?.port ?? DEFAULT_PROXY_PORT;
1358
+ // Step 1: Settings pass (removeProxyHooks + stripProxyEnv, single atomic write)
1359
+ let settingsContent;
1360
+ try {
1361
+ settingsContent = await fs.readFile(settingsPath, 'utf-8');
1362
+ }
1363
+ catch {
1364
+ settingsContent = '{}';
1365
+ }
1366
+ let parsedSettings;
1367
+ try {
1368
+ parsedSettings = JSON.parse(settingsContent);
1369
+ }
1370
+ catch {
1371
+ p.log.error('settings.json is malformed — fix it before disabling the proxy');
1372
+ process.exitCode = 1;
1373
+ return;
1374
+ }
1375
+ const changed = applyDisableToSettings(parsedSettings, managedPort);
1376
+ if (changed) {
1377
+ // Guard ENOSPC/EACCES — unhandled rejection leaves proxy in partial state
1378
+ try {
1379
+ await writeFileAtomicExclusive(settingsPath, JSON.stringify(parsedSettings, null, 2) + '\n');
1380
+ }
1381
+ catch (err) {
1382
+ p.log.error(`Could not write settings.json: ${err instanceof Error ? err.message : String(err)}`);
1383
+ process.exitCode = 1;
1384
+ return;
1385
+ }
1386
+ }
1387
+ // Step 2: Write proxy.json enabled:false (keep port/binPath/configPath)
1388
+ const disabledState = buildProxyState({
1389
+ enabled: false,
1390
+ port: priorState?.port ?? DEFAULT_PROXY_PORT,
1391
+ binPath: priorState?.binPath ?? null,
1392
+ configPath: priorState?.configPath ?? null,
1393
+ devflowVersion: getDevflowVersion(),
1394
+ });
1395
+ const writeDisabledResult = await writeProxyState(devflowDir, disabledState);
1396
+ if (!writeDisabledResult.ok) {
1397
+ p.log.error(`Could not write proxy state: ${writeDisabledResult.error}`);
1398
+ process.exitCode = 1;
1399
+ return;
1400
+ }
1401
+ // Step 3: Sync manifest
1402
+ await syncManifestFeature(devflowDir, 'proxy', false);
1403
+ // Step 4: Revert external agents to shipped defaults
1404
+ await revertExternalAgents({ installDir, devflowDir });
1405
+ p.log.success('External model routing disabled');
1406
+ // Step 5: Terminate the relay process.
1407
+ // Identity is confirmed via TCP + health before signalling — a recycled PID
1408
+ // must never be killed. avoids PF-009: all sub-operations are non-fatal.
1409
+ const spawnLockPath = path.join(devflowDir, '.proxy-spawn.lock');
1410
+ const terminateResult = await terminateRelay(pidPath, spawnLockPath, managedPort);
1411
+ switch (terminateResult) {
1412
+ case 'killed':
1413
+ p.log.info('Relay process stopped.');
1414
+ break;
1415
+ case 'not-running':
1416
+ p.log.info(color.dim('Relay process was not running.'));
1417
+ break;
1418
+ case 'identity-unconfirmed':
1419
+ p.log.warn(`PID from proxy.pid is alive but the relay identity could not be confirmed ` +
1420
+ `(port ${managedPort} is not responding as our relay). ` +
1421
+ `Inspect the process before stopping it manually.`);
1422
+ break;
1423
+ case 'kill-failed':
1424
+ p.log.warn(`Relay process (pid recorded in proxy.pid) did not stop after SIGTERM + SIGKILL. ` +
1425
+ `It may still be running — check with: kill -0 <pid>`);
1426
+ break;
1427
+ }
1428
+ // Always-print warning: already-running Claude Code sessions captured
1429
+ // ANTHROPIC_BASE_URL in their process env before the disable. The env var
1430
+ // is now stripped from settings.json so NEW sessions will not route, but
1431
+ // any session that was open during enable continues routing until restarted.
1432
+ p.log.warn('Already-running Claude Code sessions remain routed through the relay until they are restarted. ' +
1433
+ 'Restart Claude Code to fully stop routing in those sessions.');
1434
+ }
1435
+ //# sourceMappingURL=proxy.js.map