supipowers 1.5.3 → 2.0.0

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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -30,6 +30,53 @@ function wrapInFrame(content) {
30
30
  return frameTemplate.replace('<!-- CONTENT -->', content);
31
31
  }
32
32
 
33
+ function injectHelper(html) {
34
+ if (html.includes('</body>')) {
35
+ return html.replace('</body>', `${helperInjection}\n</body>`);
36
+ }
37
+ return html + helperInjection;
38
+ }
39
+
40
+ function renderHtml(raw) {
41
+ const html = isFullDocument(raw) ? raw : wrapInFrame(raw);
42
+ return injectHelper(html);
43
+ }
44
+
45
+ function resolveArtifactPath(requestPath) {
46
+ if (!requestPath || requestPath.startsWith('.')) return null;
47
+
48
+ let decodedPath;
49
+ try {
50
+ decodedPath = decodeURIComponent(requestPath);
51
+ } catch {
52
+ return null;
53
+ }
54
+
55
+ const normalizedPath = path.normalize(decodedPath).replace(/^([/\\])+/, '');
56
+ if (!normalizedPath || normalizedPath.startsWith('..') || path.isAbsolute(normalizedPath)) {
57
+ return null;
58
+ }
59
+
60
+ const artifactPath = path.resolve(SCREEN_DIR, normalizedPath);
61
+ const relative = path.relative(SCREEN_DIR, artifactPath);
62
+ if (relative.startsWith('..') || path.isAbsolute(relative) || !fs.existsSync(artifactPath)) {
63
+ return null;
64
+ }
65
+
66
+ return artifactPath;
67
+ }
68
+
69
+ function serveArtifact(res, artifactPath) {
70
+ if (artifactPath.endsWith('.html')) {
71
+ const raw = fs.readFileSync(artifactPath, 'utf-8');
72
+ res.type('html').send(renderHtml(raw));
73
+ return;
74
+ }
75
+
76
+ res.sendFile(artifactPath);
77
+ }
78
+
79
+
33
80
  // Find the newest .html file in the directory by mtime
34
81
  function getNewestScreen() {
35
82
  const files = fs.readdirSync(SCREEN_DIR)
@@ -86,26 +133,25 @@ wss.on('connection', (ws) => {
86
133
  });
87
134
  });
88
135
 
89
- // Serve newest screen with helper.js injected
136
+ // Serve newest screen at the root, and session artifacts on direct paths
90
137
  app.get('/', (req, res) => {
91
138
  const screenFile = getNewestScreen();
92
- let html;
93
-
94
139
  if (!screenFile) {
95
- html = WAITING_PAGE;
96
- } else {
97
- const raw = fs.readFileSync(screenFile, 'utf-8');
98
- html = isFullDocument(raw) ? raw : wrapInFrame(raw);
140
+ res.type('html').send(renderHtml(WAITING_PAGE));
141
+ return;
99
142
  }
100
143
 
101
- // Inject helper script
102
- if (html.includes('</body>')) {
103
- html = html.replace('</body>', `${helperInjection}\n</body>`);
104
- } else {
105
- html += helperInjection;
144
+ serveArtifact(res, screenFile);
145
+ });
146
+
147
+ app.get(/^\/(.+)$/, (req, res) => {
148
+ const artifactPath = resolveArtifactPath(req.params[0]);
149
+ if (!artifactPath) {
150
+ res.status(404).type('text').send(`Cannot GET ${req.path}`);
151
+ return;
106
152
  }
107
153
 
108
- res.type('html').send(html);
154
+ serveArtifact(res, artifactPath);
109
155
  });
110
156
 
111
157
  // Watch for new or changed .html files
@@ -6,5 +6,8 @@
6
6
  "express": "^4.21.0",
7
7
  "ws": "^8.18.0",
8
8
  "chokidar": "^4.0.0"
9
+ },
10
+ "overrides": {
11
+ "path-to-regexp": "0.1.13"
9
12
  }
10
13
  }
@@ -10,10 +10,10 @@ const POLL_INTERVAL_MS = 100;
10
10
 
11
11
  interface StartServerOptions {
12
12
  sessionDir: string;
13
+ port?: number;
13
14
  host?: string;
14
15
  urlHost?: string;
15
16
  }
16
-
17
17
  /** Start the visual companion server and wait for its connection info. */
