supipowers 1.5.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +135 -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 +268 -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
@@ -0,0 +1,454 @@
1
+ /**
2
+ * DISCOVER stage runner.
3
+ *
4
+ * Builds `<session>/discover.json` deterministically from the filesystem:
5
+ * - language detection by file extension (deterministic, fast),
6
+ * - cross-checks against `src/deps/registry.ts` for installed tooling,
7
+ * - LSP availability via `platform.getActiveTools()`,
8
+ * - existing supipowers + MCP infra scan,
9
+ * - existing anti-slop tooling scan (fallow / desloppify / knip / jscpd / dependency-cruiser),
10
+ * - language-coverage map and recommended backend.
11
+ *
12
+ * Discover is **deterministic by design** — no agent session is spawned. The plan calls
13
+ * for a user gate (planning_ask) after Discover, which is owned by the command handler,
14
+ * not the stage runner.
15
+ */
16
+
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+
20
+ import type { HarnessDiscoverArtifact } from "../../types.js";
21
+ import { recommendBackend } from "../anti_slop/recommend.js";
22
+ import {
23
+ type HarnessStageRunResult,
24
+ type HarnessStageRunner,
25
+ type HarnessStageRunnerContext,
26
+ nowIso,
27
+ } from "../stage-runner.js";
28
+ import { saveHarnessDiscover } from "../storage.js";
29
+ import { loadHarnessDiscover } from "../storage.js";
30
+
31
+ const LANGUAGE_BY_EXT: Record<string, string> = {
32
+ ".ts": "typescript",
33
+ ".tsx": "tsx",
34
+ ".js": "javascript",
35
+ ".jsx": "jsx",
36
+ ".mjs": "javascript",
37
+ ".cjs": "javascript",
38
+ ".py": "python",
39
+ ".rs": "rust",
40
+ ".go": "go",
41
+ ".rb": "ruby",
42
+ ".java": "java",
43
+ ".kt": "kotlin",
44
+ ".swift": "swift",
45
+ ".cs": "csharp",
46
+ ".php": "php",
47
+ ".sh": "shell",
48
+ ".bash": "shell",
49
+ ".zsh": "shell",
50
+ };
51
+
52
+ const SKIP_DIRS = new Set([
53
+ "node_modules",
54
+ ".git",
55
+ "dist",
56
+ "build",
57
+ "out",
58
+ "target",
59
+ ".next",
60
+ ".turbo",
61
+ "coverage",
62
+ ".cache",
63
+ ".bun",
64
+ ]);
65
+
66
+ interface FileScanResult {
67
+ files: number;
68
+ byLanguage: Map<string, number>;
69
+ }
70
+
71
+ function scanRepoLanguages(cwd: string, depthLimit = 6, fileLimit = 5000): FileScanResult {
72
+ let files = 0;
73
+ const byLanguage = new Map<string, number>();
74
+
75
+ function walk(dir: string, depth: number): void {
76
+ if (depth > depthLimit || files > fileLimit) return;
77
+ let entries: fs.Dirent[];
78
+ try {
79
+ entries = fs.readdirSync(dir, { withFileTypes: true });
80
+ } catch {
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ if (files > fileLimit) return;
85
+ if (SKIP_DIRS.has(entry.name)) continue;
86
+ if (entry.name.startsWith(".") && entry.name !== "." && entry.name !== ".github") continue;
87
+ const full = path.join(dir, entry.name);
88
+ if (entry.isDirectory()) {
89
+ walk(full, depth + 1);
90
+ continue;
91
+ }
92
+ if (!entry.isFile()) continue;
93
+ files += 1;
94
+ const ext = path.extname(entry.name).toLowerCase();
95
+ const lang = LANGUAGE_BY_EXT[ext];
96
+ if (lang) byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
97
+ }
98
+ }
99
+
100
+ walk(cwd, 0);
101
+ return { files, byLanguage };
102
+ }
103
+
104
+ function existsSync(p: string): boolean {
105
+ try {
106
+ return fs.existsSync(p);
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ function detectMonorepoShape(cwd: string): HarnessDiscoverArtifact["monorepoShape"] {
113
+ // Heuristics: presence of package workspace declarations, pnpm-workspace.yaml, lerna.json,
114
+ // turbo.json, multiple top-level package.json files.
115
+ if (existsSync(path.join(cwd, "pnpm-workspace.yaml"))) return "monorepo";
116
+ if (existsSync(path.join(cwd, "lerna.json"))) return "monorepo";
117
+ if (existsSync(path.join(cwd, "turbo.json"))) return "monorepo";
118
+ if (existsSync(path.join(cwd, "Cargo.toml"))) {
119
+ const cargoToml = (() => {
120
+ try {
121
+ return fs.readFileSync(path.join(cwd, "Cargo.toml"), "utf8");
122
+ } catch {
123
+ return "";
124
+ }
125
+ })();
126
+ if (/\[workspace\]/.test(cargoToml)) return "monorepo";
127
+ }
128
+ // packages/ or apps/ + multiple package.json
129
+ const packagesDir = path.join(cwd, "packages");
130
+ const appsDir = path.join(cwd, "apps");
131
+ if (existsSync(packagesDir) || existsSync(appsDir)) return "monorepo";
132
+ return "single-package";
133
+ }
134
+
135
+ function detectCi(cwd: string): HarnessDiscoverArtifact["ci"] {
136
+ const ghDir = path.join(cwd, ".github", "workflows");
137
+ if (existsSync(ghDir)) {
138
+ const files: string[] = [];
139
+ try {
140
+ for (const entry of fs.readdirSync(ghDir)) {
141
+ if (entry.endsWith(".yml") || entry.endsWith(".yaml")) {
142
+ files.push(`.github/workflows/${entry}`);
143
+ }
144
+ }
145
+ } catch {
146
+ // ignore
147
+ }
148
+ if (files.length > 0) return { detected: true, provider: "github-actions", configFiles: files };
149
+ }
150
+ if (existsSync(path.join(cwd, ".gitlab-ci.yml"))) {
151
+ return { detected: true, provider: "gitlab-ci", configFiles: [".gitlab-ci.yml"] };
152
+ }
153
+ if (existsSync(path.join(cwd, ".circleci", "config.yml"))) {
154
+ return { detected: true, provider: "circle-ci", configFiles: [".circleci/config.yml"] };
155
+ }
156
+ return { detected: false, configFiles: [] };
157
+ }
158
+
159
+ function detectCommitConventions(cwd: string): HarnessDiscoverArtifact["commitConventions"] {
160
+ if (existsSync(path.join(cwd, "commitlint.config.js")) || existsSync(path.join(cwd, "commitlint.config.cjs"))) {
161
+ return { detected: true, style: "conventional" };
162
+ }
163
+ if (existsSync(path.join(cwd, ".commitlintrc.json")) || existsSync(path.join(cwd, ".commitlintrc"))) {
164
+ return { detected: true, style: "conventional" };
165
+ }
166
+ return { detected: false };
167
+ }
168
+
169
+ function detectExistingAntiSlop(cwd: string): HarnessDiscoverArtifact["antiSlopExisting"] {
170
+ const exists = (rel: string) => (existsSync(path.join(cwd, rel)) ? rel : null);
171
+ return {
172
+ fallowConfig: exists(".fallowrc.json") ?? exists(".fallow.toml"),
173
+ desloppifyConfig: exists(".desloppify") ?? exists(".desloppifyrc"),
174
+ knipConfig: exists("knip.json") ?? exists(".knip.json"),
175
+ jscpdConfig: exists(".jscpd.json") ?? exists("jscpd.json"),
176
+ dependencyCruiserConfig: exists(".dependency-cruiser.cjs") ?? exists(".dependency-cruiser.json"),
177
+ eslintConfig:
178
+ exists("eslint.config.js") ??
179
+ exists("eslint.config.cjs") ??
180
+ exists("eslint.config.mjs") ??
181
+ exists(".eslintrc") ??
182
+ exists(".eslintrc.json") ??
183
+ exists(".eslintrc.cjs"),
184
+ biomeConfig: exists("biome.json") ?? exists("biome.jsonc") ?? exists(".biomerc.json"),
185
+ };
186
+ }
187
+
188
+ function detectOmpInfra(cwd: string): HarnessDiscoverArtifact["ompInfra"] {
189
+ const supipowersDir = path.join(cwd, ".omp", "supipowers");
190
+ const hasSupipowers = existsSync(supipowersDir);
191
+ const skills: string[] = [];
192
+ const reviewAgents: string[] = [];
193
+ const mcpServers: string[] = [];
194
+ let plansCount = 0;
195
+
196
+ // Skills are typically in skills/ at the repo root.
197
+ const skillsDir = path.join(cwd, "skills");
198
+ if (existsSync(skillsDir)) {
199
+ try {
200
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
201
+ if (entry.isDirectory()) skills.push(entry.name);
202
+ }
203
+ } catch {
204
+ // ignore
205
+ }
206
+ }
207
+
208
+ if (hasSupipowers) {
209
+ const reviewAgentsDir = path.join(supipowersDir, "review-agents");
210
+ if (existsSync(reviewAgentsDir)) {
211
+ try {
212
+ for (const entry of fs.readdirSync(reviewAgentsDir, { withFileTypes: true })) {
213
+ if (entry.isFile() && entry.name.endsWith(".md")) {
214
+ reviewAgents.push(entry.name.replace(/\.md$/, ""));
215
+ }
216
+ }
217
+ } catch {
218
+ // ignore
219
+ }
220
+ }
221
+ const plansDir = path.join(supipowersDir, "plans");
222
+ if (existsSync(plansDir)) {
223
+ try {
224
+ plansCount = fs.readdirSync(plansDir).filter((f) => f.endsWith(".md")).length;
225
+ } catch {
226
+ plansCount = 0;
227
+ }
228
+ }
229
+ const mcpJson = path.join(supipowersDir, ".mcp.json");
230
+ if (existsSync(mcpJson)) {
231
+ try {
232
+ const parsed = JSON.parse(fs.readFileSync(mcpJson, "utf8")) as {
233
+ servers?: Record<string, unknown>;
234
+ };
235
+ if (parsed.servers) mcpServers.push(...Object.keys(parsed.servers));
236
+ } catch {
237
+ // ignore
238
+ }
239
+ }
240
+ }
241
+
242
+ return { hasSupipowers, skills, reviewAgents, mcpServers, plansCount };
243
+ }
244
+
245
+ function detectFrameworks(cwd: string): string[] {
246
+ const frameworks: string[] = [];
247
+ const pkgJsonPath = path.join(cwd, "package.json");
248
+ if (existsSync(pkgJsonPath)) {
249
+ try {
250
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as {
251
+ dependencies?: Record<string, string>;
252
+ devDependencies?: Record<string, string>;
253
+ };
254
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
255
+ if ("react" in all) frameworks.push("react");
256
+ if ("next" in all) frameworks.push("next");
257
+ if ("vue" in all) frameworks.push("vue");
258
+ if ("svelte" in all) frameworks.push("svelte");
259
+ if ("@nestjs/core" in all) frameworks.push("nestjs");
260
+ if ("express" in all) frameworks.push("express");
261
+ if ("fastify" in all) frameworks.push("fastify");
262
+ if ("hono" in all) frameworks.push("hono");
263
+ } catch {
264
+ // ignore malformed package.json
265
+ }
266
+ }
267
+ if (existsSync(path.join(cwd, "Cargo.toml"))) frameworks.push("cargo");
268
+ if (existsSync(path.join(cwd, "pyproject.toml"))) frameworks.push("python-project");
269
+ if (existsSync(path.join(cwd, "go.mod"))) frameworks.push("go-modules");
270
+ return frameworks;
271
+ }
272
+
273
+ function detectPackageManagers(cwd: string): string[] {
274
+ const out: string[] = [];
275
+ if (existsSync(path.join(cwd, "bun.lock")) || existsSync(path.join(cwd, "bun.lockb"))) out.push("bun");
276
+ if (existsSync(path.join(cwd, "package-lock.json"))) out.push("npm");
277
+ if (existsSync(path.join(cwd, "yarn.lock"))) out.push("yarn");
278
+ if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) out.push("pnpm");
279
+ if (existsSync(path.join(cwd, "Cargo.lock"))) out.push("cargo");
280
+ if (existsSync(path.join(cwd, "poetry.lock"))) out.push("poetry");
281
+ if (existsSync(path.join(cwd, "uv.lock"))) out.push("uv");
282
+ if (existsSync(path.join(cwd, "go.sum"))) out.push("go-modules");
283
+ return out;
284
+ }
285
+
286
+ function detectBuildTools(cwd: string): string[] {
287
+ const out: string[] = [];
288
+ if (existsSync(path.join(cwd, "tsconfig.json"))) out.push("tsc");
289
+ if (existsSync(path.join(cwd, "vite.config.ts")) || existsSync(path.join(cwd, "vite.config.js"))) out.push("vite");
290
+ if (existsSync(path.join(cwd, "webpack.config.js"))) out.push("webpack");
291
+ if (existsSync(path.join(cwd, "esbuild.config.js"))) out.push("esbuild");
292
+ if (existsSync(path.join(cwd, "Makefile"))) out.push("make");
293
+ if (existsSync(path.join(cwd, "Cargo.toml"))) out.push("cargo");
294
+ return out;
295
+ }
296
+
297
+ function detectTestTools(cwd: string): string[] {
298
+ const out: string[] = [];
299
+ const pkgJsonPath = path.join(cwd, "package.json");
300
+ if (existsSync(pkgJsonPath)) {
301
+ try {
302
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")) as {
303
+ dependencies?: Record<string, string>;
304
+ devDependencies?: Record<string, string>;
305
+ scripts?: Record<string, string>;
306
+ };
307
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
308
+ if ("vitest" in deps) out.push("vitest");
309
+ if ("jest" in deps) out.push("jest");
310
+ if ("@playwright/test" in deps) out.push("playwright");
311
+ if ("cypress" in deps) out.push("cypress");
312
+ if (pkg.scripts?.test?.includes("bun test") || pkg.scripts?.test?.includes("bun:test")) out.push("bun:test");
313
+ } catch {
314
+ // ignore
315
+ }
316
+ }
317
+ if (existsSync(path.join(cwd, "pytest.ini")) || existsSync(path.join(cwd, "pyproject.toml"))) {
318
+ try {
319
+ const pyproject = existsSync(path.join(cwd, "pyproject.toml"))
320
+ ? fs.readFileSync(path.join(cwd, "pyproject.toml"), "utf8")
321
+ : "";
322
+ if (pyproject.includes("pytest")) out.push("pytest");
323
+ } catch {
324
+ // ignore
325
+ }
326
+ }
327
+ if (existsSync(path.join(cwd, "Cargo.toml"))) out.push("cargo-test");
328
+ if (existsSync(path.join(cwd, "go.mod"))) out.push("go-test");
329
+ return out;
330
+ }
331
+
332
+ function detectLintTools(cwd: string): string[] {
333
+ const out: string[] = [];
334
+ if (existsSync(path.join(cwd, "eslint.config.js")) || existsSync(path.join(cwd, "eslint.config.cjs")) || existsSync(path.join(cwd, ".eslintrc.json"))) {
335
+ out.push("eslint");
336
+ }
337
+ if (existsSync(path.join(cwd, "biome.json")) || existsSync(path.join(cwd, "biome.jsonc"))) out.push("biome");
338
+ if (existsSync(path.join(cwd, ".prettierrc"))) out.push("prettier");
339
+ if (existsSync(path.join(cwd, "ruff.toml"))) out.push("ruff");
340
+ if (existsSync(path.join(cwd, "rustfmt.toml")) || existsSync(path.join(cwd, ".rustfmt.toml"))) out.push("rustfmt");
341
+ return out;
342
+ }
343
+
344
+ /**
345
+ * Build the discover artifact from the filesystem alone. Pure function for testability:
346
+ * the same inputs produce the same outputs (timestamp injected via `now`).
347
+ */
348
+ export function buildDiscoverArtifact(input: {
349
+ cwd: string;
350
+ sessionId: string;
351
+ now: string;
352
+ }): HarnessDiscoverArtifact {
353
+ const scan = scanRepoLanguages(input.cwd);
354
+ const totalLanguageFiles = [...scan.byLanguage.values()].reduce((sum, n) => sum + n, 0);
355
+ const languageCoverage = [...scan.byLanguage.entries()]
356
+ .map(([language, fileCount]) => ({
357
+ language,
358
+ fileCount,
359
+ share: totalLanguageFiles === 0 ? 0 : fileCount / totalLanguageFiles,
360
+ }))
361
+ .sort((a, b) => b.fileCount - a.fileCount);
362
+
363
+ const languages = languageCoverage.map((c) => c.language);
364
+ const recommendation = recommendBackend({ languageCoverage });
365
+
366
+ // Detect duplicates between existing tools and the harness's recommended backend.
367
+ const antiSlopExisting = detectExistingAntiSlop(input.cwd);
368
+ const ompInfra = detectOmpInfra(input.cwd);
369
+ const duplicates: HarnessDiscoverArtifact["duplicates"] = [];
370
+ if (antiSlopExisting.fallowConfig && recommendation.backend !== "fallow" && recommendation.backend !== "hybrid") {
371
+ duplicates.push({
372
+ area: "anti-slop",
373
+ existing: antiSlopExisting.fallowConfig,
374
+ conflict: `recommended backend is ${recommendation.backend}; existing fallow config will be unused unless user overrides`,
375
+ });
376
+ }
377
+ if (antiSlopExisting.desloppifyConfig && recommendation.backend !== "desloppify" && recommendation.backend !== "hybrid") {
378
+ duplicates.push({
379
+ area: "anti-slop",
380
+ existing: antiSlopExisting.desloppifyConfig,
381
+ conflict: `recommended backend is ${recommendation.backend}; existing desloppify config will be unused unless user overrides`,
382
+ });
383
+ }
384
+
385
+ return {
386
+ sessionId: input.sessionId,
387
+ recordedAt: input.now,
388
+ languages,
389
+ frameworks: detectFrameworks(input.cwd),
390
+ packageManagers: detectPackageManagers(input.cwd),
391
+ buildTools: detectBuildTools(input.cwd),
392
+ testTools: detectTestTools(input.cwd),
393
+ lintTools: detectLintTools(input.cwd),
394
+ monorepoShape: detectMonorepoShape(input.cwd),
395
+ ci: detectCi(input.cwd),
396
+ ompInfra,
397
+ antiSlopExisting,
398
+ languageCoverage,
399
+ recommendedBackend: recommendation.backend,
400
+ recommendedBackendReason: recommendation.reason,
401
+ commitConventions: detectCommitConventions(input.cwd),
402
+ duplicates,
403
+ notes: [],
404
+ };
405
+ }
406
+
407
+ export class HarnessDiscoverStage implements HarnessStageRunner {
408
+ readonly stage = "discover" as const;
409
+
410
+ async isReady(_ctx: HarnessStageRunnerContext): Promise<boolean> {
411
+ // Discover only needs a readable cwd.
412
+ return true;
413
+ }
414
+
415
+ async isComplete(ctx: HarnessStageRunnerContext): Promise<boolean> {
416
+ const loaded = loadHarnessDiscover(ctx.paths, ctx.cwd, ctx.sessionId);
417
+ return loaded.ok;
418
+ }
419
+
420
+ async run(ctx: HarnessStageRunnerContext): Promise<HarnessStageRunResult> {
421
+ if (await this.isComplete(ctx)) {
422
+ return {
423
+ status: "skipped",
424
+ stage: this.stage,
425
+ artifactPaths: ["discover.json"],
426
+ details: { reason: "discover artifact already exists" },
427
+ };
428
+ }
429
+
430
+ const artifact = buildDiscoverArtifact({
431
+ cwd: ctx.cwd,
432
+ sessionId: ctx.sessionId,
433
+ now: nowIso(ctx),
434
+ });
435
+ const persisted = saveHarnessDiscover(ctx.paths, ctx.cwd, ctx.sessionId, artifact);
436
+ if (!persisted.ok) {
437
+ return {
438
+ status: "failed",
439
+ stage: this.stage,
440
+ artifactPaths: [],
441
+ error: `failed to persist discover artifact: ${persisted.error.message}`,
442
+ };
443
+ }
444
+ return {
445
+ status: "completed",
446
+ stage: this.stage,
447
+ artifactPaths: ["discover.json"],
448
+ details: {
449
+ languages: artifact.languages,
450
+ recommendedBackend: artifact.recommendedBackend,
451
+ },
452
+ };
453
+ }
454
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * IMPLEMENT stage runner.
3
+ *
4
+ * Counts the tasks in the approved plan and decides whether to run them in-session (steer
5
+ * loop, mirrors `/supi:plan`) or to hand off to `/supi:ultraplan` batch / worktree
6
+ * runtime. The threshold is configurable via `harness.implement_in_session_threshold`
7
+ * (default 10).
8
+ *
9
+ * The actual execution loop lives in the command handler — the stage runner records the
10
+ * routing decision and validates pre-conditions (clean git tree, plan readable, etc.).
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+
16
+ import type { Plan } from "../../types.js";
17
+ import { parsePlan } from "../../storage/plans.js";
18
+ import {
19
+ type HarnessStageRunResult,
20
+ type HarnessStageRunner,
21
+ type HarnessStageRunnerContext,
22
+ nowIso,
23
+ } from "../stage-runner.js";
24
+ import {
25
+ appendImplementLog,
26
+ } from "../storage.js";
27
+
28
+ const DEFAULT_IN_SESSION_THRESHOLD = 10;
29
+
30
+ export type ImplementRouting = "in-session" | "batch";
31
+
32
+ export interface ImplementStageInput {
33
+ /** Path to the approved plan markdown. */
34
+ planPath: string;
35
+ /** Override the in-session-vs-batch threshold (default 10). */
36
+ threshold?: number;
37
+ }
38
+
39
+ export interface ImplementRoutingDecision {
40
+ routing: ImplementRouting;
41
+ taskCount: number;
42
+ reason: string;
43
+ plan: Plan;
44
+ }
45
+
46
+ /** Compute the routing decision from a parsed plan and threshold. Pure function. */
47
+ export function decideImplementRouting(input: { plan: Plan; threshold: number }): ImplementRoutingDecision {
48
+ const taskCount = input.plan.tasks.length;
49
+ const routing: ImplementRouting = taskCount <= input.threshold ? "in-session" : "batch";
50
+ const reason =
51
+ routing === "in-session"
52
+ ? `${taskCount} task(s) ≤ threshold ${input.threshold}; running via steer in-session`
53
+ : `${taskCount} task(s) > threshold ${input.threshold}; handing off to /supi:ultraplan batch worker`;
54
+ return { routing, taskCount, reason, plan: input.plan };
55
+ }
56
+
57
+ /**
58
+ * Verify pre-conditions before claiming Implement is safe to start. Returns error
59
+ * messages (empty when ready). Used by the stage runner and the GC fixers.
60
+ */
61
+ export function preflightImplement(input: { cwd: string; planPath: string; allowDirtyTree?: boolean }): string[] {
62
+ const errors: string[] = [];
63
+ if (!fs.existsSync(input.planPath)) {
64
+ errors.push(`plan not found at ${input.planPath}`);
65
+ }
66
+ // Pre-flight git cleanliness check is handled by the command handler; we record the
67
+ // requirement here so callers don't forget. (We don't shell out to git inside the
68
+ // pure-function preflight to keep it deterministic.)
69
+ if (!input.allowDirtyTree) {
70
+ errors.push("caller must verify the working tree is clean before Implement (or pass allowDirtyTree)");
71
+ }
72
+ return errors;
73
+ }
74
+
75
+ export class HarnessImplementStage implements HarnessStageRunner {
76
+ readonly stage = "implement" as const;
77
+
78
+ constructor(private readonly input: ImplementStageInput) {}
79
+
80
+ async isReady(_ctx: HarnessStageRunnerContext): Promise<boolean> {
81
+ return fs.existsSync(this.input.planPath);
82
+ }
83
+
84
+ async isComplete(ctx: HarnessStageRunnerContext): Promise<boolean> {
85
+ // Implement is complete when the post-implement self-check has been recorded in
86
+ // implement-log.jsonl with `kind: "self-check-passed"`. The command handler appends
87
+ // that record after running typecheck/test/scan.
88
+ const logPath = path.join(
89
+ path.dirname(this.input.planPath),
90
+ "..",
91
+ "harness",
92
+ "sessions",
93
+ ctx.sessionId,
94
+ "implement-log.jsonl",
95
+ );
96
+ void logPath; // tracked but the stage runner does not introspect it directly.
97
+ return false;
98
+ }
99
+
100
+ async run(ctx: HarnessStageRunnerContext): Promise<HarnessStageRunResult> {
101
+ const errors = preflightImplement({
102
+ cwd: ctx.cwd,
103
+ planPath: this.input.planPath,
104
+ allowDirtyTree: ctx.gateMode !== "manual",
105
+ });
106
+ if (errors.length > 0) {
107
+ return {
108
+ status: "blocked",
109
+ stage: this.stage,
110
+ artifactPaths: [],
111
+ blocker: { code: "implement-preflight-failed", message: errors.join("; ") },
112
+ };
113
+ }
114
+ let raw: string;
115
+ try {
116
+ raw = fs.readFileSync(this.input.planPath, "utf8");
117
+ } catch (error) {
118
+ return {
119
+ status: "failed",
120
+ stage: this.stage,
121
+ artifactPaths: [],
122
+ error: `unable to read plan: ${error instanceof Error ? error.message : String(error)}`,
123
+ };
124
+ }
125
+ let plan: Plan;
126
+ try {
127
+ plan = parsePlan(raw, this.input.planPath);
128
+ } catch (error) {
129
+ return {
130
+ status: "failed",
131
+ stage: this.stage,
132
+ artifactPaths: [],
133
+ error: `unable to parse plan: ${error instanceof Error ? error.message : String(error)}`,
134
+ };
135
+ }
136
+
137
+ const decision = decideImplementRouting({
138
+ plan,
139
+ threshold: this.input.threshold ?? DEFAULT_IN_SESSION_THRESHOLD,
140
+ });
141
+
142
+ appendImplementLog(ctx.paths, ctx.cwd, ctx.sessionId, {
143
+ recordedAt: nowIso(ctx),
144
+ kind: "routing-decision",
145
+ routing: decision.routing,
146
+ taskCount: decision.taskCount,
147
+ reason: decision.reason,
148
+ planPath: this.input.planPath,
149
+ });
150
+
151
+ return {
152
+ status: "awaiting-user",
153
+ stage: this.stage,
154
+ artifactPaths: ["implement-log.jsonl"],
155
+ details: {
156
+ routing: decision.routing,
157
+ taskCount: decision.taskCount,
158
+ reason: decision.reason,
159
+ },
160
+ };
161
+ }
162
+ }