carthagent 0.0.0-stage → 1.0.7

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 (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,983 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Carthagent — Delivery Control</title>
7
+ <meta name="description" content="Evidence-driven software delivery, from plan to proof">
8
+ <meta name="theme-color" content="#171021">
9
+ <meta property="og:title" content="Carthagent">
10
+ <meta property="og:description" content="Evidence-driven software delivery, from plan to proof">
11
+ <script>
12
+ (() => {
13
+ const param = new URLSearchParams(location.search).get('theme');
14
+ const chosen = param || localStorage.getItem('carthagent-theme') || 'carthage';
15
+ if (param) localStorage.setItem('carthagent-theme', param);
16
+ const dark = typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;
17
+ document.documentElement.dataset.theme = chosen === 'system' ? (dark ? 'obsidian' : 'paper') : chosen;
18
+ document.documentElement.dataset.themeChoice = chosen;
19
+ })();
20
+ </script>
21
+ <style>
22
+ :root {
23
+ --mono: "IBM Plex Mono", "Cascadia Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
24
+ --sans: "Avenir Next", Avenir, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
+ --brand-copper: #d98951;
26
+ --brand-mint: #61d6a5;
27
+ }
28
+ /* Every theme must define the full token set; tests/theme.test.mjs verifies
29
+ presence plus WCAG AA contrast (4.5:1 text, 3:1 UI components). */
30
+ :root, [data-theme="carthage"] {
31
+ color-scheme: dark;
32
+ --bg: #120d1c; --chrome: #171021; --panel: #1d1429; --panel-2: #261a35; --sunken: #0d0914;
33
+ --border: #49385d; --border-strong: #67517f; --border-hover: #846c9e; --control-border: #9a86ae;
34
+ --text: #f7f1e5; --muted: #b8a9c9; --faint: #a99abb; --code: #ead9ff;
35
+ --accent: #f2c66d; --accent-ink: #211506; --accent-bg: #3b2c16; --hover-bg: #302140;
36
+ --success: #76d6aa; --warning: #f2c66d; --error: #ff969e;
37
+ }
38
+ [data-theme="obsidian"] {
39
+ color-scheme: dark;
40
+ --bg: #09110f; --chrome: #0d1714; --panel: #111d19; --panel-2: #172620; --sunken: #07100d;
41
+ --border: #23382f; --border-strong: #365247; --border-hover: #527465; --control-border: #718f82;
42
+ --text: #edf4ef; --muted: #a5b7ad; --faint: #82968c; --code: #d5e1da;
43
+ --accent: #e1955f; --accent-ink: #22130a; --accent-bg: #3a2418; --hover-bg: #1b2c26;
44
+ --success: #61d6a5; --warning: #f0bd69; --error: #ff8585;
45
+ }
46
+ [data-theme="midnight"] {
47
+ color-scheme: dark;
48
+ --bg: #0d1117; --chrome: #161b22; --panel: #161b22; --panel-2: #21262d; --sunken: #010409;
49
+ --border: #30363d; --border-strong: #3d444d; --border-hover: #484f58; --control-border: #6e7681;
50
+ --text: #e6edf3; --muted: #9198a1; --faint: #7d8590; --code: #d1d7e0;
51
+ --accent: #58a6ff; --accent-ink: #051120; --accent-bg: #12233a; --hover-bg: #262c33;
52
+ --success: #56d364; --warning: #e3b341; --error: #ff7b72;
53
+ }
54
+ [data-theme="solarized-dark"] {
55
+ color-scheme: dark;
56
+ --bg: #002b36; --chrome: #00232c; --panel: #073642; --panel-2: #0a4050; --sunken: #00212b;
57
+ --border: #0d4a5a; --border-strong: #1a5c6e; --border-hover: #2a6d82; --control-border: #51828f;
58
+ --text: #a2b2b2; --muted: #93a1a1; --faint: #8d9c9e; --code: #a5b5b5;
59
+ --accent: #5eacdc; --accent-ink: #04222e; --accent-bg: #0a3a4d; --hover-bg: #0c4353;
60
+ --success: #a4b94e; --warning: #d4a72c; --error: #f0938a;
61
+ }
62
+ [data-theme="nord"] {
63
+ color-scheme: dark;
64
+ --bg: #2e3440; --chrome: #2b303c; --panel: #3b4252; --panel-2: #3f4653; --sunken: #272c37;
65
+ --border: #4c566a; --border-strong: #5b6a82; --border-hover: #6a7a94; --control-border: #818eab;
66
+ --text: #eceff4; --muted: #c0c9d8; --faint: #a6aec0; --code: #d8dee9;
67
+ --accent: #8fc4d3; --accent-ink: #20262f; --accent-bg: #39485f; --hover-bg: #464f61;
68
+ --success: #a3be8c; --warning: #ebcb8b; --error: #e6a8ad;
69
+ }
70
+ [data-theme="contrast-dark"] {
71
+ color-scheme: dark;
72
+ --bg: #000000; --chrome: #000000; --panel: #0d0d0d; --panel-2: #1a1a1a; --sunken: #000000;
73
+ --border: #6f6f6f; --border-strong: #8f8f8f; --border-hover: #aaaaaa; --control-border: #9a9a9a;
74
+ --text: #ffffff; --muted: #b8b8b8; --faint: #949494; --code: #e0e0e0;
75
+ --accent: #82aaff; --accent-ink: #001133; --accent-bg: #16233f; --hover-bg: #1f1f1f;
76
+ --success: #5eead4; --warning: #fde047; --error: #ff8a8a;
77
+ }
78
+ [data-theme="okabe-dark"] {
79
+ color-scheme: dark;
80
+ --bg: #121316; --chrome: #15171b; --panel: #1a1c21; --panel-2: #22252b; --sunken: #0d0e11;
81
+ --border: #30333b; --border-strong: #41454f; --border-hover: #535763; --control-border: #6d727d;
82
+ --text: #e8eaed; --muted: #9ba1aa; --faint: #7e8490; --code: #c9cdd4;
83
+ --accent: #56b4e9; --accent-ink: #071824; --accent-bg: #173246; --hover-bg: #26292f;
84
+ --success: #35d6a5; --warning: #e8d24a; --error: #f07c3f;
85
+ }
86
+ [data-theme="paper"] {
87
+ color-scheme: light;
88
+ --bg: #faf9f6; --chrome: #f1efe9; --panel: #ffffff; --panel-2: #f1efe9; --sunken: #f5f3ee;
89
+ --border: #e0dcd2; --border-strong: #c9c3b4; --border-hover: #a8a296; --control-border: #7a7466;
90
+ --text: #24292f; --muted: #5c6470; --faint: #666d76; --code: #3a424d;
91
+ --accent: #5b4bd4; --accent-ink: #ffffff; --accent-bg: #eceafc; --hover-bg: #e8e5db;
92
+ --success: #157347; --warning: #8a6100; --error: #cb2d3b;
93
+ }
94
+ [data-theme="daylight"] {
95
+ color-scheme: light;
96
+ --bg: #f6f8fa; --chrome: #ffffff; --panel: #ffffff; --panel-2: #eef1f4; --sunken: #f6f8fa;
97
+ --border: #d1d9e0; --border-strong: #b7c0c9; --border-hover: #8c959f; --control-border: #7d8590;
98
+ --text: #1f2328; --muted: #59636e; --faint: #6a7280; --code: #383f47;
99
+ --accent: #0963c9; --accent-ink: #ffffff; --accent-bg: #ddf1ff; --hover-bg: #e6eaee;
100
+ --success: #1a7f37; --warning: #8a5700; --error: #cf222e;
101
+ }
102
+ [data-theme="solarized-light"] {
103
+ color-scheme: light;
104
+ --bg: #fdf6e3; --chrome: #faf2de; --panel: #fdf6e3; --panel-2: #f1e9d2; --sunken: #faf2de;
105
+ --border: #e3d8b8; --border-strong: #cbbf98; --border-hover: #ab9f78; --control-border: #8a7f61;
106
+ --text: #073642; --muted: #586e75; --faint: #5a6e75; --code: #2d4a52;
107
+ --accent: #1e6aa4; --accent-ink: #ffffff; --accent-bg: #e6f0f8; --hover-bg: #ece2c6;
108
+ --success: #636f00; --warning: #866500; --error: #c62d2b;
109
+ }
110
+ [data-theme="contrast-light"] {
111
+ color-scheme: light;
112
+ --bg: #ffffff; --chrome: #ffffff; --panel: #ffffff; --panel-2: #f2f2f2; --sunken: #f7f7f7;
113
+ --border: #767676; --border-strong: #595959; --border-hover: #404040; --control-border: #595959;
114
+ --text: #000000; --muted: #3d3d3d; --faint: #525252; --code: #1a1a1a;
115
+ --accent: #0043c8; --accent-ink: #ffffff; --accent-bg: #e0ebff; --hover-bg: #e8e8e8;
116
+ --success: #00633b; --warning: #694d00; --error: #b00020;
117
+ }
118
+ [data-theme="okabe-light"] {
119
+ color-scheme: light;
120
+ --bg: #fbfbf8; --chrome: #ffffff; --panel: #ffffff; --panel-2: #f2f2ec; --sunken: #f6f6f0;
121
+ --border: #dadad0; --border-strong: #b8b8a8; --border-hover: #90907e; --control-border: #6f7d87;
122
+ --text: #20242a; --muted: #4f5862; --faint: #667079; --code: #343c46;
123
+ --accent: #0072b2; --accent-ink: #ffffff; --accent-bg: #e8f3fc; --hover-bg: #e8e8dd;
124
+ --success: #00795b; --warning: #8a6300; --error: #a3537f;
125
+ }
126
+ * { box-sizing: border-box; }
127
+ body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 15% -10%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 34%), var(--bg); color: var(--text); font: 14px/1.5 var(--sans); }
128
+ button, input, textarea, select { font: inherit; }
129
+ button { cursor: pointer; }
130
+ code, pre, textarea.code { font-family: var(--mono); }
131
+ .shell { min-height: 100vh; display: grid; grid-template-rows: auto auto 1fr; }
132
+ header { border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--chrome) 94%, transparent); backdrop-filter: blur(14px); }
133
+ .header-row { max-width: 1440px; margin: 0 auto; min-height: 64px; padding: 0 24px; display: flex; align-items: center; gap: 20px; }
134
+ .brand { display: flex; align-items: center; gap: 11px; min-width: 0; }
135
+ .brand-mark { width: 30px; height: 30px; display: grid; place-items: center; border: 1px solid var(--border-strong); border-radius: 9px; background: linear-gradient(145deg, color-mix(in srgb, var(--accent) 28%, var(--panel)), var(--sunken)); color: var(--accent); font: 700 17px/1 var(--mono); box-shadow: inset 0 1px 0 color-mix(in srgb, white 10%, transparent), 0 8px 24px color-mix(in srgb, black 24%, transparent); }
136
+ .brand-copy { display: grid; line-height: 1.05; }
137
+ .brand strong { font: 700 16px var(--sans); letter-spacing: .015em; }
138
+ .brand span { color: var(--muted); font: 10px var(--mono); letter-spacing: .12em; text-transform: uppercase; }
139
+ .workspace { color: var(--muted); font: 12px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
140
+ .status { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font: 12px var(--mono); }
141
+ .status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--faint); }
142
+ .status.verified::before { background: var(--success); }
143
+ .status.blocked::before, .status.failed::before { background: var(--error); }
144
+ .status.verifying::before, .status.implementing::before { background: var(--warning); }
145
+ .hash { color: var(--faint); font: 12px var(--mono); }
146
+ .update-badge { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--accent); border-radius: 5px; background: var(--accent-bg); color: var(--accent); padding: 5px 10px; font: 12px var(--mono); }
147
+ .update-badge:hover { border-color: var(--border-hover); }
148
+ .update-badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
149
+ nav { border-bottom: 1px solid var(--border); background: var(--chrome); }
150
+ .tabs { max-width: 1440px; margin: 0 auto; padding: 0 24px; display: flex; gap: 22px; overflow-x: auto; }
151
+ .tab { border: 0; border-bottom: 2px solid transparent; background: none; color: var(--muted); padding: 11px 0 10px; font-size: 13px; }
152
+ .tab:hover { color: var(--text); }
153
+ .tab.active { color: var(--text); border-bottom-color: var(--accent); }
154
+ main { width: 100%; max-width: 1440px; margin: 0 auto; padding: 24px; }
155
+ .panel { display: none; }
156
+ .panel.active { display: block; }
157
+ .grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
158
+ .span-4 { grid-column: span 4; }
159
+ .span-5 { grid-column: span 5; }
160
+ .span-7 { grid-column: span 7; }
161
+ .span-8 { grid-column: span 8; }
162
+ .span-12 { grid-column: span 12; }
163
+ .card { border: 1px solid var(--border); background: linear-gradient(155deg, color-mix(in srgb, var(--panel) 98%, var(--accent) 2%), var(--panel)); border-radius: 10px; padding: 18px; min-width: 0; box-shadow: 0 14px 40px color-mix(in srgb, black 12%, transparent); }
164
+ .card h2, .card h3 { margin: 0; font-weight: 600; }
165
+ .card h2 { font-size: 14px; }
166
+ .card h3 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; }
167
+ .subtle { color: var(--muted); font-size: 12px; }
168
+ .section-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
169
+ .metric { font: 22px var(--mono); margin-top: 8px; }
170
+ .metric small { color: var(--muted); font-size: 12px; }
171
+ .list { margin: 12px 0 0; padding: 0; list-style: none; }
172
+ .list li { padding: 9px 0; border-top: 1px solid var(--border); display: flex; gap: 10px; }
173
+ .index { color: var(--faint); min-width: 24px; font: 12px var(--mono); }
174
+ .marker { display: inline-block; min-width: 15px; color: var(--faint); font: 12px var(--mono); }
175
+ li.step-done .marker { color: var(--success); }
176
+ li.step-failed .marker { color: var(--error); }
177
+ li.step-active .marker { color: var(--accent); font-weight: 700; animation: step-arrow 1.1s ease-in-out infinite; }
178
+ li.step-active .index { color: var(--accent); }
179
+ @keyframes step-arrow { 0%, 100% { transform: translateX(0); opacity: 1; } 50% { transform: translateX(4px); opacity: .45; } }
180
+ @media (prefers-reduced-motion: reduce) { li.step-active .marker { animation: none; } }
181
+ .acceptance li { display: block; }
182
+ .acceptance code { display: block; color: var(--faint); margin-top: 4px; font-size: 11px; }
183
+ .cov-ok { color: var(--success); font-weight: 700; margin-right: 6px; }
184
+ .cov-weak { color: var(--warning); font-weight: 700; margin-right: 6px; }
185
+ .cov-missing { color: var(--muted); font-weight: 700; margin-right: 6px; }
186
+ .button { border: 1px solid var(--border-strong); border-radius: 5px; background: var(--panel-2); color: var(--text); padding: 7px 11px; font-size: 12px; }
187
+ .button:hover { border-color: var(--border-hover); background: var(--hover-bg); }
188
+ .button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
189
+ .button.danger { color: var(--error); }
190
+ .button:disabled { opacity: .45; cursor: default; }
191
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; }
192
+ label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 6px; }
193
+ input, textarea, select { width: 100%; border: 1px solid var(--control-border); border-radius: 5px; background: var(--sunken); color: var(--text); padding: 9px 10px; outline: none; }
194
+ input:focus, textarea:focus, select:focus { border-color: var(--accent); }
195
+ textarea { resize: vertical; min-height: 90px; }
196
+ textarea.code { min-height: 430px; font-size: 12px; line-height: 1.55; tab-size: 2; }
197
+ .field + .field { margin-top: 14px; }
198
+ .message { min-height: 20px; margin-top: 12px; color: var(--muted); font-size: 12px; white-space: pre-wrap; }
199
+ .message.error { color: var(--error); }
200
+ .message.success { color: var(--success); }
201
+ .mono-block { margin: 0; border: 1px solid var(--border); border-radius: 5px; background: var(--sunken); color: var(--code); padding: 14px; overflow: auto; max-height: 480px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
202
+ .fingerprint { color: var(--muted); font: 12px var(--mono); overflow-wrap: anywhere; }
203
+ .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 6px; }
204
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
205
+ th { color: var(--muted); font-weight: 500; text-align: left; background: var(--sunken); }
206
+ th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
207
+ tr:last-child td { border-bottom: 0; }
208
+ td code { color: var(--code); white-space: nowrap; }
209
+ .check-state { font-family: var(--mono); }
210
+ .check-state.passed { color: var(--success); }
211
+ .check-state.failed { color: var(--error); }
212
+ .check-state.stale { color: var(--warning); }
213
+ .empty { color: var(--muted); padding: 32px 0; text-align: center; }
214
+ .gate { border-left: 2px solid var(--border-strong); padding-left: 14px; }
215
+ .gate.ready { border-left-color: var(--success); }
216
+ .gate.blocked { border-left-color: var(--error); }
217
+ .doc-tabs { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 12px; }
218
+ .doc-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
219
+ .theme-picker { position: relative; }
220
+ .theme-trigger { min-width: 164px; padding: 6px 10px; border: 1px solid var(--control-border); border-radius: 5px; background: var(--sunken); color: var(--text); text-align: left; font-size: 12px; }
221
+ .theme-trigger::after { content: "▾"; float: right; margin-left: 12px; color: var(--muted); }
222
+ .theme-menu { position: absolute; z-index: 20; top: calc(100% + 6px); right: 0; display: none; width: 230px; max-height: min(70vh, 430px); padding: 6px; overflow-y: auto; border: 1px solid var(--border-strong); border-radius: 7px; background: var(--panel); box-shadow: 0 16px 42px color-mix(in srgb, black 38%, transparent); }
223
+ .theme-picker.open .theme-menu { display: grid; gap: 2px; }
224
+ .theme-group { padding: 7px 8px 3px; color: var(--faint); font: 10px var(--mono); letter-spacing: .11em; text-transform: uppercase; }
225
+ .theme-option { width: 100%; padding: 7px 9px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: var(--text); text-align: left; font-size: 12px; }
226
+ .theme-option:hover, .theme-option:focus-visible { border-color: var(--border-hover); background: var(--hover-bg); color: var(--accent); outline: none; }
227
+ .theme-option[aria-selected="true"]::after { content: "✓"; float: right; color: var(--accent); }
228
+ .button:focus-visible, .tab:focus-visible, .theme-trigger:focus-visible,
229
+ input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
230
+ /* D2 flowchart (Plan tab): visual graph with a current-step highlight,
231
+ a swirling ring around the active node, and a slow pulse on the arrow
232
+ that leads into it. States mirror the console's computeNodeStates. */
233
+ .d2-chart { min-height: 60px; }
234
+ .d2-current { font: 600 12px var(--mono); color: var(--accent); margin: 10px 0 2px; min-height: 16px; }
235
+ .d2-current.idle { color: var(--muted); font-weight: 400; }
236
+ .d2-svg { width: 100%; height: auto; display: block; overflow: visible; }
237
+ .d2-node { stroke-width: 1.5; }
238
+ .d2-done { stroke: var(--success); fill: var(--success); fill-opacity: .10; }
239
+ .d2-active { stroke: var(--accent); fill: var(--accent); fill-opacity: .12; stroke-width: 2; }
240
+ .d2-fail { stroke: var(--error); fill: var(--error); fill-opacity: .12; }
241
+ .d2-blocked { stroke: var(--warning); fill: var(--warning); fill-opacity: .10; }
242
+ .d2-pending { stroke: var(--border); fill: var(--panel); }
243
+ .d2-label { font: 600 12px var(--mono); fill: var(--text); }
244
+ .d2-label-pending { fill: var(--muted); }
245
+ .d2-label-fail { fill: var(--error); }
246
+ .d2-label-blocked { fill: var(--warning); }
247
+ .d2-dot { fill: var(--border); }
248
+ .d2-dot-done { fill: var(--success); }
249
+ .d2-dot-active { fill: var(--accent); }
250
+ .d2-dot-fail { fill: var(--error); }
251
+ .d2-dot-blocked { fill: var(--warning); }
252
+ .d2-edge-label { font: 11px var(--mono); fill: var(--warning); }
253
+ .d2-swirl { fill: none; stroke: var(--accent); stroke-width: 2; stroke-dasharray: 5 9; transform-box: fill-box; transform-origin: center; animation: d2-swirl 2.6s linear infinite; }
254
+ .d2-edge-pulse { animation: d2-edge-pulse 2.8s ease-in-out infinite; }
255
+ .d2-arrow-pulse { animation: d2-arrow-pulse 2.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
256
+ @keyframes d2-swirl { to { transform: rotate(360deg); } }
257
+ @keyframes d2-edge-pulse { 0%, 100% { opacity: .35; stroke-width: 1.4; } 50% { opacity: 1; stroke-width: 2.6; } }
258
+ @keyframes d2-arrow-pulse { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
259
+ .d2-source { margin-top: 12px; }
260
+ .d2-source summary { cursor: pointer; color: var(--muted); font-size: 12px; }
261
+ .d2-source pre { margin-top: 8px; max-height: 220px; }
262
+ @media (prefers-reduced-motion: reduce) { .d2-swirl, .d2-edge-pulse, .d2-arrow-pulse { animation: none; } }
263
+ @media (max-width: 900px) {
264
+ .header-row { flex-wrap: wrap; padding: 12px 16px; gap: 10px 16px; }
265
+ .workspace { order: 3; flex-basis: 100%; }
266
+ .tabs, main { padding-left: 16px; padding-right: 16px; }
267
+ .span-4, .span-5, .span-7, .span-8 { grid-column: span 12; }
268
+ }
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <div class="shell">
273
+ <header>
274
+ <div class="header-row">
275
+ <div class="brand"><span class="brand-mark" aria-hidden="true">𐤀</span><span class="brand-copy"><strong>Carthagent</strong><span>plan · prove · deliver</span></span></div>
276
+ <div id="workspace" class="workspace">loading workspace</div>
277
+ <div id="status" class="status">idle</div>
278
+ <div id="header-hash" class="hash">—</div>
279
+ <button id="update-badge" class="update-badge" type="button" hidden></button>
280
+ <div id="theme-picker" class="theme-picker">
281
+ <button id="theme-trigger" class="theme-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">Carthage (default)</button>
282
+ <div id="theme-menu" class="theme-menu" role="listbox" aria-label="Color theme">
283
+ <button class="theme-option" type="button" role="option" data-theme-value="carthage">Carthage (default)</button>
284
+ <button class="theme-option" type="button" role="option" data-theme-value="system">System (auto)</button>
285
+ <div class="theme-group">Dark</div>
286
+ <button class="theme-option" type="button" role="option" data-theme-value="obsidian">Obsidian</button>
287
+ <button class="theme-option" type="button" role="option" data-theme-value="midnight">Midnight</button>
288
+ <button class="theme-option" type="button" role="option" data-theme-value="solarized-dark">Solarized Dark</button>
289
+ <button class="theme-option" type="button" role="option" data-theme-value="nord">Nord</button>
290
+ <button class="theme-option" type="button" role="option" data-theme-value="contrast-dark">High Contrast Dark</button>
291
+ <button class="theme-option" type="button" role="option" data-theme-value="okabe-dark">Okabe Dark (colorblind)</button>
292
+ <div class="theme-group">Light</div>
293
+ <button class="theme-option" type="button" role="option" data-theme-value="paper">Paper</button>
294
+ <button class="theme-option" type="button" role="option" data-theme-value="daylight">Daylight</button>
295
+ <button class="theme-option" type="button" role="option" data-theme-value="solarized-light">Solarized Light</button>
296
+ <button class="theme-option" type="button" role="option" data-theme-value="contrast-light">High Contrast Light</button>
297
+ <button class="theme-option" type="button" role="option" data-theme-value="okabe-light">Okabe Light (colorblind)</button>
298
+ </div>
299
+ </div>
300
+ <button id="run-all-header" class="button primary">Run checks</button>
301
+ </div>
302
+ </header>
303
+ <nav>
304
+ <div class="tabs">
305
+ <button class="tab active" data-panel="overview">Overview</button>
306
+ <button class="tab" data-panel="plan">Plan</button>
307
+ <button class="tab" data-panel="checks">Checks</button>
308
+ <button class="tab" data-panel="handoff">Handoff</button>
309
+ <button class="tab" data-panel="docs">Docs</button>
310
+ </div>
311
+ </nav>
312
+ <main>
313
+ <section id="overview" class="panel active">
314
+ <div class="grid">
315
+ <div class="card span-8">
316
+ <div class="section-head"><h2 id="goal">No active delivery</h2><span id="report" class="subtle"></span></div>
317
+ <div id="overview-empty" class="empty">Start a delivery in Carthagent or define a plan in the Plan tab.</div>
318
+ <div id="overview-content" hidden>
319
+ <h3>Steps</h3>
320
+ <ol id="steps" class="list"></ol>
321
+ </div>
322
+ </div>
323
+ <div class="card span-4">
324
+ <h3>Checks</h3>
325
+ <div id="check-metric" class="metric">0 <small>of 0 current</small></div>
326
+ <p id="pending-summary" class="subtle">No checks declared.</p>
327
+ <h3 style="margin-top:20px">Fingerprint</h3>
328
+ <p id="fingerprint" class="fingerprint">—</p>
329
+ </div>
330
+ <div id="acceptance-card" class="card span-12" hidden>
331
+ <h3>Acceptance</h3>
332
+ <ul id="acceptance" class="list acceptance"></ul>
333
+ </div>
334
+ </div>
335
+ </section>
336
+
337
+ <section id="plan" class="panel">
338
+ <div class="grid">
339
+ <div class="card span-7">
340
+ <div class="section-head">
341
+ <div><h2>Plan</h2><div class="subtle">Revisions relock implementation; honest matching evidence is retained.</div></div>
342
+ <div class="actions"><button id="validate-plan" class="button">Validate</button><button id="save-plan" class="button primary">Save plan</button></div>
343
+ </div>
344
+ <textarea id="plan-json" class="code" spellcheck="false" placeholder="No plan loaded"></textarea>
345
+ <div id="plan-message" class="message"></div>
346
+ </div>
347
+ <div class="card span-5">
348
+ <h3>Observed workflow</h3>
349
+ <div id="design-gate" class="gate" role="status"></div>
350
+ <label for="planning-view">View</label>
351
+ <select id="planning-view"><option value="workflow">Workflow</option><option value="architecture">Architecture</option><option value="evidence">Evidence</option></select>
352
+ <pre id="planning-detail" class="mono-block" style="white-space:pre-wrap"></pre>
353
+ <div id="d2-current" class="d2-current" hidden></div>
354
+ <div id="d2" class="d2-chart" style="margin-top:12px">No plan loaded.</div>
355
+ <details class="d2-source"><summary>D2 source</summary><pre id="d2-source-pre" class="mono-block"></pre></details>
356
+ </div>
357
+ </div>
358
+ </section>
359
+
360
+ <section id="checks" class="panel">
361
+ <div class="card">
362
+ <div class="section-head">
363
+ <div><h2>Checks</h2><div class="subtle">Commands run in the selected workspace with their declared timeout.</div></div>
364
+ <button id="run-all" class="button primary">Run all</button>
365
+ </div>
366
+ <div class="table-wrap">
367
+ <table>
368
+ <thead><tr><th>ID</th><th>Kind</th><th>Command</th><th>Timeout</th><th>Status</th><th>Duration</th><th></th></tr></thead>
369
+ <tbody id="checks-body"><tr><td colspan="7" class="empty">No checks declared.</td></tr></tbody>
370
+ </table>
371
+ </div>
372
+ <h3 style="margin-top:20px">Output</h3>
373
+ <pre id="check-output" class="mono-block" style="margin-top:10px">No check selected.</pre>
374
+ </div>
375
+ </section>
376
+
377
+ <section id="handoff" class="panel">
378
+ <div class="grid">
379
+ <div class="card span-7">
380
+ <h2>Handoff</h2>
381
+ <div class="field" style="margin-top:16px"><label for="review">Review</label><textarea id="review"></textarea></div>
382
+ <div class="field"><label for="launch">Launch command</label><input id="launch" class="code"></div>
383
+ <div class="field"><label for="limitations">Limitations, one per line</label><textarea id="limitations"></textarea></div>
384
+ <div class="actions" style="margin-top:16px"><button id="mark-verified" class="button primary">Mark verified</button><button id="mark-blocked" class="button danger">Mark blocked</button></div>
385
+ <div id="handoff-message" class="message"></div>
386
+ </div>
387
+ <div class="card span-5">
388
+ <h3>Verification</h3>
389
+ <div id="gate" class="gate" style="margin-top:14px">No active plan.</div>
390
+ </div>
391
+ </div>
392
+ </section>
393
+
394
+ <section id="docs" class="panel">
395
+ <div class="card">
396
+ <div class="section-head"><h2>Documentation</h2></div>
397
+ <div class="doc-tabs">
398
+ <button class="button doc-tab active" data-doc="readme">README</button>
399
+ <button class="button doc-tab" data-doc="delivery">Delivery prompt</button>
400
+ <button class="button doc-tab" data-doc="software-delivery">Software delivery</button>
401
+ <button class="button doc-tab" data-doc="typescript-delivery">TypeScript delivery</button>
402
+ <button class="button doc-tab" data-doc="game-development">Game development</button>
403
+ <button class="button doc-tab" data-doc="evaluation">Evaluation</button>
404
+ </div>
405
+ <pre id="doc-content" class="mono-block">Select a document.</pre>
406
+ </div>
407
+ </section>
408
+ </main>
409
+ </div>
410
+ <script>
411
+ const query = new URLSearchParams(location.search);
412
+ const suppliedToken = query.get('token');
413
+ if (suppliedToken) {
414
+ sessionStorage.setItem('carthagent-token', suppliedToken);
415
+ history.replaceState(null, '', location.pathname);
416
+ }
417
+ const token = suppliedToken || sessionStorage.getItem('carthagent-token') || '';
418
+ let state = { status: 'idle', plan: null, evidence: {}, pendingChecks: [], currentFingerprint: '' };
419
+
420
+ const byId = id => document.getElementById(id);
421
+ const node = (tag, text, className) => {
422
+ const element = document.createElement(tag);
423
+ if (text != null) element.textContent = text;
424
+ if (className) element.className = className;
425
+ return element;
426
+ };
427
+ const clear = element => { while (element.firstChild) element.removeChild(element.firstChild); };
428
+ const setMessage = (id, text, type = '') => {
429
+ const element = byId(id);
430
+ element.textContent = text;
431
+ element.className = `message ${type}`.trim();
432
+ };
433
+ async function api(path, options = {}) {
434
+ const response = await fetch(path, { ...options, headers: { 'content-type': 'application/json', 'x-carthagent-token': token, ...(options.headers || {}) } });
435
+ const data = await response.json().catch(() => ({}));
436
+ if (!response.ok) throw new Error(data.error || `Request failed (${response.status})`);
437
+ return data;
438
+ }
439
+
440
+ const themePicker = byId('theme-picker');
441
+ const themeTrigger = byId('theme-trigger');
442
+ const themeMenu = byId('theme-menu');
443
+ const themeOptions = [...themeMenu.querySelectorAll('[data-theme-value]')];
444
+ const prefersDark = () => typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;
445
+ const resolveDashboardTheme = value => value === 'system' ? (prefersDark() ? 'obsidian' : 'paper') : value;
446
+ const applyDashboardTheme = value => { document.documentElement.dataset.theme = resolveDashboardTheme(value); };
447
+ let committedTheme = document.documentElement.dataset.themeChoice || 'carthage';
448
+ const syncThemePicker = () => {
449
+ for (const option of themeOptions) option.setAttribute('aria-selected', String(option.dataset.themeValue === committedTheme));
450
+ themeTrigger.textContent = themeOptions.find(option => option.dataset.themeValue === committedTheme)?.textContent || 'Carthage (default)';
451
+ };
452
+ const closeThemePicker = (restore = true) => {
453
+ themePicker.classList.remove('open');
454
+ themeTrigger.setAttribute('aria-expanded', 'false');
455
+ if (restore) applyDashboardTheme(committedTheme);
456
+ };
457
+ syncThemePicker();
458
+ themeTrigger.addEventListener('click', () => {
459
+ const open = !themePicker.classList.contains('open');
460
+ themePicker.classList.toggle('open', open);
461
+ themeTrigger.setAttribute('aria-expanded', String(open));
462
+ if (open) themeOptions.find(option => option.dataset.themeValue === committedTheme)?.focus();
463
+ });
464
+ for (const option of themeOptions) {
465
+ const preview = () => applyDashboardTheme(option.dataset.themeValue);
466
+ option.addEventListener('mouseenter', preview);
467
+ option.addEventListener('focus', preview);
468
+ option.addEventListener('click', () => {
469
+ committedTheme = option.dataset.themeValue;
470
+ localStorage.setItem('carthagent-theme', committedTheme);
471
+ document.documentElement.dataset.themeChoice = committedTheme;
472
+ syncThemePicker();
473
+ closeThemePicker(false);
474
+ applyDashboardTheme(committedTheme);
475
+ themeTrigger.focus();
476
+ });
477
+ }
478
+ themeMenu.addEventListener('mouseleave', () => applyDashboardTheme(committedTheme));
479
+ themePicker.addEventListener('keydown', event => {
480
+ if (event.key === 'Escape') { closeThemePicker(); themeTrigger.focus(); }
481
+ });
482
+ document.addEventListener('click', event => { if (!themePicker.contains(event.target)) closeThemePicker(); });
483
+ if (typeof matchMedia === 'function') matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
484
+ if (committedTheme === 'system') document.documentElement.dataset.theme = event.matches ? 'obsidian' : 'paper';
485
+ });
486
+
487
+ document.querySelectorAll('.tab').forEach(tab => tab.addEventListener('click', () => {
488
+ document.querySelectorAll('.tab').forEach(item => item.classList.toggle('active', item === tab));
489
+ document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id === tab.dataset.panel));
490
+ if (tab.dataset.panel === 'docs') loadDoc(document.querySelector('.doc-tab.active').dataset.doc);
491
+ }));
492
+
493
+ function checkStatus(check) {
494
+ const evidence = state.evidence?.[check.id];
495
+ if (!evidence) return 'pending';
496
+ if (!evidence.passed) return 'failed';
497
+ return evidence.fingerprint === state.currentFingerprint ? 'passed' : 'stale';
498
+ }
499
+
500
+ function render() {
501
+ byId('workspace').textContent = state.workspace || 'workspace unavailable';
502
+ byId('status').textContent = state.status || 'idle';
503
+ byId('status').className = `status ${state.status || 'idle'}`;
504
+ byId('header-hash').textContent = state.currentFingerprint ? state.currentFingerprint.slice(0, 12) : '—';
505
+ const updateBadge = byId('update-badge');
506
+ if (state.update?.available) {
507
+ updateBadge.hidden = false;
508
+ if (updateBadge.dataset.copied !== 'true') updateBadge.textContent = `↑ ${state.update.latest} available`;
509
+ updateBadge.title = `Carthagent ${state.update.latest} is available (installed ${state.update.installed}) — click to copy 'ctg update'`;
510
+ } else {
511
+ updateBadge.hidden = true;
512
+ }
513
+ byId('fingerprint').textContent = state.currentFingerprint || '—';
514
+ byId('report').textContent = state.report || '';
515
+ const plan = state.plan;
516
+ byId('run-all').disabled = !plan?.checks?.length || state.planningStatus?.locked;
517
+ byId('run-all-header').disabled = !plan?.checks?.length || state.planningStatus?.locked;
518
+ renderPlanningView();
519
+ byId('mark-verified').disabled = !plan;
520
+ byId('mark-blocked').disabled = !plan;
521
+ byId('goal').textContent = plan?.goal || 'No active delivery';
522
+ byId('overview-empty').hidden = !!plan;
523
+ byId('overview-content').hidden = !plan;
524
+ byId('acceptance-card').hidden = !plan;
525
+ if (plan) {
526
+ clear(byId('steps'));
527
+ // Mirror the console's step semantics via resolveStepStates: exactly one
528
+ // current step, so a fresh plan no longer lights every row as active.
529
+ const resolvedSteps = resolveStepStates(plan, state.stepStatus, state.status);
530
+ plan.steps.forEach((step, index) => {
531
+ const resolved = resolvedSteps[index];
532
+ const item = node('li');
533
+ item.className = `step step-${resolved}`;
534
+ const glyph = resolved === 'done' ? '✓' : resolved === 'failed' ? '✗' : resolved === 'active' ? '▸' : '·';
535
+ item.append(node('span', glyph, 'marker'), node('span', String(index + 1).padStart(2, '0'), 'index'), node('span', typeof step === 'string' ? step : step.title));
536
+ byId('steps').append(item);
537
+ });
538
+ clear(byId('acceptance'));
539
+ const cov = acceptanceCoverage(plan, state.evidence || {}, state.currentFingerprint);
540
+ plan.acceptance.forEach((item, i) => {
541
+ const c = cov[i];
542
+ const cls = c.covered && !c.staticOnly ? 'cov-ok' : c.covered ? 'cov-weak' : 'cov-missing';
543
+ const mark = c.covered ? (c.staticOnly ? '~' : '✓') : '○';
544
+ const row = node('li');
545
+ const tag = node('span', mark);
546
+ tag.className = cls;
547
+ if (c.staticOnly) tag.title = 'Only static (syntax/import) checks back this criterion';
548
+ if (!c.covered) tag.title = 'No fresh passing check backs this criterion';
549
+ row.append(tag, node('span', item.requirement), node('code', item.checks.join(', ')));
550
+ byId('acceptance').append(row);
551
+ });
552
+ if (document.activeElement !== byId('plan-json')) byId('plan-json').value = JSON.stringify(plan, null, 2);
553
+ updateD2(plan);
554
+ if (document.activeElement !== byId('review')) byId('review').value = state.review || '';
555
+ if (document.activeElement !== byId('launch')) byId('launch').value = state.launch || '';
556
+ if (document.activeElement !== byId('limitations')) byId('limitations').value = (state.limitations || []).join('\n');
557
+ }
558
+ const checks = plan?.checks || [];
559
+ const current = checks.filter(check => checkStatus(check) === 'passed').length;
560
+ byId('check-metric').textContent = `${current} of ${checks.length} current`;
561
+ byId('pending-summary').textContent = state.pendingChecks?.length ? `Pending: ${state.pendingChecks.join(', ')}` : checks.length ? 'All checks are current.' : 'No checks declared.';
562
+ renderChecks(checks);
563
+ const gate = byId('gate');
564
+ gate.className = `gate ${state.status === 'blocked' ? 'blocked' : checks.length && current === checks.length ? 'ready' : ''}`;
565
+ gate.textContent = !plan ? 'No active plan.' : checks.length && current === checks.length ? 'All declared checks have current passing evidence.' : `${checks.length - current} check${checks.length - current === 1 ? '' : 's'} pending, failed, or stale.`;
566
+ }
567
+
568
+ function renderPlanningView() {
569
+ const view = state.planningView;
570
+ const tab = byId('planning-view').value;
571
+ byId('design-gate').textContent = `${view?.gate.phase || 'discovery'} · revision ${state.revision || '-'}\n${view?.gate.locked ? 'LOCKED: validate, review and approve this design revision before code.' : 'Design approved; final correctness still requires evidence.'}`;
572
+ const lines = [];
573
+ if (tab === 'architecture') {
574
+ for (const c of view?.architecture?.nodes || []) lines.push(`${c.id} [${c.layer}] ${c.title}`, ` excludes: ${(c.excludes || []).join('; ')}`);
575
+ for (const edge of view?.architecture?.edges || []) lines.push(`${edge.from} -> ${edge.to}`);
576
+ for (const p of view?.architecture?.ports || []) lines.push(`${p.owner} -> port ${p.id} -> ${p.adapter}; injected at ${p.compositionRoot}`, ` input: ${p.input}; output: ${p.output}; errors: ${p.errors}`, ` real adapter assertion: ${p.integrationAssertion}`);
577
+ } else if (tab === 'evidence') {
578
+ for (const e of view?.evidence || []) lines.push(`${e.id}: ${e.status}; executed revision ${e.executedRevision ?? '-'}`, ` ${e.logPath || 'No execution receipt'}`);
579
+ } else {
580
+ lines.push('CURRENT: ' + ((view?.current || []).map(n => n.title).join(', ') || 'No active action recorded'));
581
+ for (const n of view?.workflow?.nodes || []) lines.push(`${n.id} [${n.status}] ${n.title}`);
582
+ for (const e of view?.workflow?.edges || []) lines.push(`${e.from} -> ${e.to}${e.label ? ': ' + e.label : ''}`);
583
+ }
584
+ if (view?.blockers.length) lines.push('BLOCKERS', ...view.blockers);
585
+ lines.push(view?.provenance || 'No observed delivery state');
586
+ byId('planning-detail').textContent = lines.join('\n');
587
+ byId('d2').hidden = tab !== 'workflow';
588
+ byId('d2-current').hidden = tab !== 'workflow';
589
+ }
590
+
591
+ function renderChecks(checks) {
592
+ const body = byId('checks-body');
593
+ clear(body);
594
+ if (!checks.length) {
595
+ const row = node('tr');
596
+ const cell = node('td', 'No checks declared.', 'empty');
597
+ cell.colSpan = 7;
598
+ row.append(cell);
599
+ body.append(row);
600
+ return;
601
+ }
602
+ checks.forEach(check => {
603
+ const evidence = state.evidence?.[check.id];
604
+ const status = checkStatus(check);
605
+ const row = node('tr');
606
+ const id = node('td'); id.append(node('code', check.id));
607
+ const kind = node('td', check.kind);
608
+ const command = node('td'); command.append(node('code', check.argv.join(' ')));
609
+ const timeout = node('td', `${check.timeoutSeconds}s`);
610
+ const stateCell = node('td', status, `check-state ${status}`);
611
+ const duration = node('td', evidence?.durationMs != null ? `${evidence.durationMs}ms` : '—');
612
+ const action = node('td');
613
+ const run = node('button', 'Run', 'button');
614
+ run.disabled = Boolean(state.planningStatus?.locked);
615
+ run.addEventListener('click', () => runChecks(check.id));
616
+ action.append(run);
617
+ row.append(id, kind, command, timeout, stateCell, duration, action);
618
+ body.append(row);
619
+ });
620
+ }
621
+
622
+ async function loadStatus() {
623
+ if (!token) {
624
+ byId('workspace').textContent = 'Open the authenticated URL printed by ctg serve.';
625
+ return;
626
+ }
627
+ try { state = await api('/api/status'); render(); }
628
+ catch (error) { byId('workspace').textContent = error.message; }
629
+ }
630
+
631
+ // --- D2 flowchart (visual graph with a current-step highlight) ---
632
+ // The Plan tab used to dump raw D2 source into a <pre>. It now renders an
633
+ // SVG flowchart that mirrors the console's computeNodeStates so the active
634
+ // step is obvious: the active box wears a swirling ring and the arrow into
635
+ // it pulses slowly. The raw source stays available under a <details>.
636
+ const SVGNS = 'http' + '://www.w3.org/2000/svg';
637
+ const svgEl = tag => document.createElementNS(SVGNS, tag);
638
+
639
+ /** Parse planD2-style source into { nodes, edges } (mirrors lib/tui/d2.mjs). */
640
+ function parseD2Source(src) {
641
+ const nodes = new Map(), edges = [];
642
+ const ensure = id => { if (!nodes.has(id)) nodes.set(id, { id, label: id }); return nodes.get(id); };
643
+ for (const raw of String(src).split('\n')) {
644
+ const line = raw.trim();
645
+ if (!line || line.startsWith('#') || line.startsWith('direction:')) continue;
646
+ const edge = line.match(/^([\w.-]+)\s*-+>\s*([\w.-]+)\s*(?::\s*(.*))?$/);
647
+ if (edge) { ensure(edge[1]); ensure(edge[2]); edges.push({ from: edge[1], to: edge[2], label: (edge[3] || '').replace(/^"|"$/g, '') }); continue; }
648
+ const decl = line.match(/^([\w.-]+)\s*:\s*(.+)$/);
649
+ if (decl && !decl[2].startsWith('{')) {
650
+ const n = ensure(decl[1]); let label = decl[2].trim();
651
+ try { if (label.startsWith('"')) label = JSON.parse(label); } catch { label = label.replace(/^"|"$/g, ''); }
652
+ n.label = label;
653
+ }
654
+ }
655
+ return { nodes: [...nodes.values()], edges };
656
+ }
657
+
658
+ /** Longest-path ranks; back edges (cycles, e.g. repair -> verify) go to a side column. */
659
+ function layoutD2(graph) {
660
+ const ids = graph.nodes.map(n => n.id);
661
+ const back = new Set();
662
+ const adj = new Map(ids.map(id => [id, []]));
663
+ for (const e of graph.edges) adj.get(e.from)?.push(e);
664
+ const stack = new Set(), seen = new Set();
665
+ const dfs = id => {
666
+ if (stack.has(id) || seen.has(id)) return;
667
+ stack.add(id);
668
+ for (const e of adj.get(id) || []) { if (stack.has(e.to)) { back.add(e); continue; } dfs(e.to); }
669
+ stack.delete(id); seen.add(id);
670
+ };
671
+ const roots = ids.filter(id => !graph.edges.some(e => e.to === id));
672
+ (roots.length ? roots : ids.slice(0, 1)).forEach(dfs);
673
+ ids.forEach(dfs);
674
+ const rank = new Map(ids.map(id => [id, 0]));
675
+ for (let i = 0; i < ids.length; i++) {
676
+ let moved = false;
677
+ for (const e of graph.edges) {
678
+ if (back.has(e)) continue;
679
+ const want = rank.get(e.from) + 1;
680
+ if (rank.get(e.to) < want) { rank.set(e.to, want); moved = true; }
681
+ }
682
+ if (!moved) break;
683
+ }
684
+ const col = new Map(ids.map(id => [id, 0]));
685
+ for (const e of back) col.set(e.from, 1);
686
+ return { rank, col, back };
687
+ }
688
+
689
+ /** Per-criterion evidence coverage (mirrors coverageReport in lib/delivery.mjs). */
690
+ function acceptanceCoverage(plan, evidence, hash) {
691
+ const byId = new Map((plan.checks || []).map(c => [c.id, c]));
692
+ return (plan.acceptance || []).map(item => {
693
+ let fresh = 0;
694
+ const kinds = new Set();
695
+ for (const id of item.checks || []) {
696
+ const e = evidence[id];
697
+ if (!e || !e.passed || state.pendingChecks?.includes(id)) continue;
698
+ if (hash && e.fingerprint !== hash) continue;
699
+ fresh++;
700
+ const c = byId.get(id);
701
+ if (c) kinds.add(c.kind);
702
+ }
703
+ return { covered: fresh > 0 && fresh === item.checks.length && !state.completionIssues?.unverifiedRequirements?.includes(item.requirement), staticOnly: fresh > 0 && [...kinds].every(k => k === 'static') };
704
+ });
705
+ }
706
+
707
+ /**
708
+ * Resolve one state per plan step. Exactly one step is `active` while the
709
+ * run is in flight (the first un-done step, or the first explicitly active
710
+ * one); earlier steps read done and later steps pending. Mirrors
711
+ * computeNodeStates in lib/tui/app.mjs.
712
+ */
713
+ function resolveStepStates(plan, stepStatus, status) {
714
+ const implActive = status === 'implementing' || status === 'planned';
715
+ const raw = (plan.steps || []).map((_, i) => stepStatus?.[`step${i}`]);
716
+ let current = raw.findIndex(s => s === 'active');
717
+ if (current < 0) current = raw.findIndex(s => s !== 'done' && s !== 'failed');
718
+ return raw.map((s, i) => {
719
+ if (typeof plan.steps[i] !== 'string') return s || 'pending';
720
+ if (s === 'done') return 'done';
721
+ if (s === 'failed') return 'failed';
722
+ if (!implActive) return 'done';
723
+ if (i === current) return 'active';
724
+ if (i < current) return 'done';
725
+ return 'pending';
726
+ });
727
+ }
728
+
729
+ /** Map live delivery state to per-node states (mirrors computeNodeStates in the TUI). */
730
+ function computeD2States() {
731
+ const st = new Map();
732
+ if (state.plan?.workflow && state.planningView?.workflow) return new Map(state.planningView.workflow.nodes.map(n => [n.id, n.status]));
733
+ const plan = state.plan;
734
+ if (!plan) return st;
735
+ const ev = state.evidence || {};
736
+ const status = state.status || 'idle';
737
+ const checks = plan.checks || [];
738
+ const curHash = state.currentFingerprint;
739
+ let anyFail = false, allPass = checks.length > 0, allFresh = allPass;
740
+ for (const c of checks) {
741
+ const e = ev[c.id];
742
+ if (!e) { allPass = false; allFresh = false; }
743
+ else if (!e.passed) { anyFail = true; allPass = false; allFresh = false; }
744
+ else if (state.pendingChecks?.includes(c.id) || (curHash && e.fingerprint !== curHash)) allFresh = false;
745
+ }
746
+ st.set('goal', 'done');
747
+ resolveStepStates(plan, state.stepStatus, status).forEach((s, i) => {
748
+ st.set(`step${i}`, s === 'failed' ? 'fail' : s);
749
+ });
750
+ let verify = 'pending';
751
+ if (anyFail) verify = 'fail';
752
+ else if (status === 'verifying') verify = 'active';
753
+ else if (allFresh && status === 'verified') verify = 'done';
754
+ else if (allFresh) verify = 'done';
755
+ if (status === 'blocked') verify = anyFail ? 'fail' : 'blocked';
756
+ st.set('verify', verify);
757
+ st.set('repair', anyFail ? 'fail' : 'pending');
758
+ st.set('review', status === 'verified' ? 'done' : allFresh && !anyFail ? 'active' : 'pending');
759
+ st.set('deliver', status === 'verified' ? 'done' : status === 'blocked' ? 'blocked' : 'pending');
760
+ return st;
761
+ }
762
+
763
+ function d2EdgeColor(targetState, isBack) {
764
+ if (isBack) return 'var(--error)';
765
+ if (targetState === 'fail') return 'var(--error)';
766
+ if (targetState === 'active') return 'var(--accent)';
767
+ if (targetState === 'done') return 'var(--success)';
768
+ return 'var(--border-strong)';
769
+ }
770
+
771
+ /** Render D2 source as an SVG flowchart into #d2, highlighting the current step. */
772
+ function renderD2Chart(source, states) {
773
+ const container = byId('d2');
774
+ const cur = byId('d2-current');
775
+ const srcPre = byId('d2-source-pre');
776
+ clear(container);
777
+ if (srcPre) srcPre.textContent = source || '';
778
+ if (!source) {
779
+ container.textContent = 'No plan loaded.';
780
+ if (cur) { cur.hidden = true; cur.textContent = ''; }
781
+ return;
782
+ }
783
+ const graph = parseD2Source(source);
784
+ const { rank, col, back } = layoutD2(graph);
785
+ const st = states || new Map();
786
+ const spine = graph.nodes.filter(n => col.get(n.id) === 0).sort((a, b) => rank.get(a.id) - rank.get(b.id));
787
+ const side = graph.nodes.filter(n => col.get(n.id) === 1).sort((a, b) => rank.get(a.id) - rank.get(b.id));
788
+ const pad = 12, nodeW = 220, nodeH = 46, vGap = 30, sideGap = 84;
789
+ const pos = new Map();
790
+ spine.forEach((n, i) => pos.set(n.id, { x: pad, y: pad + i * (nodeH + vGap) }));
791
+ side.forEach(n => {
792
+ const inc = graph.edges.find(e => e.to === n.id && !back.has(e));
793
+ let row = spine.length;
794
+ if (inc) { const f = spine.findIndex(s => s.id === inc.from); if (f >= 0) row = f + 1; }
795
+ pos.set(n.id, { x: pad + nodeW + sideGap, y: pad + row * (nodeH + vGap) });
796
+ });
797
+ const W = Math.max(...[...pos.values()].map(p => p.x)) + nodeW + pad;
798
+ const H = Math.max(...[...pos.values()].map(p => p.y)) + nodeH + pad;
799
+ const svg = svgEl('svg');
800
+ svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
801
+ svg.setAttribute('class', 'd2-svg');
802
+ svg.setAttribute('role', 'img');
803
+ const svgTitle = svgEl('title'); svgTitle.textContent = 'Delivery flow'; svg.append(svgTitle);
804
+
805
+ // Edges first (behind nodes), arrowheads last (above borders) so heads stay crisp.
806
+ const edgePaths = [], arrowheads = [], edgeLabels = [];
807
+ for (const e of graph.edges) {
808
+ const a = pos.get(e.from), b = pos.get(e.to);
809
+ if (!a || !b) continue;
810
+ const isBack = back.has(e);
811
+ const targetState = st.get(e.to) || 'pending';
812
+ const toActive = targetState === 'active';
813
+ const ec = d2EdgeColor(targetState, isBack);
814
+ let pathD, tip, dir, lx, ly, anchor = 'middle';
815
+ if (isBack) {
816
+ const sx = a.x + nodeW / 2, sy = a.y, ex = b.x + nodeW / 2, ey = b.y;
817
+ pathD = `M ${sx} ${sy} C ${sx} ${sy - 48} ${ex} ${ey - 48} ${ex} ${ey}`;
818
+ tip = [ex, ey]; dir = 'down'; lx = (sx + ex) / 2; ly = Math.min(sy, ey) - 14;
819
+ } else if (col.get(e.from) === 0 && col.get(e.to) === 0) {
820
+ const sx = a.x + nodeW / 2, sy = a.y + nodeH, ex = b.x + nodeW / 2, ey = b.y;
821
+ pathD = `M ${sx} ${sy} L ${ex} ${ey}`;
822
+ tip = [ex, ey]; dir = 'down'; lx = sx + 8; ly = sy + (ey - sy) / 2; anchor = 'start';
823
+ } else {
824
+ const sx = a.x + nodeW, sy = a.y + nodeH / 2, ex = b.x, ey = b.y + nodeH / 2;
825
+ pathD = `M ${sx} ${sy} L ${ex} ${ey}`;
826
+ tip = [ex, ey]; dir = 'right'; lx = sx + (ex - sx) / 2; ly = sy - 8;
827
+ }
828
+ const path = svgEl('path');
829
+ path.setAttribute('d', pathD);
830
+ path.setAttribute('fill', 'none');
831
+ path.setAttribute('stroke', ec);
832
+ path.setAttribute('stroke-width', '1.6');
833
+ if (isBack) path.setAttribute('stroke-dasharray', '5 4');
834
+ if (toActive) path.setAttribute('class', 'd2-edge-pulse');
835
+ edgePaths.push(path);
836
+ const ah = svgEl('polygon');
837
+ const [tx, ty] = tip;
838
+ ah.setAttribute('points', dir === 'down' ? `${tx},${ty} ${tx - 5},${ty - 8} ${tx + 5},${ty - 8}` : `${tx},${ty} ${tx - 8},${ty - 5} ${tx - 8},${ty + 5}`);
839
+ ah.setAttribute('fill', ec);
840
+ if (toActive) ah.setAttribute('class', 'd2-arrow-pulse');
841
+ arrowheads.push(ah);
842
+ if (e.label) {
843
+ const lt = svgEl('text');
844
+ lt.textContent = e.label;
845
+ lt.setAttribute('x', lx); lt.setAttribute('y', ly);
846
+ lt.setAttribute('class', 'd2-edge-label');
847
+ lt.setAttribute('text-anchor', anchor);
848
+ edgeLabels.push(lt);
849
+ }
850
+ }
851
+ edgePaths.forEach(p => svg.append(p));
852
+ for (const n of graph.nodes) {
853
+ const p = pos.get(n.id);
854
+ const s = st.get(n.id) || 'pending';
855
+ if (s === 'active') {
856
+ const ring = svgEl('ellipse');
857
+ ring.setAttribute('cx', p.x + nodeW / 2);
858
+ ring.setAttribute('cy', p.y + nodeH / 2);
859
+ ring.setAttribute('rx', nodeW / 2 + 7);
860
+ ring.setAttribute('ry', nodeH / 2 + 5);
861
+ ring.setAttribute('class', 'd2-swirl');
862
+ svg.append(ring);
863
+ }
864
+ const rect = svgEl('rect');
865
+ rect.setAttribute('x', p.x); rect.setAttribute('y', p.y);
866
+ rect.setAttribute('width', nodeW); rect.setAttribute('height', nodeH);
867
+ rect.setAttribute('rx', 8);
868
+ rect.setAttribute('class', `d2-node d2-${s}`);
869
+ svg.append(rect);
870
+ const dot = svgEl('circle');
871
+ dot.setAttribute('cx', p.x + 16); dot.setAttribute('cy', p.y + nodeH / 2);
872
+ dot.setAttribute('r', 4);
873
+ dot.setAttribute('class', `d2-dot d2-dot-${s}`);
874
+ svg.append(dot);
875
+ const label = svgEl('text');
876
+ const full = String(n.label);
877
+ label.textContent = full.length > 30 ? full.slice(0, 29) + '…' : full;
878
+ label.setAttribute('x', p.x + 30); label.setAttribute('y', p.y + nodeH / 2);
879
+ label.setAttribute('class', `d2-label d2-label-${s}`);
880
+ label.setAttribute('dominant-baseline', 'middle');
881
+ const t = svgEl('title'); t.textContent = full; label.append(t);
882
+ svg.append(label);
883
+ }
884
+ arrowheads.forEach(a => svg.append(a));
885
+ edgeLabels.forEach(l => svg.append(l));
886
+ container.append(svg);
887
+
888
+ // Current-step indicator: the active node, else the next/blocked frontier, else complete.
889
+ if (cur) {
890
+ const active = graph.nodes.find(n => st.get(n.id) === 'active');
891
+ if (active) {
892
+ cur.hidden = false; cur.className = 'd2-current';
893
+ cur.textContent = `▶ Current: ${active.label}`;
894
+ } else {
895
+ const next = spine.find(n => (st.get(n.id) || 'pending') === 'pending') || spine.find(n => ['fail', 'blocked'].includes(st.get(n.id)));
896
+ if (next) {
897
+ const mark = st.get(next.id) === 'fail' ? '✗ Stuck at' : st.get(next.id) === 'blocked' ? '■ Blocked at' : 'Up next';
898
+ cur.hidden = false; cur.className = 'd2-current idle';
899
+ cur.textContent = `${mark}: ${next.label}`;
900
+ } else if (spine.length && spine.every(n => st.get(n.id) === 'done')) {
901
+ cur.hidden = false; cur.className = 'd2-current';
902
+ cur.textContent = '✓ Flow complete';
903
+ } else { cur.hidden = true; cur.textContent = ''; }
904
+ }
905
+ }
906
+ }
907
+
908
+ async function updateD2(plan) {
909
+ try {
910
+ const result = await api('/api/plan/d2', { method: 'POST', body: JSON.stringify({ plan }) });
911
+ renderD2Chart(result.d2, computeD2States());
912
+ } catch (error) {
913
+ byId('d2').textContent = error.message;
914
+ const cur = byId('d2-current'); if (cur) { cur.hidden = true; cur.textContent = ''; }
915
+ }
916
+ }
917
+
918
+ async function planAction(save) {
919
+ try {
920
+ const plan = JSON.parse(byId('plan-json').value);
921
+ const path = save ? '/api/plan/set' : '/api/plan/validate';
922
+ const result = await api(path, { method: 'POST', body: JSON.stringify({ plan }) });
923
+ setMessage('plan-message', save ? `Saved: ${result.report}` : 'Plan is valid.', 'success');
924
+ if (save) await loadStatus(); else renderD2Chart(result.d2, null);
925
+ } catch (error) { setMessage('plan-message', error.message, 'error'); }
926
+ }
927
+
928
+ async function runChecks(id) {
929
+ byId('check-output').textContent = `Running ${id}…`;
930
+ byId('run-all').disabled = true;
931
+ byId('run-all-header').disabled = true;
932
+ try {
933
+ const result = await api('/api/checks/run', { method: 'POST', body: JSON.stringify({ id }) });
934
+ byId('check-output').textContent = result.results.map(item => `${item.id} exit ${item.code}\n${item.output || ''}`).join('\n\n');
935
+ await loadStatus();
936
+ } catch (error) {
937
+ byId('check-output').textContent = error.message;
938
+ } finally {
939
+ byId('run-all').disabled = false;
940
+ byId('run-all-header').disabled = false;
941
+ }
942
+ }
943
+
944
+ async function finish(status) {
945
+ const limitations = byId('limitations').value.split('\n').map(value => value.trim()).filter(Boolean);
946
+ try {
947
+ await api('/api/finish', { method: 'POST', body: JSON.stringify({ status, review: byId('review').value, launch: byId('launch').value, limitations }) });
948
+ setMessage('handoff-message', `Status set to ${status}.`, 'success');
949
+ await loadStatus();
950
+ } catch (error) { setMessage('handoff-message', error.message, 'error'); }
951
+ }
952
+
953
+ async function loadDoc(name) {
954
+ byId('doc-content').textContent = 'Loading…';
955
+ try { byId('doc-content').textContent = (await api(`/api/docs/${encodeURIComponent(name)}`)).content; }
956
+ catch (error) { byId('doc-content').textContent = error.message; }
957
+ }
958
+
959
+ byId('planning-view').addEventListener('change', renderPlanningView);
960
+ byId('validate-plan').addEventListener('click', () => planAction(false));
961
+ byId('save-plan').addEventListener('click', () => planAction(true));
962
+ byId('run-all').addEventListener('click', () => runChecks('all'));
963
+ byId('run-all-header').addEventListener('click', () => { document.querySelector('[data-panel="checks"]').click(); runChecks('all'); });
964
+ byId('update-badge').addEventListener('click', async () => {
965
+ const badge = byId('update-badge');
966
+ try {
967
+ await navigator.clipboard.writeText('ctg update');
968
+ badge.dataset.copied = 'true';
969
+ badge.textContent = 'ctg update copied';
970
+ setTimeout(() => { delete badge.dataset.copied; }, 2500);
971
+ } catch { badge.textContent = 'run: ctg update'; }
972
+ });
973
+ byId('mark-verified').addEventListener('click', () => finish('verified'));
974
+ byId('mark-blocked').addEventListener('click', () => finish('blocked'));
975
+ document.querySelectorAll('.doc-tab').forEach(button => button.addEventListener('click', () => {
976
+ document.querySelectorAll('.doc-tab').forEach(item => item.classList.toggle('active', item === button));
977
+ loadDoc(button.dataset.doc);
978
+ }));
979
+ loadStatus();
980
+ if (token) setInterval(loadStatus, 3000); // live step/check progress while the agent runs
981
+ </script>
982
+ </body>
983
+ </html>