@webpresso/agent-kit 3.1.30 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +183 -20
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/blueprint-trust.js +14 -10
  17. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  18. package/dist/esm/audit/changeset-evidence.js +30 -0
  19. package/dist/esm/audit/changeset-required.d.ts +10 -0
  20. package/dist/esm/audit/changeset-required.js +73 -0
  21. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  22. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  24. package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
  25. package/dist/esm/audit/ci-test-perf.js +30 -0
  26. package/dist/esm/audit/command-surface.js +4 -2
  27. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  28. package/dist/esm/audit/host-substitution-risk.js +256 -0
  29. package/dist/esm/audit/registry.d.ts +4 -4
  30. package/dist/esm/audit/registry.js +13 -1
  31. package/dist/esm/audit/toolchain-isolation.js +1 -2
  32. package/dist/esm/audit/typescript-version.d.ts +2 -0
  33. package/dist/esm/audit/typescript-version.js +123 -0
  34. package/dist/esm/blueprint/core/parser.js +101 -2
  35. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  36. package/dist/esm/blueprint/core/schema.js +26 -0
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +87 -20
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  46. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  47. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  48. package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
  49. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  50. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  51. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  52. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  53. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  54. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  55. package/dist/esm/blueprint/sync/client.js +1 -1
  56. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  57. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  58. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  59. package/dist/esm/blueprint/trust/dossier.js +40 -6
  60. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  61. package/dist/esm/blueprint/trust/gates.js +20 -0
  62. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  63. package/dist/esm/blueprint/trust/promotion.js +74 -112
  64. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  65. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  66. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  67. package/dist/esm/blueprint/trust/validator.js +7 -1
  68. package/dist/esm/blueprint/utils/archive.js +27 -5
  69. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  70. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  71. package/dist/esm/build/cli-mcp-parity.js +15 -0
  72. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  73. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  74. package/dist/esm/cli/cli.d.ts +1 -1
  75. package/dist/esm/cli/cli.js +43 -0
  76. package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
  77. package/dist/esm/cli/commands/agent-launch.js +93 -4
  78. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  79. package/dist/esm/cli/commands/audit-core.js +8 -1
  80. package/dist/esm/cli/commands/audit.js +21 -5
  81. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  82. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  83. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  84. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  85. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  86. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  87. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  88. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  89. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  90. package/dist/esm/cli/commands/ci-preflight.js +30 -6
  91. package/dist/esm/cli/commands/compile.js +5 -31
  92. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  93. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  94. package/dist/esm/cli/commands/doctor.js +5 -0
  95. package/dist/esm/cli/commands/hook.d.ts +5 -0
  96. package/dist/esm/cli/commands/hook.js +15 -9
  97. package/dist/esm/cli/commands/init/config.js +2 -2
  98. package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
  99. package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
  100. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  101. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  102. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  103. package/dist/esm/cli/commands/init/convergence.js +57 -0
  104. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  105. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  106. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  107. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  108. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  109. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  110. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  111. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  112. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  113. package/dist/esm/cli/commands/init/index.d.ts +3 -0
  114. package/dist/esm/cli/commands/init/index.js +326 -173
  115. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  116. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  117. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  118. package/dist/esm/cli/commands/init/merge.js +30 -1
  119. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  120. package/dist/esm/cli/commands/init/package-root.js +10 -2
  121. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  122. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  123. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  124. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  125. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
  126. package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
  127. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  128. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  129. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  130. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
  131. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  132. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  133. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
  134. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
  135. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  136. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  137. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  138. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  139. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  140. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  141. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  142. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  143. package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
  144. package/dist/esm/cli/commands/opencode-rotate.js +147 -0
  145. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  146. package/dist/esm/cli/commands/public-readiness.js +42 -0
  147. package/dist/esm/cli/commands/quality-runner.js +3 -3
  148. package/dist/esm/cli/commands/review.d.ts +35 -3
  149. package/dist/esm/cli/commands/review.js +751 -209
  150. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  151. package/dist/esm/cli/commands/run-wait.js +122 -0
  152. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  153. package/dist/esm/cli/commands/rust-check.js +110 -0
  154. package/dist/esm/cli/commands/setup-help.js +1 -1
  155. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  156. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  157. package/dist/esm/cli/commands/typecheck.js +30 -1
  158. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  159. package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
  160. package/dist/esm/cli/commands/worktree/router.js +10 -0
  161. package/dist/esm/cli/direct-provider-launch.js +9 -5
  162. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  163. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  164. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  165. package/dist/esm/daemon/domains/git-actions.js +136 -0
  166. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  167. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  168. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  169. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  170. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  171. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  172. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  173. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  174. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  175. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  176. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  177. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  178. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  179. package/dist/esm/daemon/handlers/product.js +10 -119
  180. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  181. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  182. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  183. package/dist/esm/daemon/handlers/shared.js +16 -0
  184. package/dist/esm/daemon/methods.d.ts +8 -1
  185. package/dist/esm/daemon/methods.js +7 -0
  186. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  187. package/dist/esm/daemon/protocol/contract.js +26 -0
  188. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  189. package/dist/esm/daemon/protocol/index.js +1 -1
  190. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  191. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  192. package/dist/esm/hooks/conformance/batch.js +5 -0
  193. package/dist/esm/hooks/doctor.d.ts +5 -3
  194. package/dist/esm/hooks/doctor.js +67 -16
  195. package/dist/esm/hooks/permission-request/index.js +9 -3
  196. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  197. package/dist/esm/hooks/permission-request/policy.js +13 -0
  198. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  199. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  200. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  201. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  202. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  203. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  204. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  205. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  206. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  207. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  208. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  209. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  210. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  211. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  212. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  213. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  214. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  215. package/dist/esm/hooks/shared/types.d.ts +8 -5
  216. package/dist/esm/hooks/shared/types.js +50 -9
  217. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  218. package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
  219. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  220. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  221. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
  222. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  223. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  224. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  225. package/dist/esm/mcp/blueprint/registration.js +3 -2
  226. package/dist/esm/mcp/server.d.ts +1 -0
  227. package/dist/esm/mcp/server.js +27 -0
  228. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  229. package/dist/esm/mcp/tools/_names.js +5 -0
  230. package/dist/esm/mcp/tools/_registry.js +10 -0
  231. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  232. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  233. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  234. package/dist/esm/mcp/tools/audit.js +42 -0
  235. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  236. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  237. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  238. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  239. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  240. package/dist/esm/mcp/tools/review-run.js +216 -0
  241. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  242. package/dist/esm/mcp/tools/run-wait.js +593 -0
  243. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  244. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  245. package/dist/esm/mcp/tools/session-id.js +45 -0
  246. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  247. package/dist/esm/mcp/tools/session-info.js +49 -0
  248. package/dist/esm/output-transforms/audit.d.ts +23 -0
  249. package/dist/esm/output-transforms/audit.js +47 -0
  250. package/dist/esm/output-transforms/index.js +2 -0
  251. package/dist/esm/package.json +2 -0
  252. package/dist/esm/platform/client.d.ts +7 -1
  253. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  254. package/dist/esm/pr-description/contract.js +24 -4
  255. package/dist/esm/review/authority.js +27 -14
  256. package/dist/esm/review/availability.d.ts +33 -0
  257. package/dist/esm/review/availability.js +122 -20
  258. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  259. package/dist/esm/review/delivery-verifier.js +115 -0
  260. package/dist/esm/review/events.d.ts +3 -2
  261. package/dist/esm/review/events.js +23 -3
  262. package/dist/esm/review/execution/adapters.d.ts +11 -1
  263. package/dist/esm/review/execution/adapters.js +36 -10
  264. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  265. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  266. package/dist/esm/review/execution/output-policy.js +12 -0
  267. package/dist/esm/review/execution/review-checkout.js +121 -21
  268. package/dist/esm/review/execution/supervisor.js +36 -4
  269. package/dist/esm/review/execution/types.d.ts +9 -0
  270. package/dist/esm/review/opencode-account.d.ts +24 -0
  271. package/dist/esm/review/opencode-account.js +68 -0
  272. package/dist/esm/review/opencode-auth.d.ts +33 -0
  273. package/dist/esm/review/opencode-auth.js +79 -0
  274. package/dist/esm/review/subject.d.ts +25 -0
  275. package/dist/esm/review/subject.js +116 -26
  276. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  277. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  278. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  279. package/dist/esm/rust/affected-manifests.js +69 -0
  280. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  281. package/dist/esm/status/snapshot.js +32 -25
  282. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  283. package/dist/esm/test/duration-sequencer.js +54 -0
  284. package/dist/esm/test/shard-durations.json +922 -0
  285. package/dist/esm/test/worker-budget.d.ts +6 -0
  286. package/dist/esm/test/worker-budget.js +59 -1
  287. package/dist/esm/test-helpers/global-setup.d.ts +1 -2
  288. package/dist/esm/test-helpers/global-setup.js +79 -32
  289. package/dist/esm/typecheck/planner.d.ts +9 -1
  290. package/dist/esm/typecheck/planner.js +31 -0
  291. package/dist/esm/ultragoal/service.d.ts +7 -0
  292. package/dist/esm/ultragoal/service.js +42 -2
  293. package/dist/esm/utils/write-json-file.js +3 -1
  294. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  295. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  296. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  297. package/dist/esm/worktrees/orphan-scan.js +211 -0
  298. package/dist/esm/worktrees/registry.d.ts +8 -0
  299. package/dist/esm/worktrees/registry.js +18 -3
  300. package/package.json +18 -13
