@rsaglobaltech/specgate 0.8.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 (494) hide show
  1. package/CHANGELOG.md +1016 -0
  2. package/README.md +148 -0
  3. package/bin/specgate.js +2 -0
  4. package/bin/specgate.ts +383 -0
  5. package/dist/bin/specgate.js +342 -0
  6. package/dist/bin/specgate.js.map +1 -0
  7. package/dist/packages/core/src/application/AddRequirementUseCase.js +24 -0
  8. package/dist/packages/core/src/application/AddRequirementUseCase.js.map +1 -0
  9. package/dist/packages/core/src/application/ArchiveChangeUseCase.js +185 -0
  10. package/dist/packages/core/src/application/ArchiveChangeUseCase.js.map +1 -0
  11. package/dist/packages/core/src/application/CheckAgainstLockUseCase.js +134 -0
  12. package/dist/packages/core/src/application/CheckAgainstLockUseCase.js.map +1 -0
  13. package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js +25 -0
  14. package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js.map +1 -0
  15. package/dist/packages/core/src/application/GeneratePlanUseCase.js +135 -0
  16. package/dist/packages/core/src/application/GeneratePlanUseCase.js.map +1 -0
  17. package/dist/packages/core/src/application/LinkRequirementUseCase.js +25 -0
  18. package/dist/packages/core/src/application/LinkRequirementUseCase.js.map +1 -0
  19. package/dist/packages/core/src/application/RemovePackUseCase.js +28 -0
  20. package/dist/packages/core/src/application/RemovePackUseCase.js.map +1 -0
  21. package/dist/packages/core/src/application/UpdateRequirementStatusUseCase.js +37 -0
  22. package/dist/packages/core/src/application/UpdateRequirementStatusUseCase.js.map +1 -0
  23. package/dist/packages/core/src/application/ValidateChangeUseCase.js +86 -0
  24. package/dist/packages/core/src/application/ValidateChangeUseCase.js.map +1 -0
  25. package/dist/packages/core/src/application/ValidateProjectUseCase.js +101 -0
  26. package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -0
  27. package/dist/packages/core/src/application/ports/IDomainPackRepository.js +3 -0
  28. package/dist/packages/core/src/application/ports/IDomainPackRepository.js.map +1 -0
  29. package/dist/packages/core/src/application/ports/IHarnessConfigRepository.js +3 -0
  30. package/dist/packages/core/src/application/ports/IHarnessConfigRepository.js.map +1 -0
  31. package/dist/packages/core/src/application/ports/ILockfileRepository.js +3 -0
  32. package/dist/packages/core/src/application/ports/ILockfileRepository.js.map +1 -0
  33. package/dist/packages/core/src/application/ports/IManifestRepository.js +3 -0
  34. package/dist/packages/core/src/application/ports/IManifestRepository.js.map +1 -0
  35. package/dist/packages/core/src/application/ports/IProjectRepository.js +3 -0
  36. package/dist/packages/core/src/application/ports/IProjectRepository.js.map +1 -0
  37. package/dist/packages/core/src/application/ports/IRequirementGraphRepository.js +3 -0
  38. package/dist/packages/core/src/application/ports/IRequirementGraphRepository.js.map +1 -0
  39. package/dist/packages/core/src/application/ports/ITraceabilityRepository.js +3 -0
  40. package/dist/packages/core/src/application/ports/ITraceabilityRepository.js.map +1 -0
  41. package/dist/packages/core/src/domain/AgentPrompt.js +79 -0
  42. package/dist/packages/core/src/domain/AgentPrompt.js.map +1 -0
  43. package/dist/packages/core/src/domain/ArchivePlan.js +26 -0
  44. package/dist/packages/core/src/domain/ArchivePlan.js.map +1 -0
  45. package/dist/packages/core/src/domain/AuthoringScope.js +48 -0
  46. package/dist/packages/core/src/domain/AuthoringScope.js.map +1 -0
  47. package/dist/packages/core/src/domain/ChangeTemplates.js +133 -0
  48. package/dist/packages/core/src/domain/ChangeTemplates.js.map +1 -0
  49. package/dist/packages/core/src/domain/CucumberMessages.js +256 -0
  50. package/dist/packages/core/src/domain/CucumberMessages.js.map +1 -0
  51. package/dist/packages/core/src/domain/DeclaredArtifacts.js +117 -0
  52. package/dist/packages/core/src/domain/DeclaredArtifacts.js.map +1 -0
  53. package/dist/packages/core/src/domain/DeltaSpec.js +219 -0
  54. package/dist/packages/core/src/domain/DeltaSpec.js.map +1 -0
  55. package/dist/packages/core/src/domain/Diagnostic.js +75 -0
  56. package/dist/packages/core/src/domain/Diagnostic.js.map +1 -0
  57. package/dist/packages/core/src/domain/DomainPack.js +62 -0
  58. package/dist/packages/core/src/domain/DomainPack.js.map +1 -0
  59. package/dist/packages/core/src/domain/EmptyAttempt.js +60 -0
  60. package/dist/packages/core/src/domain/EmptyAttempt.js.map +1 -0
  61. package/dist/packages/core/src/domain/FileChangeSet.js +37 -0
  62. package/dist/packages/core/src/domain/FileChangeSet.js.map +1 -0
  63. package/dist/packages/core/src/domain/Gherkin.js +272 -0
  64. package/dist/packages/core/src/domain/Gherkin.js.map +1 -0
  65. package/dist/packages/core/src/domain/GherkinDialects.js +97 -0
  66. package/dist/packages/core/src/domain/GherkinDialects.js.map +1 -0
  67. package/dist/packages/core/src/domain/GherkinQuality.js +160 -0
  68. package/dist/packages/core/src/domain/GherkinQuality.js.map +1 -0
  69. package/dist/packages/core/src/domain/GherkinTags.js +119 -0
  70. package/dist/packages/core/src/domain/GherkinTags.js.map +1 -0
  71. package/dist/packages/core/src/domain/GitSafety.js +77 -0
  72. package/dist/packages/core/src/domain/GitSafety.js.map +1 -0
  73. package/dist/packages/core/src/domain/HarnessConfig.js +58 -0
  74. package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -0
  75. package/dist/packages/core/src/domain/HarnessReport.js +162 -0
  76. package/dist/packages/core/src/domain/HarnessReport.js.map +1 -0
  77. package/dist/packages/core/src/domain/HarnessRun.js +154 -0
  78. package/dist/packages/core/src/domain/HarnessRun.js.map +1 -0
  79. package/dist/packages/core/src/domain/InboundChange.js +127 -0
  80. package/dist/packages/core/src/domain/InboundChange.js.map +1 -0
  81. package/dist/packages/core/src/domain/Language.js +112 -0
  82. package/dist/packages/core/src/domain/Language.js.map +1 -0
  83. package/dist/packages/core/src/domain/Lockfile.js +64 -0
  84. package/dist/packages/core/src/domain/Lockfile.js.map +1 -0
  85. package/dist/packages/core/src/domain/PackContribution.js +106 -0
  86. package/dist/packages/core/src/domain/PackContribution.js.map +1 -0
  87. package/dist/packages/core/src/domain/PackDelta.js +298 -0
  88. package/dist/packages/core/src/domain/PackDelta.js.map +1 -0
  89. package/dist/packages/core/src/domain/PackSpec.js +481 -0
  90. package/dist/packages/core/src/domain/PackSpec.js.map +1 -0
  91. package/dist/packages/core/src/domain/ProfileMatch.js +87 -0
  92. package/dist/packages/core/src/domain/ProfileMatch.js.map +1 -0
  93. package/dist/packages/core/src/domain/Reconciliation.js +72 -0
  94. package/dist/packages/core/src/domain/Reconciliation.js.map +1 -0
  95. package/dist/packages/core/src/domain/RequirementGraph.js +197 -0
  96. package/dist/packages/core/src/domain/RequirementGraph.js.map +1 -0
  97. package/dist/packages/core/src/domain/RequirementPlan.js +85 -0
  98. package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -0
  99. package/dist/packages/core/src/domain/RequirementReadiness.js +115 -0
  100. package/dist/packages/core/src/domain/RequirementReadiness.js.map +1 -0
  101. package/dist/packages/core/src/domain/RequirementSyntax.js +91 -0
  102. package/dist/packages/core/src/domain/RequirementSyntax.js.map +1 -0
  103. package/dist/packages/core/src/domain/ResumeState.js +115 -0
  104. package/dist/packages/core/src/domain/ResumeState.js.map +1 -0
  105. package/dist/packages/core/src/domain/RunBudget.js +103 -0
  106. package/dist/packages/core/src/domain/RunBudget.js.map +1 -0
  107. package/dist/packages/core/src/domain/SpecDiff.js +48 -0
  108. package/dist/packages/core/src/domain/SpecDiff.js.map +1 -0
  109. package/dist/packages/core/src/domain/SpecParser.js +406 -0
  110. package/dist/packages/core/src/domain/SpecParser.js.map +1 -0
  111. package/dist/packages/core/src/domain/SpecopsManifest.js +90 -0
  112. package/dist/packages/core/src/domain/SpecopsManifest.js.map +1 -0
  113. package/dist/packages/core/src/domain/TraceabilityFormat.js +317 -0
  114. package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -0
  115. package/dist/packages/core/src/domain/TraceabilityMatrix.js +200 -0
  116. package/dist/packages/core/src/domain/TraceabilityMatrix.js.map +1 -0
  117. package/dist/packages/core/src/domain/TraceabilityMerge.js +180 -0
  118. package/dist/packages/core/src/domain/TraceabilityMerge.js.map +1 -0
  119. package/dist/packages/core/src/domain/ValidationReport.js +53 -0
  120. package/dist/packages/core/src/domain/ValidationReport.js.map +1 -0
  121. package/dist/packages/core/src/domain/ValueAnnotations.js +138 -0
  122. package/dist/packages/core/src/domain/ValueAnnotations.js.map +1 -0
  123. package/dist/packages/core/src/domain/WriteScope.js +155 -0
  124. package/dist/packages/core/src/domain/WriteScope.js.map +1 -0
  125. package/dist/packages/core/src/domain/YamlLite.js +226 -0
  126. package/dist/packages/core/src/domain/YamlLite.js.map +1 -0
  127. package/dist/packages/core/src/index.js +120 -0
  128. package/dist/packages/core/src/index.js.map +1 -0
  129. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js +276 -0
  130. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -0
  131. package/dist/packages/core/src/infrastructure/ConsoleReporter.js +19 -0
  132. package/dist/packages/core/src/infrastructure/ConsoleReporter.js.map +1 -0
  133. package/dist/packages/core/src/infrastructure/DirectorySnapshot.js +106 -0
  134. package/dist/packages/core/src/infrastructure/DirectorySnapshot.js.map +1 -0
  135. package/dist/packages/core/src/infrastructure/DiskDomainPackRepository.js +57 -0
  136. package/dist/packages/core/src/infrastructure/DiskDomainPackRepository.js.map +1 -0
  137. package/dist/packages/core/src/infrastructure/DiskHarnessConfigRepository.js +58 -0
  138. package/dist/packages/core/src/infrastructure/DiskHarnessConfigRepository.js.map +1 -0
  139. package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js +69 -0
  140. package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js.map +1 -0
  141. package/dist/packages/core/src/infrastructure/DiskLockfileRepository.js +64 -0
  142. package/dist/packages/core/src/infrastructure/DiskLockfileRepository.js.map +1 -0
  143. package/dist/packages/core/src/infrastructure/DiskManifestRepository.js +88 -0
  144. package/dist/packages/core/src/infrastructure/DiskManifestRepository.js.map +1 -0
  145. package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js +138 -0
  146. package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js.map +1 -0
  147. package/dist/packages/core/src/infrastructure/DiskPackRepository.js +130 -0
  148. package/dist/packages/core/src/infrastructure/DiskPackRepository.js.map +1 -0
  149. package/dist/packages/core/src/infrastructure/DiskProjectRepository.js +127 -0
  150. package/dist/packages/core/src/infrastructure/DiskProjectRepository.js.map +1 -0
  151. package/dist/packages/core/src/infrastructure/DiskRequirementGraphRepository.js +124 -0
  152. package/dist/packages/core/src/infrastructure/DiskRequirementGraphRepository.js.map +1 -0
  153. package/dist/packages/core/src/infrastructure/DiskTraceabilityRepository.js +53 -0
  154. package/dist/packages/core/src/infrastructure/DiskTraceabilityRepository.js.map +1 -0
  155. package/dist/packages/core/src/infrastructure/GitContributionStager.js +99 -0
  156. package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -0
  157. package/dist/packages/core/src/infrastructure/GitMergeDriver.js +103 -0
  158. package/dist/packages/core/src/infrastructure/GitMergeDriver.js.map +1 -0
  159. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +348 -0
  160. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -0
  161. package/dist/packages/core/src/infrastructure/PackChangeDeposit.js +106 -0
  162. package/dist/packages/core/src/infrastructure/PackChangeDeposit.js.map +1 -0
  163. package/dist/packages/core/src/infrastructure/RemotePackResolver.js +181 -0
  164. package/dist/packages/core/src/infrastructure/RemotePackResolver.js.map +1 -0
  165. package/dist/packages/core/src/infrastructure/SpecopsConfigFile.js +94 -0
  166. package/dist/packages/core/src/infrastructure/SpecopsConfigFile.js.map +1 -0
  167. package/dist/packages/core/test/unit/authoring-scope.test.js +102 -0
  168. package/dist/packages/core/test/unit/authoring-scope.test.js.map +1 -0
  169. package/dist/packages/core/test/unit/change-templates.test.js +96 -0
  170. package/dist/packages/core/test/unit/change-templates.test.js.map +1 -0
  171. package/dist/packages/core/test/unit/cucumber-messages.test.js +232 -0
  172. package/dist/packages/core/test/unit/cucumber-messages.test.js.map +1 -0
  173. package/dist/packages/core/test/unit/declared-artifacts.test.js +124 -0
  174. package/dist/packages/core/test/unit/declared-artifacts.test.js.map +1 -0
  175. package/dist/packages/core/test/unit/empty-attempt.test.js +79 -0
  176. package/dist/packages/core/test/unit/empty-attempt.test.js.map +1 -0
  177. package/dist/packages/core/test/unit/gherkin-dialects.test.js +115 -0
  178. package/dist/packages/core/test/unit/gherkin-dialects.test.js.map +1 -0
  179. package/dist/packages/core/test/unit/gherkin-keyword-case.test.js +110 -0
  180. package/dist/packages/core/test/unit/gherkin-keyword-case.test.js.map +1 -0
  181. package/dist/packages/core/test/unit/gherkin-quality.test.js +151 -0
  182. package/dist/packages/core/test/unit/gherkin-quality.test.js.map +1 -0
  183. package/dist/packages/core/test/unit/gherkin-tags.test.js +127 -0
  184. package/dist/packages/core/test/unit/gherkin-tags.test.js.map +1 -0
  185. package/dist/packages/core/test/unit/git-safety.test.js +104 -0
  186. package/dist/packages/core/test/unit/git-safety.test.js.map +1 -0
  187. package/dist/packages/core/test/unit/harness-report.test.js +192 -0
  188. package/dist/packages/core/test/unit/harness-report.test.js.map +1 -0
  189. package/dist/packages/core/test/unit/harness-roles.test.js +86 -0
  190. package/dist/packages/core/test/unit/harness-roles.test.js.map +1 -0
  191. package/dist/packages/core/test/unit/inbound-change.test.js +145 -0
  192. package/dist/packages/core/test/unit/inbound-change.test.js.map +1 -0
  193. package/dist/packages/core/test/unit/matrix-dependencies.test.js +100 -0
  194. package/dist/packages/core/test/unit/matrix-dependencies.test.js.map +1 -0
  195. package/dist/packages/core/test/unit/profile-match.test.js +115 -0
  196. package/dist/packages/core/test/unit/profile-match.test.js.map +1 -0
  197. package/dist/packages/core/test/unit/reconciliation.test.js +112 -0
  198. package/dist/packages/core/test/unit/reconciliation.test.js.map +1 -0
  199. package/dist/packages/core/test/unit/requirement-readiness.test.js +161 -0
  200. package/dist/packages/core/test/unit/requirement-readiness.test.js.map +1 -0
  201. package/dist/packages/core/test/unit/resume-state.test.js +124 -0
  202. package/dist/packages/core/test/unit/resume-state.test.js.map +1 -0
  203. package/dist/packages/core/test/unit/run-budget.test.js +113 -0
  204. package/dist/packages/core/test/unit/run-budget.test.js.map +1 -0
  205. package/dist/packages/core/test/unit/traceability-merge.test.js +159 -0
  206. package/dist/packages/core/test/unit/traceability-merge.test.js.map +1 -0
  207. package/dist/packages/core/test/unit/validate-project.test.js +156 -0
  208. package/dist/packages/core/test/unit/validate-project.test.js.map +1 -0
  209. package/dist/packages/core/test/unit/value-annotations.test.js +149 -0
  210. package/dist/packages/core/test/unit/value-annotations.test.js.map +1 -0
  211. package/dist/packages/core/test/unit/write-scope.test.js +165 -0
  212. package/dist/packages/core/test/unit/write-scope.test.js.map +1 -0
  213. package/dist/packages/core/test/unit/yaml-lite.test.js +78 -0
  214. package/dist/packages/core/test/unit/yaml-lite.test.js.map +1 -0
  215. package/dist/scripts/adopt_project.js +11 -0
  216. package/dist/scripts/adopt_project.js.map +1 -0
  217. package/dist/scripts/agents/commands.js +100 -0
  218. package/dist/scripts/agents/commands.js.map +1 -0
  219. package/dist/scripts/agents/init.js +482 -0
  220. package/dist/scripts/agents/init.js.map +1 -0
  221. package/dist/scripts/alm/cli.js +154 -0
  222. package/dist/scripts/alm/cli.js.map +1 -0
  223. package/dist/scripts/alm/clients.js +30 -0
  224. package/dist/scripts/alm/clients.js.map +1 -0
  225. package/dist/scripts/alm/core.js +258 -0
  226. package/dist/scripts/alm/core.js.map +1 -0
  227. package/dist/scripts/alm/port.js +116 -0
  228. package/dist/scripts/alm/port.js.map +1 -0
  229. package/dist/scripts/alm/providers/azure.js +79 -0
  230. package/dist/scripts/alm/providers/azure.js.map +1 -0
  231. package/dist/scripts/alm/providers/github.js +124 -0
  232. package/dist/scripts/alm/providers/github.js.map +1 -0
  233. package/dist/scripts/alm/providers/http-client.js +47 -0
  234. package/dist/scripts/alm/providers/http-client.js.map +1 -0
  235. package/dist/scripts/alm/providers/index.js +107 -0
  236. package/dist/scripts/alm/providers/index.js.map +1 -0
  237. package/dist/scripts/alm/providers/jira.js +129 -0
  238. package/dist/scripts/alm/providers/jira.js.map +1 -0
  239. package/dist/scripts/bundle_pack.js +10 -0
  240. package/dist/scripts/bundle_pack.js.map +1 -0
  241. package/dist/scripts/change/archive.js +68 -0
  242. package/dist/scripts/change/archive.js.map +1 -0
  243. package/dist/scripts/change/artifacts.js +143 -0
  244. package/dist/scripts/change/artifacts.js.map +1 -0
  245. package/dist/scripts/change/cli.js +39 -0
  246. package/dist/scripts/change/cli.js.map +1 -0
  247. package/dist/scripts/change/delta.js +20 -0
  248. package/dist/scripts/change/delta.js.map +1 -0
  249. package/dist/scripts/change/instructions.js +339 -0
  250. package/dist/scripts/change/instructions.js.map +1 -0
  251. package/dist/scripts/ci_init.js +12 -0
  252. package/dist/scripts/ci_init.js.map +1 -0
  253. package/dist/scripts/cli/commands/alm/PullCommand.js +238 -0
  254. package/dist/scripts/cli/commands/alm/PullCommand.js.map +1 -0
  255. package/dist/scripts/cli/commands/change/ChangeCommand.js +867 -0
  256. package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -0
  257. package/dist/scripts/cli/commands/config/CiInitCommand.js +144 -0
  258. package/dist/scripts/cli/commands/config/CiInitCommand.js.map +1 -0
  259. package/dist/scripts/cli/commands/config/CompletionCommand.js +160 -0
  260. package/dist/scripts/cli/commands/config/CompletionCommand.js.map +1 -0
  261. package/dist/scripts/cli/commands/config/ConfigInitCommand.js +108 -0
  262. package/dist/scripts/cli/commands/config/ConfigInitCommand.js.map +1 -0
  263. package/dist/scripts/cli/commands/config/ConfigSetCommand.js +166 -0
  264. package/dist/scripts/cli/commands/config/ConfigSetCommand.js.map +1 -0
  265. package/dist/scripts/cli/commands/config/GenAgentContractCommand.js +212 -0
  266. package/dist/scripts/cli/commands/config/GenAgentContractCommand.js.map +1 -0
  267. package/dist/scripts/cli/commands/harness/RunCommand.js +1922 -0
  268. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -0
  269. package/dist/scripts/cli/commands/mcp/McpInstallCommand.js +114 -0
  270. package/dist/scripts/cli/commands/mcp/McpInstallCommand.js.map +1 -0
  271. package/dist/scripts/cli/commands/mcp/index.js +29 -0
  272. package/dist/scripts/cli/commands/mcp/index.js.map +1 -0
  273. package/dist/scripts/cli/commands/pack/BundlePackCommand.js +59 -0
  274. package/dist/scripts/cli/commands/pack/BundlePackCommand.js.map +1 -0
  275. package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js +559 -0
  276. package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js.map +1 -0
  277. package/dist/scripts/cli/commands/pack/InferPackCommand.js +304 -0
  278. package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -0
  279. package/dist/scripts/cli/commands/pack/InitPackCommand.js +368 -0
  280. package/dist/scripts/cli/commands/pack/InitPackCommand.js.map +1 -0
  281. package/dist/scripts/cli/commands/pack/LintPackCommand.js +631 -0
  282. package/dist/scripts/cli/commands/pack/LintPackCommand.js.map +1 -0
  283. package/dist/scripts/cli/commands/pack/pack-args.js +83 -0
  284. package/dist/scripts/cli/commands/pack/pack-args.js.map +1 -0
  285. package/dist/scripts/cli/commands/project/AdoptCommand.js +414 -0
  286. package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -0
  287. package/dist/scripts/cli/commands/project/InitFromPackCommand.js +188 -0
  288. package/dist/scripts/cli/commands/project/InitFromPackCommand.js.map +1 -0
  289. package/dist/scripts/cli/commands/project/InitProjectCommand.js +791 -0
  290. package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -0
  291. package/dist/scripts/cli/commands/project/OnboardCommand.js +655 -0
  292. package/dist/scripts/cli/commands/project/OnboardCommand.js.map +1 -0
  293. package/dist/scripts/cli/commands/project/UpdateCommand.js +206 -0
  294. package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -0
  295. package/dist/scripts/cli/commands/project/WizardCommand.js +166 -0
  296. package/dist/scripts/cli/commands/project/WizardCommand.js.map +1 -0
  297. package/dist/scripts/cli/commands/quality/DoctorCommand.js +553 -0
  298. package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -0
  299. package/dist/scripts/cli/commands/quality/FixCommand.js +201 -0
  300. package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -0
  301. package/dist/scripts/cli/commands/quality/LicenseCheckCommand.js +187 -0
  302. package/dist/scripts/cli/commands/quality/LicenseCheckCommand.js.map +1 -0
  303. package/dist/scripts/cli/commands/quality/ReportCommand.js +557 -0
  304. package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -0
  305. package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +764 -0
  306. package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -0
  307. package/dist/scripts/cli/commands/spec/DoneCommand.js +116 -0
  308. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -0
  309. package/dist/scripts/cli/commands/spec/PlanCommand.js +397 -0
  310. package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -0
  311. package/dist/scripts/cli/commands/spec/ReqCommand.js +303 -0
  312. package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -0
  313. package/dist/scripts/cli/commands/spec/StatusCommand.js +206 -0
  314. package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -0
  315. package/dist/scripts/cli/commands/spec/StudioCommand.js +241 -0
  316. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -0
  317. package/dist/scripts/cli/commands/specops/ContributeCommand.js +313 -0
  318. package/dist/scripts/cli/commands/specops/ContributeCommand.js.map +1 -0
  319. package/dist/scripts/cli/commands/specops/DiffCommand.js +394 -0
  320. package/dist/scripts/cli/commands/specops/DiffCommand.js.map +1 -0
  321. package/dist/scripts/cli/commands/specops/SyncCommand.js +380 -0
  322. package/dist/scripts/cli/commands/specops/SyncCommand.js.map +1 -0
  323. package/dist/scripts/completion.js +24 -0
  324. package/dist/scripts/completion.js.map +1 -0
  325. package/dist/scripts/config_init.js +12 -0
  326. package/dist/scripts/config_init.js.map +1 -0
  327. package/dist/scripts/config_set.js +20 -0
  328. package/dist/scripts/config_set.js.map +1 -0
  329. package/dist/scripts/docs/blocks.js +137 -0
  330. package/dist/scripts/docs/blocks.js.map +1 -0
  331. package/dist/scripts/docs/build-site.js +425 -0
  332. package/dist/scripts/docs/build-site.js.map +1 -0
  333. package/dist/scripts/docs/nav.js +198 -0
  334. package/dist/scripts/docs/nav.js.map +1 -0
  335. package/dist/scripts/docs/record-terminal.js +477 -0
  336. package/dist/scripts/docs/record-terminal.js.map +1 -0
  337. package/dist/scripts/docs/terminal-storyboard.js +121 -0
  338. package/dist/scripts/docs/terminal-storyboard.js.map +1 -0
  339. package/dist/scripts/doctor.js +14 -0
  340. package/dist/scripts/doctor.js.map +1 -0
  341. package/dist/scripts/done.js +17 -0
  342. package/dist/scripts/done.js.map +1 -0
  343. package/dist/scripts/expand_domain_pack.js +18 -0
  344. package/dist/scripts/expand_domain_pack.js.map +1 -0
  345. package/dist/scripts/fix.js +12 -0
  346. package/dist/scripts/fix.js.map +1 -0
  347. package/dist/scripts/gen_agent_contract.js +14 -0
  348. package/dist/scripts/gen_agent_contract.js.map +1 -0
  349. package/dist/scripts/gen_mcp_tools.js +264 -0
  350. package/dist/scripts/gen_mcp_tools.js.map +1 -0
  351. package/dist/scripts/harness/init.js +358 -0
  352. package/dist/scripts/harness/init.js.map +1 -0
  353. package/dist/scripts/harness/prompt.js +18 -0
  354. package/dist/scripts/harness/prompt.js.map +1 -0
  355. package/dist/scripts/harness/report.js +323 -0
  356. package/dist/scripts/harness/report.js.map +1 -0
  357. package/dist/scripts/harness/run.js +33 -0
  358. package/dist/scripts/harness/run.js.map +1 -0
  359. package/dist/scripts/infer_pack.js +17 -0
  360. package/dist/scripts/infer_pack.js.map +1 -0
  361. package/dist/scripts/init_from_pack.js +16 -0
  362. package/dist/scripts/init_from_pack.js.map +1 -0
  363. package/dist/scripts/init_pack.js +16 -0
  364. package/dist/scripts/init_pack.js.map +1 -0
  365. package/dist/scripts/init_project.js +17 -0
  366. package/dist/scripts/init_project.js.map +1 -0
  367. package/dist/scripts/lib/agent.js +99 -0
  368. package/dist/scripts/lib/agent.js.map +1 -0
  369. package/dist/scripts/lib/capability-specs.js +80 -0
  370. package/dist/scripts/lib/capability-specs.js.map +1 -0
  371. package/dist/scripts/lib/command.js +17 -0
  372. package/dist/scripts/lib/command.js.map +1 -0
  373. package/dist/scripts/lib/diagnostics.js +62 -0
  374. package/dist/scripts/lib/diagnostics.js.map +1 -0
  375. package/dist/scripts/lib/monorepo-fanout.js +118 -0
  376. package/dist/scripts/lib/monorepo-fanout.js.map +1 -0
  377. package/dist/scripts/lib/placeholders.js +190 -0
  378. package/dist/scripts/lib/placeholders.js.map +1 -0
  379. package/dist/scripts/lib/project-root.js +124 -0
  380. package/dist/scripts/lib/project-root.js.map +1 -0
  381. package/dist/scripts/lib/requirement-graph.js +31 -0
  382. package/dist/scripts/lib/requirement-graph.js.map +1 -0
  383. package/dist/scripts/lib/surface.js +609 -0
  384. package/dist/scripts/lib/surface.js.map +1 -0
  385. package/dist/scripts/license_check.js +15 -0
  386. package/dist/scripts/license_check.js.map +1 -0
  387. package/dist/scripts/lint_pack.js +18 -0
  388. package/dist/scripts/lint_pack.js.map +1 -0
  389. package/dist/scripts/merge-traceability.js +106 -0
  390. package/dist/scripts/merge-traceability.js.map +1 -0
  391. package/dist/scripts/onboard.js +22 -0
  392. package/dist/scripts/onboard.js.map +1 -0
  393. package/dist/scripts/plan.js +17 -0
  394. package/dist/scripts/plan.js.map +1 -0
  395. package/dist/scripts/plugin/gate-hook.js +171 -0
  396. package/dist/scripts/plugin/gate-hook.js.map +1 -0
  397. package/dist/scripts/report.js +17 -0
  398. package/dist/scripts/report.js.map +1 -0
  399. package/dist/scripts/req.js +22 -0
  400. package/dist/scripts/req.js.map +1 -0
  401. package/dist/scripts/schema/cli.js +215 -0
  402. package/dist/scripts/schema/cli.js.map +1 -0
  403. package/dist/scripts/schema/registry.js +264 -0
  404. package/dist/scripts/schema/registry.js.map +1 -0
  405. package/dist/scripts/specops/add.js +180 -0
  406. package/dist/scripts/specops/add.js.map +1 -0
  407. package/dist/scripts/specops/against_lock.js +100 -0
  408. package/dist/scripts/specops/against_lock.js.map +1 -0
  409. package/dist/scripts/specops/as_change.js +49 -0
  410. package/dist/scripts/specops/as_change.js.map +1 -0
  411. package/dist/scripts/specops/contribute.js +24 -0
  412. package/dist/scripts/specops/contribute.js.map +1 -0
  413. package/dist/scripts/specops/diff.js +32 -0
  414. package/dist/scripts/specops/diff.js.map +1 -0
  415. package/dist/scripts/specops/lock.js +39 -0
  416. package/dist/scripts/specops/lock.js.map +1 -0
  417. package/dist/scripts/specops/manifest.js +90 -0
  418. package/dist/scripts/specops/manifest.js.map +1 -0
  419. package/dist/scripts/specops/remove.js +102 -0
  420. package/dist/scripts/specops/remove.js.map +1 -0
  421. package/dist/scripts/specops/sync.js +27 -0
  422. package/dist/scripts/specops/sync.js.map +1 -0
  423. package/dist/scripts/specops/verify.js +149 -0
  424. package/dist/scripts/specops/verify.js.map +1 -0
  425. package/dist/scripts/status.js +17 -0
  426. package/dist/scripts/status.js.map +1 -0
  427. package/dist/scripts/studio.js +14 -0
  428. package/dist/scripts/studio.js.map +1 -0
  429. package/dist/scripts/update.js +17 -0
  430. package/dist/scripts/update.js.map +1 -0
  431. package/dist/scripts/validate_specs.js +10 -0
  432. package/dist/scripts/validate_specs.js.map +1 -0
  433. package/examples/project.config.example +19 -0
  434. package/examples/project.yaml.example +17 -0
  435. package/package.json +105 -0
  436. package/templates/adopt/AI_RULES.md.tpl +27 -0
  437. package/templates/adopt/adr-readme.md.tpl +5 -0
  438. package/templates/adopt/baseline.feature.tpl +9 -0
  439. package/templates/adopt/spec.md.tpl +25 -0
  440. package/templates/adopt/traceability.md.tpl +11 -0
  441. package/templates/backend/AI_RULES.md.tpl +35 -0
  442. package/templates/backend/features/core/health.feature.tpl +7 -0
  443. package/templates/base/.devcontainer/devcontainer.json.tpl +18 -0
  444. package/templates/base/.dockerignore.tpl +11 -0
  445. package/templates/base/.env.dev.app.tpl +13 -0
  446. package/templates/base/.env.dev.infra.tpl +6 -0
  447. package/templates/base/.env.dev.tpl +16 -0
  448. package/templates/base/.env.example.tpl +17 -0
  449. package/templates/base/.env.feature.app.tpl +11 -0
  450. package/templates/base/.env.feature.infra.tpl +4 -0
  451. package/templates/base/.env.feature.tpl +16 -0
  452. package/templates/base/.env.prod.app.tpl +13 -0
  453. package/templates/base/.env.prod.infra.tpl +6 -0
  454. package/templates/base/.env.prod.tpl +16 -0
  455. package/templates/base/.github/workflows/specs.yml.tpl +26 -0
  456. package/templates/base/.gitignore.tpl +9 -0
  457. package/templates/base/.pre-commit-config.yaml.tpl +10 -0
  458. package/templates/base/AGENTS.md.tpl +32 -0
  459. package/templates/base/README.md.tpl +32 -0
  460. package/templates/base/docker-compose.yml.tpl +13 -0
  461. package/templates/base/docs/specs/adr/README.md.tpl +10 -0
  462. package/templates/base/docs/specs/aggregates.md.tpl +5 -0
  463. package/templates/base/docs/specs/commands.md.tpl +13 -0
  464. package/templates/base/docs/specs/domain-model.md.tpl +30 -0
  465. package/templates/base/docs/specs/events.md.tpl +5 -0
  466. package/templates/base/docs/specs/review-checklist.md.tpl +18 -0
  467. package/templates/base/docs/specs/runtime-environments.md.tpl +26 -0
  468. package/templates/base/docs/specs/status-model.md.tpl +16 -0
  469. package/templates/base/docs/specs/traceability.md.tpl +7 -0
  470. package/templates/base/docs/specs/use-cases.md.tpl +5 -0
  471. package/templates/base/spec.md.tpl +63 -0
  472. package/templates/ci/azure.yml.tpl +40 -0
  473. package/templates/ci/github.yml.tpl +40 -0
  474. package/templates/ci/gitlab.yml.tpl +21 -0
  475. package/templates/ci/jenkins.tpl +35 -0
  476. package/templates/contracts/AI_RULES.md.tpl +55 -0
  477. package/templates/contracts/asyncapi-stub.yaml.tpl +24 -0
  478. package/templates/contracts/contract-traceability.md.tpl +10 -0
  479. package/templates/contracts/openapi-stub.yaml.tpl +29 -0
  480. package/templates/contracts/pacts-gitkeep.tpl +0 -0
  481. package/templates/contracts/test-strategy.md.tpl +94 -0
  482. package/templates/frontend/AI_RULES.md.tpl +34 -0
  483. package/templates/frontend/features/core/health.feature.tpl +6 -0
  484. package/templates/harness/harness.config.yaml.tpl +74 -0
  485. package/templates/harness/prompt-prefix.md.tpl +60 -0
  486. package/templates/mobile/AI_RULES.md.tpl +56 -0
  487. package/templates/mobile/features/core/health.feature.tpl +7 -0
  488. package/templates/modules/auth/backend/registration_and_login.feature.tpl +10 -0
  489. package/templates/modules/auth/frontend/login.feature.tpl +5 -0
  490. package/templates/modules/billing/backend/billing_settlement.feature.tpl +5 -0
  491. package/templates/modules/billing/frontend/billing_summary.feature.tpl +5 -0
  492. package/templates/modules/dashboard/backend/dashboard_kpi.feature.tpl +5 -0
  493. package/templates/modules/dashboard/frontend/dashboard.feature.tpl +5 -0
  494. package/vendor/mermaid.min.js +3636 -0