18
18
  export async function startVisualServer(opts: StartServerOptions): Promise<VisualServerInfo | null> {
19
19
  const host = opts.host ?? DEFAULT_HOST;
@@ -31,6 +31,7 @@ export async function startVisualServer(opts: StartServerOptions): Promise<Visua
31
31
  env: {
32
32
  ...process.env,
33
33
  SUPI_VISUAL_DIR: opts.sessionDir,
34
+ SUPI_VISUAL_PORT: opts.port !== undefined ? String(opts.port) : undefined,
34
35
  SUPI_VISUAL_HOST: host,
35
36
  SUPI_VISUAL_URL_HOST: urlHost,
36
37
  },
@@ -0,0 +1,64 @@
1
+ import { normalizeLineEndings } from "../text.js";
2
+ import type { WorkspaceTarget } from "../types.js";
3
+ import { filterPathsForWorkspaceTarget, normalizeRepoPath } from "./path-mapping.js";
4
+
5
+ const GIT_LOG_RECORD_SEPARATOR = "\u001e";
6
+ const GIT_LOG_FIELD_SEPARATOR = "\u001f";
7
+
8
+ export interface GitCommitWithFiles {
9
+ hash: string;
10
+ message: string;
11
+ files: string[];
12
+ }
13
+
14
+ export function parseGitLogWithFiles(gitLog: string): GitCommitWithFiles[] {
15
+ return normalizeLineEndings(gitLog)
16
+ .split(GIT_LOG_RECORD_SEPARATOR)
17
+ .map((record) => record.trim())
18
+ .filter(Boolean)
19
+ .flatMap((record) => {
20
+ const lines = record
21
+ .split("\n")
22
+ .map((line) => line.trim())
23
+ .filter(Boolean);
24
+
25
+ const header = lines.shift();
26
+ if (!header) {
27
+ return [];
28
+ }
29
+
30
+ const [hash, message] = header.split(GIT_LOG_FIELD_SEPARATOR);
31
+ if (!hash || !message) {
32
+ return [];
33
+ }
34
+
35
+ return [{
36
+ hash: hash.trim(),
37
+ message: message.trim(),
38
+ files: lines.map(normalizeRepoPath).filter(Boolean),
39
+ }];
40
+ });
41
+ }
42
+
43
+ export function filterGitLogWithFilesToWorkspaceTarget<TTarget extends WorkspaceTarget>(
44
+ gitLog: string,
45
+ targets: TTarget[],
46
+ target: TTarget,
47
+ ): GitCommitWithFiles[] {
48
+ return parseGitLogWithFiles(gitLog)
49
+ .map((commit) => ({
50
+ ...commit,
51
+ files: filterPathsForWorkspaceTarget(targets, target, commit.files),
52
+ }))
53
+ .filter((commit) => commit.files.length > 0);
54
+ }
55
+
56
+ export function filterGitLogOnelineToWorkspaceTarget<TTarget extends WorkspaceTarget>(
57
+ gitLog: string,
58
+ targets: TTarget[],
59
+ target: TTarget,
60
+ ): string {
61
+ return filterGitLogWithFilesToWorkspaceTarget(gitLog, targets, target)
62
+ .map((commit) => `${commit.hash.slice(0, 7)} ${commit.message}`)
63
+ .join("\n");
64
+ }
@@ -0,0 +1,23 @@
1
+ function getWorkspaceTargetLockKey(commandName: string, targetId: string): string {
2
+ return `${commandName}:${targetId}`;
3
+ }
4
+
5
+ const activeWorkspaceTargetLocks = new Set<string>();
6
+
7
+ export function tryAcquireWorkspaceTargetLock(commandName: string, targetId: string): boolean {
8
+ const key = getWorkspaceTargetLockKey(commandName, targetId);
9
+ if (activeWorkspaceTargetLocks.has(key)) {
10
+ return false;
11
+ }
12
+
13
+ activeWorkspaceTargetLocks.add(key);
14
+ return true;
15
+ }
16
+
17
+ export function releaseWorkspaceTargetLock(commandName: string, targetId: string): void {
18
+ activeWorkspaceTargetLocks.delete(getWorkspaceTargetLockKey(commandName, targetId));
19
+ }
20
+
21
+ export function isWorkspaceTargetLocked(commandName: string, targetId: string): boolean {
22
+ return activeWorkspaceTargetLocks.has(getWorkspaceTargetLockKey(commandName, targetId));
23
+ }
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { PackageManagerId } from "../types.js";
4
+
5
+ interface RootManifest {
6
+ packageManager?: string;
7
+ workspaces?: unknown;
8
+ }
9
+
10
+ export interface PackageManagerCommand {
11
+ command: string;
12
+ args: string[];
13
+ }
14
+
15
+ export interface ResolvedPackageManager {
16
+ id: PackageManagerId;
17
+ runScript(scriptName: string): PackageManagerCommand;
18
+ buildCommand: PackageManagerCommand;
19
+ }
20
+
21
+ const LOCKFILE_ORDER: Array<{ file: string; manager: PackageManagerId }> = [
22
+ { file: "bun.lock", manager: "bun" },
23
+ { file: "bun.lockb", manager: "bun" },
24
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
25
+ { file: "yarn.lock", manager: "yarn" },
26
+ { file: "package-lock.json", manager: "npm" },
27
+ { file: "npm-shrinkwrap.json", manager: "npm" },
28
+ ];
29
+
30
+ function readRootManifest(repoRoot: string): RootManifest | null {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf-8")) as RootManifest;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function parsePackageManagerField(value: string | undefined): PackageManagerId | null {
39
+ if (!value) {
40
+ return null;
41
+ }
42
+
43
+ const normalized = value.trim();
44
+ if (normalized === "bun" || normalized.startsWith("bun@")) return "bun";
45
+ if (normalized === "npm" || normalized.startsWith("npm@")) return "npm";
46
+ if (normalized === "pnpm" || normalized.startsWith("pnpm@")) return "pnpm";
47
+ if (normalized === "yarn" || normalized.startsWith("yarn@")) return "yarn";
48
+ return null;
49
+ }
50
+
51
+ function hasPackageJsonWorkspaces(manifest: RootManifest | null): boolean {
52
+ if (!manifest) {
53
+ return false;
54
+ }
55
+
56
+ if (Array.isArray(manifest.workspaces)) {
57
+ return manifest.workspaces.some((entry) => typeof entry === "string" && entry.length > 0);
58
+ }
59
+
60
+ if (manifest.workspaces && typeof manifest.workspaces === "object" && !Array.isArray(manifest.workspaces)) {
61
+ const packages = (manifest.workspaces as { packages?: unknown }).packages;
62
+ return Array.isArray(packages) && packages.some((entry) => typeof entry === "string" && entry.length > 0);
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ export function getRunScriptCommand(
69
+ packageManager: PackageManagerId,
70
+ scriptName: string,
71
+ ): PackageManagerCommand {
72
+ switch (packageManager) {
73
+ case "bun":
74
+ return { command: "bun", args: ["run", scriptName] };
75
+ case "npm":
76
+ return { command: "npm", args: ["run", scriptName] };
77
+ case "pnpm":
78
+ return { command: "pnpm", args: ["run", scriptName] };
79
+ case "yarn":
80
+ return { command: "yarn", args: [scriptName] };
81
+ }
82
+ }
83
+
84
+ export function detectPackageManager(repoRoot: string): PackageManagerId {
85
+ const manifest = readRootManifest(repoRoot);
86
+ const manifestManager = parsePackageManagerField(manifest?.packageManager);
87
+ if (manifestManager) {
88
+ return manifestManager;
89
+ }
90
+
91
+ for (const lockfile of LOCKFILE_ORDER) {
92
+ if (fs.existsSync(path.join(repoRoot, lockfile.file))) {
93
+ return lockfile.manager;
94
+ }
95
+ }
96
+
97
+ if (fs.existsSync(path.join(repoRoot, "pnpm-workspace.yaml"))) {
98
+ return "pnpm";
99
+ }
100
+
101
+ if (hasPackageJsonWorkspaces(manifest)) {
102
+ return "npm";
103
+ }
104
+
105
+ return "bun";
106
+ }
107
+
108
+ export function resolvePackageManager(repoRoot: string): ResolvedPackageManager {
109
+ const id = detectPackageManager(repoRoot);
110
+ return {
111
+ id,
112
+ runScript(scriptName: string) {
113
+ return getRunScriptCommand(id, scriptName);
114
+ },
115
+ buildCommand: getRunScriptCommand(id, "build"),
116
+ };
117
+ }
@@ -0,0 +1,75 @@
1
+ import type { WorkspaceTarget } from "../types.js";
2
+ import { ROOT_WORKSPACE_RELATIVE_DIR, normalizeWorkspaceRelativePath } from "./targets.js";
3
+
4
+ export function normalizeRepoPath(value: string): string {
5
+ return normalizeWorkspaceRelativePath(value);
6
+ }
7
+
8
+ export function findWorkspaceTargetForPath<TTarget extends WorkspaceTarget>(
9
+ targets: TTarget[],
10
+ repoRelativePath: string,
11
+ ): TTarget | null {
12
+ const normalizedPath = normalizeRepoPath(repoRelativePath);
13
+ let bestMatch: TTarget | null = null;
14
+ let bestSpecificity = -1;
15
+
16
+ for (const target of targets) {
17
+ const scope = normalizeRepoPath(target.relativeDir);
18
+ const isMatch = scope === ROOT_WORKSPACE_RELATIVE_DIR
19
+ ? true
20
+ : normalizedPath === scope || normalizedPath.startsWith(`${scope}/`);
21
+
22
+ if (!isMatch) {
23
+ continue;
24
+ }
25
+
26
+ const specificity = scope === ROOT_WORKSPACE_RELATIVE_DIR ? 0 : scope.length;
27
+ if (specificity > bestSpecificity) {
28
+ bestMatch = target;
29
+ bestSpecificity = specificity;
30
+ }
31
+ }
32
+
33
+ return bestMatch;
34
+ }
35
+
36
+ export function filterPathsForWorkspaceTarget<TTarget extends WorkspaceTarget>(
37
+ targets: TTarget[],
38
+ target: TTarget,
39
+ repoRelativePaths: string[],
40
+ ): string[] {
41
+ return repoRelativePaths.filter((repoRelativePath) =>
42
+ findWorkspaceTargetForPath(targets, repoRelativePath)?.id === target.id,
43
+ );
44
+ }
45
+
46
+ export function partitionPathsByWorkspaceTarget<TTarget extends WorkspaceTarget>(
47
+ targets: TTarget[],
48
+ repoRelativePaths: string[],
49
+ ): Map<string, string[]> {
50
+ const partitions = new Map<string, string[]>();
51
+
52
+ for (const repoRelativePath of repoRelativePaths) {
53
+ const owner = findWorkspaceTargetForPath(targets, repoRelativePath);
54
+ if (!owner) {
55
+ continue;
56
+ }
57
+
58
+ const existing = partitions.get(owner.id);
59
+ if (existing) {
60
+ existing.push(repoRelativePath);
61
+ } else {
62
+ partitions.set(owner.id, [repoRelativePath]);
63
+ }
64
+ }
65
+
66
+ return partitions;
67
+ }
68
+
69
+ export function getChangedWorkspaceTargets<TTarget extends WorkspaceTarget>(
70
+ targets: TTarget[],
71
+ repoRelativePaths: string[],
72
+ ): TTarget[] {
73
+ const changedIds = new Set(partitionPathsByWorkspaceTarget(targets, repoRelativePaths).keys());
74
+ return targets.filter((target) => changedIds.has(target.id));
75
+ }
@@ -0,0 +1,92 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Version of the slug derivation scheme. Bumping this is a breaking change: every user's
6
+ * global state directories become orphaned because their previously derived slugs no longer
7
+ * match what {@link projectSlugFromRepoRoot} produces. Do not bump without a migration plan.
8
+ */
9
+ export const SLUG_SCHEMA_VERSION = 1;
10
+
11
+ /**
12
+ * Fixed length of the hex hash suffix that appears after the human-readable basename portion.
13
+ * 16 hex chars = 64 bits, which makes accidental collisions vanishingly unlikely while keeping
14
+ * slugs readable on disk.
15
+ */
16
+ const HASH_HEX_LEN = 16;
17
+
18
+ /**
19
+ * Maximum length of the basename portion. Keeps slugs filesystem-safe on Windows where
20
+ * total path budget can be tight, while still preserving enough context for humans to
21
+ * recognize which repo owns a given global session directory.
22
+ */
23
+ const MAX_BASENAME_PORTION_LEN = 40;
24
+
25
+ function sanitizeBasename(raw: string): string {
26
+ // Lowercase, collapse anything that is not alphanumeric to a single hyphen, and trim hyphens.
27
+ const lowered = raw.toLowerCase();
28
+ const replaced = lowered.replace(/[^a-z0-9]+/g, "-");
29
+ const trimmed = replaced.replace(/^-+|-+$/g, "");
30
+ // Fall back to a stable placeholder when the basename is entirely non-alphanumeric
31
+ // (e.g. `/` or a directory named `___`). The hash portion still disambiguates.
32
+ const base = trimmed.length > 0 ? trimmed : "project";
33
+ return base.length > MAX_BASENAME_PORTION_LEN
34
+ ? base.slice(0, MAX_BASENAME_PORTION_LEN)
35
+ : base;
36
+ }
37
+
38
+ /**
39
+ * Derive a deterministic project slug from the absolute repo root path.
40
+ *
41
+ * Guarantees:
42
+ * - Stable for the same absolute input across calls and platforms.
43
+ * - Two distinct absolute inputs never produce the same slug (up to SHA-256 collision bounds).
44
+ * - Two inputs that differ only in case still produce distinct slugs on case-sensitive filesystems.
45
+ * - Normalization-stable: trailing slashes and redundant separators do not change the slug.
46
+ *
47
+ * Fail-closed:
48
+ * - Throws on non-absolute, empty, or whitespace-only input. The delta spec requires fail-closed
49
+ * behavior so migration and runtime truth never silently merge distinct projects.
50
+ */
51
+ export function projectSlugFromRepoRoot(repoRoot: string): string {
52
+ if (typeof repoRoot !== "string") {
53
+ throw new TypeError("projectSlugFromRepoRoot: repoRoot must be a string");
54
+ }
55
+
56
+ const trimmed = repoRoot.trim();
57
+ if (trimmed.length === 0) {
58
+ throw new Error("projectSlugFromRepoRoot: repoRoot must not be empty");
59
+ }
60
+
61
+ if (!path.isAbsolute(trimmed)) {
62
+ throw new Error(
63
+ `projectSlugFromRepoRoot: repoRoot must be an absolute path, received: ${repoRoot}`,
64
+ );
65
+ }
66
+
67
+ // path.normalize collapses redundant separators but preserves trailing slashes; strip them
68
+ // so "/foo/" and "/foo" produce the same slug. Preserve filesystem root "/".
69
+ const normalized = stripTrailingSeparators(path.normalize(trimmed));
70
+
71
+ // Always hash the normalized absolute path so the slug is a pure function of filesystem
72
+ // identity. The hash portion disambiguates across projects that share a basename.
73
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, HASH_HEX_LEN);
74
+
75
+ const basenamePortion = sanitizeBasename(path.basename(normalized));
76
+
77
+ return `${basenamePortion}-${hash}`;
78
+ }
79
+
80
+ function stripTrailingSeparators(value: string): string {
81
+ // Never strip the filesystem root ("/" on POSIX, "C:\\" on Windows).
82
+ if (value.length <= 1) return value;
83
+ let end = value.length;
84
+ while (end > 1 && (value[end - 1] === "/" || value[end - 1] === "\\")) {
85
+ end--;
86
+ }
87
+ // Preserve Windows drive roots like "C:\\" (length 3) — stripping would yield "C:" which is not absolute.
88
+ if (/^[A-Za-z]:[\/\\]?$/.test(value.slice(0, Math.min(3, value.length)))) {
89
+ return value.slice(0, Math.max(end, 3));
90
+ }
91
+ return value.slice(0, end);
92
+ }
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Platform } from "../platform/types.js";
4
+
5
+ interface RootManifest {
6
+ workspaces?: unknown;
7
+ }
8
+
9
+ function hasWorkspaceManifest(repoRoot: string): boolean {
10
+ if (fs.existsSync(path.join(repoRoot, "pnpm-workspace.yaml"))) {
11
+ return true;
12
+ }
13
+
14
+ try {
15
+ const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf-8")) as RootManifest;
16
+ if (Array.isArray(manifest.workspaces)) {
17
+ return manifest.workspaces.some((entry) => typeof entry === "string" && entry.length > 0);
18
+ }
19
+
20
+ const workspaces = manifest.workspaces;
21
+ if (!workspaces || typeof workspaces !== "object" || Array.isArray(workspaces)) {
22
+ return false;
23
+ }
24
+
25
+ const packages = (workspaces as { packages?: unknown }).packages;
26
+ return Array.isArray(packages) && packages.some((entry) => typeof entry === "string" && entry.length > 0);
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export function resolveRepoRootFromFs(cwd: string): string {
33
+ let current = path.resolve(cwd);
34
+ let packageRoot: string | null = null;
35
+
36
+ while (true) {
37
+ if (fs.existsSync(path.join(current, "package.json"))) {
38
+ packageRoot ??= current;
39
+ if (hasWorkspaceManifest(current)) {
40
+ return current;
41
+ }
42
+ }
43
+
44
+ const parent = path.dirname(current);
45
+ if (parent === current) {
46
+ return packageRoot ?? path.resolve(cwd);
47
+ }
48
+ current = parent;
49
+ }
50
+ }
51
+
52
+ function resolveGitDirForRepoRoot(repoRoot: string): string | null {
53
+ const dotGitPath = path.join(repoRoot, ".git");
54
+ if (!fs.existsSync(dotGitPath)) {
55
+ return null;
56
+ }
57
+
58
+ const dotGitStats = fs.statSync(dotGitPath);
59
+ if (dotGitStats.isDirectory()) {
60
+ return path.resolve(dotGitPath);
61
+ }
62
+
63
+ if (!dotGitStats.isFile()) {
64
+ throw new Error(`Unable to resolve repo identity from ${repoRoot}: .git is neither a file nor directory`);
65
+ }
66
+
67
+ const rawPointer = fs.readFileSync(dotGitPath, "utf-8").trim();
68
+ const match = /^gitdir:\s*(.+)$/i.exec(rawPointer);
69
+ if (!match) {
70
+ throw new Error(`Unable to resolve repo identity from ${repoRoot}: invalid .git gitdir pointer`);
71
+ }
72
+
73
+ return path.resolve(repoRoot, match[1].trim());
74
+ }
75
+
76
+ function resolveCommonGitDir(gitDir: string): string {
77
+ if (!fs.existsSync(gitDir)) {
78
+ throw new Error(`Unable to resolve repo identity: gitdir does not exist at ${gitDir}`);
79
+ }
80
+
81
+ const commondirPath = path.join(gitDir, "commondir");
82
+ if (!fs.existsSync(commondirPath)) {
83
+ return gitDir;
84
+ }
85
+
86
+ const commondirValue = fs.readFileSync(commondirPath, "utf-8").trim();
87
+ if (commondirValue.length === 0) {
88
+ throw new Error(`Unable to resolve repo identity: empty commondir file at ${commondirPath}`);
89
+ }
90
+
91
+ const commonDir = path.resolve(gitDir, commondirValue);
92
+ if (!fs.existsSync(commonDir)) {
93
+ throw new Error(`Unable to resolve repo identity: common git dir does not exist at ${commonDir}`);
94
+ }
95
+
96
+ return commonDir;
97
+ }
98
+
99
+ export function resolveRepoIdentityRootFromFs(cwd: string): string {
100
+ const repoRoot = resolveRepoRootFromFs(cwd);
101
+ const gitDir = resolveGitDirForRepoRoot(repoRoot);
102
+ if (gitDir === null) {
103
+ return repoRoot;
104
+ }
105
+
106
+ const resolvedGitDir = path.resolve(gitDir);
107
+ const commonDir = path.resolve(resolveCommonGitDir(resolvedGitDir));
108
+ if (path.basename(commonDir) !== ".git") {
109
+ if (commonDir === resolvedGitDir) {
110
+ return repoRoot;
111
+ }
112
+ throw new Error(`Unable to resolve repo identity from ${repoRoot}: expected common git dir ending in .git, received ${commonDir}`);
113
+ }
114
+
115
+ const identityRoot = path.dirname(commonDir);
116
+ if (!path.isAbsolute(identityRoot) || identityRoot === commonDir) {
117
+ throw new Error(`Unable to resolve repo identity from ${repoRoot}: invalid identity root ${identityRoot}`);
118
+ }
119
+
120
+ return identityRoot;
121
+ }
122
+
123
+ export async function resolveRepoRoot(platform: Pick<Platform, "exec">, cwd: string): Promise<string> {
124
+ try {
125
+ const result = await platform.exec("git", ["rev-parse", "--show-toplevel"], { cwd });
126
+ if (result.code === 0) {
127
+ const repoRoot = result.stdout.trim();
128
+ if (repoRoot.length > 0) {
129
+ return repoRoot;
130
+ }
131
+ }
132
+ } catch {
133
+ // Fall back to filesystem-based workspace detection when git is unavailable.
134
+ }
135
+
136
+ return resolveRepoRootFromFs(cwd);
137
+ }