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,348 @@
1
+ /**
2
+ * Pipeline driver for the harness.
3
+ *
4
+ * Mirrors `src/ultraplan/authoring/pipeline.ts` but with harness's own stage list.
5
+ * Drives the pipeline forward stage-by-stage until either:
6
+ * - the pipeline reaches Validate successfully (returns `promoted: true`),
7
+ * - it lands at a gate stage (returns `awaiting-user`),
8
+ * - or a stage returns `failed` / `blocked`.
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ import type { Platform, PlatformPaths } from "../platform/types.js";
15
+ import type {
16
+ HarnessGateMode,
17
+ HarnessStage,
18
+ ModelConfig,
19
+ } from "../types.js";
20
+ import {
21
+ type HarnessStageRunResult,
22
+ type HarnessStageRunner,
23
+ type HarnessStageRunnerContext,
24
+ } from "./stage-runner.js";
25
+ import { HarnessDiscoverStage } from "./stages/discover.js";
26
+ import { HarnessResearchStage } from "./stages/research.js";
27
+ import {
28
+ HarnessDesignStage,
29
+ type DesignStageInput,
30
+ defaultDesignSpecFromDiscover,
31
+ } from "./stages/design.js";
32
+ import { HarnessPlanStage, type PlanStageInput } from "./stages/plan.js";
33
+ import { HarnessImplementStage, type ImplementStageInput } from "./stages/implement.js";
34
+ import { HarnessValidateStage, type ValidateStageInput } from "./stages/validate.js";
35
+ import { loadHarnessDesignSpecJson, loadHarnessDiscover } from "./storage.js";
36
+ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
37
+ import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
38
+ import { getProjectStatePath } from "../workspace/state-paths.js";
39
+
40
+ /** Progress event emitted by the pipeline driver for UI feedback. */
41
+ export type HarnessPipelineProgressEvent =
42
+ | { type: "stage-started"; stage: HarnessStage }
43
+ | { type: "stage-skipped"; stage: HarnessStage }
44
+ | { type: "stage-completed"; stage: HarnessStage; detail?: string }
45
+ | { type: "stage-blocked"; stage: HarnessStage; detail: string }
46
+ | { type: "stage-failed"; stage: HarnessStage; detail: string }
47
+ | { type: "awaiting-user"; stage: HarnessStage; detail?: string };
48
+
49
+ const STAGE_ORDER: readonly HarnessStage[] = [
50
+ "discover",
51
+ "research",
52
+ "design",
53
+ "plan",
54
+ "implement",
55
+ "validate",
56
+ ];
57
+
58
+ /** Default gate set per mode. */
59
+ const GATE_STAGES_DEFAULT: ReadonlySet<HarnessStage> = new Set([
60
+ "discover",
61
+ "design",
62
+ "plan",
63
+ "validate",
64
+ ]);
65
+ const GATE_STAGES_MANUAL: ReadonlySet<HarnessStage> = new Set([
66
+ "discover",
67
+ "research",
68
+ "design",
69
+ "plan",
70
+ "implement",
71
+ "validate",
72
+ ]);
73
+
74
+ function gateStagesFor(mode: HarnessGateMode): ReadonlySet<HarnessStage> {
75
+ switch (mode) {
76
+ case "default":
77
+ return GATE_STAGES_DEFAULT;
78
+ case "auto":
79
+ return new Set();
80
+ case "manual":
81
+ return GATE_STAGES_MANUAL;
82
+ }
83
+ }
84
+
85
+ export interface BuildRunnerInput {
86
+ /** Required when running the design stage. */
87
+ designInput?: DesignStageInput;
88
+ /** Optional override for the plan stage (filename only). */
89
+ planInput?: PlanStageInput;
90
+ /** Required when running the implement stage. */
91
+ implementInput?: ImplementStageInput;
92
+ /** Required when running the validate stage. */
93
+ validateInput?: ValidateStageInput;
94
+ }
95
+
96
+ export function buildHarnessRunner(stage: HarnessStage, input: BuildRunnerInput): HarnessStageRunner {
97
+ switch (stage) {
98
+ case "discover":
99
+ return new HarnessDiscoverStage();
100
+ case "research":
101
+ return new HarnessResearchStage();
102
+ case "design":
103
+ if (!input.designInput) {
104
+ throw new Error("buildHarnessRunner: design stage requires designInput");
105
+ }
106
+ return new HarnessDesignStage(input.designInput);
107
+ case "plan":
108
+ return new HarnessPlanStage(input.planInput);
109
+ case "implement":
110
+ if (!input.implementInput) {
111
+ throw new Error("buildHarnessRunner: implement stage requires implementInput");
112
+ }
113
+ return new HarnessImplementStage(input.implementInput);
114
+ case "validate":
115
+ if (!input.validateInput) {
116
+ throw new Error("buildHarnessRunner: validate stage requires validateInput");
117
+ }
118
+ return new HarnessValidateStage(input.validateInput);
119
+ }
120
+ }
121
+
122
+ export interface PipelineRunOutcome {
123
+ stage: HarnessStage;
124
+ status: HarnessStageRunResult["status"];
125
+ /** True when the pipeline reached Validate with a passing report. */
126
+ promoted: boolean;
127
+ message?: string;
128
+ /** Trace of every stage visited this run. */
129
+ trace: { stage: HarnessStage; status: HarnessStageRunResult["status"] }[];
130
+ }
131
+
132
+ export interface PipelineDriverInput {
133
+ platform: Platform;
134
+ paths: PlatformPaths;
135
+ cwd: string;
136
+ sessionId: string;
137
+ modelConfig: ModelConfig;
138
+ gates: HarnessGateMode;
139
+ /** Stage-specific inputs supplied by the command handler. */
140
+ stageInputs: BuildRunnerInput;
141
+ /**
142
+ * When set, start the pipeline at this stage (skipping earlier stages even when not
143
+ * complete). Used by per-stage subcommands.
144
+ */
145
+ startStage?: HarnessStage;
146
+ /** Hard cap on stage iterations. */
147
+ safetyLimit?: number;
148
+ /** Optional callback for progress events (wire up a progress widget). */
149
+ onProgress?: (event: HarnessPipelineProgressEvent) => void;
150
+ }
151
+
152
+ // ── Stage input derivation ──────────────────────────────────────────
153
+
154
+ function ensureStageInputs(
155
+ input: PipelineDriverInput,
156
+ stage: HarnessStage,
157
+ ): BuildRunnerInput {
158
+ const base = input.stageInputs;
159
+
160
+ switch (stage) {
161
+ case "design": {
162
+ if (base.designInput) return base;
163
+ const discover = loadHarnessDiscover(input.paths, input.cwd, input.sessionId);
164
+ if (!discover.ok) return base;
165
+ const spec = defaultDesignSpecFromDiscover(
166
+ discover.value,
167
+ input.sessionId,
168
+ new Date().toISOString(),
169
+ );
170
+ return { ...base, designInput: { spec } };
171
+ }
172
+ case "implement": {
173
+ if (base.implementInput) return base;
174
+ const planName = `harness-${input.sessionId}.md`;
175
+ const plansDir = getProjectStatePath(input.paths, input.cwd, "plans");
176
+ const planPath = path.join(plansDir, planName);
177
+ if (!fs.existsSync(planPath)) return base;
178
+ const threshold = DEFAULT_HARNESS_CONFIG.implement_in_session_threshold ?? 10;
179
+ return { ...base, implementInput: { planPath, threshold } };
180
+ }
181
+ case "validate": {
182
+ if (base.validateInput) return base;
183
+ const designResult = loadHarnessDesignSpecJson(
184
+ input.paths,
185
+ input.cwd,
186
+ input.sessionId,
187
+ );
188
+ if (!designResult.ok) return base;
189
+ const spec = designResult.value;
190
+ const adapter = buildBackendAdapter(spec.antiSlop.backend);
191
+ return {
192
+ ...base,
193
+ validateInput: {
194
+ backend: spec.antiSlop.backend,
195
+ adapter: adapter ?? undefined,
196
+ scoreFloor: spec.antiSlop.hooks.score_floor,
197
+ hooks: spec.antiSlop.hooks,
198
+ },
199
+ };
200
+ }
201
+ default:
202
+ return base;
203
+ }
204
+ }
205
+
206
+ function formatStageDetail(result: HarnessStageRunResult): string {
207
+ const d = result.details;
208
+ if (!d) return result.status;
209
+ if (result.stage === "discover" && Array.isArray(d.languages)) {
210
+ const langs = (d.languages as string[]).slice(0, 3).join(", ");
211
+ return `${langs}${d.languages.length > 3 ? ", …" : ""} · ${d.recommendedBackend ?? "?"}`;
212
+ }
213
+ if (result.stage === "design") {
214
+ const backend = d.backend ?? "?";
215
+ const layers = typeof d.layerCount === "number" ? `${d.layerCount} layers` : "";
216
+ return layers ? `${backend} · ${layers}` : `${backend}`;
217
+ }
218
+ if (result.stage === "validate" && typeof d.passed === "boolean") {
219
+ return d.passed ? "passed" : "issues found";
220
+ }
221
+ return result.status;
222
+ }
223
+
224
+ function awaitUserDetail(result: HarnessStageRunResult): string {
225
+ return formatStageDetail(result) || "awaiting review";
226
+ }
227
+ // ── Pipeline loop ───────────────────────────────────────────────────
228
+ /**
229
+ * Run the pipeline forward until completion or a gate. Each stage:
230
+ * - is skipped when its `isComplete` returns true;
231
+ * - blocks/fails surface immediately;
232
+ * - awaiting-user returns to the caller with the gate decision.
233
+ */
234
+ export async function runHarnessPipelineUntilGate(
235
+ input: PipelineDriverInput,
236
+ ): Promise<PipelineRunOutcome> {
237
+ const trace: { stage: HarnessStage; status: HarnessStageRunResult["status"] }[] = [];
238
+ const gateStages = gateStagesFor(input.gates);
239
+ const safetyLimit = input.safetyLimit ?? 12;
240
+ let safety = 0;
241
+
242
+ let stageIndex = input.startStage ? STAGE_ORDER.indexOf(input.startStage) : 0;
243
+ if (stageIndex < 0) stageIndex = 0;
244
+
245
+ while (safety < safetyLimit) {
246
+ safety += 1;
247
+ if (stageIndex >= STAGE_ORDER.length) break;
248
+ const stage = STAGE_ORDER[stageIndex];
249
+
250
+ input.onProgress?.({ type: "stage-started", stage });
251
+
252
+ const stageInputs = ensureStageInputs(input, stage);
253
+ const runner = buildHarnessRunner(stage, stageInputs);
254
+
255
+ const isComplete = await runner.isComplete({
256
+ platform: input.platform,
257
+ paths: input.paths,
258
+ cwd: input.cwd,
259
+ sessionId: input.sessionId,
260
+ modelConfig: input.modelConfig,
261
+ gateMode: input.gates,
262
+ });
263
+
264
+ if (isComplete) {
265
+ input.onProgress?.({ type: "stage-skipped", stage });
266
+ trace.push({ stage, status: "skipped" });
267
+ stageIndex += 1;
268
+ continue;
269
+ }
270
+
271
+ const ctx: HarnessStageRunnerContext = {
272
+ platform: input.platform,
273
+ paths: input.paths,
274
+ cwd: input.cwd,
275
+ sessionId: input.sessionId,
276
+ modelConfig: input.modelConfig,
277
+ gateMode: input.gates,
278
+ };
279
+
280
+ const result = await runner.run(ctx);
281
+
282
+ // In auto mode, awaiting-user is equivalent to completed — normalize
283
+ // both the trace entry and any outcome derived from it so the UI never
284
+ // shows a confusing mix of checkmarks and "awaiting user".
285
+ const isGate = gateStages.has(stage);
286
+ const normalizedStatus: HarnessStageRunResult["status"] =
287
+ result.status === "awaiting-user" && !isGate
288
+ ? "completed"
289
+ : result.status;
290
+ trace.push({ stage, status: normalizedStatus });
291
+
292
+ const detail = formatStageDetail(result);
293
+
294
+ if (result.status === "failed") {
295
+ input.onProgress?.({ type: "stage-failed", stage, detail: result.error ?? detail });
296
+ return {
297
+ stage, status: result.status, promoted: false,
298
+ message: result.error, trace,
299
+ };
300
+ }
301
+
302
+ if (result.status === "blocked") {
303
+ input.onProgress?.({ type: "stage-blocked", stage, detail: result.blocker?.message ?? detail });
304
+ return {
305
+ stage, status: result.status, promoted: false,
306
+ message: result.blocker?.message, trace,
307
+ };
308
+ }
309
+
310
+ // In auto mode, awaiting-user is equivalent to completed — the pipeline
311
+ // continues without stopping. Only surface the distinction when gated.
312
+ if (normalizedStatus === "awaiting-user" && isGate) {
313
+ input.onProgress?.({ type: "awaiting-user", stage, detail: awaitUserDetail(result) });
314
+ } else {
315
+ input.onProgress?.({ type: "stage-completed", stage, detail });
316
+ }
317
+
318
+ if (isGate && normalizedStatus !== "skipped") {
319
+ return {
320
+ stage,
321
+ status: "awaiting-user",
322
+ promoted: false,
323
+ trace,
324
+ };
325
+ }
326
+
327
+ if (stage === "validate" && (normalizedStatus === "completed" || normalizedStatus === "skipped")) {
328
+ return {
329
+ stage: "validate",
330
+ status: normalizedStatus,
331
+ promoted: true,
332
+ trace,
333
+ };
334
+ }
335
+
336
+ stageIndex += 1;
337
+ }
338
+
339
+ return {
340
+ stage: "validate",
341
+ status: "failed",
342
+ promoted: false,
343
+ message: "harness pipeline exceeded its safety cap",
344
+ trace,
345
+ };
346
+ }
347
+
348
+ export const HARNESS_STAGE_ORDER = STAGE_ORDER;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Path helpers for the harness pipeline.
3
+ *
4
+ * Mirrors the `src/ultraplan/project-paths.ts` pattern:
5
+ * - per-session global state under `~/.omp/supipowers/projects/<slug>/harness/<sessionId>/`,
6
+ * - repo-scoped marker + score files under `<repo>/.omp/supipowers/harness/`,
7
+ * - persistent slop queue lives under the global path so concurrent worktrees of one repo
8
+ * converge on a single queue (resolved through `resolveRepoIdentityRootFromFs`).
9
+ *
10
+ * Filename constants are exported so call sites never inline the strings — keeping the
11
+ * `getHarnessXxxPath` family as the only entry point makes the storage layout refactorable.
12
+ */
13
+
14
+ import path from "node:path";
15
+
16
+ import type { PlatformPaths } from "../platform/types.js";
17
+ import { projectSlugFromRepoRoot } from "../workspace/project-slug.js";
18
+ import { resolveRepoIdentityRootFromFs } from "../workspace/repo-root.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Canonical directory + filename constants.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Root directory under the project slug for harness pipeline state. */
25
+ const HARNESS_DIRNAME = "harness";
26
+
27
+ /** Per-session subdirectory containing every artifact for one pipeline run. */
28
+ const HARNESS_SESSIONS_DIRNAME = "sessions";
29
+
30
+ /** Append-only slop-queue file. JSONL, one record per line. */
31
+ export const HARNESS_QUEUE_FILENAME = "queue.jsonl";
32
+
33
+ /** Aggregate score JSON. Computed at the end of Validate and every GC run. */
34
+ export const HARNESS_SCORE_FILENAME = "score.json";
35
+
36
+ /** Append-only score history. JSONL, one record per Validate / GC computation. */
37
+ export const HARNESS_SCORE_HISTORY_FILENAME = "score-history.jsonl";
38
+
39
+ /** Repo-local marker file. Presence means the harness is installed for this repo. */
40
+ export const HARNESS_MARKER_FILENAME = "marker.json";
41
+
42
+ /** Per-session manifest. Holds HarnessSession state. */
43
+ export const HARNESS_MANIFEST_FILENAME = "manifest.json";
44
+
45
+ /** Per-session artifact filenames. */
46
+ export const HARNESS_DISCOVER_FILENAME = "discover.json";
47
+ export const HARNESS_DESIGN_SPEC_FILENAME = "design-spec.md";
48
+ export const HARNESS_DESIGN_SPEC_JSON_FILENAME = "design-spec.json";
49
+ export const HARNESS_DECISIONS_FILENAME = "decisions.jsonl";
50
+ export const HARNESS_VALIDATE_REPORT_FILENAME = "validate-report.json";
51
+ export const HARNESS_IMPLEMENT_LOG_FILENAME = "implement-log.jsonl";
52
+ export const HARNESS_PIPELINE_LOG_FILENAME = "pipeline-log.jsonl";
53
+ export const HARNESS_RESEARCH_DIRNAME = "research";
54
+
55
+ /** Tier 1 / Tier 2 output paths in the repo. */
56
+ export const HARNESS_AGENTS_MD_FILENAME = "AGENTS.md";
57
+ export const HARNESS_DOCS_DIRNAME = "docs";
58
+ export const HARNESS_DOCS_ARCHITECTURE_FILENAME = "architecture.md";
59
+ export const HARNESS_DOCS_GOLDEN_PRINCIPLES_FILENAME = "golden-principles.md";
60
+ export const HARNESS_FALLOW_CONFIG_FILENAME = ".fallowrc.json";
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Internal resolution helpers.
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function resolveProjectIdentityRoot(cwd: string): string {
67
+ return path.resolve(resolveRepoIdentityRootFromFs(cwd));
68
+ }
69
+
70
+ /**
71
+ * Canonical harness root: `~/.omp/supipowers/projects/<slug>/harness`.
72
+ */
73
+ export function getHarnessProjectRoot(paths: PlatformPaths, cwd: string): string {
74
+ const repoIdentityRoot = resolveProjectIdentityRoot(cwd);
75
+ const slug = projectSlugFromRepoRoot(repoIdentityRoot);
76
+ return paths.global("projects", slug, HARNESS_DIRNAME);
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Global per-project paths.
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /** Project-scoped persistent slop queue (shared across worktrees). */
84
+ export function getHarnessQueuePath(paths: PlatformPaths, cwd: string): string {
85
+ return path.join(getHarnessProjectRoot(paths, cwd), HARNESS_QUEUE_FILENAME);
86
+ }
87
+
88
+ /** Project-scoped score history (JSONL). */
89
+ export function getHarnessScoreHistoryPath(paths: PlatformPaths, cwd: string): string {
90
+ return path.join(getHarnessProjectRoot(paths, cwd), HARNESS_SCORE_HISTORY_FILENAME);
91
+ }
92
+
93
+ /** Per-project sessions directory (parent of all per-session dirs). */
94
+ export function getHarnessSessionsDir(paths: PlatformPaths, cwd: string): string {
95
+ return path.join(getHarnessProjectRoot(paths, cwd), HARNESS_SESSIONS_DIRNAME);
96
+ }
97
+
98
+ /** Per-session directory. */
99
+ export function getHarnessSessionDir(
100
+ paths: PlatformPaths,
101
+ cwd: string,
102
+ sessionId: string,
103
+ ): string {
104
+ return path.join(getHarnessSessionsDir(paths, cwd), sessionId);
105
+ }
106
+
107
+ export function getHarnessManifestPath(
108
+ paths: PlatformPaths,
109
+ cwd: string,
110
+ sessionId: string,
111
+ ): string {
112
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_MANIFEST_FILENAME);
113
+ }
114
+
115
+ export function getHarnessDiscoverPath(
116
+ paths: PlatformPaths,
117
+ cwd: string,
118
+ sessionId: string,
119
+ ): string {
120
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_DISCOVER_FILENAME);
121
+ }
122
+
123
+ export function getHarnessResearchDir(
124
+ paths: PlatformPaths,
125
+ cwd: string,
126
+ sessionId: string,
127
+ ): string {
128
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_RESEARCH_DIRNAME);
129
+ }
130
+
131
+ /**
132
+ * Path to a single research topic markdown. Topic is sanitized for filesystem use; callers
133
+ * pass the already-canonical topic slug.
134
+ */
135
+ export function getHarnessResearchTopicPath(
136
+ paths: PlatformPaths,
137
+ cwd: string,
138
+ sessionId: string,
139
+ topicSlug: string,
140
+ ): string {
141
+ return path.join(getHarnessResearchDir(paths, cwd, sessionId), `${topicSlug}.md`);
142
+ }
143
+
144
+ export function getHarnessDesignSpecPath(
145
+ paths: PlatformPaths,
146
+ cwd: string,
147
+ sessionId: string,
148
+ ): string {
149
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_DESIGN_SPEC_FILENAME);
150
+ }
151
+
152
+ export function getHarnessDesignSpecJsonPath(
153
+ paths: PlatformPaths,
154
+ cwd: string,
155
+ sessionId: string,
156
+ ): string {
157
+ return path.join(
158
+ getHarnessSessionDir(paths, cwd, sessionId),
159
+ HARNESS_DESIGN_SPEC_JSON_FILENAME,
160
+ );
161
+ }
162
+
163
+ export function getHarnessDecisionsPath(
164
+ paths: PlatformPaths,
165
+ cwd: string,
166
+ sessionId: string,
167
+ ): string {
168
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_DECISIONS_FILENAME);
169
+ }
170
+
171
+ export function getHarnessValidateReportPath(
172
+ paths: PlatformPaths,
173
+ cwd: string,
174
+ sessionId: string,
175
+ ): string {
176
+ return path.join(
177
+ getHarnessSessionDir(paths, cwd, sessionId),
178
+ HARNESS_VALIDATE_REPORT_FILENAME,
179
+ );
180
+ }
181
+
182
+ export function getHarnessImplementLogPath(
183
+ paths: PlatformPaths,
184
+ cwd: string,
185
+ sessionId: string,
186
+ ): string {
187
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_IMPLEMENT_LOG_FILENAME);
188
+ }
189
+
190
+ export function getHarnessPipelineLogPath(
191
+ paths: PlatformPaths,
192
+ cwd: string,
193
+ sessionId: string,
194
+ ): string {
195
+ return path.join(getHarnessSessionDir(paths, cwd, sessionId), HARNESS_PIPELINE_LOG_FILENAME);
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Repo-local paths (committable subset under .omp/supipowers/harness/).
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /** Repo-local harness directory: `<repo>/.omp/supipowers/harness/`. */
203
+ export function getHarnessRepoLocalDir(paths: PlatformPaths, cwd: string): string {
204
+ return paths.project(cwd, "harness");
205
+ }
206
+
207
+ /** Marker file path. Presence registers the harness for this repo. */
208
+ export function getHarnessMarkerPath(paths: PlatformPaths, cwd: string): string {
209
+ return path.join(getHarnessRepoLocalDir(paths, cwd), HARNESS_MARKER_FILENAME);
210
+ }
211
+
212
+ /** Repo-local score snapshot. Mirrors the global per-project score for committable artifacts. */
213
+ export function getHarnessRepoScorePath(paths: PlatformPaths, cwd: string): string {
214
+ return path.join(getHarnessRepoLocalDir(paths, cwd), HARNESS_SCORE_FILENAME);
215
+ }
216
+
217
+ /** Tier 1 — repo-root AGENTS.md. */
218
+ export function getHarnessAgentsMdPath(_paths: PlatformPaths, cwd: string): string {
219
+ return path.join(cwd, HARNESS_AGENTS_MD_FILENAME);
220
+ }
221
+
222
+ /** Tier 1 — repo-root docs/architecture.md. */
223
+ export function getHarnessArchitectureDocPath(_paths: PlatformPaths, cwd: string): string {
224
+ return path.join(cwd, HARNESS_DOCS_DIRNAME, HARNESS_DOCS_ARCHITECTURE_FILENAME);
225
+ }
226
+
227
+ /** Tier 1 — repo-root docs/golden-principles.md. */
228
+ export function getHarnessGoldenPrinciplesPath(_paths: PlatformPaths, cwd: string): string {
229
+ return path.join(cwd, HARNESS_DOCS_DIRNAME, HARNESS_DOCS_GOLDEN_PRINCIPLES_FILENAME);
230
+ }
231
+
232
+ /** Tier 1 — repo-root .fallowrc.json. */
233
+ export function getHarnessFallowConfigPath(_paths: PlatformPaths, cwd: string): string {
234
+ return path.join(cwd, HARNESS_FALLOW_CONFIG_FILENAME);
235
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Stage runner abstraction for the harness pipeline.
3
+ *
4
+ * Mirrors `src/ultraplan/authoring/stage-runner.ts` shape so future tooling that introspects
5
+ * stage state (status renderers, debug loggers) can apply the same patterns to both pipelines
6
+ * without case analysis. We do NOT subclass the ultraplan stage runner — both implement the
7
+ * same abstract `StageRunner` shape independently because the artifact paths and stage
8
+ * identifiers are different.
9
+ *
10
+ * Each stage:
11
+ * - resolves the model + agent prompt for the stage,
12
+ * - builds the assignment prompt deterministically from prior artifacts on disk,
13
+ * - spawns a fresh `platform.createAgentSession` and awaits its completion,
14
+ * - validates the produced artifact (TypeBox / shape check),
15
+ * - transitions the session manifest atomically.
16
+ *
17
+ * Stage runners are idempotent: if `isComplete` returns true, `run` returns
18
+ * `status: "skipped"` without spawning an agent.
19
+ */
20
+
21
+ import type { Platform, PlatformPaths } from "../platform/types.js";
22
+ import type {
23
+ HarnessGateMode,
24
+ HarnessStage,
25
+ HarnessStageStatus,
26
+ ModelConfig,
27
+ } from "../types.js";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Public types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export interface HarnessStageRunnerContext {
34
+ platform: Platform;
35
+ paths: PlatformPaths;
36
+ cwd: string;
37
+ sessionId: string;
38
+ /** Loaded once at the start of the pipeline; passed in for model resolution. */
39
+ modelConfig: ModelConfig;
40
+ /** Gate mode currently in effect; stages may use it to short-circuit user prompts. */
41
+ gateMode: HarnessGateMode;
42
+ /**
43
+ * Optional override for `now()` so tests can produce deterministic timestamps. Production
44
+ * code defaults to `() => new Date().toISOString()`.
45
+ */
46
+ now?: () => string;
47
+ /** Optional override for the agent session model. Tests use this to bypass resolution. */
48
+ modelOverride?: { model: string; thinkingLevel: string | null };
49
+ }
50
+
51
+ export type HarnessStageRunStatus =
52
+ | "completed" // Stage ran to completion and persisted its artifact.
53
+ | "skipped" // Pre-conditions said the stage was already done.
54
+ | "blocked" // Structured blocker (e.g. validation failure with no recovery).
55
+ | "awaiting-user" // Stage produced output but needs a user gate before advancing.
56
+ | "failed"; // Programming-level failure surfaced for the operator.
57
+
58
+ export interface HarnessStageRunResult {
59
+ status: HarnessStageRunStatus;
60
+ stage: HarnessStage;
61
+ /** Artifact paths touched (relative to <session>/). */
62
+ artifactPaths: string[];
63
+ blocker?: { code: string; message: string };
64
+ error?: string;
65
+ details?: Record<string, unknown>;
66
+ }
67
+
68
+ export interface HarnessStageRunner {
69
+ readonly stage: HarnessStage;
70
+ isReady(ctx: HarnessStageRunnerContext): Promise<boolean>;
71
+ isComplete(ctx: HarnessStageRunnerContext): Promise<boolean>;
72
+ run(ctx: HarnessStageRunnerContext): Promise<HarnessStageRunResult>;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Convenience helpers
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function nowIso(ctx: HarnessStageRunnerContext): string {
80
+ return (ctx.now ?? (() => new Date().toISOString()))();
81
+ }
82
+
83
+ export function buildHarnessAgentDisplayName(stage: HarnessStage, discriminator?: string): string {
84
+ return discriminator ? `harness-${stage}/${discriminator}` : `harness-${stage}`;
85
+ }
86
+
87
+ export function toHarnessStageStatus(status: HarnessStageRunStatus): HarnessStageStatus {
88
+ switch (status) {
89
+ case "completed":
90
+ case "skipped":
91
+ return "done";
92
+ case "blocked":
93
+ case "failed":
94
+ return "blocked";
95
+ case "awaiting-user":
96
+ return "awaiting-user";
97
+ }
98
+ }
99
+
100
+ /** Generate a deterministic session id: `harness-<base36 ms>-<6-hex random>`. */
101
+ export function newHarnessSessionId(now: () => Date = () => new Date()): string {
102
+ const ms = now().getTime();
103
+ const rand = Math.floor(Math.random() * 0xffffff)
104
+ .toString(16)
105
+ .padStart(6, "0");
106
+ return `harness-${ms.toString(36)}-${rand}`;
107
+ }