@@ -0,0 +1,1922 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * `specgate harness run` — the spec-driven delivery loop for AI coding agents.
5
+ *
6
+ * A spec-driven repo is already an environment for an agent: `plan` is the
7
+ * task queue, the feature file + AI_RULES.md are the per-task context,
8
+ * `validate --strict-tdd` + the project test command are the reward signal,
9
+ * and `done` is the state transition. This command is the missing
10
+ * orchestration layer — it runs plan → context → agent → verify → done
11
+ * without a human copy-pasting prompts.
12
+ *
13
+ * For each pending requirement, in an isolated `git worktree` on a fresh
14
+ * `harness/REQ-NNN` branch:
15
+ * 1. Build a self-contained prompt (Gherkin + AI_RULES + paths + retry feedback).
16
+ * 2. Shell out to the user-configured agent ({prompt_file} placeholder).
17
+ * 3. Gate: `validate --strict-tdd` + the project test command.
18
+ * 4. Green → `done REQ-NNN` + commit. Red → retry N times feeding the failure.
19
+ * 5. Emit a pass/fail/attempts report.
20
+ *
21
+ * Vendor-neutral by construction: the agent is any shell command. The
22
+ * harness never merges a branch — a human reviews and merges.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.RunCommand = exports.RUNS_DIR = void 0;
59
+ exports.setJsonMode = setJsonMode;
60
+ exports.error = error;
61
+ exports.parseArgs = parseArgs;
62
+ exports.printReport = printReport;
63
+ exports.runLevels = runLevels;
64
+ const fs = __importStar(require("node:fs"));
65
+ const os = __importStar(require("node:os"));
66
+ const path = __importStar(require("node:path"));
67
+ const crypto = __importStar(require("node:crypto"));
68
+ const node_child_process_1 = require("node:child_process");
69
+ const command_1 = require("../../../lib/command");
70
+ const project_root_1 = require("../../../lib/project-root");
71
+ const prompt_1 = require("../../../harness/prompt");
72
+ const GherkinQuality_1 = require("../../../../packages/core/src/domain/GherkinQuality");
73
+ const HarnessConfigFile_1 = require("../../../../packages/core/src/infrastructure/HarnessConfigFile");
74
+ const agent_1 = require("../../../lib/agent");
75
+ const WriteScope_1 = require("../../../../packages/core/src/domain/WriteScope");
76
+ const EmptyAttempt_1 = require("../../../../packages/core/src/domain/EmptyAttempt");
77
+ const DeclaredArtifacts_1 = require("../../../../packages/core/src/domain/DeclaredArtifacts");
78
+ const ResumeState_1 = require("../../../../packages/core/src/domain/ResumeState");
79
+ const RequirementReadiness_1 = require("../../../../packages/core/src/domain/RequirementReadiness");
80
+ const ProfileMatch_1 = require("../../../../packages/core/src/domain/ProfileMatch");
81
+ const TraceabilityFormat_1 = require("../../../../packages/core/src/domain/TraceabilityFormat");
82
+ const RequirementPlan_1 = require("../../../../packages/core/src/domain/RequirementPlan");
83
+ const RunBudget_1 = require("../../../../packages/core/src/domain/RunBudget");
84
+ const CucumberMessages_1 = require("../../../../packages/core/src/domain/CucumberMessages");
85
+ const HarnessRun_1 = require("../../../../packages/core/src/domain/HarnessRun");
86
+ // Three levels up from dist/scripts/cli/commands/harness is dist/scripts,
87
+ // where the command entry points live.
88
+ const SCRIPTS_DIR = path.join(__dirname, "..", "..", "..");
89
+ const PLAN_SCRIPT = path.join(SCRIPTS_DIR, "plan.js");
90
+ const DONE_SCRIPT = path.join(SCRIPTS_DIR, "done.js");
91
+ const VALIDATE_SCRIPT = path.join(SCRIPTS_DIR, "validate_specs.js");
92
+ /**
93
+ * What a worker process runs.
94
+ *
95
+ * It must be the file the command registry spawns, not this module: this one
96
+ * defines the command but does not execute it, so pointing a worker at
97
+ * `__filename` starts a process that loads, does nothing and exits 0 — which
98
+ * the parent reports as "Worker produced no report".
99
+ */
100
+ const WORKER_ENTRY = path.join(SCRIPTS_DIR, "harness", "run.js");
101
+ /**
102
+ * In `--format json` mode stdout carries exactly one JSON document and nothing
103
+ * else — rule 1 of the agent contract (ADR-0017). It did not: progress lines
104
+ * went to stdout alongside the report, so
105
+ * `harness run --format json 2>/dev/null | jq .` did not parse. Nobody noticed
106
+ * because nothing parsed it until the worker pool did.
107
+ */
108
+ let jsonMode = false;
109
+ function setJsonMode(on) {
110
+ jsonMode = Boolean(on);
111
+ }
112
+ function info(msg) {
113
+ (jsonMode ? process.stderr : process.stdout).write(`ℹ️ [harness] ${msg}\n`);
114
+ }
115
+ function warn(msg) {
116
+ (jsonMode ? process.stderr : process.stdout).write(`⚠️ [harness] ${msg}\n`);
117
+ }
118
+ function error(msg) {
119
+ process.stderr.write(`❌ [harness] ${msg}\n`);
120
+ }
121
+ function usage() {
122
+ process.stdout.write("Usage:\n" +
123
+ " specgate harness run [options]\n\n" +
124
+ "Runs the plan → agent → verify → done loop for every pending requirement.\n\n" +
125
+ " --agent <cmd> Agent command; must contain the {prompt_file} placeholder.\n" +
126
+ ' e.g. --agent "claude -p < {prompt_file}"\n' +
127
+ " --test-cmd <cmd> Project test command run as part of the gate (optional).\n" +
128
+ " Substitutes {req}, {scenario} and {feature_file}, so the\n" +
129
+ " gate can run the scenario under test.\n" +
130
+ " --max-attempts <n> Retries per requirement, feeding back the failure (default 3).\n" +
131
+ " --concurrency <n> Requirements in flight at once (default 1). Only requirements\n" +
132
+ " that do not depend on each other ever run together.\n" +
133
+ " --req <REQ-NNN> Limit to specific requirement(s); repeatable.\n" +
134
+ " --project-dir <path> Project root (auto-detected from cwd if omitted).\n" +
135
+ " --base-branch <ref> Branch/ref each worktree is cut from (default: current HEAD).\n" +
136
+ " --timeout <seconds> Per-agent-invocation timeout (default 1200).\n" +
137
+ " --keep-worktrees Do not remove worktrees after each requirement.\n" +
138
+ " --force Recreate harness/REQ-NNN branches that already exist.\n" +
139
+ " --format <text|json> Report format (default text).\n" +
140
+ " --dry-run Build prompts and print them; never invoke the agent.\n\n" +
141
+ "CI mode (unattended runners — a nightly job that leaves PRs to review):\n" +
142
+ " --resume Continue an interrupted run: reuse the existing\n" +
143
+ " harness/REQ-NNN branch and worktree instead of\n" +
144
+ " skipping it, and pick up at the attempt it stopped on.\n" +
145
+ " --budget-seconds <n> Wall-clock ceiling for the whole run. On reaching it\n" +
146
+ " the run stops cleanly and still reports what it did.\n" +
147
+ " --max-requirements <n> Attempt at most n requirements in this run.\n" +
148
+ " --skip-not-ready Skip requirements an agent could not succeed at — no\n" +
149
+ " feature, an unrunnable scenario, unmet dependencies,\n" +
150
+ " Deprecated or Needs Clarification. Default: warn and\n" +
151
+ " run them anyway.\n" +
152
+ " --strict-artifacts Fail an attempt whose diff never touches the paths the\n" +
153
+ " matrix declares for the requirement (default: warn).\n" +
154
+ " --push Push each green harness/REQ-NNN branch to the remote.\n" +
155
+ " --remote <name> Remote to push to (default origin).\n" +
156
+ " --pr-cmd <cmd> Command run after a successful push, with {branch} and\n" +
157
+ ' {req} placeholders. e.g. --pr-cmd "gh pr create --head {branch} \\\n' +
158
+ " --title '{req} via harness' --fill\"\n\n" +
159
+ "`--agent`, `--test-cmd`, `push`, `remote` and `pr_cmd` may also be set in harness.config.yaml.\n");
160
+ }
161
+ function parseArgs(argv) {
162
+ const args = {
163
+ projectDir: ".",
164
+ agent: "",
165
+ testCmd: "",
166
+ maxAttempts: 0,
167
+ concurrency: 0,
168
+ reqs: [],
169
+ baseBranch: "",
170
+ // 600 was the original guess. Both real runs disproved it: the first
171
+ // REQ-001 attempt hit 900s while the agent installed dependencies and
172
+ // worked, and 1500 was needed comfortably. A default that times out on
173
+ // ordinary work turns every first attempt into a wasted one.
174
+ timeout: 1200,
175
+ keepWorktrees: false,
176
+ force: false,
177
+ resume: false,
178
+ format: "text",
179
+ dryRun: false,
180
+ strictArtifacts: false,
181
+ skipNotReady: false,
182
+ budgetSeconds: 0,
183
+ maxRequirements: 0,
184
+ push: false,
185
+ remote: "",
186
+ prCmd: "",
187
+ };
188
+ for (let i = 0; i < argv.length; i += 1) {
189
+ const token = argv[i];
190
+ if (token === "--project-dir") {
191
+ args.projectDir = argv[++i] || "";
192
+ }
193
+ else if (token === "--agent") {
194
+ args.agent = argv[++i] || "";
195
+ }
196
+ else if (token === "--test-cmd") {
197
+ args.testCmd = argv[++i] || "";
198
+ }
199
+ else if (token === "--max-attempts") {
200
+ const n = Number(argv[++i]);
201
+ if (!Number.isInteger(n) || n < 1) {
202
+ throw new Error("--max-attempts must be a positive integer");
203
+ }
204
+ args.maxAttempts = n;
205
+ }
206
+ else if (token === "--concurrency") {
207
+ const n = Number(argv[++i]);
208
+ if (!Number.isInteger(n) || n < 1) {
209
+ throw new Error("--concurrency must be a positive integer");
210
+ }
211
+ args.concurrency = n;
212
+ }
213
+ else if (token === "--req") {
214
+ const r = argv[++i] || "";
215
+ if (!/^REQ-\d+$/.test(r))
216
+ throw new Error(`--req expects REQ-NNN, got: ${r}`);
217
+ args.reqs.push(r);
218
+ }
219
+ else if (token === "--base-branch") {
220
+ args.baseBranch = argv[++i] || "";
221
+ }
222
+ else if (token === "--timeout") {
223
+ const n = Number(argv[++i]);
224
+ if (!Number.isInteger(n) || n < 1)
225
+ throw new Error("--timeout must be a positive integer");
226
+ args.timeout = n;
227
+ }
228
+ else if (token === "--push") {
229
+ args.push = true;
230
+ }
231
+ else if (token === "--remote") {
232
+ args.remote = argv[++i] || "";
233
+ }
234
+ else if (token === "--pr-cmd") {
235
+ args.prCmd = argv[++i] || "";
236
+ }
237
+ else if (token === "--keep-worktrees") {
238
+ args.keepWorktrees = true;
239
+ }
240
+ else if (token === "--force") {
241
+ args.force = true;
242
+ }
243
+ else if (token === "--resume") {
244
+ args.resume = true;
245
+ }
246
+ else if (token === "--format") {
247
+ args.format = argv[++i] || "";
248
+ }
249
+ else if (token === "--dry-run") {
250
+ args.dryRun = true;
251
+ }
252
+ else if (token === "--strict-artifacts") {
253
+ args.strictArtifacts = true;
254
+ }
255
+ else if (token === "--skip-not-ready") {
256
+ args.skipNotReady = true;
257
+ }
258
+ else if (token === "--budget-seconds") {
259
+ const n = Number(argv[++i]);
260
+ if (!Number.isInteger(n) || n < 1) {
261
+ throw new Error("--budget-seconds must be a positive integer");
262
+ }
263
+ args.budgetSeconds = n;
264
+ }
265
+ else if (token === "--max-requirements") {
266
+ const n = Number(argv[++i]);
267
+ if (!Number.isInteger(n) || n < 1) {
268
+ throw new Error("--max-requirements must be a positive integer");
269
+ }
270
+ args.maxRequirements = n;
271
+ }
272
+ else if (token === "--help" || token === "-h") {
273
+ usage();
274
+ process.exit(0);
275
+ }
276
+ else {
277
+ throw new Error(`Unknown argument: ${token}`);
278
+ }
279
+ }
280
+ // `--force` throws the branch away; `--resume` continues it. Asking for both
281
+ // is asking for two opposite things, and silently picking one is how work
282
+ // gets deleted after an interruption — the exact loss --resume exists to stop.
283
+ if (args.force && args.resume) {
284
+ throw new Error("--force and --resume are opposites: --force deletes the existing branch, " +
285
+ "--resume continues it. Pick one.");
286
+ }
287
+ if (!["text", "json"].includes(args.format)) {
288
+ throw new Error(`Invalid --format: ${args.format}. Expected: text | json.`);
289
+ }
290
+ return args;
291
+ }
292
+ function git(projectDir, gitArgs, opts = {}) {
293
+ return (0, node_child_process_1.spawnSync)("git", ["-C", projectDir, ...gitArgs], { encoding: "utf8", ...opts });
294
+ }
295
+ function isGitClean(projectDir) {
296
+ const r = git(projectDir, ["status", "--porcelain"]);
297
+ if (r.status !== 0) {
298
+ throw new Error(`git status failed: ${r.stderr || r.stdout}`);
299
+ }
300
+ return r.stdout.trim() === "";
301
+ }
302
+ function branchExists(projectDir, branch) {
303
+ const r = git(projectDir, ["branch", "--list", branch]);
304
+ return r.status === 0 && r.stdout.trim() !== "";
305
+ }
306
+ function runPlan(projectDir) {
307
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [PLAN_SCRIPT, "--project-dir", projectDir, "--format", "json"], {
308
+ encoding: "utf8",
309
+ });
310
+ if (r.status !== 0) {
311
+ throw new Error(`plan failed:\n${r.stderr || r.stdout}`);
312
+ }
313
+ let parsed;
314
+ try {
315
+ parsed = JSON.parse(r.stdout);
316
+ }
317
+ catch (err) {
318
+ throw new Error(`plan produced invalid JSON: ${err.message}`);
319
+ }
320
+ return parsed;
321
+ }
322
+ // Generous maxBuffer for captured subprocess output — Maven/Gradle first runs
323
+ // can easily produce >1 MB of dependency-download log, and `spawnSync`'s
324
+ // default 1 MB ceiling otherwise kills the gate with ENOBUFS.
325
+ const SUBPROCESS_MAX_BUFFER = 64 * 1024 * 1024;
326
+ /** Run the gate (validate --strict-tdd, then the optional test command). */
327
+ /**
328
+ * What Cucumber says it did, when it can be asked (F5).
329
+ *
330
+ * The gate's question has always been "did the command exit zero?", and H14
331
+ * showed what that misses: `1 scenario (1 passed) · 0 steps · exit 0` is a
332
+ * scenario that tested nothing and reported success. Cucumber has published a
333
+ * machine-readable channel for years; this reads it.
334
+ *
335
+ * Two ways in, and neither guesses:
336
+ *
337
+ * - `message_report` in `harness.config.yaml` — the project writes the stream
338
+ * wherever it likes and says where. Works for any command.
339
+ * - a **direct** `cucumber-js` invocation, which the harness appends
340
+ * `--format message:<tmp>` to. Deliberately narrow: `npm test` may well run
341
+ * Cucumber and there is no way to know from here, so it is left alone.
342
+ *
343
+ * Returns `null` when there is nothing to read — a project that does not use
344
+ * Cucumber must not be failed by a check that never applied.
345
+ */
346
+ function readGateMessages(worktreeDir, reportPath) {
347
+ if (!reportPath)
348
+ return null;
349
+ const file = path.isAbsolute(reportPath) ? reportPath : path.join(worktreeDir, reportPath);
350
+ let raw;
351
+ try {
352
+ raw = fs.readFileSync(file, "utf8");
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ const run = (0, CucumberMessages_1.parseCucumberMessages)(raw);
358
+ return run.parsed ? run : null;
359
+ }
360
+ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}) {
361
+ const validate = (0, node_child_process_1.spawnSync)(process.execPath, [VALIDATE_SCRIPT, worktreeDir, "--strict-tdd"], {
362
+ encoding: "utf8",
363
+ timeout: timeoutMs,
364
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
365
+ });
366
+ if (validate.status !== 0) {
367
+ return {
368
+ ok: false,
369
+ stage: "validate --strict-tdd",
370
+ output: validate.stdout + validate.stderr,
371
+ hint: "",
372
+ };
373
+ }
374
+ if (testCmd) {
375
+ let resolved = (0, HarnessRun_1.substituteGateCommand)(testCmd, req);
376
+ // Where the message stream will be, if there is one to have.
377
+ let reportPath = settings.messageReport || "";
378
+ if (!reportPath && (0, CucumberMessages_1.invokesCucumberDirectly)(resolved)) {
379
+ reportPath = path.join(".harness", `messages-${req.requirement || "run"}-${crypto.randomBytes(3).toString("hex")}.ndjson`);
380
+ fs.mkdirSync(path.join(worktreeDir, ".harness"), { recursive: true });
381
+ resolved = (0, CucumberMessages_1.withMessageReport)(resolved, reportPath);
382
+ }
383
+ // A fresh worktree carries only what git tracks, so a project with
384
+ // dependencies has no node_modules here. Verified the hard way: an agent
385
+ // spent its first attempt installing them and timed out. The gate command
386
+ // is the right place to say so, since only the project knows how.
387
+ const test = (0, node_child_process_1.spawnSync)(resolved, {
388
+ shell: true,
389
+ cwd: worktreeDir,
390
+ encoding: "utf8",
391
+ timeout: timeoutMs,
392
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
393
+ });
394
+ if (test.status !== 0) {
395
+ // Name the command. A gate that silently does the wrong thing — running
396
+ // the whole suite because a filter did not apply, say — produces a
397
+ // failure indistinguishable from a real one, and the operator has no way
398
+ // to tell without the command in front of them.
399
+ return {
400
+ ok: false,
401
+ stage: `test command: ${resolved}`,
402
+ output: test.stdout + test.stderr,
403
+ };
404
+ }
405
+ // Exit zero is where the old gate stopped. This is the part that catches a
406
+ // suite that passed without covering the requirement at all.
407
+ const messages = readGateMessages(worktreeDir, reportPath);
408
+ if (messages) {
409
+ const findings = (0, CucumberMessages_1.checkGateRun)(messages, {
410
+ requirement: req.requirement,
411
+ scenarioId: req.scenarioId || req.scenario_id,
412
+ featureFile: (0, HarnessRun_1.featureFilePath)(req),
413
+ });
414
+ const errors = findings.filter((f) => f.severity === "error");
415
+ const warnings = findings.filter((f) => f.severity !== "error");
416
+ if (errors.length > 0) {
417
+ return {
418
+ ok: false,
419
+ stage: "cucumber messages",
420
+ output: `The test command exited 0, but the run it reported does not support ` +
421
+ `that verdict:\n\n` +
422
+ errors.map((e) => ` ${e.message}\n fix: ${e.fix}`).join("\n\n"),
423
+ };
424
+ }
425
+ if (warnings.length > 0) {
426
+ return { ok: true, stage: "", output: "", hint: warnings[0].message };
427
+ }
428
+ }
429
+ }
430
+ return { ok: true, stage: "", output: "", hint: "" };
431
+ }
432
+ /**
433
+ * Undo whatever an advisory agent touched.
434
+ *
435
+ * A reviewer returns text. If it also edited files, those edits would be gated
436
+ * and committed as if the implementer had made them, and nobody would know
437
+ * which agent wrote what. Discarding is cheaper to reason about than trusting
438
+ * the reviewer to behave.
439
+ */
440
+ /**
441
+ * Keep a copy of exactly what each agent was given.
442
+ *
443
+ * It goes in the *worktree*, so `git add -A` commits it with the work and it
444
+ * arrives in the branch a human reviews — which is the point — while the main
445
+ * tree stays clean. It used to be written to the project directory, which
446
+ * dirtied it and blocked the next run.
447
+ *
448
+ * The file name carries the role, so an attempt that ran a reviewer and then an
449
+ * implementer leaves two files and it is obvious which prompt went to whom.
450
+ *
451
+ * Best-effort: never fail a run over a bookkeeping write.
452
+ */
453
+ function archivePrompt(worktreeDir, requirement, attempt, step, prompt) {
454
+ try {
455
+ const dir = path.join(worktreeDir, ...EmptyAttempt_1.PROMPT_ARCHIVE_DIR.split("/"));
456
+ fs.mkdirSync(dir, { recursive: true });
457
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
458
+ const role = step.profile || (step.advisory ? "reviewer" : "agent");
459
+ fs.writeFileSync(path.join(dir, `${requirement}-${ts}-attempt-${attempt}-${role}.md`), prompt, "utf8");
460
+ }
461
+ catch {
462
+ /* never fail the run on an audit-log write */
463
+ }
464
+ }
465
+ function discardWorktreeChanges(worktreeDir) {
466
+ git(worktreeDir, ["checkout", "--", "."]);
467
+ // The prompt archive is untracked, so an unqualified `clean -fd` deletes it —
468
+ // taking with it the record of what the reviewer and every earlier attempt
469
+ // were given. Auditability is the reason the archive exists; discarding the
470
+ // reviewer's edits must not discard the evidence.
471
+ git(worktreeDir, ["clean", "-fd", "-e", EmptyAttempt_1.PROMPT_ARCHIVE_DIR]);
472
+ }
473
+ /**
474
+ * Did the agent edit the contract it was being judged against? (A1, closes H16)
475
+ *
476
+ * The prompt asks it not to; nothing checked. An agent that cannot make the
477
+ * scenario pass can relax the scenario, or add a permissive line to
478
+ * `AI_RULES.md`, and the gate approves — `validate --strict-tdd` verifies that
479
+ * the feature exists and is in the matrix, never that it still says what it
480
+ * said. "Specs as executable contracts" stops being true the moment the
481
+ * executor may edit the contract.
482
+ *
483
+ * Run **before** the gate, not after: once the scenario has been loosened, a
484
+ * green gate means nothing, so there is no point asking it.
485
+ *
486
+ * The diff of the offending paths goes back into the next attempt's prompt.
487
+ * That is not politeness — in practice the agent almost always did it without
488
+ * meaning to, and being shown the hunk is what corrects it. A refusal with no
489
+ * evidence just gets repeated.
490
+ */
491
+ /**
492
+ * What the agent wrote, as git sees it.
493
+ *
494
+ * `-uall` is not optional. Plain `--porcelain` collapses a wholly new directory
495
+ * into a single `?? src/` entry, so `src/Health.java` never appears and any
496
+ * check reading it concludes the file was never written. Found by an agent that
497
+ * had correctly created both declared artifacts and was reported as having
498
+ * touched neither.
499
+ */
500
+ function worktreeChanges(worktreeDir) {
501
+ const status = git(worktreeDir, ["status", "--porcelain", "-uall"]);
502
+ if (status.status !== 0)
503
+ return null;
504
+ return (0, WriteScope_1.parseGitStatus)(status.stdout || "");
505
+ }
506
+ /**
507
+ * Did the agent write anything at all? (H19)
508
+ *
509
+ * The gate runs *before* `specgate done`, so at gate time the requirement is still
510
+ * `Draft` — and `--strict-tdd`'s "no Test Artifact = TBD past Draft" rule does
511
+ * not apply to a Draft row. `done` then flips the status to `Implemented` and
512
+ * nothing validates again. The consequence, reproduced on 2026-08-26 with
513
+ * `--agent "cat {prompt_file} > /dev/null"` against a freshly generated
514
+ * project: **an agent that wrote zero files reported `pass (1 attempt)`**, with
515
+ * the row moved to `Implemented` and its Test artifact still `TBD`.
516
+ *
517
+ * `--strict-artifacts` cannot catch this either: it compares the diff against
518
+ * the paths the row declares, and a row that declares none has nothing to
519
+ * compare.
520
+ *
521
+ * So the harness asks the one question that needs no declaration and has no
522
+ * legitimate negative answer: did anything change? An agent that produced no
523
+ * files cannot have implemented a requirement, whatever the gate says about the
524
+ * documents around it. That makes this a hard failure rather than an opt-in
525
+ * flag, under the rule ADR-0023 sets out — a content check is a gate only when
526
+ * failing it is always a defect, and this one is.
527
+ *
528
+ * The prompt archive does not count. The harness writes it itself, so counting
529
+ * it would mean the harness's own bookkeeping vouches for the agent.
530
+ *
531
+ * Run **before** the gate, for the same reason `checkWriteScopeInWorktree` is:
532
+ * a green gate over an empty diff proves nothing, so there is no point asking
533
+ * it. It is also the cheaper order — an empty attempt stops before the project's
534
+ * test command runs.
535
+ */
536
+ function producedNothing(worktreeDir) {
537
+ return (0, EmptyAttempt_1.isEmptyAttempt)(touchedPaths(worktreeDir));
538
+ }
539
+ function checkWriteScopeInWorktree(worktreeDir, settings) {
540
+ const changes = worktreeChanges(worktreeDir);
541
+ if (!changes)
542
+ return null;
543
+ const violations = (0, WriteScope_1.checkWriteScope)(changes, {
544
+ protectedPaths: settings.protectedPaths.length ? settings.protectedPaths : undefined,
545
+ allowPaths: settings.allowPaths,
546
+ });
547
+ if (violations.length === 0)
548
+ return null;
549
+ const diff = git(worktreeDir, ["diff", "--", ...violations.map((v) => v.path)]);
550
+ const evidence = (diff.stdout || "").trim();
551
+ return {
552
+ violations,
553
+ message: `agent_touched_protected_path — the agent modified ${violations.length} file(s) it is ` +
554
+ `not allowed to change:\n` +
555
+ violations.map((v) => ` ${v.path} (protected by \`${v.pattern}\`)`).join("\n") +
556
+ `\n\nThese files are the contract this requirement is judged against. Editing them ` +
557
+ `makes a passing gate meaningless. Revert them and satisfy the scenario as written.\n` +
558
+ (evidence ? `\nWhat was changed:\n\n\`\`\`diff\n${evidence}\n\`\`\`\n` : "") +
559
+ `\nIf the change is genuinely required, it belongs in a spec change ` +
560
+ `(\`specgate change new\`), reviewed by a person — not in this attempt.`,
561
+ };
562
+ }
563
+ /**
564
+ * Every path the agent wrote in this worktree — added and modified alike.
565
+ *
566
+ * Shared with the write-scope guard, which is why A2 costs almost nothing: the
567
+ * diff was already being read.
568
+ */
569
+ function touchedPaths(worktreeDir) {
570
+ const changes = worktreeChanges(worktreeDir);
571
+ return changes ? [...changes.modified, ...changes.added] : [];
572
+ }
573
+ /**
574
+ * Did the green diff touch what the matrix said it would? (A2)
575
+ *
576
+ * The row declares `test_artifact` and `technical_artifact` and the prompt
577
+ * hands both to the agent, but nothing checked the diff contained them. An
578
+ * agent can implement somewhere else, pass the scenario, and leave the matrix
579
+ * pointing at a file where the logic does not live — the documentary lie
580
+ * `AI_RULES.md` forbids this repository.
581
+ *
582
+ * Run after a **green** gate, because the claim being checked is about a diff
583
+ * that already works. A red attempt has a more urgent problem.
584
+ *
585
+ * A warning by default. An implementation can legitimately land in a shared
586
+ * module that already exists, and a hard failure there would be the kind of
587
+ * gate that rejects good work — which already cost two runs on REQ-002.
588
+ * `--strict-artifacts` is for a project that wants it enforced.
589
+ */
590
+ function checkDeclaredArtifactsInWorktree(worktreeDir, req, strict) {
591
+ const findings = (0, DeclaredArtifacts_1.checkDeclaredArtifacts)({
592
+ touched: touchedPaths(worktreeDir),
593
+ testArtifact: req.testArtifact || req.test_artifact,
594
+ technicalArtifact: req.technicalArtifact || req.technical_artifact,
595
+ requirement: req.requirement,
596
+ }, strict);
597
+ return findings.length > 0 ? findings : null;
598
+ }
599
+ function attemptRequirement(req, ctx) {
600
+ const { worktreeDir, settings, timeoutMs, hint } = ctx;
601
+ /**
602
+ * What the gate said last time, recovered from the prompt archive when this
603
+ * is a resumed run. Without it the agent starts the resumed attempt blind and
604
+ * repeats whatever it already got wrong.
605
+ */
606
+ let previousFailure = (ctx.resumeAt && ctx.resumeAt.previousFailure) || "";
607
+ /**
608
+ * What each attempt cost and where it ended.
609
+ *
610
+ * Wall-clock, because it is the one cost the harness can measure without the
611
+ * agent's cooperation: an agent is any shell command, and only the agent
612
+ * knows what it spent in tokens. `specgate harness report` is built on this.
613
+ */
614
+ const attemptLog = [];
615
+ /** The reviewer's findings from the previous attempt, fed into this prompt. */
616
+ let reviewFindings = "";
617
+ const firstAttempt = Math.max(1, (ctx.resumeAt && ctx.resumeAt.attempt) || 1);
618
+ for (let attempt = firstAttempt; attempt <= settings.maxAttempts; attempt += 1) {
619
+ info(`${req.requirement}: attempt ${attempt}/${settings.maxAttempts}`);
620
+ const attemptStart = Date.now();
621
+ let agentMs = 0;
622
+ /** Which roles ran this attempt, in order — the run record's answer to "who did this". */
623
+ const profilesUsed = [];
624
+ const record = (stage) => {
625
+ attemptLog.push({
626
+ attempt,
627
+ endedAt: stage,
628
+ agentMs,
629
+ totalMs: Date.now() - attemptStart,
630
+ profiles: [...profilesUsed],
631
+ });
632
+ };
633
+ // An attempt is a sequence of agent invocations, all bound to this one
634
+ // requirement. Attempt 1 is a single implementing step; a retry may run an
635
+ // advisory reviewer first, whose findings feed the step that follows.
636
+ const steps = (0, HarnessRun_1.planAttempt)(attempt, {
637
+ attemptProfiles: settings.attemptProfiles,
638
+ reviewProfile: settings.reviewProfile || null,
639
+ });
640
+ let stepFailed = false;
641
+ try {
642
+ for (const step of steps) {
643
+ // Built per step, not per attempt: the reviewer runs first and the
644
+ // implementing step that follows must see what it said. Building once
645
+ // up front meant findings only ever reached the *next* attempt, which
646
+ // is not what a reviewer is for.
647
+ const prompt = (0, prompt_1.buildPrompt)(req, worktreeDir, {
648
+ promptPrefix: settings.promptPrefix,
649
+ hint,
650
+ previousFailure: previousFailure || undefined,
651
+ reviewFindings: step.advisory ? undefined : reviewFindings || undefined,
652
+ attempt,
653
+ maxAttempts: settings.maxAttempts,
654
+ });
655
+ const promptFile = path.join(os.tmpdir(), `csda-harness-prompt-${req.requirement}-${crypto.randomBytes(4).toString("hex")}.md`);
656
+ fs.writeFileSync(promptFile, prompt, "utf8");
657
+ archivePrompt(worktreeDir, req.requirement, attempt, step, prompt);
658
+ // D1: a profile chosen by matching this requirement, when one matches.
659
+ // The role ladder still wins — `attempt_profiles` names a profile for
660
+ // *this step*, and a step that says which role it is has already
661
+ // answered the question.
662
+ const profile = step.profile || ctx.matchedProfile || null;
663
+ const agentCommand = profile
664
+ ? settings.profileAgents[profile] || settings.agent
665
+ : settings.agent;
666
+ const command = (0, HarnessRun_1.substituteAgentCommand)(agentCommand, promptFile);
667
+ if (profile) {
668
+ info(`${req.requirement}: ${step.advisory ? "reviewing" : "running"} as '${profile}'`);
669
+ }
670
+ const agentStart = Date.now();
671
+ const agent = (0, node_child_process_1.spawnSync)(command, {
672
+ shell: true,
673
+ cwd: worktreeDir,
674
+ encoding: "utf8",
675
+ timeout: timeoutMs,
676
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
677
+ stdio: ["ignore", "pipe", "pipe"],
678
+ });
679
+ // Node reports a timeout as an errno-carrying Error; the base `Error`
680
+ // type the spawnSync signature declares does not have `code`.
681
+ agentMs += Date.now() - agentStart;
682
+ profilesUsed.push(profile ?? (step.advisory ? "reviewer" : "agent"));
683
+ fs.rmSync(promptFile, { force: true });
684
+ const agentError = agent.error;
685
+ if (agentError?.code === "ETIMEDOUT") {
686
+ if (step.advisory) {
687
+ warn(`${req.requirement}: reviewer timed out — continuing without findings`);
688
+ continue;
689
+ }
690
+ previousFailure = `Agent timed out after ${ctx.timeoutSeconds}s.`;
691
+ warn(`${req.requirement}: agent timed out`);
692
+ record("agent-timeout");
693
+ stepFailed = true;
694
+ break;
695
+ }
696
+ if (step.advisory) {
697
+ // The reviewer advises and nothing more. Its findings become input to
698
+ // the next prompt; anything it wrote is discarded, so it cannot reach
699
+ // the gate. That is the line between this and a committee — the gate
700
+ // stays the only judge, and a reviewer can never approve.
701
+ reviewFindings = `${agent.stdout || ""}${agent.stderr || ""}`.trim();
702
+ discardWorktreeChanges(worktreeDir);
703
+ if (agent.status !== 0) {
704
+ warn(`${req.requirement}: reviewer exited ${agent.status} — findings may be partial`);
705
+ }
706
+ continue;
707
+ }
708
+ if (agent.status !== 0) {
709
+ previousFailure = `Agent exited ${agent.status}.\n${agent.stdout || ""}${agent.stderr || ""}`;
710
+ warn(`${req.requirement}: agent exited ${agent.status}`);
711
+ record("agent-error");
712
+ stepFailed = true;
713
+ break;
714
+ }
715
+ }
716
+ }
717
+ finally {
718
+ /* each step removes its own prompt file */
719
+ }
720
+ if (stepFailed)
721
+ continue;
722
+ // Before the gate: an agent that wrote nothing cannot have implemented
723
+ // anything, and the gate would approve it. See `producedNothing` (H19).
724
+ if (producedNothing(worktreeDir)) {
725
+ previousFailure =
726
+ `You produced no files. Nothing in the worktree changed, so there is ` +
727
+ `nothing implementing ${req.requirement}.\n\n` +
728
+ `If you could not write, that is the problem to report — a non-interactive ` +
729
+ `agent without tool permissions reads the prompt and cannot act on it. ` +
730
+ `See docs/automation.md.`;
731
+ warn(`${req.requirement}: the agent produced no files — nothing to gate`);
732
+ warn(` fix: check the agent's write permissions (see docs/automation.md)`);
733
+ record("no-op");
734
+ continue;
735
+ }
736
+ // Before the gate: a scenario the agent has just loosened cannot fail, so a
737
+ // green gate proves nothing. See `checkWriteScopeInWorktree`.
738
+ const scope = checkWriteScopeInWorktree(worktreeDir, settings);
739
+ if (scope) {
740
+ previousFailure = scope.message;
741
+ warn(`${req.requirement}: ${scope.violations.length} protected path(s) modified`);
742
+ for (const v of scope.violations)
743
+ warn(` ${v.path} (${v.pattern})`);
744
+ record("write-scope");
745
+ continue;
746
+ }
747
+ const gate = runGate(worktreeDir, settings.testCmd, timeoutMs, req, settings);
748
+ if (!gate.ok) {
749
+ previousFailure =
750
+ `Gate failed at: ${gate.stage}\n\n` + (gate.hint ? `⚠ ${gate.hint}\n\n` : "") + gate.output;
751
+ warn(`${req.requirement}: gate failed at ${gate.stage}`);
752
+ if (gate.hint)
753
+ warn(gate.hint);
754
+ record("gate");
755
+ continue;
756
+ }
757
+ // Green — but does the diff contain what the row promised? (A2)
758
+ const artifacts = checkDeclaredArtifactsInWorktree(worktreeDir, req, Boolean(ctx.strictArtifacts));
759
+ if (artifacts) {
760
+ for (const f of artifacts) {
761
+ warn(`${req.requirement}: ${f.message} [${f.code}]`);
762
+ if (f.fix)
763
+ warn(` fix: ${f.fix}`);
764
+ }
765
+ if (ctx.strictArtifacts) {
766
+ previousFailure =
767
+ `The gate passed, but the diff never touches the artifacts the matrix ` +
768
+ `declares for ${req.requirement}:\n\n` +
769
+ artifacts.map((f) => ` ${f.message}\n fix: ${f.fix}`).join("\n\n") +
770
+ `\n\nA matrix that points at a file where the logic does not live is a ` +
771
+ `documentary lie. Implement it there, or correct the row.`;
772
+ record("artifacts");
773
+ continue;
774
+ }
775
+ }
776
+ // Close the loop inside the worktree.
777
+ const done = (0, node_child_process_1.spawnSync)(process.execPath, [DONE_SCRIPT, req.requirement, "--project-dir", worktreeDir], { encoding: "utf8" });
778
+ if (done.status !== 0) {
779
+ previousFailure = `done ${req.requirement} failed:\n${done.stdout}${done.stderr}`;
780
+ warn(`${req.requirement}: done failed`);
781
+ record("done");
782
+ continue;
783
+ }
784
+ git(worktreeDir, ["add", "-A"]);
785
+ const commit = git(worktreeDir, [
786
+ "commit",
787
+ "-m",
788
+ `feat(${req.requirement}): implement via specgate harness\n\nAttempt ${attempt}/${settings.maxAttempts}.`,
789
+ ]);
790
+ if (commit.status !== 0) {
791
+ previousFailure = `git commit failed:\n${commit.stderr || commit.stdout}`;
792
+ warn(`${req.requirement}: commit failed`);
793
+ record("commit");
794
+ continue;
795
+ }
796
+ record("pass");
797
+ return { result: "pass", attempts: attempt, attemptLog };
798
+ }
799
+ // Every attempt is spent. Commit what the agent produced anyway, on the
800
+ // branch, before the worktree is removed.
801
+ //
802
+ // It used to be discarded: `continue` moved to the next attempt and the
803
+ // worktree was deleted at the end, so a failed requirement left a branch
804
+ // identical to its base and nothing to look at. Diagnosing a failure then
805
+ // cost a second full agent run with --keep-worktrees purely to see what had
806
+ // been written — fifteen minutes to recover information the first run had.
807
+ //
808
+ // The commit subject says it failed, and `specgate done` never ran, so the
809
+ // requirement is still Draft in the matrix. A human decides whether the work
810
+ // is worth keeping; git decides nothing.
811
+ const preserved = preserveFailedAttempt(worktreeDir, req, previousFailure);
812
+ return {
813
+ result: "fail",
814
+ // Attempts *this* run spent. On a resume that starts at 3 of 3, one attempt
815
+ // was spent here, and reporting 3 would double-count the earlier run's.
816
+ attempts: settings.maxAttempts - firstAttempt + 1,
817
+ error: previousFailure,
818
+ workPreserved: preserved,
819
+ attemptLog,
820
+ };
821
+ }
822
+ /** Where a run's record lands. Local to the machine — see `writeRunRecord`. */
823
+ exports.RUNS_DIR = path.join(".harness", "runs");
824
+ /**
825
+ * Write what a run did, so the next question about it has an answer.
826
+ *
827
+ * The harness printed a report and forgot it. That is fine for one run and
828
+ * useless for the question that decides whether agent roles are worth paying
829
+ * for (E2-01): **what does a delivered requirement cost, and how often does the
830
+ * first attempt work?** Neither is answerable from memory, and both are
831
+ * answerable from a directory of these.
832
+ *
833
+ * The agent command is deliberately *not* recorded: it is a shell command a
834
+ * user composed, and it is exactly the kind of string that ends up carrying an
835
+ * API key.
836
+ *
837
+ * The directory ignores itself. Two reasons, and the first is not optional:
838
+ * the harness refuses to start on a dirty tree, so a ledger git could see
839
+ * would mean the first run makes the second one refuse — H2 of the closure
840
+ * plan, where the harness blocked itself by writing into the project. The
841
+ * second is that a file rewritten by every run is a merge conflict waiting to
842
+ * happen; these numbers are local measurements, not shared history.
843
+ *
844
+ * Best-effort — a run that produced branches must not fail over bookkeeping.
845
+ */
846
+ function writeRunRecord(projectDir, record) {
847
+ try {
848
+ const dir = path.join(projectDir, exports.RUNS_DIR);
849
+ fs.mkdirSync(dir, { recursive: true });
850
+ const ignore = path.join(dir, ".gitignore");
851
+ if (!fs.existsSync(ignore))
852
+ fs.writeFileSync(ignore, "*\n", "utf8");
853
+ const file = path.join(dir, `${record.startedAt.replace(/[:.]/g, "-")}.json`);
854
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
855
+ return file;
856
+ }
857
+ catch {
858
+ return null;
859
+ }
860
+ }
861
+ /**
862
+ * The repository's main line, for the staleness check.
863
+ *
864
+ * `origin/HEAD` is authoritative when it is set; otherwise the conventional
865
+ * names, in order. A repository with none of them simply gets no warning —
866
+ * guessing wrong would be worse than staying quiet.
867
+ */
868
+ function defaultBranch(projectDir) {
869
+ const head = git(projectDir, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
870
+ if (head.status === 0) {
871
+ const ref = String(head.stdout).trim().replace("refs/remotes/", "");
872
+ if (ref)
873
+ return ref;
874
+ }
875
+ for (const name of ["main", "master"]) {
876
+ if (git(projectDir, ["rev-parse", "--verify", "--quiet", name]).status === 0)
877
+ return name;
878
+ }
879
+ return null;
880
+ }
881
+ /** How many commits `target` has that `ref` does not. */
882
+ function commitsBehind(projectDir, ref, target) {
883
+ const r = git(projectDir, ["rev-list", "--count", `${ref}..${target}`]);
884
+ if (r.status !== 0)
885
+ return 0;
886
+ return Number(String(r.stdout).trim()) || 0;
887
+ }
888
+ /**
889
+ * Warn when the base a requirement is cut from is missing work that the main
890
+ * line already has.
891
+ *
892
+ * This is H9. `--base-branch harness/REQ-001` behaves exactly as git says it
893
+ * should — the new branch inherits its base, not `main` — and it cost two
894
+ * agent runs to discover, because a fix that had landed on `main` was simply
895
+ * not there and the gate failed for a reason that had nothing to do with the
896
+ * requirement. Deriving the base (below) removes the need to pass the flag;
897
+ * this says so out loud when the derived base is stale anyway.
898
+ */
899
+ function warnIfBaseIsStale(projectDir, reqId, base) {
900
+ const mainLine = defaultBranch(projectDir);
901
+ if (!mainLine || mainLine === base)
902
+ return;
903
+ const behind = commitsBehind(projectDir, base, mainLine);
904
+ if (behind === 0)
905
+ return;
906
+ warn(`${reqId}: base ${base} is ${behind} commit(s) behind ${mainLine}. ` +
907
+ `A fix that landed on ${mainLine} is not in this worktree — a gate failure may not be about ${reqId}.`);
908
+ }
909
+ /**
910
+ * Files the harness writes itself, and whose conflicts on an integration base
911
+ * therefore mean nothing.
912
+ *
913
+ * Two sibling requirement branches *always* conflict here: each run ends with
914
+ * `specgate done REQ-NNN`, which edits the same traceability matrix. The
915
+ * integration base exists only so an agent can see the code its dependencies
916
+ * produced; the matrix state on a throwaway branch is consulted by nobody, and
917
+ * each real `harness/REQ-NNN` branch keeps its own row untouched.
918
+ */
919
+ const HARNESS_WRITTEN = ["docs/specs/traceability.md"];
920
+ /**
921
+ * Resolve conflicts in generated files by keeping the base's version, and
922
+ * report any conflict that is left.
923
+ *
924
+ * @returns conflicted paths that are *not* generated — the ones a human owns
925
+ */
926
+ function resolveGeneratedConflicts(worktree) {
927
+ const listed = git(worktree, ["diff", "--name-only", "--diff-filter=U"]);
928
+ if (listed.status !== 0)
929
+ return ["(could not list conflicts)"];
930
+ const conflicted = String(listed.stdout)
931
+ .split("\n")
932
+ .map((l) => l.trim())
933
+ .filter(Boolean);
934
+ const rest = conflicted.filter((p) => !HARNESS_WRITTEN.includes(p));
935
+ if (rest.length > 0)
936
+ return rest;
937
+ for (const file of conflicted) {
938
+ git(worktree, ["checkout", "--ours", "--", file]);
939
+ git(worktree, ["add", "--", file]);
940
+ }
941
+ const commit = git(worktree, ["commit", "--no-edit"]);
942
+ return commit.status === 0 ? [] : ["(could not conclude the merge)"];
943
+ }
944
+ /**
945
+ * Where a requirement's worktree is cut from.
946
+ *
947
+ * A requirement that builds on another needs that other's code to exist, and
948
+ * the only place it exists during a run is the dependency's own
949
+ * `harness/REQ-NNN` branch. Deriving this is what closes H12's other half:
950
+ * nobody has to know the order and pass `--base-branch` by hand.
951
+ *
952
+ * Dependencies that are already `DONE` contribute nothing here — their work is
953
+ * in the run's base already.
954
+ *
955
+ * @param deps the requirement's dependencies, in declaration order
956
+ * @param passed REQ → branch, for the dependencies that passed in this run
957
+ */
958
+ function deriveBase(projectDir, reqId, deps, passed, ctx) {
959
+ const branches = deps.map((d) => passed.get(d)).filter(Boolean);
960
+ if (branches.length === 0)
961
+ return { base: ctx.baseRef };
962
+ if (branches.length === 1)
963
+ return { base: branches[0] };
964
+ // More than one dependency produced code, on branches that know nothing of
965
+ // each other. Cutting from one would silently omit the rest, so they are
966
+ // integrated into a throwaway base first. This is not the merge the harness
967
+ // refuses to do — that one is into a branch a human reviews; this is
968
+ // assembling the context the requirement was declared to need, and a
969
+ // conflict here is a real finding rather than a nuisance.
970
+ const integration = `harness/base/${reqId}`;
971
+ git(projectDir, ["branch", "-D", integration]);
972
+ const cut = git(projectDir, ["branch", integration, ctx.baseRef]);
973
+ if (cut.status !== 0) {
974
+ return { error: `could not create ${integration}: ${cut.stderr || cut.stdout}` };
975
+ }
976
+ const worktree = path.join(os.tmpdir(), `csda-harness-base-${reqId}-${crypto.randomBytes(4).toString("hex")}`);
977
+ const add = git(projectDir, ["worktree", "add", worktree, integration]);
978
+ if (add.status !== 0) {
979
+ return { error: `could not check out ${integration}: ${add.stderr || add.stdout}` };
980
+ }
981
+ try {
982
+ for (const branch of branches) {
983
+ const merge = git(worktree, ["merge", "--no-edit", branch]);
984
+ if (merge.status === 0)
985
+ continue;
986
+ const unresolved = resolveGeneratedConflicts(worktree);
987
+ if (unresolved.length > 0) {
988
+ git(worktree, ["merge", "--abort"]);
989
+ return {
990
+ error: `${branch} conflicts with the other dependencies of ${reqId} in ` +
991
+ `${unresolved.join(", ")}. Those are source files; a human has to decide.`,
992
+ };
993
+ }
994
+ }
995
+ }
996
+ finally {
997
+ git(projectDir, ["worktree", "remove", "--force", worktree]);
998
+ }
999
+ info(`${reqId}: base is ${branches.join(" + ")} integrated into ${integration}`);
1000
+ return { base: integration };
1001
+ }
1002
+ /**
1003
+ * Commit a failed attempt so the branch carries what the agent wrote.
1004
+ *
1005
+ * Returns false when there was nothing to commit — an agent that produced no
1006
+ * files at all, which is itself worth knowing and is reported as such.
1007
+ */
1008
+ /**
1009
+ * The worktree already checked out on `branch`, if git still knows of one.
1010
+ *
1011
+ * After an interruption the worktree survives — the process died before its
1012
+ * `finally` could remove it — holding the agent's uncommitted partial work.
1013
+ * Re-using it is the difference between resuming and starting over.
1014
+ */
1015
+ function existingWorktreeFor(projectDir, branch) {
1016
+ const listed = git(projectDir, ["worktree", "list", "--porcelain"]);
1017
+ if (listed.status !== 0)
1018
+ return null;
1019
+ let current = null;
1020
+ for (const line of (listed.stdout || "").split("\n")) {
1021
+ if (line.startsWith("worktree "))
1022
+ current = line.slice("worktree ".length).trim();
1023
+ else if (line.trim() === `branch refs/heads/${branch}` && current) {
1024
+ return fs.existsSync(current) ? current : null;
1025
+ }
1026
+ }
1027
+ return null;
1028
+ }
1029
+ /**
1030
+ * Where a resumed run picks up, and what the gate last said.
1031
+ *
1032
+ * Read from the prompt archive rather than the run ledger. The ledger is
1033
+ * written when a run *finishes*, and `--resume` is for the runs that do not —
1034
+ * a killed run leaves `.harness/runs/` empty. Measured, not assumed.
1035
+ *
1036
+ * The archive is reachable the same way in both endings: still uncommitted in a
1037
+ * surviving worktree after an interruption, and committed onto the branch by
1038
+ * `preserveFailedAttempt` once the attempts are spent, so a freshly added
1039
+ * worktree checks it out.
1040
+ */
1041
+ function resumeFrom(worktreeDir, requirement) {
1042
+ const dir = path.join(worktreeDir, ...EmptyAttempt_1.PROMPT_ARCHIVE_DIR.split("/"));
1043
+ let names = [];
1044
+ try {
1045
+ names = fs.readdirSync(dir);
1046
+ }
1047
+ catch {
1048
+ return { attempt: 1, previousFailure: "" };
1049
+ }
1050
+ // Interrupted or exhausted? The two deserve different answers, and the branch
1051
+ // says which: `preserveFailedAttempt` leaves a `wip(...): FAILED the gate`
1052
+ // commit when the attempts run out, and an interruption leaves none.
1053
+ //
1054
+ // exhausted → the last attempt reached a verdict → resume at the next one
1055
+ // interrupted → it never did → re-run it, without charging the budget
1056
+ const head = git(worktreeDir, ["log", "-1", "--format=%s"]);
1057
+ const lastCompleted = /^wip\([^)]*\): FAILED the gate/.test((head.stdout || "").trim());
1058
+ const point = (0, ResumeState_1.resumePoint)(names, requirement, lastCompleted);
1059
+ if (!point.latest)
1060
+ return { attempt: 1, previousFailure: "" };
1061
+ let previousFailure = "";
1062
+ try {
1063
+ previousFailure = (0, ResumeState_1.previousFailureFromPrompt)(fs.readFileSync(path.join(dir, point.latest.fileName), "utf8"));
1064
+ }
1065
+ catch {
1066
+ /* an unreadable archive is not a reason to refuse to resume */
1067
+ }
1068
+ return { attempt: point.attempt, previousFailure };
1069
+ }
1070
+ function preserveFailedAttempt(worktreeDir, req, failure) {
1071
+ if (isGitClean(worktreeDir))
1072
+ return false;
1073
+ git(worktreeDir, ["add", "-A"]);
1074
+ const firstLine = String(failure || "").split("\n")[0] || "gate failed";
1075
+ const commit = git(worktreeDir, [
1076
+ "commit",
1077
+ "-m",
1078
+ `wip(${req.requirement}): FAILED the gate — do not merge as is\n\n` +
1079
+ `${firstLine}\n\n` +
1080
+ "Committed by `specgate harness run` so the attempt is reviewable rather than\n" +
1081
+ "discarded. The requirement is still Draft: `specgate done` never ran.",
1082
+ ]);
1083
+ return commit.status === 0;
1084
+ }
1085
+ /**
1086
+ * Is this requirement's scenario worth spending an agent on? (A3)
1087
+ *
1088
+ * The harness gates a requirement by running its scenario. If Cucumber sees no
1089
+ * steps in that scenario, the gate reports `0 steps · exit 0` and the run is
1090
+ * recorded as a pass — the agent is paid for, the branch is published, and
1091
+ * nothing was verified. That is H14 seen from the harness: not a weak signal, a
1092
+ * counterfeit one.
1093
+ *
1094
+ * So the check runs **before** `git worktree add`, before the prompt, before
1095
+ * the agent: an attempt costs up to `max_attempts` × the timeout, and there is
1096
+ * no point buying that against a scenario that cannot fail.
1097
+ *
1098
+ * Only errors block. Warnings — a thin scenario, a vague step — are printed and
1099
+ * allowed through: they weaken the signal without faking it, and a project
1100
+ * brought in with `specgate adopt` would otherwise be unable to run the harness at
1101
+ * all. `specgate validate --strict-scenarios` is where a project opts into the
1102
+ * stricter reading.
1103
+ */
1104
+ /**
1105
+ * Is this requirement fit to hand to an agent? (B2, and A3's guard folded in)
1106
+ *
1107
+ * `plan` has always known that a requirement's feature does not exist, that its
1108
+ * dependencies are unmet, or that its row is Deprecated. `harness run` never
1109
+ * used any of it as a filter, so the agent found out halfway through and the
1110
+ * run paid `max_attempts` × the timeout to discover it. That is the worst
1111
+ * cost-to-result attempt in the loop.
1112
+ *
1113
+ * The rules are in `core/domain/RequirementReadiness`; what happens here is
1114
+ * delivery, and it is not uniform:
1115
+ *
1116
+ * - **an unrunnable scenario always skips**, flag or no flag. That guard came
1117
+ * from A3 and is not a matter of preference: Cucumber passes an empty
1118
+ * scenario, so the reward signal is counterfeit and a green run would prove
1119
+ * nothing (H14).
1120
+ * - **every other blocker warns and runs anyway**, unless `--skip-not-ready`.
1121
+ * Default behaviour stays what it was — this ships in a minor — and a person
1122
+ * who wants to point an agent at a half-ready requirement is allowed to.
1123
+ */
1124
+ function readinessOf(projectDir, req) {
1125
+ const rel = (0, HarnessRun_1.featureFilePath)(req);
1126
+ const file = rel ? path.resolve(projectDir, rel) : "";
1127
+ const featureExists = Boolean(file && fs.existsSync(file));
1128
+ let scenarioFindings = [];
1129
+ if (featureExists) {
1130
+ try {
1131
+ scenarioFindings = (0, GherkinQuality_1.analyseGherkinSource)(fs.readFileSync(file, "utf8"), rel);
1132
+ }
1133
+ catch {
1134
+ // An unreadable feature is the gate's problem to report, not a reason to
1135
+ // refuse the run here.
1136
+ }
1137
+ }
1138
+ const readiness = (0, RequirementReadiness_1.requirementReadiness)({
1139
+ requirement: req.requirement,
1140
+ status: req.status || "",
1141
+ featureFile: rel,
1142
+ // `plan` decides whether a feature is missing; when the harness is handed a
1143
+ // requirement some other way, fall back to looking.
1144
+ featureExists: req.featureExists !== undefined ? Boolean(req.featureExists) : featureExists,
1145
+ scenarioFindings,
1146
+ // Deliberately **not** `blockedBy`. That comes from the plan snapshot taken
1147
+ // before the run, so a requirement whose predecessor passed *in this run*
1148
+ // still reads as blocked — and with `--skip-not-ready` it would be skipped
1149
+ // after the scheduler had correctly unblocked it. Measured: REQ-002 was
1150
+ // reported "depends on REQ-001, which is not done" moments after REQ-001
1151
+ // passed and its branch became REQ-002's base.
1152
+ //
1153
+ // `runLevels` owns ordering during a run: it schedules by level and marks
1154
+ // dependents `blocked` when a predecessor actually fails. Readiness answers
1155
+ // the questions the scheduler cannot.
1156
+ blockedBy: [],
1157
+ technicalDeclared: RequirementPlan_1.RequirementPlan.isMeaningful(req.technicalArtifact || req.technical_artifact),
1158
+ testDeclared: RequirementPlan_1.RequirementPlan.isMeaningful(req.testArtifact || req.test_artifact),
1159
+ });
1160
+ return {
1161
+ ...readiness,
1162
+ scenarioErrors: scenarioFindings.filter((f) => f.severity === "error"),
1163
+ };
1164
+ }
1165
+ /** The settings a context carries, tolerating the worker path's shape. */
1166
+ function settingsOf(ctx) {
1167
+ return (ctx && ctx.settings) || {};
1168
+ }
1169
+ /**
1170
+ * The bounded context a requirement belongs to, as `expand` wrote it (D1).
1171
+ *
1172
+ * Read from the matrix rather than the pack: a project may install several
1173
+ * packs, and the matrix is its own record of what it has.
1174
+ */
1175
+ function contextOfRequirement(projectDir, requirement) {
1176
+ const matrix = path.join(projectDir, "docs", "specs", "traceability.md");
1177
+ try {
1178
+ return (0, TraceabilityFormat_1.parseMatrixContexts)(fs.readFileSync(matrix, "utf8"))[requirement] || "";
1179
+ }
1180
+ catch {
1181
+ return "";
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Can git commit here? Returns the problem, or `""`.
1186
+ *
1187
+ * Asked before the first worktree rather than at the commit step. The harness
1188
+ * commits for every green requirement and again to preserve a failed attempt,
1189
+ * so a missing identity costs a whole attempt — agent time included — to
1190
+ * discover something a single command fixes.
1191
+ */
1192
+ function gitIdentityProblem(projectDir) {
1193
+ const missing = ["user.name", "user.email"].filter((key) => {
1194
+ const r = git(projectDir, ["config", "--get", key]);
1195
+ return r.status !== 0 || !String(r.stdout || "").trim();
1196
+ });
1197
+ if (missing.length === 0)
1198
+ return "";
1199
+ return (`git has no ${missing.join(" and ")} configured, and the harness commits on ` +
1200
+ `every requirement it lands. It would fail after the agent had already run.\n` +
1201
+ `Fix: git config user.name "Your Name" && git config user.email "you@example.com"`);
1202
+ }
1203
+ function processRequirement(req, ctx) {
1204
+ const { projectDir, baseRef, keepWorktrees, force } = ctx;
1205
+ const branch = `harness/${req.requirement}`;
1206
+ // D1: which profile this requirement matches, resolved once. The bounded
1207
+ // context comes from the matrix, where `expand` derived it — see
1208
+ // `boundedContextOf`.
1209
+ const matchedProfile = (0, ProfileMatch_1.selectProfile)(settingsOf(ctx).profileRules || [], {
1210
+ requirement: req.requirement,
1211
+ boundedContext: contextOfRequirement(projectDir, req.requirement),
1212
+ featureFile: (0, HarnessRun_1.featureFilePath)(req),
1213
+ category: req.category,
1214
+ });
1215
+ if (matchedProfile)
1216
+ info(`${req.requirement}: profile '${matchedProfile}' matched`);
1217
+ const readiness = readinessOf(projectDir, req);
1218
+ const unrunnable = readiness.blockers.find((b) => b.code === "requirement_scenario_unrunnable");
1219
+ const hardStop = unrunnable || (ctx.skipNotReady && !readiness.ready);
1220
+ for (const b of readiness.blockers) {
1221
+ const line = `${req.requirement}: ${b.message} [${b.code}]`;
1222
+ if (b.severity === "error")
1223
+ error(line);
1224
+ else
1225
+ warn(line);
1226
+ if (b.fix)
1227
+ warn(` fix: ${b.fix}`);
1228
+ }
1229
+ // The readiness blocker says *that* the scenario cannot fail; these say
1230
+ // which line and which keyword. Collapsing A3's findings into one summary
1231
+ // would have cost the only part a person can act on directly.
1232
+ if (unrunnable) {
1233
+ for (const f of readiness.scenarioErrors || []) {
1234
+ error(`${req.requirement}: ${f.file}:${f.line} ${f.message} [${f.code}]`);
1235
+ }
1236
+ }
1237
+ if (hardStop) {
1238
+ return {
1239
+ requirement: req.requirement,
1240
+ category: req.category,
1241
+ result: "skipped",
1242
+ attempts: 0,
1243
+ branch,
1244
+ error: unrunnable
1245
+ ? `Its scenario would pass without testing anything, so the gate could not ` +
1246
+ `tell success from failure. Run \`specgate validate <dir> --strict-scenarios\` ` +
1247
+ `to see every one.`
1248
+ : `Not ready for an agent: ${readiness.blockers
1249
+ .filter((b) => b.severity === "error")
1250
+ .map((b) => b.code)
1251
+ .join(", ")}. Fix the blockers above, or drop --skip-not-ready to run it anyway.`,
1252
+ };
1253
+ }
1254
+ const worktreeDir = path.join(os.tmpdir(), `csda-harness-${req.requirement}-${crypto.randomBytes(4).toString("hex")}`);
1255
+ // Three ways to meet an existing branch, and until C3 there were only two:
1256
+ // skip it, or delete it with --force. After an interruption — a crash, a
1257
+ // Ctrl-C, a spend limit — neither is what anyone wants.
1258
+ let resuming = false;
1259
+ if (branchExists(projectDir, branch)) {
1260
+ if (ctx.resume) {
1261
+ resuming = true;
1262
+ }
1263
+ else if (!force) {
1264
+ return {
1265
+ requirement: req.requirement,
1266
+ category: req.category,
1267
+ result: "skipped",
1268
+ attempts: 0,
1269
+ branch,
1270
+ error: `Branch ${branch} already exists. Re-run with --resume to continue it, ` +
1271
+ `or --force to recreate it from scratch.`,
1272
+ };
1273
+ }
1274
+ else {
1275
+ git(projectDir, ["branch", "-D", branch]);
1276
+ }
1277
+ }
1278
+ else if (ctx.resume) {
1279
+ // Nothing to resume is not an error: --resume over a whole plan should run
1280
+ // the requirements that never started, not refuse the lot.
1281
+ info(`${req.requirement}: nothing to resume, starting fresh`);
1282
+ }
1283
+ warnIfBaseIsStale(projectDir, req.requirement, baseRef);
1284
+ const startedAt = Date.now();
1285
+ // A surviving worktree still holds the partial work; re-attaching to it is
1286
+ // what makes this a resume rather than a restart.
1287
+ const survivor = resuming ? existingWorktreeFor(projectDir, branch) : null;
1288
+ const dir = survivor || worktreeDir;
1289
+ if (!survivor) {
1290
+ const args = resuming
1291
+ ? ["worktree", "add", dir, branch]
1292
+ : ["worktree", "add", "-b", branch, dir, baseRef];
1293
+ const add = git(projectDir, args);
1294
+ if (add.status !== 0) {
1295
+ return {
1296
+ requirement: req.requirement,
1297
+ category: req.category,
1298
+ result: "fail",
1299
+ attempts: 0,
1300
+ branch,
1301
+ error: `git worktree add failed:\n${add.stderr || add.stdout}`,
1302
+ };
1303
+ }
1304
+ }
1305
+ try {
1306
+ const resumeAt = resuming ? resumeFrom(dir, req.requirement) : null;
1307
+ if (resumeAt && resumeAt.attempt > 1) {
1308
+ info(`${req.requirement}: resuming at attempt ${resumeAt.attempt}`);
1309
+ }
1310
+ const outcome = attemptRequirement(req, {
1311
+ ...ctx,
1312
+ worktreeDir: dir,
1313
+ resumeAt,
1314
+ matchedProfile,
1315
+ });
1316
+ const result = {
1317
+ requirement: req.requirement,
1318
+ category: req.category,
1319
+ branch,
1320
+ base: baseRef,
1321
+ durationMs: Date.now() - startedAt,
1322
+ ...outcome,
1323
+ };
1324
+ if (outcome.result === "pass") {
1325
+ Object.assign(result, publishBranch(projectDir, branch, req, ctx.settings));
1326
+ }
1327
+ return result;
1328
+ }
1329
+ finally {
1330
+ // `dir`, not `worktreeDir`: a resumed run re-attached to the worktree that
1331
+ // survived the interruption, and `worktreeDir` names a path that was never
1332
+ // created. Removing the wrong one leaves the real worktree registered and
1333
+ // the next --resume unable to attach.
1334
+ if (!keepWorktrees) {
1335
+ git(projectDir, ["worktree", "remove", "--force", dir]);
1336
+ }
1337
+ else {
1338
+ info(`${req.requirement}: worktree kept at ${dir}`);
1339
+ }
1340
+ }
1341
+ }
1342
+ /**
1343
+ * CI mode (B7): after a green requirement, optionally push the branch and
1344
+ * open a PR/MR via a user-configured command. Publication problems never
1345
+ * flip a pass to a fail — the code is good; the human just has to publish
1346
+ * manually — but they are reported.
1347
+ */
1348
+ function publishBranch(projectDir, branch, req, settings) {
1349
+ const published = {};
1350
+ if (!settings.push)
1351
+ return published;
1352
+ const push = git(projectDir, ["push", "--force-with-lease", "-u", settings.remote, branch]);
1353
+ if (push.status !== 0) {
1354
+ published.pushed = false;
1355
+ published.publishError = `git push failed:\n${push.stderr || push.stdout}`;
1356
+ warn(`${req.requirement}: push to ${settings.remote} failed`);
1357
+ return published;
1358
+ }
1359
+ published.pushed = true;
1360
+ info(`${req.requirement}: pushed ${branch} to ${settings.remote}`);
1361
+ if (settings.prCmd) {
1362
+ const command = settings.prCmd
1363
+ .split("{branch}")
1364
+ .join(branch)
1365
+ .split("{req}")
1366
+ .join(req.requirement);
1367
+ const pr = (0, node_child_process_1.spawnSync)(command, {
1368
+ shell: true,
1369
+ cwd: projectDir,
1370
+ encoding: "utf8",
1371
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
1372
+ });
1373
+ if (pr.status !== 0) {
1374
+ published.prCreated = false;
1375
+ published.publishError = `pr command failed:\n${pr.stderr || pr.stdout}`;
1376
+ warn(`${req.requirement}: pr command failed`);
1377
+ }
1378
+ else {
1379
+ published.prCreated = true;
1380
+ const firstLine = (pr.stdout || "").trim().split("\n").pop();
1381
+ if (firstLine)
1382
+ published.prOutput = firstLine;
1383
+ info(`${req.requirement}: pr command succeeded${firstLine ? ` → ${firstLine}` : ""}`);
1384
+ }
1385
+ }
1386
+ return published;
1387
+ }
1388
+ /**
1389
+ * How much of a failing gate's output the text report shows.
1390
+ *
1391
+ * Runners put the useful part at the end, so the tail is what a human needs.
1392
+ * Twenty lines is enough for a failing assertion with its stack, and short
1393
+ * enough that ten failed requirements do not bury the summary.
1394
+ */
1395
+ const FAILURE_TAIL_LINES = 20;
1396
+ function printReport(results, format) {
1397
+ if (format === "json") {
1398
+ const summary = results.reduce((acc, r) => {
1399
+ acc[r.result] = (acc[r.result] || 0) + 1;
1400
+ return acc;
1401
+ }, {});
1402
+ process.stdout.write(JSON.stringify({ schemaVersion: 1, total: results.length, summary, results }, null, 2) + "\n");
1403
+ return;
1404
+ }
1405
+ process.stdout.write("\n── harness report ──\n");
1406
+ for (const r of results) {
1407
+ const icon = r.result === "pass"
1408
+ ? "✅"
1409
+ : r.result === "skipped"
1410
+ ? "⏭️ "
1411
+ : r.result === "blocked"
1412
+ ? "⛔"
1413
+ : "❌";
1414
+ process.stdout.write(` ${icon} ${r.requirement} ${r.result} (${r.attempts} attempt${r.attempts === 1 ? "" : "s"}) → ${r.branch}\n`);
1415
+ if (r.result === "blocked") {
1416
+ // Nothing ran, so there is no gate output to show — only the reason.
1417
+ process.stdout.write(` ${r.error}\n`);
1418
+ continue;
1419
+ }
1420
+ if (r.result !== "pass" && r.error) {
1421
+ // The full gate output — the test failure that explains *why* — was
1422
+ // captured and then thrown away here, leaving "Gate failed at: test
1423
+ // command" and nothing to act on. With the worktree removed by default,
1424
+ // that made a failed run undiagnosable. Show the tail, where runners put
1425
+ // the actual failure, and name the two flags that give more.
1426
+ const lines = String(r.error).split("\n");
1427
+ const head = lines[0];
1428
+ const tail = lines
1429
+ .slice(1)
1430
+ .filter((l) => l.trim() !== "")
1431
+ .slice(-FAILURE_TAIL_LINES);
1432
+ process.stdout.write(` ${head}\n`);
1433
+ for (const line of tail)
1434
+ process.stdout.write(` │ ${line}\n`);
1435
+ if (tail.length > 0) {
1436
+ process.stdout.write(" └ full output: --format json · reproduce: --keep-worktrees\n");
1437
+ }
1438
+ if (r.workPreserved) {
1439
+ process.stdout.write(` ↳ the attempt is committed on ${r.branch} — review it\n`);
1440
+ }
1441
+ else if (r.result === "fail") {
1442
+ process.stdout.write(" ↳ the agent produced no files\n");
1443
+ }
1444
+ }
1445
+ if (r.pushed) {
1446
+ process.stdout.write(` pushed${r.prCreated ? ` · PR created${r.prOutput ? `: ${r.prOutput}` : ""}` : ""}\n`);
1447
+ }
1448
+ if (r.publishError) {
1449
+ process.stdout.write(` publish issue: ${String(r.publishError).split("\n")[0]}\n`);
1450
+ }
1451
+ }
1452
+ const pass = results.filter((r) => r.result === "pass").length;
1453
+ const fail = results.filter((r) => r.result === "fail").length;
1454
+ const skip = results.filter((r) => r.result === "skipped").length;
1455
+ const blocked = results.filter((r) => r.result === "blocked").length;
1456
+ // Blocked is counted apart from failed on purpose: one broken predecessor
1457
+ // used to produce N failures and N wasted agent invocations, which said
1458
+ // nothing about the N-1 requirements that were never attempted.
1459
+ const blockedNote = blocked > 0 ? ` · ${blocked} blocked` : "";
1460
+ process.stdout.write(`\n ${pass} passed · ${fail} failed · ${skip} skipped${blockedNote}\n`);
1461
+ if (pass > 0) {
1462
+ process.stdout.write(` Review and merge the harness/* branches you trust.\n`);
1463
+ }
1464
+ }
1465
+ /**
1466
+ * Run one level.
1467
+ *
1468
+ * At concurrency 1 this is the loop the harness has always had: one
1469
+ * requirement, in this process, synchronously. Above 1 each requirement is
1470
+ * handed to a worker process running this same script with `--req`, because
1471
+ * every step inside `processRequirement` is a blocking `spawnSync` and there
1472
+ * is no way to interleave two of them in one process without rewriting all of
1473
+ * it.
1474
+ *
1475
+ * The worker is not a reduced version of the real thing: it *is* `harness run`
1476
+ * scoped to one requirement, so a parallel run and a serial run execute the
1477
+ * same code.
1478
+ */
1479
+ async function dispatchLevel(runnable, byId, hintByReq, ctx, concurrency) {
1480
+ if (concurrency <= 1) {
1481
+ const results = [];
1482
+ for (const id of runnable) {
1483
+ // Asked before each one, not once for the level. Independent
1484
+ // requirements all land in a single level — the common case — so a
1485
+ // ceiling checked only between levels never fires. Measured: a
1486
+ // `--budget-seconds 2` run of three 3-second agents ran all three.
1487
+ if (ctx.mayStart && !ctx.mayStart()) {
1488
+ results.push(notAttempted(byId.get(id), id, ctx.stopReason()));
1489
+ continue;
1490
+ }
1491
+ results.push(processRequirement(byId.get(id), {
1492
+ ...ctx,
1493
+ baseRef: ctx.baseFor ? ctx.baseFor(id) : ctx.baseRef,
1494
+ hint: hintByReq.get(id),
1495
+ }));
1496
+ }
1497
+ return results;
1498
+ }
1499
+ return runWorkers(runnable, byId, ctx, concurrency);
1500
+ }
1501
+ /** A requirement the budget never let start. Not a failure — it was not tried. */
1502
+ function notAttempted(req, id, reason) {
1503
+ return {
1504
+ requirement: id,
1505
+ category: req ? req.category : "",
1506
+ result: "skipped",
1507
+ attempts: 0,
1508
+ branch: `harness/${id}`,
1509
+ error: `Not attempted: ${reason}`,
1510
+ };
1511
+ }
1512
+ /**
1513
+ * Run requirements in parallel worker processes, at most `concurrency` at a
1514
+ * time, and collect their reports.
1515
+ *
1516
+ * Each worker prints a one-requirement JSON report; the parent parses it. A
1517
+ * worker that dies without printing one is reported as a failure carrying its
1518
+ * output, rather than vanishing from the run.
1519
+ */
1520
+ function runWorkers(runnable, byId, ctx, concurrency) {
1521
+ return new Promise((resolve) => {
1522
+ const results = [];
1523
+ const queue = [...runnable];
1524
+ const running = new Map();
1525
+ const fill = () => {
1526
+ while (running.size < concurrency && queue.length > 0) {
1527
+ const id = queue.shift();
1528
+ // Same question as the sequential path: a worker already in flight is
1529
+ // never interrupted, but the next one need not be started.
1530
+ if (ctx.mayStart && !ctx.mayStart()) {
1531
+ results.push(notAttempted(byId ? byId.get(id) : null, id, ctx.stopReason()));
1532
+ continue;
1533
+ }
1534
+ const child = (0, node_child_process_1.spawn)(process.execPath, [WORKER_ENTRY, ...workerArgs(id, ctx)], {
1535
+ env: { ...process.env, CSDA_HARNESS_WORKER: "1" },
1536
+ });
1537
+ let stdout = "";
1538
+ let stderr = "";
1539
+ child.stdout.on("data", (d) => {
1540
+ stdout += d;
1541
+ });
1542
+ // A worker's prose goes straight through, so a long run is not silent.
1543
+ // Its JSON report is the only thing the parent parses.
1544
+ child.stderr.on("data", (d) => {
1545
+ stderr += d;
1546
+ process.stderr.write(d);
1547
+ });
1548
+ running.set(id, child);
1549
+ info(`${id}: started (${running.size}/${concurrency} in flight)`);
1550
+ child.on("close", () => {
1551
+ running.delete(id);
1552
+ results.push(parseWorkerReport(id, stdout, stderr));
1553
+ if (queue.length === 0 && running.size === 0)
1554
+ resolve(results);
1555
+ else
1556
+ fill();
1557
+ });
1558
+ }
1559
+ };
1560
+ fill();
1561
+ if (running.size === 0)
1562
+ resolve(results);
1563
+ });
1564
+ }
1565
+ /**
1566
+ * The argv a worker gets: this same command, scoped to one requirement and
1567
+ * pinned to concurrency 1 so a worker never spawns workers of its own.
1568
+ *
1569
+ * Settings are passed explicitly rather than left to the worker's own read of
1570
+ * `harness.config.yaml`, so a profile resolved once in the parent cannot
1571
+ * resolve differently in a child.
1572
+ */
1573
+ function workerArgs(id, ctx) {
1574
+ return [
1575
+ "--req",
1576
+ id,
1577
+ "--project-dir",
1578
+ ctx.projectDir,
1579
+ "--format",
1580
+ "json",
1581
+ "--concurrency",
1582
+ "1",
1583
+ "--base-branch",
1584
+ ctx.baseFor ? ctx.baseFor(id) : ctx.baseRef,
1585
+ "--timeout",
1586
+ String(ctx.timeoutSeconds),
1587
+ "--max-attempts",
1588
+ String(ctx.settings.maxAttempts),
1589
+ ...(ctx.force ? ["--force"] : []),
1590
+ ...(ctx.keepWorktrees ? ["--keep-worktrees"] : []),
1591
+ ...(ctx.settings.agent ? ["--agent", ctx.settings.agent] : []),
1592
+ ...(ctx.settings.testCmd ? ["--test-cmd", ctx.settings.testCmd] : []),
1593
+ ];
1594
+ }
1595
+ /** A worker's report, or an honest failure describing why there is none. */
1596
+ function parseWorkerReport(id, stdout, stderr) {
1597
+ try {
1598
+ const parsed = JSON.parse(stdout);
1599
+ const one = (parsed.results || []).find((r) => r.requirement === id);
1600
+ if (one)
1601
+ return one;
1602
+ }
1603
+ catch {
1604
+ // fall through
1605
+ }
1606
+ return {
1607
+ requirement: id,
1608
+ category: "",
1609
+ result: "fail",
1610
+ attempts: 0,
1611
+ branch: `harness/${id}`,
1612
+ error: `Worker produced no report.\n${(stderr || stdout || "").slice(-2000)}`,
1613
+ };
1614
+ }
1615
+ /**
1616
+ * Run the queue level by level.
1617
+ *
1618
+ * **Why concurrency 1 stays on the in-process path.** Every step of a
1619
+ * requirement — the gate, the agent, `done`, git — is a `spawnSync`, and
1620
+ * §12.11 of the closure plan is a list of eleven defects that only appeared
1621
+ * when this loop ran against a real agent. Converting all of it to async so
1622
+ * that one requirement could run "in parallel" with nothing would put the one
1623
+ * path that has actually been exercised behind an untested rewrite. So the
1624
+ * default path is the path it has always been, and only `--concurrency > 1`
1625
+ * dispatches through worker processes.
1626
+ *
1627
+ * **Why a failure blocks rather than fails.** Without dependencies expressed,
1628
+ * `harness run` processed the matrix in order and a broken predecessor made
1629
+ * every successor fail too — N failures for one cause, and N wasted agent
1630
+ * invocations. A requirement whose dependency failed has not been attempted;
1631
+ * calling that a failure would be a lie in the report.
1632
+ */
1633
+ async function runLevels(pending, ctx, opts) {
1634
+ const { concurrency, hintByReq, runOne } = opts;
1635
+ /**
1636
+ * The run's ceiling (C1). Checked before starting a level, never inside one:
1637
+ * interrupting an attempt mid-flight would throw away the money already spent
1638
+ * on it and leave a worktree nobody asked for. A budget bounds what a run
1639
+ * *begins*.
1640
+ */
1641
+ const limits = {
1642
+ budgetSeconds: ctx.budgetSeconds || 0,
1643
+ maxRequirements: ctx.maxRequirements || 0,
1644
+ };
1645
+ const budgetStartedAt = Date.now();
1646
+ let started = 0;
1647
+ let stoppedBy = null;
1648
+ const { levels, cycles, dependsOn, graph } = (0, HarnessRun_1.scheduleLevels)(pending);
1649
+ const byId = new Map(pending.map((r) => [r.requirement, r]));
1650
+ if (cycles.length > 0) {
1651
+ // `validate` is the gate that reports the cycle properly. Here we only
1652
+ // have to refuse to loop forever, and say which requirements are stuck.
1653
+ for (const cycle of cycles) {
1654
+ warn(`Dependency cycle, not attempted: ${[...cycle, cycle[0]].join(" → ")}`);
1655
+ }
1656
+ }
1657
+ const results = [];
1658
+ const blocked = new Set();
1659
+ /** REQ → the branch its work landed on, for the requirements that follow it. */
1660
+ const passedBranches = new Map();
1661
+ for (const level of levels) {
1662
+ const runnable = level.filter((id) => !blocked.has(id));
1663
+ for (const id of level) {
1664
+ if (!blocked.has(id))
1665
+ continue;
1666
+ const req = byId.get(id);
1667
+ results.push({
1668
+ requirement: id,
1669
+ category: req ? req.category : "",
1670
+ result: "blocked",
1671
+ attempts: 0,
1672
+ branch: `harness/${id}`,
1673
+ error: `Not attempted: ${(dependsOn[id] || []).filter((d) => blocked.has(d) || results.some((r) => r.requirement === d && r.result !== "pass")).join(", ")} did not pass.`,
1674
+ });
1675
+ }
1676
+ if (runnable.length === 0)
1677
+ continue;
1678
+ // A requirement is cut from its dependencies' branches, not from the run's
1679
+ // base: that is where the code it builds on exists during a run.
1680
+ const derivationFailures = new Map();
1681
+ const baseFor = (id) => {
1682
+ const derived = deriveBase(ctx.projectDir, id, dependsOn[id] || [], passedBranches, ctx);
1683
+ if (derived.error) {
1684
+ derivationFailures.set(id, derived.error);
1685
+ return ctx.baseRef;
1686
+ }
1687
+ return derived.base;
1688
+ };
1689
+ const bases = new Map(runnable.map((id) => [id, baseFor(id)]));
1690
+ const attemptable = runnable.filter((id) => !derivationFailures.has(id));
1691
+ for (const [id, reason] of derivationFailures) {
1692
+ const req = byId.get(id);
1693
+ results.push({
1694
+ requirement: id,
1695
+ category: req ? req.category : "",
1696
+ result: "blocked",
1697
+ attempts: 0,
1698
+ branch: `harness/${id}`,
1699
+ error: `Not attempted: could not assemble its base. ${reason}`,
1700
+ });
1701
+ for (const dependent of graph.transitiveDependents(id))
1702
+ blocked.add(dependent);
1703
+ }
1704
+ if (attemptable.length === 0)
1705
+ continue;
1706
+ if (stoppedBy)
1707
+ break;
1708
+ // The ceiling bites *inside* the level, through `mayStart` below —
1709
+ // independent requirements all land in one level, which is the common
1710
+ // case, so a check that only ran between levels would let a run of
1711
+ // fourteen sail past `--max-requirements 2`. Measured: it did exactly
1712
+ // that, and again with `--budget-seconds`.
1713
+ const levelResults = await runOne(attemptable, byId, hintByReq, {
1714
+ ...ctx,
1715
+ baseFor: (id) => bases.get(id),
1716
+ // The level asks before starting each requirement. `started` counts
1717
+ // what was actually begun, so the count ceiling and the clock agree.
1718
+ mayStart: () => {
1719
+ if (stoppedBy)
1720
+ return false;
1721
+ const verdict = (0, RunBudget_1.budgetVerdict)(limits, { startedAt: budgetStartedAt, started }, Date.now());
1722
+ if (verdict.stop) {
1723
+ stoppedBy = verdict;
1724
+ return false;
1725
+ }
1726
+ started += 1;
1727
+ return true;
1728
+ },
1729
+ stopReason: () => (stoppedBy ? stoppedBy.reason : ""),
1730
+ }, concurrency);
1731
+ results.push(...levelResults);
1732
+ for (const r of levelResults) {
1733
+ if (r.result === "pass")
1734
+ passedBranches.set(r.requirement, r.branch);
1735
+ }
1736
+ for (const r of levelResults) {
1737
+ if (r.result !== "pass") {
1738
+ for (const dependent of graph.transitiveDependents(r.requirement)) {
1739
+ blocked.add(dependent);
1740
+ }
1741
+ }
1742
+ }
1743
+ }
1744
+ // Once the budget is spent, later levels are not even walked: their
1745
+ // requirements are named as not attempted, with the same reason.
1746
+ if (stoppedBy) {
1747
+ for (const level of levels) {
1748
+ for (const id of level) {
1749
+ if (results.some((r) => r.requirement === id))
1750
+ continue;
1751
+ results.push(notAttempted(byId.get(id), id, stoppedBy.reason));
1752
+ }
1753
+ }
1754
+ }
1755
+ // Requirements caught in a cycle never reach a level. Report them rather
1756
+ // than dropping them from the run.
1757
+ for (const cycle of cycles) {
1758
+ for (const id of cycle) {
1759
+ if (results.some((r) => r.requirement === id))
1760
+ continue;
1761
+ const req = byId.get(id);
1762
+ results.push({
1763
+ requirement: id,
1764
+ category: req ? req.category : "",
1765
+ result: "blocked",
1766
+ attempts: 0,
1767
+ branch: `harness/${id}`,
1768
+ error: `Not attempted: caught in a dependency cycle (${cycle.join(" → ")}). Run \`specgate validate\` for the fix.`,
1769
+ });
1770
+ }
1771
+ }
1772
+ return results;
1773
+ }
1774
+ class RunCommand extends command_1.BaseCommand {
1775
+ async execute() {
1776
+ try {
1777
+ const args = parseArgs(this.args);
1778
+ setJsonMode(args.format === "json");
1779
+ const projectDir = (0, project_root_1.resolveProjectDir)(args.projectDir, { requireSentinel: true });
1780
+ const fileConfig = (0, HarnessConfigFile_1.readHarnessConfig)(projectDir);
1781
+ const settings = (0, HarnessConfigFile_1.resolveHarnessSettings)(fileConfig, args);
1782
+ // D1: profile match rules live in `.harness/profiles.yaml`, and a project
1783
+ // may declare profiles without ever writing a `harness.config.yaml`.
1784
+ // Reaching them through the config reader made them silently absent
1785
+ // there — the same gap the cost hints had, found the same way.
1786
+ if (settings.profileRules.length === 0) {
1787
+ const rules = (0, HarnessConfigFile_1.readProfileRules)(projectDir);
1788
+ if (rules.length > 0) {
1789
+ settings.profileRules = rules;
1790
+ settings.profileAgents = { ...settings.profileAgents };
1791
+ for (const rule of rules) {
1792
+ if (!settings.profileAgents[rule.name]) {
1793
+ settings.profileAgents[rule.name] = (0, HarnessConfigFile_1.resolveProfileAgent)(projectDir, rule.name);
1794
+ }
1795
+ }
1796
+ }
1797
+ }
1798
+ // A ladder of per-attempt profiles configures agents just as much as a
1799
+ // single `agent:` does, so a project that declares one is configured.
1800
+ const hasAgent = Boolean(settings.agent) || settings.attemptProfiles.length > 0;
1801
+ if (!args.dryRun && !hasAgent) {
1802
+ throw new Error('No agent configured. Pass --agent "<cmd with {prompt_file}>", set `agent:` ' +
1803
+ "in harness.config.yaml, or declare `attempt_profiles:`.");
1804
+ }
1805
+ const plan = runPlan(projectDir);
1806
+ let pending = (plan.requirements || []).filter((r) => r.category !== "DONE");
1807
+ if (args.reqs.length > 0) {
1808
+ const wanted = new Set(args.reqs);
1809
+ pending = pending.filter((r) => wanted.has(r.requirement));
1810
+ const found = new Set(pending.map((r) => r.requirement));
1811
+ for (const want of args.reqs) {
1812
+ if (!found.has(want))
1813
+ warn(`${want} is not a pending requirement — skipped.`);
1814
+ }
1815
+ }
1816
+ if (pending.length === 0) {
1817
+ info("No pending requirements. Nothing to do.");
1818
+ process.exit(0);
1819
+ }
1820
+ const hintByReq = new Map((plan.next_steps || []).map((s) => [s.requirement, s.hint]));
1821
+ if (args.dryRun) {
1822
+ info(`Dry run — ${pending.length} requirement(s) would be processed:`);
1823
+ for (const req of pending) {
1824
+ const prompt = (0, prompt_1.buildPrompt)(req, projectDir, {
1825
+ promptPrefix: settings.promptPrefix,
1826
+ hint: hintByReq.get(req.requirement),
1827
+ });
1828
+ process.stdout.write(`\n${"═".repeat(72)}\n${req.requirement} (${req.category}) → branch harness/${req.requirement}\n${"═".repeat(72)}\n`);
1829
+ process.stdout.write(prompt + "\n");
1830
+ }
1831
+ process.exit(0);
1832
+ }
1833
+ if (!isGitClean(projectDir)) {
1834
+ throw new Error("Working tree is not clean. Commit or stash your changes before running the harness.");
1835
+ }
1836
+ // Git needs to know who is committing, and the harness commits — once per
1837
+ // green requirement, and again to preserve a failed attempt. Without an
1838
+ // identity it discovers this *after* the agent has run and the gate has
1839
+ // passed, so a full attempt is paid for and thrown away by a one-line
1840
+ // config. Found by CI, which has no global git identity and produced
1841
+ // `Author identity unknown` at the commit step of every requirement.
1842
+ const identity = gitIdentityProblem(projectDir);
1843
+ if (identity)
1844
+ throw new Error(identity);
1845
+ const baseRef = args.baseBranch || "HEAD";
1846
+ const concurrencyNote = settings.concurrency > 1 ? `, up to ${settings.concurrency} at a time` : "";
1847
+ info(`Processing ${pending.length} requirement(s) from base ${baseRef}${concurrencyNote}.`);
1848
+ // One prune, in the parent, before anything is created. It used to run per
1849
+ // requirement, which is harmless in series and a race in parallel:
1850
+ // `git worktree prune` running while a sibling is inside
1851
+ // `git worktree add` can remove the record of the worktree being created.
1852
+ if (!process.env.CSDA_HARNESS_WORKER) {
1853
+ git(projectDir, ["worktree", "prune"]);
1854
+ }
1855
+ const ctx = {
1856
+ projectDir,
1857
+ baseRef,
1858
+ settings,
1859
+ timeoutMs: args.timeout * 1000,
1860
+ timeoutSeconds: args.timeout,
1861
+ keepWorktrees: args.keepWorktrees,
1862
+ force: args.force,
1863
+ resume: args.resume,
1864
+ skipNotReady: args.skipNotReady,
1865
+ budgetSeconds: args.budgetSeconds,
1866
+ maxRequirements: args.maxRequirements,
1867
+ strictArtifacts: args.strictArtifacts,
1868
+ };
1869
+ const startedAt = new Date().toISOString();
1870
+ const results = await runLevels(pending, ctx, {
1871
+ concurrency: settings.concurrency,
1872
+ hintByReq,
1873
+ runOne: dispatchLevel,
1874
+ });
1875
+ // Only the run that a person started writes a record. A worker is one
1876
+ // slice of that run, and its own record would be a second copy of the
1877
+ // same requirement — `harness report` reads every file in the directory,
1878
+ // so duplicates land straight in "cost per delivered REQ".
1879
+ const recordPath = process.env.CSDA_HARNESS_WORKER
1880
+ ? null
1881
+ : writeRunRecord(projectDir, {
1882
+ schemaVersion: 1,
1883
+ startedAt,
1884
+ finishedAt: new Date().toISOString(),
1885
+ baseRef,
1886
+ concurrency: settings.concurrency,
1887
+ maxAttempts: settings.maxAttempts,
1888
+ // Whether this run had a ceiling at all. Reading the ledger later,
1889
+ // a short run is a different thing depending on the answer.
1890
+ bounded: (0, RunBudget_1.hasBudget)({
1891
+ budgetSeconds: args.budgetSeconds,
1892
+ maxRequirements: args.maxRequirements,
1893
+ }),
1894
+ results,
1895
+ });
1896
+ printReport(results, args.format);
1897
+ if (recordPath && args.format !== "json") {
1898
+ info(`Run recorded in ${path.relative(projectDir, recordPath)} — \`specgate harness report\``);
1899
+ }
1900
+ // A blocked requirement was never attempted, so it is not a pass — but it
1901
+ // is also not evidence that anything is broken beyond the failure that
1902
+ // caused it. It still fails the run, because work was left undone.
1903
+ const failed = results.filter((r) => r.result !== "pass").length;
1904
+ process.exit(failed > 0 ? 1 : 0);
1905
+ }
1906
+ catch (err) {
1907
+ let isJson = false;
1908
+ try {
1909
+ isJson = parseArgs(this.args).format === "json";
1910
+ }
1911
+ catch {
1912
+ /* ignore */
1913
+ }
1914
+ const io = (0, agent_1.agentIo)(isJson);
1915
+ return io.fail({ results: null }, [
1916
+ { severity: "error", code: "harness_run_error", message: err.message || String(err) },
1917
+ ]);
1918
+ }
1919
+ }
1920
+ }
1921
+ exports.RunCommand = RunCommand;
1922
+ //# sourceMappingURL=RunCommand.js.map