@@ -0,0 +1,325 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { convergeUnitFromTarget, deriveCommitAllowlist } from "./convergence.js";
4
+ export { resolveUntrackedCollision } from "./untracked-collision.js";
5
+ /** Bounded timeout for non-hook Git probes (status, add, rev-parse, diff-tree). */
6
+ export const GIT_PROBE_TIMEOUT_MS = 15_000;
7
+ /**
8
+ * Default wall for the single hookful `git commit`. Consumer pre-commit chains
9
+ * (format + audits) routinely exceed the probe budget; Node's spawnSync kills
10
+ * the child with SIGTERM when `timeout` elapses, which left staged residue and
11
+ * multi-line partial hook output that looked like success.
12
+ *
13
+ * 10 minutes is a durable consumer budget, not infinite hang. Operators can
14
+ * raise/lower via `WP_SETUP_COMMIT_TIMEOUT_MS` or `AuthorCommitOptions.commitTimeoutMs`.
15
+ */
16
+ export const DEFAULT_GIT_COMMIT_TIMEOUT_MS = 600_000;
17
+ const GIT_MAX_BUFFER = 1024 * 1024;
18
+ function git(repoRoot, args, timeoutMs) {
19
+ const result = spawnSync("git", [...args], {
20
+ cwd: repoRoot,
21
+ encoding: "utf8",
22
+ // Node: timeout > 0 arms SIGTERM; 0 / omitted means no artificial wall.
23
+ ...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
24
+ maxBuffer: GIT_MAX_BUFFER,
25
+ stdio: ["ignore", "pipe", "pipe"],
26
+ });
27
+ const timedOut = result.error !== undefined && "code" in result.error && result.error.code === "ETIMEDOUT";
28
+ return {
29
+ ok: result.status === 0 && !result.error,
30
+ stdout: (result.stdout ?? "").trim(),
31
+ stderr: (result.stderr ?? "").trim(),
32
+ timedOut,
33
+ signal: result.signal,
34
+ };
35
+ }
36
+ /**
37
+ * Resolve the hookful-commit timeout. Preference order:
38
+ * 1. explicit `commitTimeoutMs` option (tests / programmatic callers)
39
+ * 2. `WP_SETUP_COMMIT_TIMEOUT_MS` env (operator override)
40
+ * 3. {@link DEFAULT_GIT_COMMIT_TIMEOUT_MS}
41
+ *
42
+ * Values must be finite numbers ≥ 0. `0` disables the artificial wall (Node default).
43
+ */
44
+ export function resolveCommitTimeoutMs(options) {
45
+ const env = options?.env ?? process.env;
46
+ if (typeof options?.commitTimeoutMs === "number" &&
47
+ Number.isFinite(options.commitTimeoutMs) &&
48
+ options.commitTimeoutMs >= 0) {
49
+ return options.commitTimeoutMs;
50
+ }
51
+ const raw = env.WP_SETUP_COMMIT_TIMEOUT_MS;
52
+ if (typeof raw === "string" && raw.trim().length > 0) {
53
+ const parsed = Number.parseInt(raw, 10);
54
+ if (Number.isFinite(parsed) && parsed >= 0)
55
+ return parsed;
56
+ }
57
+ return DEFAULT_GIT_COMMIT_TIMEOUT_MS;
58
+ }
59
+ function appendHookOutput(diagnosis, hookOutput) {
60
+ const trimmed = hookOutput?.trim();
61
+ if (!trimmed)
62
+ return diagnosis;
63
+ const snippet = trimmed.length > 400 ? `${trimmed.slice(0, 400)}…` : trimmed;
64
+ return `${diagnosis}\n--- hook output (truncated) ---\n${snippet}`;
65
+ }
66
+ /** First-class timeout diagnosis; partial hook stdout/stderr is diagnostic only. */
67
+ export function formatCommitTimeoutError(options) {
68
+ const budget = options.timeoutMs > 0
69
+ ? `${options.timeoutMs}ms`
70
+ : "the configured budget (no artificial wall was set, but the process was still interrupted)";
71
+ const diagnosis = `Convergence commit timed out after ${budget} while repository hooks were still running. ` +
72
+ `Node spawnSync kills the git process with SIGTERM on timeout, so no setup commit was authored. ` +
73
+ `Staged and worktree changes were preserved for remediation. ` +
74
+ `Re-run after hooks complete, raise WP_SETUP_COMMIT_TIMEOUT_MS / commitTimeoutMs, or fix slow hooks. ` +
75
+ `Partial hook lines such as "format applied" / "audit … passed" are not success.`;
76
+ return appendHookOutput(diagnosis, options.hookOutput);
77
+ }
78
+ /**
79
+ * First-class interrupt diagnosis for non-timeout signal kills (operator kill,
80
+ * external SIGTERM/SIGKILL). Must not surface raw hook "passed" lines as the
81
+ * leading operator message.
82
+ */
83
+ export function formatCommitInterruptError(options) {
84
+ const diagnosis = `Convergence commit was interrupted by ${options.signal} before completion, so no setup commit was authored. ` +
85
+ `Staged and worktree changes were preserved for remediation. ` +
86
+ `This is not a successful setup run. ` +
87
+ `Partial hook lines such as "format applied" / "audit … passed" are not success.`;
88
+ return appendHookOutput(diagnosis, options.hookOutput);
89
+ }
90
+ /**
91
+ * Classify a failed hookful `git commit` result.
92
+ *
93
+ * - `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
94
+ * - non-null `signal` without timedOut → interrupt diagnosis (names the signal)
95
+ * - otherwise → hook/git stderr when present, else a generic preserved-index message
96
+ *
97
+ * Never lead with bare hook "passed" lines for timeout/interrupt paths.
98
+ */
99
+ export function classifyCommitFailure(result, timeoutMs) {
100
+ const hookOutput = [result.stderr, result.stdout].filter(Boolean).join("\n");
101
+ if (result.timedOut) {
102
+ return formatCommitTimeoutError({ timeoutMs, hookOutput });
103
+ }
104
+ if (result.signal) {
105
+ return formatCommitInterruptError({ signal: result.signal, hookOutput });
106
+ }
107
+ return hookOutput || "git commit failed; hook changes were preserved.";
108
+ }
109
+ /**
110
+ * Prefix every line of a setup failure so multi-line timeout diagnoses stay
111
+ * labeled when printed via console.error (avoids a single `wp setup: ` on the
112
+ * first line and bare hook "passed" lines after it).
113
+ */
114
+ export function formatWpSetupError(message) {
115
+ const body = message.trimEnd();
116
+ if (body.length === 0)
117
+ return "wp setup: Unable to author convergence commit.";
118
+ return body
119
+ .split("\n")
120
+ .map((line) => `wp setup: ${line}`)
121
+ .join("\n");
122
+ }
123
+ function lines(value) {
124
+ return value.length === 0 ? [] : value.split("\n").filter(Boolean).sort();
125
+ }
126
+ function readPorcelainStatus(repoRoot, gitPath) {
127
+ const result = spawnSync("git", [
128
+ "status",
129
+ "--porcelain=v1",
130
+ "-z",
131
+ "--untracked-files=all",
132
+ "--ignored=matching",
133
+ "--",
134
+ gitPath,
135
+ ], {
136
+ cwd: repoRoot,
137
+ encoding: "utf8",
138
+ timeout: GIT_PROBE_TIMEOUT_MS,
139
+ maxBuffer: GIT_MAX_BUFFER,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ });
142
+ if (result.status !== 0 || result.error)
143
+ return null;
144
+ const fields = result.stdout.split("\0");
145
+ const entries = [];
146
+ for (let index = 0; index < fields.length; index += 1) {
147
+ const record = fields[index];
148
+ if (!record || record.length < 4)
149
+ continue;
150
+ const code = record.slice(0, 2);
151
+ entries.push({ code, gitPath: record.slice(3) });
152
+ if (code.includes("R") || code.includes("C"))
153
+ index += 1;
154
+ }
155
+ return entries;
156
+ }
157
+ function isIgnored(repoRoot, gitPath) {
158
+ return git(repoRoot, ["check-ignore", "--no-index", "-q", "--", gitPath], GIT_PROBE_TIMEOUT_MS)
159
+ .ok;
160
+ }
161
+ export function collectAppliedConvergeUnits(repoRoot, candidates) {
162
+ const units = [];
163
+ const seen = new Set();
164
+ for (const candidate of candidates) {
165
+ let candidateUnit;
166
+ try {
167
+ candidateUnit = convergeUnitFromTarget(repoRoot, candidate.targetPath, {
168
+ action: "modify",
169
+ scope: "repo-committed",
170
+ reason: candidate.reason,
171
+ });
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ const statuses = readPorcelainStatus(repoRoot, candidateUnit.gitPath);
177
+ if (!statuses)
178
+ continue;
179
+ for (const { code, gitPath } of statuses) {
180
+ if (seen.has(gitPath))
181
+ continue;
182
+ let unit;
183
+ try {
184
+ unit = convergeUnitFromTarget(repoRoot, join(repoRoot, gitPath), {
185
+ action: code === "??" || code === "!!" || code.includes("A")
186
+ ? "create"
187
+ : code.includes("D")
188
+ ? "delete"
189
+ : "modify",
190
+ scope: "repo-committed",
191
+ reason: candidate.reason,
192
+ });
193
+ }
194
+ catch {
195
+ continue;
196
+ }
197
+ units.push({
198
+ ...unit,
199
+ scope: code === "!!" || (code === "??" && isIgnored(repoRoot, gitPath))
200
+ ? "repo-ignored"
201
+ : "repo-committed",
202
+ });
203
+ seen.add(gitPath);
204
+ }
205
+ }
206
+ return units;
207
+ }
208
+ export function authorConvergenceCommit(options) {
209
+ const allowlist = deriveCommitAllowlist(options.appliedUnits);
210
+ if (options.isCi)
211
+ return { status: "skipped-ci", ok: true, allowlist };
212
+ if (allowlist.length === 0)
213
+ return { status: "no-change", ok: true, allowlist };
214
+ const probe = (args) => git(options.repoRoot, args, GIT_PROBE_TIMEOUT_MS);
215
+ const before = probe(["rev-parse", "--verify", "HEAD"]);
216
+ if (!before.ok) {
217
+ return { status: "failed", ok: false, allowlist, error: before.stderr || "Missing HEAD." };
218
+ }
219
+ const add = probe(["add", "--", ...allowlist]);
220
+ if (!add.ok) {
221
+ return { status: "failed", ok: false, allowlist, error: add.stderr || "git add failed." };
222
+ }
223
+ const staged = probe(["diff", "--cached", "--name-only"]);
224
+ if (!staged.ok) {
225
+ return { status: "failed", ok: false, allowlist, error: staged.stderr || "git diff failed." };
226
+ }
227
+ if (lines(staged.stdout).length === 0) {
228
+ return { status: "no-change", ok: true, allowlist };
229
+ }
230
+ const commitTimeoutMs = resolveCommitTimeoutMs({
231
+ commitTimeoutMs: options.commitTimeoutMs,
232
+ });
233
+ const commit = git(options.repoRoot, ["commit", "-m", options.message], commitTimeoutMs);
234
+ if (!commit.ok) {
235
+ return {
236
+ status: "failed",
237
+ ok: false,
238
+ allowlist,
239
+ error: classifyCommitFailure(commit, commitTimeoutMs),
240
+ };
241
+ }
242
+ const committedHead = probe(["rev-parse", "--verify", "HEAD"]);
243
+ if (!committedHead.ok) {
244
+ return {
245
+ status: "failed",
246
+ ok: false,
247
+ allowlist,
248
+ error: committedHead.stderr || "Unable to validate the authored commit.",
249
+ };
250
+ }
251
+ const committedParent = probe(["rev-parse", "--verify", `${committedHead.stdout}^`]);
252
+ if (!committedParent.ok || committedParent.stdout !== before.stdout) {
253
+ return {
254
+ status: "failed",
255
+ ok: false,
256
+ allowlist,
257
+ commit: committedHead.stdout,
258
+ error: "The observed commit does not directly follow the recorded HEAD; no rollback was attempted. Inspect Git history and recover manually.",
259
+ };
260
+ }
261
+ const committedPathsResult = probe([
262
+ "diff-tree",
263
+ "--root",
264
+ "--no-commit-id",
265
+ "--name-only",
266
+ "-r",
267
+ committedHead.stdout,
268
+ ]);
269
+ if (!committedPathsResult.ok) {
270
+ return {
271
+ status: "failed",
272
+ ok: false,
273
+ allowlist,
274
+ commit: committedHead.stdout,
275
+ error: committedPathsResult.stderr || "Unable to inspect the authored commit.",
276
+ };
277
+ }
278
+ const committedPaths = lines(committedPathsResult.stdout);
279
+ const allowed = new Set(allowlist);
280
+ const committed = new Set(committedPaths);
281
+ const unexpectedPaths = committedPaths.filter((path) => !allowed.has(path));
282
+ const missingPaths = allowlist.filter((path) => !committed.has(path));
283
+ if (unexpectedPaths.length === 0 && missingPaths.length === 0) {
284
+ return {
285
+ status: "committed",
286
+ ok: true,
287
+ allowlist,
288
+ commit: committedHead.stdout,
289
+ };
290
+ }
291
+ const currentHead = probe(["rev-parse", "--verify", "HEAD"]);
292
+ if (!currentHead.ok || currentHead.stdout !== committedHead.stdout) {
293
+ return {
294
+ status: "failed",
295
+ ok: false,
296
+ allowlist,
297
+ commit: committedHead.stdout,
298
+ unexpectedPaths,
299
+ missingPaths,
300
+ error: "The setup-authored commit changed before validation; no rollback was attempted. Inspect Git history and recover manually.",
301
+ };
302
+ }
303
+ const rollback = probe(["reset", "--mixed", before.stdout]);
304
+ if (!rollback.ok) {
305
+ return {
306
+ status: "failed",
307
+ ok: false,
308
+ allowlist,
309
+ commit: committedHead.stdout,
310
+ unexpectedPaths,
311
+ missingPaths,
312
+ error: rollback.stderr || "Failed to undo the path-invalid setup commit.",
313
+ };
314
+ }
315
+ return {
316
+ status: "rolled-back",
317
+ ok: false,
318
+ allowlist,
319
+ commit: committedHead.stdout,
320
+ unexpectedPaths,
321
+ missingPaths,
322
+ error: `A commit hook changed setup commit scope (unexpected: ${unexpectedPaths.join(", ") || "none"}; missing: ${missingPaths.join(", ") || "none"}). ` +
323
+ "The setup commit was undone and all worktree changes were retained. Update the hook so it does not stage unrelated paths, then stage or discard those paths and rerun wp setup.",
324
+ };
325
+ }
@@ -0,0 +1,13 @@
1
+ export declare const CONVERGENCE_STATE_SCHEMA_VERSION: 1;
2
+ export declare const CONVERGENCE_STATE_PATH = ".webpresso/converged.json";
3
+ export interface ConvergenceState {
4
+ readonly schemaVersion: typeof CONVERGENCE_STATE_SCHEMA_VERSION;
5
+ readonly agentKitVersion: string;
6
+ readonly host: string;
7
+ readonly sentinels: Readonly<Record<string, string>>;
8
+ readonly writtenAt: string;
9
+ }
10
+ export declare function parseConvergenceState(value: unknown): ConvergenceState | null;
11
+ export declare function readConvergenceState(repoRoot: string): ConvergenceState | null;
12
+ export declare function isConvergenceStateCurrent(state: ConvergenceState | null, expected: Pick<ConvergenceState, "agentKitVersion" | "host" | "sentinels">): boolean;
13
+ export declare function writeConvergenceState(repoRoot: string, state: ConvergenceState): void;
@@ -0,0 +1,54 @@
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { writeJsonFile } from "#shared-utils/write-json-file.js";
4
+ export const CONVERGENCE_STATE_SCHEMA_VERSION = 1;
5
+ export const CONVERGENCE_STATE_PATH = ".webpresso/converged.json";
6
+ function isStringRecord(value) {
7
+ return (value !== null &&
8
+ typeof value === "object" &&
9
+ !Array.isArray(value) &&
10
+ Object.values(value).every((entry) => typeof entry === "string"));
11
+ }
12
+ export function parseConvergenceState(value) {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value))
14
+ return null;
15
+ const record = value;
16
+ if (record.schemaVersion !== CONVERGENCE_STATE_SCHEMA_VERSION ||
17
+ typeof record.agentKitVersion !== "string" ||
18
+ typeof record.host !== "string" ||
19
+ typeof record.writtenAt !== "string" ||
20
+ !isStringRecord(record.sentinels)) {
21
+ return null;
22
+ }
23
+ return {
24
+ schemaVersion: CONVERGENCE_STATE_SCHEMA_VERSION,
25
+ agentKitVersion: record.agentKitVersion,
26
+ host: record.host,
27
+ sentinels: record.sentinels,
28
+ writtenAt: record.writtenAt,
29
+ };
30
+ }
31
+ export function readConvergenceState(repoRoot) {
32
+ try {
33
+ return parseConvergenceState(JSON.parse(readFileSync(join(resolve(repoRoot), CONVERGENCE_STATE_PATH), "utf8")));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function isConvergenceStateCurrent(state, expected) {
40
+ if (!state ||
41
+ state.agentKitVersion !== expected.agentKitVersion ||
42
+ state.host !== expected.host) {
43
+ return false;
44
+ }
45
+ return Object.entries(expected.sentinels).every(([name, value]) => state.sentinels[name] === value);
46
+ }
47
+ export function writeConvergenceState(repoRoot, state) {
48
+ const statePath = join(resolve(repoRoot), CONVERGENCE_STATE_PATH);
49
+ mkdirSync(dirname(statePath), { recursive: true });
50
+ writeJsonFile(statePath, state, {
51
+ atomic: true,
52
+ indent: 2,
53
+ });
54
+ }
@@ -0,0 +1,23 @@
1
+ import type { WriteAction } from "./write-plan.js";
2
+ export declare const CONVERGE_SCOPES: readonly ["repo-committed", "repo-ignored"];
3
+ export type ConvergeScope = (typeof CONVERGE_SCOPES)[number];
4
+ export interface ConvergeUnit {
5
+ readonly repoRoot: string;
6
+ readonly targetPath: string;
7
+ readonly gitPath: string;
8
+ readonly action: WriteAction;
9
+ readonly scope: ConvergeScope;
10
+ readonly reason: string;
11
+ }
12
+ export interface CreateConvergeUnitInput {
13
+ readonly repoRoot: string;
14
+ readonly targetPath: string;
15
+ readonly gitPath: string;
16
+ readonly action: WriteAction;
17
+ readonly scope: ConvergeScope;
18
+ readonly reason: string;
19
+ }
20
+ export declare function createConvergeUnit(input: CreateConvergeUnitInput): ConvergeUnit;
21
+ export declare function convergeUnitFromTarget(repoRoot: string, targetPath: string, input: Pick<CreateConvergeUnitInput, "action" | "scope" | "reason">): ConvergeUnit;
22
+ /** Applied units are the sole authority for authored commit scope. */
23
+ export declare function deriveCommitAllowlist(appliedUnits: readonly ConvergeUnit[]): string[];
@@ -0,0 +1,57 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ export const CONVERGE_SCOPES = ["repo-committed", "repo-ignored"];
3
+ function normalizeGitPath(value) {
4
+ return value.replaceAll("\\", "/").replace(/^\.\//u, "");
5
+ }
6
+ function assertInsideRepo(repoRoot, targetPath) {
7
+ const relativePath = relative(repoRoot, targetPath);
8
+ if (relativePath.length === 0 ||
9
+ relativePath === ".." ||
10
+ relativePath.startsWith(`..${sep}`) ||
11
+ isAbsolute(relativePath)) {
12
+ throw new Error(`Convergence target must be a file inside the repository: ${targetPath}`);
13
+ }
14
+ return normalizeGitPath(relativePath);
15
+ }
16
+ export function createConvergeUnit(input) {
17
+ if (!isAbsolute(input.repoRoot)) {
18
+ throw new Error(`Convergence repo root must be absolute: ${input.repoRoot}`);
19
+ }
20
+ if (!isAbsolute(input.targetPath)) {
21
+ throw new Error(`Convergence target must be absolute: ${input.targetPath}`);
22
+ }
23
+ if (!CONVERGE_SCOPES.includes(input.scope)) {
24
+ throw new Error(`Unsupported convergence scope: ${input.scope}`);
25
+ }
26
+ const repoRoot = resolve(input.repoRoot);
27
+ const targetPath = resolve(input.targetPath);
28
+ const derivedGitPath = assertInsideRepo(repoRoot, targetPath);
29
+ const gitPath = normalizeGitPath(input.gitPath);
30
+ if (gitPath.length === 0 || isAbsolute(gitPath) || gitPath !== derivedGitPath) {
31
+ throw new Error(`Convergence git path must match its repo-anchored target: expected ${derivedGitPath}, received ${input.gitPath}`);
32
+ }
33
+ return {
34
+ repoRoot,
35
+ targetPath,
36
+ gitPath,
37
+ action: input.action,
38
+ scope: input.scope,
39
+ reason: input.reason,
40
+ };
41
+ }
42
+ export function convergeUnitFromTarget(repoRoot, targetPath, input) {
43
+ const anchoredRoot = resolve(repoRoot);
44
+ const anchoredTarget = resolve(targetPath);
45
+ return createConvergeUnit({
46
+ ...input,
47
+ repoRoot: anchoredRoot,
48
+ targetPath: anchoredTarget,
49
+ gitPath: assertInsideRepo(anchoredRoot, anchoredTarget),
50
+ });
51
+ }
52
+ /** Applied units are the sole authority for authored commit scope. */
53
+ export function deriveCommitAllowlist(appliedUnits) {
54
+ return [
55
+ ...new Set(appliedUnits.filter((unit) => unit.scope === "repo-committed").map((unit) => unit.gitPath)),
56
+ ].sort();
57
+ }
@@ -0,0 +1,19 @@
1
+ export type GitConvergenceStatus = "ready" | "fast-forwarded" | "not-git" | "unborn" | "detached" | "dirty" | "diverged" | "failed";
2
+ export interface GitConvergenceResult {
3
+ readonly status: GitConvergenceStatus;
4
+ readonly ok: boolean;
5
+ readonly repoRoot: string;
6
+ readonly head?: string;
7
+ readonly upstream?: string;
8
+ readonly shouldReplan: boolean;
9
+ readonly canCommit: boolean;
10
+ readonly warnings: readonly string[];
11
+ readonly error?: string;
12
+ }
13
+ export interface PrepareGitConvergenceOptions {
14
+ readonly repoRoot: string;
15
+ readonly isCi?: boolean;
16
+ readonly fetchTimeoutMs?: number;
17
+ readonly gitCommand?: string;
18
+ }
19
+ export declare function prepareRepositoryForConvergence(options: PrepareGitConvergenceOptions): GitConvergenceResult;
@@ -0,0 +1,168 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ const DEFAULT_GIT_TIMEOUT_MS = 5_000;
4
+ const GIT_MAX_BUFFER = 1024 * 1024;
5
+ function git(repoRoot, args, timeoutMs, command = "git") {
6
+ const result = spawnSync(command, [...args], {
7
+ cwd: repoRoot,
8
+ encoding: "utf8",
9
+ timeout: timeoutMs,
10
+ maxBuffer: GIT_MAX_BUFFER,
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ });
13
+ return {
14
+ ok: result.status === 0 && !result.error,
15
+ stdout: result.stdout.trim(),
16
+ stderr: result.stderr.trim(),
17
+ timedOut: result.error !== undefined && "code" in result.error && result.error.code === "ETIMEDOUT",
18
+ };
19
+ }
20
+ function failed(repoRoot, status, error) {
21
+ return {
22
+ status,
23
+ ok: false,
24
+ repoRoot,
25
+ shouldReplan: false,
26
+ canCommit: false,
27
+ warnings: [],
28
+ error,
29
+ };
30
+ }
31
+ function parseAheadBehindCounts(stdout) {
32
+ const parts = stdout.trim().split(/\s+/u);
33
+ if (parts.length !== 2)
34
+ return null;
35
+ if (!parts.every((part) => /^\d+$/u.test(part)))
36
+ return null;
37
+ const [ahead, behind] = parts.map((part) => Number.parseInt(part, 10));
38
+ if (ahead === undefined ||
39
+ behind === undefined ||
40
+ !Number.isSafeInteger(ahead) ||
41
+ !Number.isSafeInteger(behind) ||
42
+ ahead < 0 ||
43
+ behind < 0) {
44
+ return null;
45
+ }
46
+ return { ahead, behind };
47
+ }
48
+ export function prepareRepositoryForConvergence(options) {
49
+ const repoRoot = resolve(options.repoRoot);
50
+ const timeoutMs = options.fetchTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
51
+ const gitCommand = options.gitCommand ?? "git";
52
+ const runGit = (args) => git(repoRoot, args, timeoutMs, gitCommand);
53
+ const inside = runGit(["rev-parse", "--is-inside-work-tree"]);
54
+ if (!inside.ok || inside.stdout !== "true") {
55
+ return failed(repoRoot, "not-git", "Repository convergence requires a Git worktree.");
56
+ }
57
+ const headResult = runGit(["rev-parse", "--verify", "HEAD"]);
58
+ if (!headResult.ok) {
59
+ return failed(repoRoot, "unborn", "Repository convergence requires an existing HEAD commit.");
60
+ }
61
+ const head = headResult.stdout;
62
+ const branch = runGit(["symbolic-ref", "--quiet", "--short", "HEAD"]);
63
+ const detached = !branch.ok;
64
+ if (detached && !options.isCi) {
65
+ return failed(repoRoot, "detached", "Repository convergence refuses a detached HEAD.");
66
+ }
67
+ const trackedStatus = runGit(["status", "--porcelain=v1", "--untracked-files=no"]);
68
+ if (!trackedStatus.ok) {
69
+ return failed(repoRoot, "failed", trackedStatus.stderr || "Unable to inspect Git status.");
70
+ }
71
+ if (trackedStatus.stdout.length > 0) {
72
+ return failed(repoRoot, "dirty", "Repository convergence requires a clean tracked index and worktree.");
73
+ }
74
+ const warnings = [];
75
+ if (detached) {
76
+ warnings.push("Detached HEAD accepted in CI; skipped upstream update and authored commit.");
77
+ return {
78
+ status: "ready",
79
+ ok: true,
80
+ repoRoot,
81
+ head,
82
+ shouldReplan: false,
83
+ canCommit: false,
84
+ warnings,
85
+ };
86
+ }
87
+ const upstreamResult = runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
88
+ if (!upstreamResult.ok) {
89
+ warnings.push("No upstream is configured; skipped fetch and fast-forward.");
90
+ return {
91
+ status: "ready",
92
+ ok: true,
93
+ repoRoot,
94
+ head,
95
+ shouldReplan: false,
96
+ canCommit: !options.isCi,
97
+ warnings,
98
+ };
99
+ }
100
+ const upstream = upstreamResult.stdout;
101
+ const remoteSeparator = upstream.indexOf("/");
102
+ const remote = remoteSeparator === -1 ? upstream : upstream.slice(0, remoteSeparator);
103
+ const fetch = runGit(["fetch", "--quiet", "--no-tags", remote]);
104
+ if (!fetch.ok) {
105
+ warnings.push(fetch.timedOut
106
+ ? `Upstream fetch timed out after ${timeoutMs}ms; continuing from local refs.`
107
+ : `Upstream fetch unavailable; continuing from local refs${fetch.stderr ? `: ${fetch.stderr}` : "."}`);
108
+ }
109
+ const counts = runGit(["rev-list", "--left-right", "--count", `HEAD...${upstream}`]);
110
+ if (!counts.ok) {
111
+ return {
112
+ ...failed(repoRoot, "failed", counts.stderr || "Unable to compare HEAD with upstream."),
113
+ head,
114
+ upstream,
115
+ warnings,
116
+ };
117
+ }
118
+ const parsedCounts = parseAheadBehindCounts(counts.stdout);
119
+ if (!parsedCounts) {
120
+ return {
121
+ ...failed(repoRoot, "failed", `Unable to parse HEAD/upstream divergence counts from git rev-list output: ${JSON.stringify(counts.stdout)}.`),
122
+ head,
123
+ upstream,
124
+ warnings,
125
+ };
126
+ }
127
+ const { ahead, behind } = parsedCounts;
128
+ if (ahead > 0 && behind > 0) {
129
+ return {
130
+ ...failed(repoRoot, "diverged", `HEAD has diverged from ${upstream}; reconcile it manually.`),
131
+ head,
132
+ upstream,
133
+ warnings,
134
+ };
135
+ }
136
+ if (behind > 0) {
137
+ const merge = runGit(["merge", "--ff-only", upstream]);
138
+ if (!merge.ok) {
139
+ return {
140
+ ...failed(repoRoot, "failed", merge.stderr || `Unable to fast-forward from ${upstream}.`),
141
+ head,
142
+ upstream,
143
+ warnings,
144
+ };
145
+ }
146
+ const nextHead = runGit(["rev-parse", "--verify", "HEAD"]);
147
+ return {
148
+ status: "fast-forwarded",
149
+ ok: true,
150
+ repoRoot,
151
+ head: nextHead.ok ? nextHead.stdout : undefined,
152
+ upstream,
153
+ shouldReplan: true,
154
+ canCommit: !options.isCi,
155
+ warnings,
156
+ };
157
+ }
158
+ return {
159
+ status: "ready",
160
+ ok: true,
161
+ repoRoot,
162
+ head,
163
+ upstream,
164
+ shouldReplan: false,
165
+ canCommit: !options.isCi,
166
+ warnings,
167
+ };
168
+ }
@@ -55,8 +55,10 @@ export const GENERATED_PATHS_BLOCK = {
55
55
  ".runtime/",
56
56
  ".opencode/",
57
57
  ".codex/",
58
+ ".grok/",
58
59
  "_worktrees/",
59
60
  ".webpresso/hooks-manifest.json",
61
+ ".webpresso/converged.json",
60
62
  ".claude/settings.json",
61
63
  ".claude/settings.local.json",
62
64
  ".claude/agents/",