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
@@ -0,0 +1,1010 @@
1
+ /**
2
+ * `/supi:harness` command dispatcher.
3
+ *
4
+ * Sub-commands:
5
+ * - bare entry (no args) — detect installation; start guided setup or prompt harden/rebuild/cancel.
6
+ * - discover — run/advance the discover stage.
7
+ * - research — run/advance the research stage.
8
+ * - design — run/advance the design stage (requires Discover + Research).
9
+ * - plan-draft — render and persist the plan from the in-flight design spec.
10
+ * - implement — route plan to in-session steer or batch.
11
+ * - validate — run validate sub-checks.
12
+ * - resume — pick up in-flight session.
13
+ * - status — print stage + score badge.
14
+ * - gc — drain queue + drift report.
15
+ * - next — pop next unresolved entry.
16
+ * - resolve <id> — mark entry resolved.
17
+ * - backlog — list every open entry.
18
+ * - score — recompute and print score.
19
+ */
20
+
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+
24
+ import type { Platform, PlatformPaths } from "../platform/types.js";
25
+ import { notifyError, notifyInfo } from "../notifications/renderer.js";
26
+ import { modelRegistry } from "../config/model-registry-instance.js";
27
+ import { loadModelConfig } from "../config/model-config.js";
28
+ import { getProjectStatePath } from "../workspace/state-paths.js";
29
+ import { loadMarker, describeMarker, resolveBareEntry } from "./bare-entry.js";
30
+ import {
31
+ backlog as readBacklog,
32
+ next as nextQueueEntry,
33
+ resolve as resolveQueueEntry,
34
+ } from "./anti_slop/queue.js";
35
+ import {
36
+ listHarnessSessions,
37
+ loadHarnessDesignSpecJson,
38
+ loadHarnessDiscover,
39
+ loadHarnessSession,
40
+ loadHarnessValidateReport,
41
+ readSlopQueue,
42
+ saveHarnessSession,
43
+ } from "./storage.js";
44
+ import { getHarnessSessionDir } from "./project-paths.js";
45
+ import { computeScore } from "./anti_slop/score.js";
46
+ import {
47
+ type BuildRunnerInput,
48
+ type HarnessPipelineProgressEvent,
49
+ type PipelineRunOutcome,
50
+ HARNESS_STAGE_ORDER,
51
+ runHarnessPipelineUntilGate,
52
+ } from "./pipeline.js";
53
+ import { defaultDesignSpecFromDiscover } from "./stages/design.js";
54
+ import { newHarnessSessionId } from "./stage-runner.js";
55
+ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
56
+ import { getWorkingTreeStatus } from "../git/status.js";
57
+ import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
58
+ import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
59
+
60
+ modelRegistry.register({
61
+ id: "harness",
62
+ category: "command",
63
+ label: "Harness",
64
+ harnessRoleHint: "plan",
65
+ });
66
+
67
+ export interface HarnessCommandContext {
68
+ cwd: string;
69
+ hasUI?: boolean;
70
+ ui: {
71
+ notify(message: string, type?: "info" | "warning" | "error"): void;
72
+ select?: (title: string, options: unknown[]) => Promise<string | null>;
73
+ input?: (label: string) => Promise<string | null>;
74
+ };
75
+ }
76
+
77
+ export const HARNESS_SUBCOMMANDS = [
78
+ { name: "discover", description: "Run/advance the discover stage" },
79
+ { name: "research", description: "Run/advance the research stage" },
80
+ { name: "design", description: "Run/advance the design stage (requires Discover + Research)" },
81
+ { name: "plan-draft", description: "Render and persist the plan from the in-flight design spec" },
82
+ { name: "implement", description: "Route plan to in-session steer or batch" },
83
+ { name: "validate", description: "Run validate sub-checks" },
84
+ { name: "resume", description: "Pick up an in-flight session" },
85
+ { name: "status", description: "Print stage + score badge" },
86
+ { name: "gc", description: "Drain queue + drift report" },
87
+ { name: "next", description: "Pop the next unresolved queue entry" },
88
+ { name: "resolve", description: "Mark a queue entry resolved" },
89
+ { name: "backlog", description: "List every open queue entry" },
90
+ { name: "score", description: "Recompute and display the score" },
91
+ ] as const;
92
+
93
+ type HarnessSubcommand = (typeof HARNESS_SUBCOMMANDS)[number]["name"];
94
+
95
+ const SUBCOMMAND_NAMES: Set<string> = new Set(HARNESS_SUBCOMMANDS.map((s) => s.name));
96
+
97
+ const HARNESS_STAGE_LABELS: Readonly<Record<HarnessStage, string>> = {
98
+ discover: "Discover codebase",
99
+ research: "Research topics",
100
+ design: "Design harness",
101
+ plan: "Draft plan",
102
+ implement: "Apply artifacts",
103
+ validate: "Validate results",
104
+ };
105
+
106
+
107
+ export function parseHarnessArgs(raw: string | undefined): HarnessCommandRequest {
108
+ if (!raw || raw.trim().length === 0) return { subcommand: null, args: [] };
109
+ const tokens = raw.trim().split(/\s+/);
110
+ const head = tokens[0];
111
+ if (SUBCOMMAND_NAMES.has(head)) {
112
+ return { subcommand: head, args: tokens.slice(1) };
113
+ }
114
+ return { subcommand: null, args: tokens };
115
+ }
116
+
117
+ export interface HarnessCommandRequest {
118
+ subcommand: string | null;
119
+ args: string[];
120
+ }
121
+
122
+ // ── Progress (status-bar + one final notification) ───────────────
123
+
124
+ function createHarnessProgress(ctx: HarnessCommandContext) {
125
+ const SO = ["discover", "research", "design", "plan", "implement", "validate"] as HarnessStage[];
126
+ let done = 0;
127
+ let cur: HarnessStage | null = null;
128
+ const completed: string[] = [];
129
+
130
+ function refresh() {
131
+ const label = cur ? HARNESS_STAGE_LABELS[cur] : "Complete";
132
+ const spinner = cur ? "\u25cc" : "\u2713";
133
+ (ctx.ui as any).setStatus?.("supi-harness", ` ${spinner} harness: ${label} (${done}/${SO.length})`);
134
+ }
135
+ refresh();
136
+
137
+ return {
138
+ onProgress(event: HarnessPipelineProgressEvent) {
139
+ switch (event.type) {
140
+ case "stage-started":
141
+ cur = event.stage;
142
+ break;
143
+ case "stage-completed": {
144
+ done += 1; cur = null;
145
+ const mark = "\u2713";
146
+ completed.push(`${mark} ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "done"}`);
147
+ break;
148
+ }
149
+ case "stage-skipped":
150
+ done += 1; cur = null;
151
+ completed.push(`\u2013 ${HARNESS_STAGE_LABELS[event.stage]}: skipped`);
152
+ break;
153
+ case "awaiting-user":
154
+ done += 1; cur = null;
155
+ completed.push(`\u25cb ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "awaiting review"}`);
156
+ break;
157
+ case "stage-failed": case "stage-blocked":
158
+ cur = null;
159
+ completed.push(`\u2717 ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "failed"}`);
160
+ break;
161
+ }
162
+ refresh();
163
+ },
164
+ summary(): string { return completed.join("\n"); },
165
+ dispose() { (ctx.ui as any).setStatus?.("supi-harness", undefined); },
166
+ };
167
+ }
168
+
169
+ function summarizeTrace(outcome: PipelineRunOutcome): string {
170
+ const lines = outcome.trace.map((t) => {
171
+ const label = HARNESS_STAGE_LABELS[t.stage];
172
+ const mark = t.status === "completed" ? "\u2713" : t.status === "skipped" ? "\u2013" : t.status === "awaiting-user" ? "\u25cb" : "\u2717";
173
+ return ` ${mark} ${label}: ${t.status}`;
174
+ });
175
+ const extra = outcome.message ? `\n \u2192 ${outcome.message}` : "";
176
+ return lines.join("\n") + extra;
177
+ }
178
+ // ── Top-level dispatcher ─────────────────────────────────────────
179
+
180
+ export async function handleHarness(
181
+ platform: Platform,
182
+ ctx: HarnessCommandContext,
183
+ rawArgs?: string,
184
+ ): Promise<void> {
185
+ const request = parseHarnessArgs(rawArgs);
186
+ try {
187
+ switch (request.subcommand) {
188
+ case null: await handleBareEntry(platform, ctx); return;
189
+ case "status": await handleStatus(platform, ctx); return;
190
+ case "score": await handleScore(platform, ctx); return;
191
+ case "next": await handleNext(platform, ctx); return;
192
+ case "resolve": await handleResolve(platform, ctx, request.args[0]); return;
193
+ case "backlog": await handleBacklog(platform, ctx); return;
194
+ case "gc": await handleGc(platform, ctx); return;
195
+ case "discover": await handleStageCommand(platform, ctx, "discover", request.args); return;
196
+ case "research": await handleStageCommand(platform, ctx, "research", request.args); return;
197
+ case "design": await handleStageCommand(platform, ctx, "design", request.args); return;
198
+ case "plan-draft": await handleStageCommand(platform, ctx, "plan", request.args); return;
199
+ case "implement": await handleStageCommand(platform, ctx, "implement", request.args); return;
200
+ case "validate": await handleStageCommand(platform, ctx, "validate", request.args); return;
201
+ case "resume": await handleResume(platform, ctx, request.args); return;
202
+ default:
203
+ notifyError(ctx, "Unknown harness subcommand", `\`${request.subcommand}\` is not recognized.`);
204
+ return;
205
+ }
206
+ } catch (error) {
207
+ notifyError(ctx, "Harness command failed", error instanceof Error ? error.message : String(error));
208
+ }
209
+ }
210
+
211
+ // ── Bare entry ────────────────────────────────────────────────────
212
+
213
+ async function runPipelineWithProgress(
214
+ platform: Platform,
215
+ ctx: HarnessCommandContext,
216
+ sessionId: string,
217
+ gates: HarnessGateMode,
218
+ stageInputs: BuildRunnerInput,
219
+ startStage?: HarnessStage,
220
+ ): Promise<PipelineRunOutcome> {
221
+ const harnessProgress = createHarnessProgress(ctx);
222
+ const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
223
+ const outcome = await pipelineDriver({
224
+ platform, paths: platform.paths, cwd: ctx.cwd, sessionId,
225
+ modelConfig, gates, stageInputs, startStage,
226
+ onProgress: harnessProgress.onProgress,
227
+ });
228
+ // Single consolidated notification.
229
+ const body = harnessProgress.summary() || "(no stages executed)";
230
+ if (outcome.status === "failed" || outcome.status === "blocked") {
231
+ notifyError(ctx, "/supi:harness", body);
232
+ } else {
233
+ notifyInfo(ctx, "/supi:harness", body);
234
+ }
235
+ harnessProgress.dispose();
236
+ return outcome;
237
+ }
238
+
239
+
240
+ // ── Rebuild gate loop ──────────────────────────────────────────────
241
+
242
+ function nextStageAfterGate(stage: HarnessStage): HarnessStage | undefined {
243
+ const idx = HARNESS_STAGE_ORDER.indexOf(stage);
244
+ return idx >= 0 && idx < HARNESS_STAGE_ORDER.length - 1
245
+ ? HARNESS_STAGE_ORDER[idx + 1]
246
+ : undefined;
247
+ }
248
+
249
+ async function presentGateForStage(
250
+ stage: HarnessStage,
251
+ platform: Platform,
252
+ ctx: HarnessCommandContext,
253
+ sessionId: string,
254
+ ): Promise<"continue" | "stop"> {
255
+ if (!ctx.ui.select) return "continue";
256
+
257
+ switch (stage) {
258
+ case "discover": {
259
+ const d = loadHarnessDiscover(platform.paths, ctx.cwd, sessionId);
260
+ const summary = d.ok
261
+ ? `Languages: ${d.value.languages.join(", ")}\nRecommended backend: ${d.value.recommendedBackend}`
262
+ : "(unable to load discover artifact)";
263
+ const choice = await ctx.ui.select(
264
+ `Discover findings\n\n${summary}\n\nContinue to research + design?`,
265
+ ["Continue", "Stop"],
266
+ );
267
+ return choice === "Continue" ? "continue" : "stop";
268
+ }
269
+ case "design": {
270
+ const spec = loadHarnessDesignSpecJson(platform.paths, ctx.cwd, sessionId);
271
+ const backend = spec.ok ? spec.value.antiSlop.backend : "?";
272
+ const layers = spec.ok ? spec.value.layerRules.length : 0;
273
+ const principles = spec.ok ? spec.value.goldenPrinciples.length : 0;
274
+ const summary = `Backend: ${backend}\nLayer rules: ${layers}\nGolden principles: ${principles}\n\nThe design spec has been auto-derived from your codebase. You can edit it at:\n <session>/design-spec.json`;
275
+ const choice = await ctx.ui.select(
276
+ `Design spec ready\n\n${summary}\n\nContinue to plan?`,
277
+ ["Continue", "Stop — I'll customize the design"],
278
+ );
279
+ return choice === "Continue" ? "continue" : "stop";
280
+ }
281
+ case "plan": {
282
+ const plansDir = getProjectStatePath(platform.paths, ctx.cwd, "plans");
283
+ const planPath = path.join(plansDir, `harness-${sessionId}.md`);
284
+ let taskCount = 0;
285
+ if (fs.existsSync(planPath)) {
286
+ const content = fs.readFileSync(planPath, "utf8");
287
+ taskCount = (content.match(/^### Task \d+:/gm) || []).length;
288
+ }
289
+ const summary = `${taskCount} tasks drafted.\n\nPlan: ${planPath}`;
290
+ const choice = await ctx.ui.select(
291
+ `Plan draft\n\n${summary}\n\nApprove and apply?`,
292
+ ["Approve and continue", "Stop — I need to review the plan"],
293
+ );
294
+ return choice === "Approve and continue" ? "continue" : "stop";
295
+ }
296
+ case "validate": {
297
+ const report = loadHarnessValidateReport(platform.paths, ctx.cwd, sessionId);
298
+ const passed = report.ok ? report.value.passed : false;
299
+ const score = report.ok ? report.value.score.lenient : "?";
300
+ const findingCount = report.ok
301
+ ? report.value.checks.reduce((sum, c) => sum + c.findings.length, 0)
302
+ : 0;
303
+ const summary = `Passed: ${passed}\nScore (lenient): ${score}\nFindings: ${findingCount}`;
304
+ const choice = await ctx.ui.select(
305
+ `Validation ${passed ? "passed" : "found issues"}\n\n${summary}\n\nAccept results?`,
306
+ ["Accept", "Reject — I'll fix issues first"],
307
+ );
308
+ return choice === "Accept" ? "continue" : "stop";
309
+ }
310
+ default:
311
+ return "continue";
312
+ }
313
+ }
314
+
315
+ interface DesignAnalysisOutput {
316
+ layerArchitecture: "single" | "two" | "three" | "custom";
317
+ customLayerNames?: string[];
318
+ goldenPrinciples: string[];
319
+ tasteInvariants: string[];
320
+ }
321
+
322
+ function buildDesignAnalysisPrompt(discover: { languages: string[]; frameworks: string[]; packageManagers: string[]; buildTools: string[]; testTools: string[]; lintTools: string[]; monorepoShape: string; recommendedBackend: string }): string {
323
+ const facts = [
324
+ `Languages: ${discover.languages.join(", ") || "(none detected)"}`,
325
+ `Frameworks: ${discover.frameworks.join(", ") || "(none detected)"}`,
326
+ `Package manager: ${discover.packageManagers.join(", ") || "(none detected)"}`,
327
+ `Build tools: ${discover.buildTools.join(", ") || "(none detected)"}`,
328
+ `Test tools: ${discover.testTools.join(", ") || "(none detected)"}`,
329
+ `Lint tools: ${discover.lintTools.join(", ") || "(none detected)"}`,
330
+ `Repo shape: ${discover.monorepoShape}`,
331
+ ].join("\n");
332
+
333
+ const langHints = discover.languages.includes("typescript") || discover.languages.includes("tsx")
334
+ ? "\nFor TypeScript codebases, good principles include:\n" +
335
+ '- "Every exported function has an explicit return type"\n' +
336
+ '- "No `as any` casts in production code"\n' +
337
+ '- "Imports are sorted: built-ins \u2192 external \u2192 internal"\n' +
338
+ '- "Error boundaries at every async boundary"'
339
+ : "";
340
+
341
+ return `You are configuring a coding harness for a codebase. Suggest a complete design configuration. You MUST provide at least 3 golden principles and at least 1 taste invariant — even for simple projects there are always mechanical rules worth enforcing.
342
+
343
+ Codebase facts:
344
+ ${facts}${langHints}
345
+
346
+ Respond with ONLY a JSON object (no markdown fences, no explanation):
347
+ {
348
+ "layerArchitecture": "single" | "two" | "three" | "custom",
349
+ "customLayerNames": ["name1", "name2"],
350
+ "goldenPrinciples": ["Rule 1", "Rule 2", "Rule 3"],
351
+ "tasteInvariants": ["Rule 1"]
352
+ }
353
+
354
+ RULES (follow strictly):
355
+ - goldenPrinciples: 3-10 mechanical rules an AI coding agent must follow. Every project has at least 3: type safety, error handling, and code organization. Be specific to the detected languages and tools.
356
+ - tasteInvariants: 1-8 style/format rules that tooling can check. Every project has at least 1: consistent naming or import ordering.
357
+ - layerArchitecture: use "single" for small projects, "two" for lib+app, "three" for domain/app/infra patterns. Prefer "two" when a package.json suggests a build step.
358
+ - NEVER return empty arrays. If unsure, use the examples as defaults.
359
+
360
+ Output ONLY the JSON.`;
361
+ }
362
+
363
+ async function spawnDesignAnalysisSubagent(
364
+ platform: Platform,
365
+ cwd: string,
366
+ sessionId: string,
367
+ ): Promise<DesignAnalysisOutput | null> {
368
+ const discover = loadHarnessDiscover(platform.paths, cwd, sessionId);
369
+ if (!discover.ok) return null;
370
+
371
+ const prompt = buildDesignAnalysisPrompt(discover.value);
372
+
373
+ let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
374
+ try {
375
+ session = await platform.createAgentSession({
376
+ cwd,
377
+ agentId: `harness-design-analyze-${sessionId}`,
378
+ agentDisplayName: "harness-design-analyze",
379
+ });
380
+ await session.prompt(prompt, { expandPromptTemplates: false });
381
+
382
+ // Extract the last assistant message.
383
+ const messages = session.state.messages as Array<{ role?: string; content?: unknown }>;
384
+ let lastText = "";
385
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
386
+ const msg = messages[i];
387
+ if (msg?.role === "assistant" && msg.content) {
388
+ const content = typeof msg.content === "string"
389
+ ? msg.content
390
+ : Array.isArray(msg.content)
391
+ ? msg.content.map((p: any) => (typeof p === "string" ? p : p?.text ?? "")).join("")
392
+ : "";
393
+ lastText = content.trim();
394
+ if (lastText) break;
395
+ }
396
+ }
397
+
398
+ if (!lastText) return null;
399
+
400
+ // Strip markdown fences and parse JSON.
401
+ const jsonText = lastText.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
402
+ const parsed = JSON.parse(jsonText) as DesignAnalysisOutput;
403
+
404
+ // Validate basic shape.
405
+ if (!parsed.layerArchitecture || !Array.isArray(parsed.goldenPrinciples) || !Array.isArray(parsed.tasteInvariants)) {
406
+ return null;
407
+ }
408
+
409
+ return parsed;
410
+ } catch {
411
+ return null;
412
+ } finally {
413
+ if (session) {
414
+ try { await session.dispose(); } catch { /* best-effort */ }
415
+ }
416
+ }
417
+ }
418
+
419
+
420
+ async function runDesignQa(
421
+ platform: Platform,
422
+ ctx: HarnessCommandContext,
423
+ sessionId: string,
424
+ ): Promise<HarnessDesignSpec> {
425
+ const discover = loadHarnessDiscover(platform.paths, ctx.cwd, sessionId);
426
+ const base: HarnessDesignSpec = discover.ok
427
+ ? defaultDesignSpecFromDiscover(discover.value, sessionId, new Date().toISOString())
428
+ : {
429
+ sessionId,
430
+ recordedAt: new Date().toISOString(),
431
+ layerRules: [] as HarnessDesignSpec["layerRules"],
432
+ tasteInvariants: [] as string[],
433
+ tooling: { lint: null, structuralTest: null, eval: null } as HarnessDesignSpec["tooling"],
434
+ goldenPrinciples: [] as string[],
435
+ docsTree: ["docs/architecture.md", "docs/golden-principles.md"],
436
+ validationGates: [],
437
+ ci: {
438
+ provider: "github-actions",
439
+ trigger: { mode: "branches", branches: ["dev", "main"] },
440
+ localCommand: "bun run harness:quality",
441
+ workflowPath: ".github/workflows/harness-quality.yml",
442
+ },
443
+ supipowersWiring: { addReviewAgent: true, wireChecksGate: false },
444
+ antiSlop: {
445
+ backend: "fallow" as HarnessDesignSpec["antiSlop"]["backend"],
446
+ hooks: DEFAULT_HARNESS_CONFIG.anti_slop,
447
+ skillTargets: [],
448
+ },
449
+ };
450
+
451
+ if (!ctx.ui.select) return base;
452
+
453
+ // ── Spawn subagent to pre-fill design suggestions ──
454
+ notifyInfo(ctx, "Analyzing codebase", "Spawning design analysis subagent…");
455
+ const analysis = await spawnDesignAnalysisSubagent(platform, ctx.cwd, sessionId);
456
+
457
+ if (analysis && ctx.ui.select) {
458
+ // Ensure the subagent didn't return empty arrays.
459
+ sanitizeAnalysisDefaults(analysis, discover.ok ? discover.value.languages : []);
460
+
461
+ // Build a summary of the subagent's suggestions.
462
+ const layerLabel =
463
+ analysis.layerArchitecture === "single" ? "Single-bucket — no layer enforcement" :
464
+ analysis.layerArchitecture === "two" ? "Two-layer (lib + app)" :
465
+ analysis.layerArchitecture === "three" ? "Three-layer (domain / application / infrastructure)" :
466
+ analysis.customLayerNames?.length ? `Custom: ${analysis.customLayerNames.join(", ")}` :
467
+ "Custom";
468
+
469
+ const summary = [
470
+ `Layer architecture: ${layerLabel}`,
471
+ `Golden principles (${analysis.goldenPrinciples.length}):`,
472
+ ...analysis.goldenPrinciples.map((p) => ` • ${p}`),
473
+ `Taste invariants (${analysis.tasteInvariants.length}):`,
474
+ ...analysis.tasteInvariants.map((p) => ` • ${p}`),
475
+ ].join("\n");
476
+
477
+ const choice = await ctx.ui.select(
478
+ `Design suggestions\n\n${summary}\n\nUse these suggestions?`,
479
+ ["Accept all suggestions", "Edit each section manually", "Skip — use bare defaults"],
480
+ );
481
+
482
+ if (choice === "Accept all suggestions") {
483
+ applyDesignAnalysis(base, analysis);
484
+ await askCiAndTooling(ctx, base);
485
+ return base;
486
+ }
487
+
488
+ if (choice === "Skip — use bare defaults") {
489
+ await askCiAndTooling(ctx, base);
490
+ return base;
491
+ }
492
+ }
493
+
494
+ // ── Manual Q&A (fallback when subagent fails or user chooses to edit) ──
495
+ const layerChoice = await ctx.ui.select(
496
+ "Design: how is your codebase layered?",
497
+ [
498
+ "Single-bucket — no layer enforcement",
499
+ "Two-layer (e.g., shared lib + app code)",
500
+ "Three-layer (domain / application / infrastructure)",
501
+ "Custom — I'll describe each layer",
502
+ ],
503
+ );
504
+
505
+ if (layerChoice === "Custom — I'll describe each layer" && ctx.ui.input) {
506
+ const raw = await ctx.ui.input(
507
+ "Enter layer names, comma-separated (e.g., 'ui, domain, data'):",
508
+ );
509
+ if (raw) {
510
+ const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
511
+ base.layerRules = names.map((name) => ({
512
+ layer: name,
513
+ globs: [`src/${name}/**`],
514
+ allowedImports: [] as string[],
515
+ forbiddenImports: [] as string[],
516
+ }));
517
+ }
518
+ } else if (layerChoice?.startsWith("Two-layer")) {
519
+ base.layerRules = [
520
+ { layer: "lib", globs: ["src/lib/**"], allowedImports: [], forbiddenImports: [] },
521
+ { layer: "app", globs: ["src/app/**"], allowedImports: ["lib"], forbiddenImports: [] },
522
+ ];
523
+ } else if (layerChoice?.startsWith("Three-layer")) {
524
+ base.layerRules = [
525
+ { layer: "domain", globs: ["src/domain/**"], allowedImports: [], forbiddenImports: [] },
526
+ { layer: "application", globs: ["src/application/**"], allowedImports: ["domain"], forbiddenImports: [] },
527
+ { layer: "infrastructure", globs: ["src/infrastructure/**"], allowedImports: ["domain", "application"], forbiddenImports: [] },
528
+ ];
529
+ }
530
+
531
+ // ── Q2: Golden principles ──
532
+ if (ctx.ui.input) {
533
+ const raw = await ctx.ui.input(
534
+ "Golden principles — one per line, empty to skip:\n" +
535
+ "Examples: 'Never throw raw errors', 'Every module exports a contract', 'Tests before implementation'",
536
+ );
537
+ if (raw) {
538
+ base.goldenPrinciples = raw.split("\n").map((s) => s.trim()).filter(Boolean);
539
+ }
540
+ }
541
+
542
+ // ── Q3: Taste invariants ──
543
+ if (ctx.ui.input) {
544
+ const raw = await ctx.ui.input(
545
+ "Taste invariants — one per line, empty to skip:\n" +
546
+ "Examples: 'No files over 200 lines', 'Functions max 4 params', 'No console.log in production'",
547
+ );
548
+ if (raw) {
549
+ base.tasteInvariants = raw.split("\n").map((s) => s.trim()).filter(Boolean);
550
+ }
551
+ }
552
+
553
+ await askCiAndTooling(ctx, base);
554
+ return base;
555
+ }
556
+
557
+ function applyDesignAnalysis(spec: HarnessDesignSpec, analysis: DesignAnalysisOutput): void {
558
+ switch (analysis.layerArchitecture) {
559
+ case "two":
560
+ spec.layerRules = [
561
+ { layer: "lib", globs: ["src/lib/**"], allowedImports: [], forbiddenImports: [] },
562
+ { layer: "app", globs: ["src/app/**"], allowedImports: ["lib"], forbiddenImports: [] },
563
+ ];
564
+ break;
565
+ case "three":
566
+ spec.layerRules = [
567
+ { layer: "domain", globs: ["src/domain/**"], allowedImports: [], forbiddenImports: [] },
568
+ { layer: "application", globs: ["src/application/**"], allowedImports: ["domain"], forbiddenImports: [] },
569
+ { layer: "infrastructure", globs: ["src/infrastructure/**"], allowedImports: ["domain", "application"], forbiddenImports: [] },
570
+ ];
571
+ break;
572
+ case "custom":
573
+ if (analysis.customLayerNames?.length) {
574
+ spec.layerRules = analysis.customLayerNames.map((name) => ({
575
+ layer: name,
576
+ globs: [`src/${name}/**`],
577
+ allowedImports: [],
578
+ forbiddenImports: [],
579
+ }));
580
+ }
581
+ break;
582
+ // "single" — leave layerRules empty
583
+ }
584
+
585
+ spec.goldenPrinciples = analysis.goldenPrinciples.slice(0, 10);
586
+ spec.tasteInvariants = analysis.tasteInvariants.slice(0, 8);
587
+ }
588
+
589
+ function sanitizeAnalysisDefaults(analysis: DesignAnalysisOutput, languages: string[]): void {
590
+ const isTS = languages.some((l) => l === "typescript" || l === "tsx");
591
+
592
+ if (analysis.goldenPrinciples.length === 0) {
593
+ analysis.goldenPrinciples = isTS
594
+ ? [
595
+ "Every exported function has an explicit return type",
596
+ "No `as any` casts in production code",
597
+ "Error boundaries at every async boundary — never let a promise reject unhandled",
598
+ ]
599
+ : [
600
+ "Every function handles its error path explicitly",
601
+ "No dead code — remove unused imports, variables, and functions",
602
+ "Tests exist for every public API before merging",
603
+ ];
604
+ }
605
+
606
+ if (analysis.tasteInvariants.length === 0) {
607
+ analysis.tasteInvariants = isTS
608
+ ? [
609
+ "Imports sorted: built-ins, then external packages, then internal modules",
610
+ "Consistent naming: camelCase for variables, PascalCase for types and classes",
611
+ ]
612
+ : [
613
+ "Imports are grouped and sorted alphabetically",
614
+ "Consistent naming conventions across the codebase",
615
+ ];
616
+ }
617
+ }
618
+
619
+ function localCommandOptions(base: HarnessDesignSpec): string[] {
620
+ const command = base.ci.localCommand;
621
+ const manager = command.split(/\s+/)[0] || "bun";
622
+ const runPrefix = manager === "npm" ? "npm run" : manager;
623
+ const ciCommand = manager === "npm" ? "npm run ci" : `${runPrefix} ci`;
624
+ const checkCommand = manager === "npm" ? "npm run check" : `${runPrefix} check`;
625
+ return Array.from(new Set([
626
+ `${command} (recommended — dedicated harness quality command)`,
627
+ `${ciCommand} (reuse existing CI script if present)`,
628
+ `${checkCommand} (reuse existing check script if present)`,
629
+ "Custom command",
630
+ ]));
631
+ }
632
+
633
+ async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
634
+ if (!ctx.ui.select) return;
635
+
636
+ const triggerChoice = await ctx.ui.select(
637
+ "CI trigger: when should harness quality checks run?",
638
+ [
639
+ "Only PRs to dev and main (recommended)",
640
+ "All pull requests",
641
+ "Only PRs to main",
642
+ "Custom target branches",
643
+ ],
644
+ );
645
+ if (triggerChoice === "All pull requests") {
646
+ base.ci.trigger = { mode: "all-prs" };
647
+ } else if (triggerChoice === "Only PRs to main") {
648
+ base.ci.trigger = { mode: "branches", branches: ["main"] };
649
+ } else if (triggerChoice === "Custom target branches" && ctx.ui.input) {
650
+ const raw = await ctx.ui.input("Target branches for CI, comma-separated:");
651
+ const branches = raw?.split(",").map((branch) => branch.trim()).filter(Boolean) ?? [];
652
+ if (branches.length > 0) base.ci.trigger = { mode: "branches", branches };
653
+ } else {
654
+ base.ci.trigger = { mode: "branches", branches: ["dev", "main"] };
655
+ }
656
+
657
+ const toolChoice = await ctx.ui.select(
658
+ "Local validation command CI should call",
659
+ localCommandOptions(base),
660
+ );
661
+ if (toolChoice === "Custom command" && ctx.ui.input) {
662
+ const custom = await ctx.ui.input("Local command CI should run:");
663
+ if (custom?.trim()) base.ci.localCommand = custom.trim();
664
+ } else if (toolChoice) {
665
+ base.ci.localCommand = toolChoice.replace(/\s+\(.+\)$/, "");
666
+ }
667
+ }
668
+
669
+
670
+
671
+ async function runRebuildWithGates(
672
+ platform: Platform,
673
+ ctx: HarnessCommandContext,
674
+ sessionId: string,
675
+ ): Promise<void> {
676
+ let startStage: HarnessStage | undefined = undefined;
677
+ let stageInputs: BuildRunnerInput = {};
678
+
679
+ while (true) {
680
+ const outcome = await runPipelineWithProgress(
681
+ platform, ctx, sessionId, "default", stageInputs, startStage,
682
+ );
683
+
684
+ if (outcome.promoted) {
685
+ notifyInfo(ctx, "Harness rebuild complete", "All stages passed.");
686
+ return;
687
+ }
688
+
689
+ if (outcome.status === "failed" || outcome.status === "blocked") {
690
+ return; // runPipelineWithProgress already notified the error
691
+ }
692
+
693
+ // ── Discover gate: present findings, then run design Q&A ──
694
+ if (outcome.stage === "discover") {
695
+ const choice = await presentGateForStage(outcome.stage, platform, ctx, sessionId);
696
+ if (choice === "stop") {
697
+ notifyInfo(ctx, "Harness rebuild paused", "Stopped at discover. Run /supi:harness research to continue.");
698
+ return;
699
+ }
700
+ // Run design Q&A and feed the custom spec to the design stage.
701
+ const customSpec = await runDesignQa(platform, ctx, sessionId);
702
+ stageInputs = { designInput: { spec: customSpec } };
703
+ // Advance to research (runs automatically, then design uses the custom spec).
704
+ startStage = "research";
705
+ continue;
706
+ }
707
+
708
+ // ── Design gate: show the resulting spec, clear design input ──
709
+ if (outcome.stage === "design") {
710
+ const choice = await presentGateForStage(outcome.stage, platform, ctx, sessionId);
711
+ if (choice === "stop") {
712
+ notifyInfo(ctx, "Harness rebuild paused", "Stopped at design. Run /supi:harness plan-draft to continue.");
713
+ return;
714
+ }
715
+ stageInputs = {}; // design spec already persisted
716
+ startStage = nextStageAfterGate(outcome.stage);
717
+ if (!startStage) { notifyInfo(ctx, "Harness rebuild complete", ""); return; }
718
+ continue;
719
+ }
720
+
721
+ // ── Other gates (plan, validate): standard gate UI ──
722
+ const choice = await presentGateForStage(outcome.stage, platform, ctx, sessionId);
723
+ if (choice === "stop") {
724
+ const next = nextStageAfterGate(outcome.stage);
725
+ const hint = next ? `Run /supi:harness ${next} to continue.` : "";
726
+ notifyInfo(ctx, "Harness rebuild paused", `Stopped at ${outcome.stage}. ${hint}`);
727
+ return;
728
+ }
729
+
730
+ startStage = nextStageAfterGate(outcome.stage);
731
+ if (!startStage) {
732
+ notifyInfo(ctx, "Harness rebuild complete", "All stages passed.");
733
+ return;
734
+ }
735
+ }
736
+ }
737
+
738
+ async function handleBareEntry(platform: Platform, ctx: HarnessCommandContext): Promise<void> {
739
+ const marker = loadMarker(platform.paths, ctx.cwd);
740
+
741
+ // ── Pre-flight: verify git tree is clean ──
742
+ const status = await getWorkingTreeStatus(
743
+ (cmd, args, opts) => platform.exec(cmd, args, opts),
744
+ ctx.cwd,
745
+ );
746
+ if (status.dirty) {
747
+ const fileList = status.files.slice(0, 10).join("\n ");
748
+ const more = status.files.length > 10 ? `\n ...and ${status.files.length - 10} more` : "";
749
+ notifyError(ctx, "/supi:harness blocked",
750
+ `Working tree is dirty (${status.files.length} file(s)). Commit or stash changes first.\n\nDirty files:\n ${fileList}${more}`,
751
+ );
752
+ return;
753
+ }
754
+
755
+ // ── Fresh install (guided only) ──
756
+ if (!marker) {
757
+ if (!ctx.ui.select) {
758
+ notifyInfo(ctx, "No harness installed", "Run `/supi:harness discover` to begin guided setup.");
759
+ return;
760
+ }
761
+ const sessionId = newHarnessSessionId();
762
+ const p = saveHarnessSession(platform.paths, ctx.cwd, freshSession(sessionId, ctx.cwd, "discover"));
763
+ if (!p.ok) { notifyError(ctx, "/supi:harness", p.error.message); return; }
764
+ notifyInfo(ctx, "Harness guided setup", `Starting guided setup (session ${sessionId})...`);
765
+ await runRebuildWithGates(platform, ctx, sessionId);
766
+ return;
767
+ }
768
+
769
+ if (!ctx.ui.select) {
770
+ notifyInfo(ctx, "Harness already installed", describeMarker(marker));
771
+ return;
772
+ }
773
+
774
+ const decision = await resolveBareEntry({
775
+ paths: platform.paths, cwd: ctx.cwd,
776
+ prompt: async (opts) => {
777
+ const labels = opts.choices.map((c) => c.label);
778
+ const s = await ctx.ui.select!(opts.title, labels as unknown as string[]);
779
+ if (s === null || s === undefined) return null;
780
+ const idx = labels.indexOf(s);
781
+ return idx >= 0 ? opts.choices[idx].value : null;
782
+ },
783
+ });
784
+
785
+ if (decision.kind !== "rerun") {
786
+ notifyError(ctx, "/supi:harness", "Harness marker disappeared before rerun selection could be applied.");
787
+ return;
788
+ }
789
+
790
+ if (decision.mode === "cancel") {
791
+ notifyInfo(ctx, "Harness rerun cancelled", "No changes made.");
792
+ return;
793
+ }
794
+
795
+ const existingSessions = listHarnessSessions(platform.paths, ctx.cwd);
796
+ const sessionId =
797
+ decision.mode === "harden" && existingSessions.length > 0 ? existingSessions[0] : newHarnessSessionId();
798
+
799
+ if (sessionId !== existingSessions[0]) {
800
+ const p = saveHarnessSession(platform.paths, ctx.cwd, freshSession(sessionId, ctx.cwd, "discover"));
801
+ if (!p.ok) { notifyError(ctx, "/supi:harness", p.error.message); return; }
802
+ }
803
+
804
+ const modeLabel = decision.mode === "harden" ? "Gap-fill" : "Full rebuild";
805
+ notifyInfo(ctx, `Harness ${decision.mode}`, `${modeLabel} (session ${sessionId}) — pipeline running...`);
806
+
807
+ if (decision.mode === "harden") {
808
+ // Harden: gap-fill, no user gates.
809
+ await runPipelineWithProgress(platform, ctx, sessionId, "auto", {});
810
+ } else {
811
+ // Rebuild: full regeneration with user gates at each stage.
812
+ await runRebuildWithGates(platform, ctx, sessionId);
813
+ }
814
+ }
815
+
816
+ // ── Subcommand handlers ──────────────────────────────────────────
817
+
818
+ async function handleStatus(platform: Platform, ctx: HarnessCommandContext): Promise<void> {
819
+ const marker = loadMarker(platform.paths, ctx.cwd);
820
+ notifyInfo(ctx, "Harness status", describeMarker(marker));
821
+ if (!marker) return;
822
+ const q = readSlopQueue(platform.paths, ctx.cwd);
823
+ if (q.ok) {
824
+ const open = q.value.filter((e) => e.state === "open").length;
825
+ const resolved = q.value.filter((e) => e.state === "resolved").length;
826
+ notifyInfo(ctx, "Slop queue", `${open} open, ${resolved} resolved (total ${q.value.length})`);
827
+ }
828
+ }
829
+
830
+ async function handleScore(platform: Platform, ctx: HarnessCommandContext): Promise<void> {
831
+ const q = readSlopQueue(platform.paths, ctx.cwd);
832
+ if (!q.ok) { notifyError(ctx, "Score unavailable", q.error.message); return; }
833
+ const s = computeScore({ computedAt: new Date().toISOString(), entries: q.value });
834
+ notifyInfo(ctx, "Harness score", `lenient ${s.lenient} / strict ${s.strict}`);
835
+ }
836
+
837
+ async function handleNext(platform: Platform, ctx: HarnessCommandContext): Promise<void> {
838
+ const r = nextQueueEntry(platform.paths, ctx.cwd);
839
+ if (!r.ok) { notifyError(ctx, "Queue read failed", r.error.message); return; }
840
+ if (!r.value) { notifyInfo(ctx, "Slop queue empty", "Nothing to triage."); return; }
841
+ const range = r.value.range ? `:${r.value.range.startLine}` : "";
842
+ notifyInfo(ctx, `Next: ${r.value.id}`, `[${r.value.severity}] ${r.value.kind} at ${r.value.file}${range} — ${r.value.message}`);
843
+ }
844
+
845
+ async function handleResolve(platform: Platform, ctx: HarnessCommandContext, id: string | undefined): Promise<void> {
846
+ if (!id) { notifyError(ctx, "Missing id", "Usage: /supi:harness resolve <id>"); return; }
847
+ const r = resolveQueueEntry(platform.paths, ctx.cwd, id);
848
+ if (!r.ok) { notifyError(ctx, "Resolve failed", r.error.message); return; }
849
+ if (!r.value) { notifyError(ctx, "Resolve failed", `id ${id} not found in queue`); return; }
850
+ notifyInfo(ctx, `Resolved ${id}`, `${r.value.kind} at ${r.value.file}`);
851
+ }
852
+
853
+ async function handleBacklog(platform: Platform, ctx: HarnessCommandContext): Promise<void> {
854
+ const r = readBacklog(platform.paths, ctx.cwd);
855
+ if (!r.ok) { notifyError(ctx, "Backlog read failed", r.error.message); return; }
856
+ if (r.value.length === 0) { notifyInfo(ctx, "Backlog empty", "Queue has no open entries."); return; }
857
+ const summary = r.value.slice(0, 10).map((e) =>
858
+ `${e.id} ${e.severity.padEnd(7)} ${e.kind.padEnd(15)} ${e.file}`).join("\n");
859
+ const more = r.value.length > 10 ? `\n…and ${r.value.length - 10} more.` : "";
860
+ notifyInfo(ctx, `Backlog (${r.value.length})`, summary + more);
861
+ }
862
+
863
+ async function handleGc(_p: Platform, ctx: HarnessCommandContext): Promise<void> {
864
+ notifyInfo(ctx, "/supi:harness gc",
865
+ "v1 GC drives the queue via /supi:harness next + resolve. Auto-fix lands after /supi:harness design.");
866
+ }
867
+
868
+ // ── Registration ──────────────────────────────────────────────────
869
+
870
+ export function registerHarnessCommand(platform: Platform): void {
871
+ platform.registerCommand("supi:harness", {
872
+ description: "Install or maintain the harness pipeline.",
873
+ getArgumentCompletions(prefix: string) {
874
+ const lower = prefix.toLowerCase();
875
+ const matches = HARNESS_SUBCOMMANDS
876
+ .filter((sc) => sc.name.startsWith(lower))
877
+ .map((sc) => ({ value: `${sc.name} `, label: sc.name, description: sc.description }));
878
+ return matches.length > 0 ? matches : null;
879
+ },
880
+ async handler(args: string | undefined, ctx: HarnessCommandContext) {
881
+ await handleHarness(platform, ctx, args);
882
+ },
883
+ });
884
+ }
885
+
886
+ // ── Per-stage subcommands ────────────────────────────────────────
887
+
888
+ type PipelineDriver = typeof runHarnessPipelineUntilGate;
889
+ let pipelineDriver: PipelineDriver = runHarnessPipelineUntilGate;
890
+
891
+ export function setHarnessPipelineDriver(d: PipelineDriver | null): void {
892
+ pipelineDriver = d ?? runHarnessPipelineUntilGate;
893
+ }
894
+
895
+ const SID_PAT = /^harness-[0-9a-z]+-[0-9a-f]+$/;
896
+
897
+ function parseSessionFlag(args: string[]): string | null {
898
+ for (let i = 0; i < args.length; i++) {
899
+ if (args[i] === "--session" && i + 1 < args.length) return args[i + 1];
900
+ if (args[i].startsWith("--session=")) return args[i].slice("--session=".length);
901
+ }
902
+ return null;
903
+ }
904
+
905
+ function nowIso(): string { return new Date().toISOString(); }
906
+ function projectName(cwd: string): string { return path.basename(cwd) || "harness"; }
907
+
908
+ function freshSession(id: string, cwd: string, stage: HarnessStage): HarnessSession {
909
+ const ts = nowIso();
910
+ return { sessionId: id, projectName: projectName(cwd), startedAt: ts, updatedAt: ts, stage,
911
+ stageStatus: "pending", gateMode: "default", iteration: 1, blocker: null, artifacts: {} };
912
+ }
913
+
914
+ export interface ResolveSessionResult { sessionId: string; created: boolean; }
915
+
916
+ export function resolveHarnessSessionId(
917
+ paths: PlatformPaths, cwd: string, args: string[],
918
+ opts: { autoCreate: boolean; stage: HarnessStage },
919
+ ): ResolveSessionResult | { error: string } {
920
+ const ex = parseSessionFlag(args);
921
+ if (ex) {
922
+ if (!SID_PAT.test(ex)) return { error: `invalid session id "${ex}"` };
923
+ return { sessionId: ex, created: false };
924
+ }
925
+ const existing = listHarnessSessions(paths, cwd);
926
+ if (existing.length > 0) return { sessionId: existing[0], created: false };
927
+ if (!opts.autoCreate) return { error: "no harness session found. Run /supi:harness discover first." };
928
+ const id = newHarnessSessionId();
929
+ const p = saveHarnessSession(paths, cwd, freshSession(id, cwd, opts.stage));
930
+ if (!p.ok) return { error: `unable to persist: ${p.error.message}` };
931
+ return { sessionId: id, created: true };
932
+ }
933
+
934
+ function buildStageInputs(
935
+ paths: PlatformPaths, cwd: string, sid: string, stage: HarnessStage,
936
+ ): { input: BuildRunnerInput } | { error: string } {
937
+ switch (stage) {
938
+ case "discover": case "research": case "plan": return { input: {} };
939
+ case "design": {
940
+ const existing = loadHarnessDesignSpecJson(paths, cwd, sid);
941
+ if (existing.ok) return { input: { designInput: { spec: existing.value } } };
942
+ const d = loadHarnessDiscover(paths, cwd, sid);
943
+ if (!d.ok) return { error: "design requires discover stage. Run /supi:harness discover first." };
944
+ return { input: { designInput: { spec: defaultDesignSpecFromDiscover(d.value, sid, nowIso()) } } };
945
+ }
946
+ case "implement": {
947
+ const plansDir = getProjectStatePath(paths, cwd, "plans");
948
+ const planPath = path.join(plansDir, `harness-${sid}.md`);
949
+ if (!fs.existsSync(planPath)) return { error: `implement requires plan at ${planPath}.` };
950
+ return { input: { implementInput: { planPath, threshold: DEFAULT_HARNESS_CONFIG.implement_in_session_threshold ?? 10 } } };
951
+ }
952
+ case "validate": {
953
+ const dr = loadHarnessDesignSpecJson(paths, cwd, sid);
954
+ if (!dr.ok) return { error: "validate requires design. Run /supi:harness design first." };
955
+ const s = dr.value;
956
+ return { input: { validateInput: {
957
+ backend: s.antiSlop.backend, adapter: buildBackendAdapter(s.antiSlop.backend) ?? undefined,
958
+ scoreFloor: s.antiSlop.hooks.score_floor, hooks: s.antiSlop.hooks,
959
+ } } };
960
+ }
961
+ }
962
+ }
963
+
964
+ function summarizeOutcome(o: PipelineRunOutcome): string {
965
+ const t = o.message ? ` — ${o.message}` : "";
966
+ return `stage=${o.stage} status=${o.status}${o.promoted ? " (promoted)" : ""}${t}`;
967
+ }
968
+
969
+ export async function handleStageCommand(
970
+ platform: Platform, ctx: HarnessCommandContext, stage: HarnessStage, args: string[],
971
+ ): Promise<void> {
972
+ const res = resolveHarnessSessionId(platform.paths, ctx.cwd, args, { autoCreate: stage === "discover", stage });
973
+ if ("error" in res) { notifyError(ctx, `/supi:harness ${stage}`, res.error); return; }
974
+ const built = buildStageInputs(platform.paths, ctx.cwd, res.sessionId, stage);
975
+ if ("error" in built) { notifyError(ctx, `/supi:harness ${stage}`, built.error); return; }
976
+ if (res.created) notifyInfo(ctx, `Started session ${res.sessionId}`, `Fresh session for stage ${stage}.`);
977
+
978
+ if (stage === "design" && !loadHarnessDesignSpecJson(platform.paths, ctx.cwd, res.sessionId).ok) {
979
+ notifyInfo(ctx, "Using default design spec", "Edit <session>/design-spec.json to customize.");
980
+ }
981
+
982
+ await runPipelineWithProgress(platform, ctx, res.sessionId, "auto", built.input, stage);
983
+ }
984
+
985
+ async function handleResume(platform: Platform, ctx: HarnessCommandContext, args: string[]): Promise<void> {
986
+ const explicit = parseSessionFlag(args);
987
+ const sessions = listHarnessSessions(platform.paths, ctx.cwd);
988
+ if (sessions.length === 0) { notifyInfo(ctx, "/supi:harness resume", "No sessions found."); return; }
989
+ const target = explicit ?? sessions[0];
990
+ const s = loadHarnessSession(platform.paths, ctx.cwd, target);
991
+ if (!s.ok) { notifyError(ctx, "/supi:harness resume", `Unable to load ${target}: ${s.error.message}`); return; }
992
+ const next = nextSubcommandFor(s.value.stage, s.value.stageStatus);
993
+ notifyInfo(ctx, `Resume ${target}`, `stage=${s.value.stage} status=${s.value.stageStatus}. Run /supi:harness ${next}.`);
994
+ }
995
+
996
+ function nextSubcommandFor(stage: HarnessSession["stage"], status: HarnessSession["stageStatus"]): string {
997
+ if (status === "awaiting-user" || status === "blocked") return cliNameFor(stage);
998
+ switch (stage) {
999
+ case "discover": return "research";
1000
+ case "research": return "design";
1001
+ case "design": return "plan-draft";
1002
+ case "plan": return "implement";
1003
+ case "implement": return "validate";
1004
+ case "validate": return "validate";
1005
+ }
1006
+ }
1007
+
1008
+ function cliNameFor(stage: HarnessSession["stage"]): string {
1009
+ return stage === "plan" ? "plan-draft" : stage;
1010
+ }