@cluesmith/codev 3.0.0 → 3.0.2

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 (311) hide show
  1. package/LICENSE +201 -0
  2. package/bin/af.js +2 -2
  3. package/bin/afx.js +8 -0
  4. package/bin/consult.js +2 -1
  5. package/bin/team.js +2 -1
  6. package/{dashboard/dist/assets/index-nr0cXxtf.css → dashboard-dist/assets/index-BWYZtvwm.css} +1 -1
  7. package/dashboard-dist/assets/index-Dn9zmcKp.js +195 -0
  8. package/dashboard-dist/assets/index-Dn9zmcKp.js.map +1 -0
  9. package/{dashboard/dist → dashboard-dist}/index.html +2 -2
  10. package/dist/agent-farm/cli.d.ts.map +1 -1
  11. package/dist/agent-farm/cli.js +27 -19
  12. package/dist/agent-farm/cli.js.map +1 -1
  13. package/dist/agent-farm/commands/architect.d.ts +4 -4
  14. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  15. package/dist/agent-farm/commands/architect.js +23 -12
  16. package/dist/agent-farm/commands/architect.js.map +1 -1
  17. package/dist/agent-farm/commands/attach.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/attach.js +101 -11
  19. package/dist/agent-farm/commands/attach.js.map +1 -1
  20. package/dist/agent-farm/commands/bench.d.ts +1 -1
  21. package/dist/agent-farm/commands/bench.js +1 -1
  22. package/dist/agent-farm/commands/cron.js +1 -1
  23. package/dist/agent-farm/commands/cron.js.map +1 -1
  24. package/dist/agent-farm/commands/db.d.ts +3 -3
  25. package/dist/agent-farm/commands/db.js +4 -4
  26. package/dist/agent-farm/commands/db.js.map +1 -1
  27. package/dist/agent-farm/commands/open.js +2 -2
  28. package/dist/agent-farm/commands/open.js.map +1 -1
  29. package/dist/agent-farm/commands/rename.js +1 -1
  30. package/dist/agent-farm/commands/rename.js.map +1 -1
  31. package/dist/agent-farm/commands/send.js +3 -3
  32. package/dist/agent-farm/commands/send.js.map +1 -1
  33. package/dist/agent-farm/commands/shell.js +3 -3
  34. package/dist/agent-farm/commands/shell.js.map +1 -1
  35. package/dist/agent-farm/commands/spawn-roles.d.ts +9 -5
  36. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -1
  37. package/dist/agent-farm/commands/spawn-roles.js +48 -28
  38. package/dist/agent-farm/commands/spawn-roles.js.map +1 -1
  39. package/dist/agent-farm/commands/spawn-worktree.d.ts +5 -5
  40. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  41. package/dist/agent-farm/commands/spawn-worktree.js +80 -13
  42. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  43. package/dist/agent-farm/commands/spawn.d.ts +6 -6
  44. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  45. package/dist/agent-farm/commands/spawn.js +90 -53
  46. package/dist/agent-farm/commands/spawn.js.map +1 -1
  47. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  48. package/dist/agent-farm/commands/status.js +48 -3
  49. package/dist/agent-farm/commands/status.js.map +1 -1
  50. package/dist/agent-farm/commands/team-update.d.ts +1 -1
  51. package/dist/agent-farm/commands/team-update.js +1 -1
  52. package/dist/agent-farm/commands/team.d.ts +1 -1
  53. package/dist/agent-farm/commands/team.js +1 -1
  54. package/dist/agent-farm/commands/tower-cloud.d.ts +2 -2
  55. package/dist/agent-farm/commands/tower-cloud.js +3 -3
  56. package/dist/agent-farm/commands/tower-cloud.js.map +1 -1
  57. package/dist/agent-farm/commands/tower.js +2 -2
  58. package/dist/agent-farm/commands/tower.js.map +1 -1
  59. package/dist/agent-farm/db/index.d.ts.map +1 -1
  60. package/dist/agent-farm/db/index.js +47 -0
  61. package/dist/agent-farm/db/index.js.map +1 -1
  62. package/dist/agent-farm/db/schema.d.ts +1 -1
  63. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  64. package/dist/agent-farm/db/schema.js +1 -1
  65. package/dist/agent-farm/db/types.d.ts +1 -1
  66. package/dist/agent-farm/lib/cloud-config.js +2 -2
  67. package/dist/agent-farm/lib/cloud-config.js.map +1 -1
  68. package/dist/agent-farm/lib/tower-client.d.ts +6 -230
  69. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  70. package/dist/agent-farm/lib/tower-client.js +6 -293
  71. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  72. package/dist/agent-farm/lib/tunnel-client.js +1 -1
  73. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  74. package/dist/agent-farm/servers/message-write.d.ts +24 -0
  75. package/dist/agent-farm/servers/message-write.d.ts.map +1 -0
  76. package/dist/agent-farm/servers/message-write.js +63 -0
  77. package/dist/agent-farm/servers/message-write.js.map +1 -0
  78. package/dist/agent-farm/servers/overview.d.ts +4 -1
  79. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  80. package/dist/agent-farm/servers/overview.js +38 -5
  81. package/dist/agent-farm/servers/overview.js.map +1 -1
  82. package/dist/agent-farm/servers/send-buffer.d.ts +4 -3
  83. package/dist/agent-farm/servers/send-buffer.d.ts.map +1 -1
  84. package/dist/agent-farm/servers/send-buffer.js +7 -4
  85. package/dist/agent-farm/servers/send-buffer.js.map +1 -1
  86. package/dist/agent-farm/servers/tower-cron-parser.js +1 -1
  87. package/dist/agent-farm/servers/tower-cron-parser.js.map +1 -1
  88. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  89. package/dist/agent-farm/servers/tower-cron.js +4 -5
  90. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  91. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-instances.js +6 -5
  93. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  94. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/tower-routes.js +19 -16
  96. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  97. package/dist/agent-farm/servers/tower-server.js +19 -4
  98. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  99. package/dist/agent-farm/servers/tower-terminals.d.ts +2 -2
  100. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  101. package/dist/agent-farm/servers/tower-terminals.js +47 -18
  102. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  103. package/dist/agent-farm/servers/tower-tunnel.js +2 -2
  104. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  105. package/dist/agent-farm/servers/tower-utils.d.ts +9 -6
  106. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  107. package/dist/agent-farm/servers/tower-utils.js +12 -6
  108. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  109. package/dist/agent-farm/state.js +1 -1
  110. package/dist/agent-farm/state.js.map +1 -1
  111. package/dist/agent-farm/types.d.ts +11 -2
  112. package/dist/agent-farm/types.d.ts.map +1 -1
  113. package/dist/agent-farm/utils/config.d.ts +11 -0
  114. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  115. package/dist/agent-farm/utils/config.js +21 -0
  116. package/dist/agent-farm/utils/config.js.map +1 -1
  117. package/dist/agent-farm/utils/harness.d.ts +85 -0
  118. package/dist/agent-farm/utils/harness.d.ts.map +1 -0
  119. package/dist/agent-farm/utils/harness.js +212 -0
  120. package/dist/agent-farm/utils/harness.js.map +1 -0
  121. package/dist/agent-farm/utils/server-utils.d.ts +13 -0
  122. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  123. package/dist/agent-farm/utils/server-utils.js +38 -0
  124. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  125. package/dist/cli.d.ts +1 -1
  126. package/dist/cli.d.ts.map +1 -1
  127. package/dist/cli.js +19 -5
  128. package/dist/cli.js.map +1 -1
  129. package/dist/commands/adopt.js +2 -2
  130. package/dist/commands/adopt.js.map +1 -1
  131. package/dist/commands/consult/index.d.ts +37 -3
  132. package/dist/commands/consult/index.d.ts.map +1 -1
  133. package/dist/commands/consult/index.js +220 -150
  134. package/dist/commands/consult/index.js.map +1 -1
  135. package/dist/commands/doctor.d.ts.map +1 -1
  136. package/dist/commands/doctor.js +54 -8
  137. package/dist/commands/doctor.js.map +1 -1
  138. package/dist/commands/init.js +1 -1
  139. package/dist/commands/init.js.map +1 -1
  140. package/dist/commands/porch/artifacts.d.ts +69 -0
  141. package/dist/commands/porch/artifacts.d.ts.map +1 -0
  142. package/dist/commands/porch/artifacts.js +368 -0
  143. package/dist/commands/porch/artifacts.js.map +1 -0
  144. package/dist/commands/porch/checks.d.ts +8 -1
  145. package/dist/commands/porch/checks.d.ts.map +1 -1
  146. package/dist/commands/porch/checks.js +98 -1
  147. package/dist/commands/porch/checks.js.map +1 -1
  148. package/dist/commands/porch/index.d.ts +18 -7
  149. package/dist/commands/porch/index.d.ts.map +1 -1
  150. package/dist/commands/porch/index.js +299 -61
  151. package/dist/commands/porch/index.js.map +1 -1
  152. package/dist/commands/porch/next.d.ts.map +1 -1
  153. package/dist/commands/porch/next.js +72 -74
  154. package/dist/commands/porch/next.js.map +1 -1
  155. package/dist/commands/porch/notify.d.ts +2 -2
  156. package/dist/commands/porch/notify.js +5 -5
  157. package/dist/commands/porch/notify.js.map +1 -1
  158. package/dist/commands/porch/prompts.d.ts +2 -1
  159. package/dist/commands/porch/prompts.d.ts.map +1 -1
  160. package/dist/commands/porch/prompts.js +23 -14
  161. package/dist/commands/porch/prompts.js.map +1 -1
  162. package/dist/commands/porch/state.d.ts +27 -2
  163. package/dist/commands/porch/state.d.ts.map +1 -1
  164. package/dist/commands/porch/state.js +96 -9
  165. package/dist/commands/porch/state.js.map +1 -1
  166. package/dist/commands/porch/types.d.ts +8 -0
  167. package/dist/commands/porch/types.d.ts.map +1 -1
  168. package/dist/lib/config.d.ts +14 -0
  169. package/dist/lib/config.d.ts.map +1 -1
  170. package/dist/lib/config.js +7 -0
  171. package/dist/lib/config.js.map +1 -1
  172. package/dist/lib/forge-contracts.d.ts +10 -1
  173. package/dist/lib/forge-contracts.d.ts.map +1 -1
  174. package/dist/lib/forge-contracts.js.map +1 -1
  175. package/dist/lib/forge.d.ts.map +1 -1
  176. package/dist/lib/forge.js +25 -7
  177. package/dist/lib/forge.js.map +1 -1
  178. package/dist/lib/team-github.d.ts +49 -0
  179. package/dist/lib/team-github.d.ts.map +1 -1
  180. package/dist/lib/team-github.js +115 -6
  181. package/dist/lib/team-github.js.map +1 -1
  182. package/dist/terminal/pty-manager.d.ts.map +1 -1
  183. package/dist/terminal/pty-manager.js +2 -0
  184. package/dist/terminal/pty-manager.js.map +1 -1
  185. package/dist/terminal/ws-protocol.d.ts +10 -8
  186. package/dist/terminal/ws-protocol.d.ts.map +1 -1
  187. package/dist/terminal/ws-protocol.js +3 -0
  188. package/dist/terminal/ws-protocol.js.map +1 -1
  189. package/package.json +28 -25
  190. package/scripts/forge/gitea/auth-status.sh +0 -0
  191. package/scripts/forge/gitea/issue-comment.sh +0 -0
  192. package/scripts/forge/gitea/issue-list.sh +0 -0
  193. package/scripts/forge/gitea/issue-view.sh +0 -0
  194. package/scripts/forge/gitea/pr-exists.sh +4 -1
  195. package/scripts/forge/gitea/pr-list.sh +0 -0
  196. package/scripts/forge/gitea/pr-merge.sh +0 -0
  197. package/scripts/forge/gitea/pr-view.sh +0 -0
  198. package/scripts/forge/gitea/recently-closed.sh +0 -0
  199. package/scripts/forge/gitea/recently-merged.sh +0 -0
  200. package/scripts/forge/gitea/repo-archive.sh +0 -0
  201. package/scripts/forge/gitea/user-identity.sh +0 -0
  202. package/scripts/forge/github/auth-status.sh +0 -0
  203. package/scripts/forge/github/issue-comment.sh +0 -0
  204. package/scripts/forge/github/issue-list.sh +2 -2
  205. package/scripts/forge/github/issue-view.sh +0 -0
  206. package/scripts/forge/github/on-it-timestamps.sh +0 -0
  207. package/scripts/forge/github/pr-diff.sh +0 -0
  208. package/scripts/forge/github/pr-exists.sh +3 -1
  209. package/scripts/forge/github/pr-list.sh +2 -2
  210. package/scripts/forge/github/pr-merge.sh +0 -0
  211. package/scripts/forge/github/pr-search.sh +0 -0
  212. package/scripts/forge/github/pr-view.sh +0 -0
  213. package/scripts/forge/github/recently-closed.sh +0 -0
  214. package/scripts/forge/github/recently-merged.sh +0 -0
  215. package/scripts/forge/github/repo-archive.sh +0 -0
  216. package/scripts/forge/github/team-activity.sh +0 -0
  217. package/scripts/forge/github/user-identity.sh +0 -0
  218. package/scripts/forge/gitlab/auth-status.sh +0 -0
  219. package/scripts/forge/gitlab/issue-comment.sh +0 -0
  220. package/scripts/forge/gitlab/issue-list.sh +0 -0
  221. package/scripts/forge/gitlab/issue-view.sh +0 -0
  222. package/scripts/forge/gitlab/pr-diff.sh +0 -0
  223. package/scripts/forge/gitlab/pr-exists.sh +3 -1
  224. package/scripts/forge/gitlab/pr-list.sh +0 -0
  225. package/scripts/forge/gitlab/pr-merge.sh +0 -0
  226. package/scripts/forge/gitlab/pr-search.sh +0 -0
  227. package/scripts/forge/gitlab/pr-view.sh +0 -0
  228. package/scripts/forge/gitlab/recently-closed.sh +0 -0
  229. package/scripts/forge/gitlab/recently-merged.sh +0 -0
  230. package/scripts/forge/gitlab/repo-archive.sh +0 -0
  231. package/scripts/forge/gitlab/user-identity.sh +0 -0
  232. package/scripts/forge/linear/auth-status.sh +15 -0
  233. package/scripts/forge/linear/issue-comment.sh +38 -0
  234. package/scripts/forge/linear/issue-list.sh +35 -0
  235. package/scripts/forge/linear/issue-view.sh +37 -0
  236. package/scripts/forge/linear/recently-closed.sh +45 -0
  237. package/scripts/forge/linear/user-identity.sh +15 -0
  238. package/scripts/postinstall.mjs +51 -0
  239. package/skeleton/.claude/skills/afx/SKILL.md +144 -0
  240. package/skeleton/.claude/skills/codev/SKILL.md +3 -3
  241. package/skeleton/.claude/skills/consult/SKILL.md +1 -1
  242. package/skeleton/.claude/skills/porch/SKILL.md +2 -2
  243. package/skeleton/.claude/skills/update-arch-docs/SKILL.md +107 -0
  244. package/skeleton/DEPENDENCIES.md +1 -1
  245. package/skeleton/builders.md +4 -4
  246. package/skeleton/protocol-schema.json +2 -2
  247. package/skeleton/protocols/air/builder-prompt.md +6 -6
  248. package/skeleton/protocols/air/prompts/pr.md +17 -5
  249. package/skeleton/protocols/air/protocol.md +1 -2
  250. package/skeleton/protocols/aspir/builder-prompt.md +28 -6
  251. package/skeleton/protocols/aspir/prompts/review.md +11 -0
  252. package/skeleton/protocols/aspir/protocol.json +9 -1
  253. package/skeleton/protocols/aspir/protocol.md +1 -1
  254. package/skeleton/protocols/aspir/templates/plan.md +0 -20
  255. package/skeleton/protocols/aspir/templates/spec.md +0 -28
  256. package/skeleton/protocols/bugfix/builder-prompt.md +6 -6
  257. package/skeleton/protocols/bugfix/prompts/pr.md +16 -4
  258. package/skeleton/protocols/experiment/builder-prompt.md +15 -0
  259. package/skeleton/protocols/experiment/protocol.md +1 -6
  260. package/skeleton/protocols/maintain/builder-prompt.md +12 -11
  261. package/skeleton/protocols/maintain/prompts/maintain.md +88 -0
  262. package/skeleton/protocols/maintain/prompts/review.md +84 -0
  263. package/skeleton/protocols/maintain/protocol.json +14 -68
  264. package/skeleton/protocols/maintain/protocol.md +163 -394
  265. package/skeleton/protocols/maintain/templates/maintenance-run.md +18 -49
  266. package/skeleton/protocols/protocol-schema.json +1 -1
  267. package/skeleton/protocols/research/builder-prompt.md +85 -0
  268. package/skeleton/protocols/research/protocol.json +118 -0
  269. package/skeleton/protocols/research/protocol.md +169 -0
  270. package/skeleton/protocols/spike/builder-prompt.md +1 -1
  271. package/skeleton/protocols/spike/protocol.md +4 -4
  272. package/skeleton/protocols/spir/builder-prompt.md +28 -5
  273. package/skeleton/protocols/spir/prompts/review.md +11 -0
  274. package/skeleton/protocols/spir/protocol.json +9 -1
  275. package/skeleton/protocols/spir/protocol.md +6 -5
  276. package/skeleton/protocols/spir/templates/plan.md +0 -20
  277. package/skeleton/protocols/spir/templates/spec.md +0 -28
  278. package/skeleton/resources/commands/agent-farm.md +84 -84
  279. package/skeleton/resources/commands/codev.md +1 -1
  280. package/skeleton/resources/commands/consult.md +16 -7
  281. package/skeleton/resources/commands/overview.md +14 -15
  282. package/skeleton/resources/risk-triage.md +2 -2
  283. package/skeleton/resources/workflow-reference.md +14 -14
  284. package/skeleton/roles/architect.md +39 -39
  285. package/skeleton/roles/builder.md +22 -14
  286. package/skeleton/templates/AGENTS.md +5 -3
  287. package/skeleton/templates/CLAUDE.md +5 -3
  288. package/skeleton/templates/arch.md +100 -30
  289. package/skeleton/templates/cheatsheet.md +12 -12
  290. package/skeleton/templates/lessons-learned.md +45 -7
  291. package/templates/3d-viewer.html +16 -18
  292. package/templates/tower.html +1 -1
  293. package/dashboard/dist/assets/index-CTrlY11L.js +0 -194
  294. package/dashboard/dist/assets/index-CTrlY11L.js.map +0 -1
  295. package/skeleton/.claude/skills/af/SKILL.md +0 -146
  296. package/skeleton/porch/prompts/understand.md +0 -61
  297. package/skeleton/porch/prompts/verify.md +0 -58
  298. package/skeleton/protocols/maintain/prompts/audit.md +0 -111
  299. package/skeleton/protocols/maintain/prompts/clean.md +0 -91
  300. package/skeleton/protocols/maintain/prompts/sync.md +0 -113
  301. package/skeleton/protocols/maintain/prompts/verify.md +0 -110
  302. package/skeleton/protocols/tick/builder-prompt.md +0 -65
  303. package/skeleton/protocols/tick/consult-types/impl-review.md +0 -72
  304. package/skeleton/protocols/tick/consult-types/plan-review.md +0 -59
  305. package/skeleton/protocols/tick/consult-types/pr-review.md +0 -72
  306. package/skeleton/protocols/tick/consult-types/spec-review.md +0 -55
  307. package/skeleton/protocols/tick/protocol.json +0 -151
  308. package/skeleton/protocols/tick/protocol.md +0 -277
  309. package/skeleton/protocols/tick/templates/plan.md +0 -67
  310. package/skeleton/protocols/tick/templates/review.md +0 -89
  311. package/skeleton/protocols/tick/templates/spec.md +0 -61
@@ -8,12 +8,14 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import chalk from 'chalk';
10
10
  import { globSync } from 'glob';
11
- import { readState, writeState, createInitialState, findStatusPath, getProjectDir, getStatusPath, resolveProjectId, resolveArtifactBaseName, } from './state.js';
11
+ import { readState, writeStateAndCommit, createInitialState, findStatusPath, getArtifactRoot, getProjectDir, getStatusPath, resolveProjectId, resolveArtifactBaseName, } from './state.js';
12
12
  import { loadProtocol, getPhaseConfig, getNextPhase, getPhaseChecks, getPhaseGate, isPhased, isBuildVerify, getVerifyConfig, } from './protocol.js';
13
- import { findPlanFile, extractPhasesFromFile, getCurrentPlanPhase, getPhaseContent, allPlanPhasesComplete, } from './plan.js';
13
+ import { findPlanFile, extractPlanPhases, getCurrentPlanPhase, getPhaseContent, allPlanPhasesComplete, } from './plan.js';
14
+ import { getResolver, LocalResolver } from './artifacts.js';
14
15
  import { runPhaseChecks, formatCheckResults, allChecksPassed, } from './checks.js';
15
16
  import { loadCheckOverrides } from './config.js';
16
17
  import { loadConfig } from '../../lib/config.js';
18
+ import { version } from '../../version.js';
17
19
  // ============================================================================
18
20
  // Output Helpers
19
21
  // ============================================================================
@@ -24,6 +26,26 @@ function header(text) {
24
26
  function section(title, content) {
25
27
  return `\n${chalk.bold(title)}:\n${content}`;
26
28
  }
29
+ /**
30
+ * Return a resolver scoped to `artifactRoot` when it differs from the caller's
31
+ * cwd-rooted resolver. The incoming `resolver` is typically built from
32
+ * `process.cwd()` (the main workspace), but `findStatusPath` may resolve a
33
+ * project to a builder worktree under `.builders/`. Checks like `plan_exists`
34
+ * must read from that worktree, not from the main tree (bugfix #676).
35
+ *
36
+ * For the LOCAL backend we rebuild the resolver against `artifactRoot` so file
37
+ * lookups point at `<artifactRoot>/codev/...`. For other backends (e.g. CLI)
38
+ * artifact location is already independent of the filesystem, so we keep the
39
+ * caller's resolver.
40
+ */
41
+ function scopeResolver(workspaceRoot, artifactRoot, resolver) {
42
+ if (artifactRoot === workspaceRoot)
43
+ return resolver;
44
+ // Only rebuild if the existing resolver is a LocalResolver (path-dependent).
45
+ if (resolver && !(resolver instanceof LocalResolver))
46
+ return resolver;
47
+ return getResolver(workspaceRoot, artifactRoot);
48
+ }
27
49
  /**
28
50
  * Log override/skip notices before running checks.
29
51
  * Only emits output when overrides are actually in use.
@@ -58,7 +80,7 @@ function logCheckOverrides(phaseCheckNames, resolvedChecks, overrides) {
58
80
  * porch status <id>
59
81
  * Shows current state and prescriptive next steps.
60
82
  */
61
- export async function status(workspaceRoot, projectId) {
83
+ export async function status(workspaceRoot, projectId, resolver) {
62
84
  const statusPath = findStatusPath(workspaceRoot, projectId);
63
85
  if (!statusPath) {
64
86
  throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`);
@@ -79,7 +101,8 @@ export async function status(workspaceRoot, projectId) {
79
101
  // Status icons
80
102
  const icon = (status) => {
81
103
  switch (status) {
82
- case 'complete': return chalk.green('✓');
104
+ case 'verified': return chalk.green('✓');
105
+ case 'complete': return chalk.green('✓'); // backward compat
83
106
  case 'in_progress': return chalk.yellow('►');
84
107
  default: return chalk.gray('○');
85
108
  }
@@ -95,11 +118,11 @@ export async function status(workspaceRoot, projectId) {
95
118
  if (currentPlanPhase) {
96
119
  console.log('');
97
120
  console.log(chalk.bold(`CURRENT: ${currentPlanPhase.id} - ${currentPlanPhase.title}`));
98
- // Show phase content from plan
99
- const planPath = findPlanFile(workspaceRoot, state.id, state.title);
100
- if (planPath) {
101
- const content = fs.readFileSync(planPath, 'utf-8');
102
- const phaseContent = getPhaseContent(content, currentPlanPhase.id);
121
+ // Show phase content from plan (via resolver if available)
122
+ const planContent = resolver?.getPlanContent(state.id, state.title)
123
+ ?? (() => { const p = findPlanFile(workspaceRoot, state.id, state.title); return p ? fs.readFileSync(p, 'utf-8') : null; })();
124
+ if (planContent) {
125
+ const phaseContent = getPhaseContent(planContent, currentPlanPhase.id);
103
126
  if (phaseContent) {
104
127
  console.log(section('FROM THE PLAN', phaseContent.slice(0, 500)));
105
128
  }
@@ -146,11 +169,16 @@ export async function status(workspaceRoot, projectId) {
146
169
  * porch check <id>
147
170
  * Runs the phase checks and reports results.
148
171
  */
149
- export async function check(workspaceRoot, projectId) {
172
+ export async function check(workspaceRoot, projectId, resolver) {
150
173
  const statusPath = findStatusPath(workspaceRoot, projectId);
151
174
  if (!statusPath) {
152
175
  throw new Error(`Project ${projectId} not found.`);
153
176
  }
177
+ // Scope artifact reads + check cwd to the worktree that owns this status.yaml.
178
+ // `findStatusPath` searches `.builders/*` first; resolver/cwd must match so
179
+ // checks like `plan_exists` see files in the same worktree (bugfix #676).
180
+ const artifactRoot = getArtifactRoot(statusPath);
181
+ const scopedResolver = scopeResolver(workspaceRoot, artifactRoot, resolver);
154
182
  const state = readState(statusPath);
155
183
  const protocol = loadProtocol(workspaceRoot, state.protocol);
156
184
  const overrides = loadCheckOverrides(workspaceRoot);
@@ -161,7 +189,7 @@ export async function check(workspaceRoot, projectId) {
161
189
  console.log(chalk.dim('No checks defined for this phase.'));
162
190
  return;
163
191
  }
164
- const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(workspaceRoot, state.id, state.title) };
192
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(artifactRoot, state.id, state.title, scopedResolver) };
165
193
  console.log('');
166
194
  console.log(chalk.bold('RUNNING CHECKS...'));
167
195
  logCheckOverrides(phaseCheckNames, checks, overrides);
@@ -174,7 +202,7 @@ export async function check(workspaceRoot, projectId) {
174
202
  console.log('');
175
203
  return;
176
204
  }
177
- const results = await runPhaseChecks(checks, workspaceRoot, checkEnv);
205
+ const results = await runPhaseChecks(checks, artifactRoot, checkEnv, undefined, scopedResolver);
178
206
  console.log(formatCheckResults(results));
179
207
  console.log('');
180
208
  if (allChecksPassed(results)) {
@@ -191,17 +219,50 @@ export async function check(workspaceRoot, projectId) {
191
219
  * porch done <id>
192
220
  * Advances to next phase if checks pass. Refuses if checks fail.
193
221
  */
194
- export async function done(workspaceRoot, projectId) {
222
+ export async function done(workspaceRoot, projectId, resolver, options) {
195
223
  const statusPath = findStatusPath(workspaceRoot, projectId);
196
224
  if (!statusPath) {
197
225
  throw new Error(`Project ${projectId} not found.`);
198
226
  }
199
227
  let state = readState(statusPath);
228
+ // Record-only mode: --pr or --merged writes PR metadata and exits immediately.
229
+ // Does NOT run checks, does NOT advance the phase, does NOT mark build_complete.
230
+ if (options?.pr !== undefined) {
231
+ if (!options.branch)
232
+ throw new Error('--pr requires --branch <name>');
233
+ if (!state.pr_history)
234
+ state.pr_history = [];
235
+ state.pr_history.push({
236
+ phase: state.phase,
237
+ pr_number: options.pr,
238
+ branch: options.branch,
239
+ created_at: new Date().toISOString(),
240
+ });
241
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} record PR #${options.pr}`);
242
+ console.log(chalk.green(`Recorded PR #${options.pr} (branch: ${options.branch}) in pr_history.`));
243
+ return;
244
+ }
245
+ if (options?.merged !== undefined) {
246
+ if (!state.pr_history)
247
+ throw new Error(`No PR history found for project ${projectId}`);
248
+ const entry = state.pr_history.find(e => e.pr_number === options.merged);
249
+ if (!entry)
250
+ throw new Error(`PR #${options.merged} not found in pr_history`);
251
+ entry.merged = true;
252
+ entry.merged_at = new Date().toISOString();
253
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} PR #${options.merged} merged`);
254
+ console.log(chalk.green(`Marked PR #${options.merged} as merged.`));
255
+ return;
256
+ }
200
257
  const protocol = loadProtocol(workspaceRoot, state.protocol);
201
258
  const overrides = loadCheckOverrides(workspaceRoot);
202
259
  const phaseConfig = getPhaseConfig(protocol, state.phase);
203
260
  const phaseCheckNames = phaseConfig?.checks ?? [];
204
261
  const checks = getPhaseChecks(protocol, state.phase, overrides ?? undefined);
262
+ // Scope artifact reads + check cwd to the worktree that owns this status.yaml
263
+ // (bugfix #676 — see check() for rationale).
264
+ const artifactRoot = getArtifactRoot(statusPath);
265
+ const scopedResolver = scopeResolver(workspaceRoot, artifactRoot, resolver);
205
266
  // Run checks first — but skip if the gate was just approved (approve already ran them)
206
267
  if (phaseCheckNames.length > 0) {
207
268
  const gate = getPhaseGate(protocol, state.phase);
@@ -213,12 +274,12 @@ export async function done(workspaceRoot, projectId) {
213
274
  console.log(chalk.dim('Checks skipped (gate approved <60s ago).'));
214
275
  }
215
276
  else {
216
- const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(workspaceRoot, state.id, state.title) };
277
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(artifactRoot, state.id, state.title, scopedResolver) };
217
278
  console.log('');
218
279
  console.log(chalk.bold('RUNNING CHECKS...'));
219
280
  logCheckOverrides(phaseCheckNames, checks, overrides);
220
281
  if (Object.keys(checks).length > 0) {
221
- const results = await runPhaseChecks(checks, workspaceRoot, checkEnv);
282
+ const results = await runPhaseChecks(checks, artifactRoot, checkEnv, undefined, scopedResolver);
222
283
  console.log(formatCheckResults(results));
223
284
  if (!allChecksPassed(results)) {
224
285
  console.log('');
@@ -235,7 +296,7 @@ export async function done(workspaceRoot, projectId) {
235
296
  // For build_verify phases: mark build as complete for verification
236
297
  if (isBuildVerify(protocol, state.phase) && !state.build_complete) {
237
298
  state.build_complete = true;
238
- writeState(statusPath, state);
299
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${state.phase} build-complete`);
239
300
  console.log('');
240
301
  console.log(chalk.green('BUILD COMPLETE. Ready for verification.'));
241
302
  console.log(`\n Run: porch next ${state.id} (to get verification tasks)`);
@@ -298,9 +359,17 @@ export async function done(workspaceRoot, projectId) {
298
359
  }
299
360
  }
300
361
  }
301
- // Check for gate
362
+ // Check for gate — auto-request if not yet requested
302
363
  const gate = getPhaseGate(protocol, state.phase);
303
364
  if (gate && state.gates[gate]?.status !== 'approved') {
365
+ // Auto-request the gate if it hasn't been requested yet
366
+ if (!state.gates[gate]) {
367
+ state.gates[gate] = { status: 'pending' };
368
+ }
369
+ if (!state.gates[gate].requested_at) {
370
+ state.gates[gate].requested_at = new Date().toISOString();
371
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gate} gate-requested`);
372
+ }
304
373
  console.log('');
305
374
  console.log(chalk.yellow(`GATE REQUIRED: ${gate}`));
306
375
  console.log(`\n Run: porch gate ${state.id}`);
@@ -320,13 +389,13 @@ export async function done(workspaceRoot, projectId) {
320
389
  }
321
390
  }
322
391
  // Advance to next protocol phase
323
- advanceProtocolPhase(workspaceRoot, state, protocol, statusPath);
392
+ await advanceProtocolPhase(workspaceRoot, state, protocol, statusPath, scopedResolver);
324
393
  }
325
- function advanceProtocolPhase(workspaceRoot, state, protocol, statusPath) {
394
+ async function advanceProtocolPhase(workspaceRoot, state, protocol, statusPath, resolver) {
326
395
  const nextPhase = getNextPhase(protocol, state.phase);
327
396
  if (!nextPhase) {
328
- state.phase = 'complete';
329
- writeState(statusPath, state);
397
+ state.phase = 'verified';
398
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} protocol complete`);
330
399
  console.log('');
331
400
  console.log(chalk.green.bold('🎉 PROTOCOL COMPLETE'));
332
401
  console.log(`\n Project ${state.id} has completed the ${state.protocol} protocol.`);
@@ -337,16 +406,17 @@ function advanceProtocolPhase(workspaceRoot, state, protocol, statusPath) {
337
406
  state.iteration = 1;
338
407
  // If entering a phased phase (implement), extract plan phases
339
408
  if (isPhased(protocol, nextPhase.id)) {
340
- const planPath = findPlanFile(workspaceRoot, state.id, state.title);
341
- if (planPath) {
342
- state.plan_phases = extractPhasesFromFile(planPath);
343
- // extractPhasesFromFile already marks first phase as in_progress
409
+ const planContent = resolver?.getPlanContent(state.id, state.title)
410
+ ?? (() => { const p = findPlanFile(workspaceRoot, state.id, state.title); return p ? fs.readFileSync(p, 'utf-8') : null; })();
411
+ if (planContent) {
412
+ state.plan_phases = extractPlanPhases(planContent);
413
+ // extractPlanPhases already marks first phase as in_progress
344
414
  if (state.plan_phases.length > 0) {
345
415
  state.current_plan_phase = state.plan_phases[0].id;
346
416
  }
347
417
  }
348
418
  }
349
- writeState(statusPath, state);
419
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${nextPhase.id} phase-transition`);
350
420
  console.log('');
351
421
  console.log(chalk.green(`ADVANCING TO: ${nextPhase.id} - ${nextPhase.name}`));
352
422
  // If we just entered implement phase, show phase 1 info and the critical warning
@@ -355,11 +425,11 @@ function advanceProtocolPhase(workspaceRoot, state, protocol, statusPath) {
355
425
  const nextPlanPhase = state.plan_phases[1];
356
426
  console.log('');
357
427
  console.log(chalk.bold(`YOUR TASK: ${firstPhase.id} - "${firstPhase.title}"`));
358
- // Show phase content from plan
359
- const planPath = findPlanFile(workspaceRoot, state.id, state.title);
360
- if (planPath) {
361
- const content = fs.readFileSync(planPath, 'utf-8');
362
- const phaseContent = getPhaseContent(content, firstPhase.id);
428
+ // Show phase content from plan (via resolver if available)
429
+ const planContentForDisplay = resolver?.getPlanContent(state.id, state.title)
430
+ ?? (() => { const p = findPlanFile(workspaceRoot, state.id, state.title); return p ? fs.readFileSync(p, 'utf-8') : null; })();
431
+ if (planContentForDisplay) {
432
+ const phaseContent = getPhaseContent(planContentForDisplay, firstPhase.id);
363
433
  if (phaseContent) {
364
434
  console.log(section('FROM THE PLAN', phaseContent.slice(0, 800)));
365
435
  }
@@ -383,7 +453,7 @@ function advanceProtocolPhase(workspaceRoot, state, protocol, statusPath) {
383
453
  * porch gate <id>
384
454
  * Requests human approval for current gate.
385
455
  */
386
- export async function gate(workspaceRoot, projectId) {
456
+ export async function gate(workspaceRoot, projectId, resolver) {
387
457
  const statusPath = findStatusPath(workspaceRoot, projectId);
388
458
  if (!statusPath) {
389
459
  throw new Error(`Project ${projectId} not found.`);
@@ -402,22 +472,22 @@ export async function gate(workspaceRoot, projectId) {
402
472
  }
403
473
  if (!state.gates[gateName].requested_at) {
404
474
  state.gates[gateName].requested_at = new Date().toISOString();
405
- writeState(statusPath, state);
475
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-requested`);
406
476
  }
407
477
  console.log('');
408
478
  console.log(chalk.bold(`GATE: ${gateName}`));
409
479
  console.log('');
410
480
  // Show relevant artifact and open it for review
411
- const artifact = getArtifactForPhase(workspaceRoot, state);
481
+ const artifact = getArtifactForPhase(workspaceRoot, state, resolver);
412
482
  if (artifact) {
413
483
  const fullPath = path.join(workspaceRoot, artifact);
414
484
  if (fs.existsSync(fullPath)) {
415
485
  console.log(` Artifact: ${artifact}`);
416
486
  console.log('');
417
487
  console.log(chalk.cyan(' Opening artifact for human review...'));
418
- // Use af open to display in annotation viewer
488
+ // Use afx open to display in annotation viewer
419
489
  const { spawn } = await import('node:child_process');
420
- spawn('af', ['open', fullPath], {
490
+ spawn('afx', ['open', fullPath], {
421
491
  stdio: 'inherit',
422
492
  detached: true
423
493
  }).unref();
@@ -436,15 +506,34 @@ export async function gate(workspaceRoot, projectId) {
436
506
  * porch approve <id> <gate> --a-human-explicitly-approved-this
437
507
  * Human approves a gate. Requires explicit flag to prevent automated approvals.
438
508
  */
439
- export async function approve(workspaceRoot, projectId, gateName, hasHumanFlag) {
509
+ export async function approve(workspaceRoot, projectId, gateName, hasHumanFlag, resolver) {
440
510
  const statusPath = findStatusPath(workspaceRoot, projectId);
441
511
  if (!statusPath) {
442
512
  throw new Error(`Project ${projectId} not found.`);
443
513
  }
514
+ // Scope artifact reads + check cwd to the worktree that owns this status.yaml
515
+ // (bugfix #676 — see check() for rationale).
516
+ const artifactRoot = getArtifactRoot(statusPath);
517
+ const scopedResolver = scopeResolver(workspaceRoot, artifactRoot, resolver);
444
518
  const state = readState(statusPath);
519
+ // Convenience: for verify-approval, auto-complete porch done if build_complete is false
520
+ if (gateName === 'verify-approval' && state.phase === 'verify' && !state.build_complete) {
521
+ state.build_complete = true;
522
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} verify build-complete (auto)`);
523
+ }
524
+ // Auto-create gate entry for upgraded projects (e.g., verify-approval missing after upgrade)
445
525
  if (!state.gates[gateName]) {
446
- const knownGates = Object.keys(state.gates).join(', ');
447
- throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`);
526
+ const protocol = loadProtocol(workspaceRoot, state.protocol);
527
+ const phaseGate = getPhaseGate(protocol, state.phase);
528
+ if (phaseGate === gateName) {
529
+ // Gate belongs to the current phase — initialize it
530
+ state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() };
531
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-created (upgrade)`);
532
+ }
533
+ else {
534
+ const knownGates = Object.keys(state.gates).join(', ');
535
+ throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`);
536
+ }
448
537
  }
449
538
  if (state.gates[gateName].status === 'approved') {
450
539
  console.log(chalk.yellow(`Gate ${gateName} is already approved.`));
@@ -468,12 +557,12 @@ export async function approve(workspaceRoot, projectId, gateName, hasHumanFlag)
468
557
  const phaseCheckNames = phaseConfig?.checks ?? [];
469
558
  const checks = getPhaseChecks(protocol, state.phase, overrides ?? undefined);
470
559
  if (phaseCheckNames.length > 0) {
471
- const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(workspaceRoot, state.id, state.title) };
560
+ const checkEnv = { PROJECT_ID: state.id, PROJECT_TITLE: resolveArtifactBaseName(artifactRoot, state.id, state.title, scopedResolver) };
472
561
  console.log('');
473
562
  console.log(chalk.bold('RUNNING CHECKS...'));
474
563
  logCheckOverrides(phaseCheckNames, checks, overrides);
475
564
  if (Object.keys(checks).length > 0) {
476
- const results = await runPhaseChecks(checks, workspaceRoot, checkEnv);
565
+ const results = await runPhaseChecks(checks, artifactRoot, checkEnv, undefined, scopedResolver);
477
566
  console.log(formatCheckResults(results));
478
567
  if (!allChecksPassed(results)) {
479
568
  console.log('');
@@ -488,17 +577,27 @@ export async function approve(workspaceRoot, projectId, gateName, hasHumanFlag)
488
577
  }
489
578
  state.gates[gateName].status = 'approved';
490
579
  state.gates[gateName].approved_at = new Date().toISOString();
491
- writeState(statusPath, state);
580
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-approved`);
492
581
  console.log('');
493
582
  console.log(chalk.green(`Gate ${gateName} approved.`));
494
- console.log(`\n Run: porch done ${state.id} (to advance)`);
583
+ // For verify-approval: auto-advance to terminal state (convenience — one command)
584
+ // NOTE: The 'verified' state is committed to the builder branch, which may not
585
+ // be merged back to main. The closed GitHub Issue serves as the canonical "done"
586
+ // signal on main. State alignment (making status.yaml on main authoritative) is
587
+ // tracked as future work per spec 653.
588
+ if (gateName === 'verify-approval') {
589
+ await advanceProtocolPhase(workspaceRoot, state, protocol, statusPath, resolver);
590
+ }
591
+ else {
592
+ console.log(`\n Run: porch done ${state.id} (to advance)`);
593
+ }
495
594
  console.log('');
496
595
  }
497
596
  /**
498
597
  * porch rollback <id> <phase>
499
598
  * Rewinds project to an earlier phase, clearing downstream gates and resetting build state.
500
599
  */
501
- export async function rollback(workspaceRoot, projectId, targetPhase) {
600
+ export async function rollback(workspaceRoot, projectId, targetPhase, resolver) {
502
601
  const statusPath = findStatusPath(workspaceRoot, projectId);
503
602
  if (!statusPath) {
504
603
  throw new Error(`Project ${projectId} not found.`);
@@ -515,7 +614,7 @@ export async function rollback(workspaceRoot, projectId, targetPhase) {
515
614
  const currentIndex = protocol.phases.findIndex(p => p.id === state.phase);
516
615
  const targetIndex = protocol.phases.findIndex(p => p.id === targetPhase);
517
616
  // Handle completed projects (phase not in protocol phases array)
518
- if (state.phase === 'complete') {
617
+ if (state.phase === 'verified' || state.phase === 'complete') {
519
618
  // Allow rollback from complete state to any valid phase
520
619
  }
521
620
  else if (currentIndex === -1) {
@@ -538,11 +637,12 @@ export async function rollback(workspaceRoot, projectId, targetPhase) {
538
637
  state.iteration = 1;
539
638
  state.build_complete = false;
540
639
  state.history = [];
541
- // If rolling back to a phased phase, re-extract plan phases from plan file
640
+ // If rolling back to a phased phase, re-extract plan phases
542
641
  if (isPhased(protocol, targetPhase)) {
543
- const planPath = findPlanFile(workspaceRoot, state.id, state.title);
544
- if (planPath) {
545
- state.plan_phases = extractPhasesFromFile(planPath);
642
+ const planContent = resolver?.getPlanContent(state.id, state.title)
643
+ ?? (() => { const p = findPlanFile(workspaceRoot, state.id, state.title); return p ? fs.readFileSync(p, 'utf-8') : null; })();
644
+ if (planContent) {
645
+ state.plan_phases = extractPlanPhases(planContent);
546
646
  if (state.plan_phases.length > 0) {
547
647
  state.current_plan_phase = state.plan_phases[0].id;
548
648
  }
@@ -559,7 +659,7 @@ export async function rollback(workspaceRoot, projectId, targetPhase) {
559
659
  state.plan_phases = [];
560
660
  state.current_plan_phase = null;
561
661
  }
562
- writeState(statusPath, state);
662
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} rollback ${previousPhase} → ${targetPhase}`);
563
663
  console.log('');
564
664
  console.log(chalk.green(`ROLLED BACK: ${previousPhase} → ${targetPhase}`));
565
665
  console.log(` Project: ${state.id}`);
@@ -572,7 +672,7 @@ export async function rollback(workspaceRoot, projectId, targetPhase) {
572
672
  * Initialize a new project.
573
673
  *
574
674
  * Idempotent: if status.yaml already exists, preserves it and reports
575
- * current state. This supports `af spawn --resume` where the builder
675
+ * current state. This supports `afx spawn --resume` where the builder
576
676
  * may re-run `porch init` after a session restart.
577
677
  */
578
678
  export async function init(workspaceRoot, protocolName, projectId, projectName) {
@@ -608,7 +708,7 @@ export async function init(workspaceRoot, protocolName, projectId, projectName)
608
708
  return;
609
709
  }
610
710
  const state = createInitialState(protocol, projectId, projectName, workspaceRoot);
611
- writeState(statusPath, state);
711
+ await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} init ${protocolName}`);
612
712
  console.log('');
613
713
  console.log(chalk.green(`Project initialized: ${projectId}-${projectName}`));
614
714
  console.log(` Protocol: ${protocolName}`);
@@ -647,8 +747,77 @@ function getNextAction(state, protocol) {
647
747
  }
648
748
  return `Complete the phase work, then run: porch done ${state.id}`;
649
749
  }
650
- function getArtifactForPhase(workspaceRoot, state) {
651
- const baseName = resolveArtifactBaseName(workspaceRoot, state.id, state.title);
750
+ /**
751
+ * porch pending
752
+ * List all projects with gates awaiting human approval.
753
+ * Scans both the local workspace and any builder worktrees under .builders/*.
754
+ */
755
+ export async function pending(workspaceRoot) {
756
+ const seen = new Set(); // dedupe by status.yaml path
757
+ const results = [];
758
+ // Build the list of project directories to scan: main + every builder worktree.
759
+ const projectsDirs = [path.join(workspaceRoot, 'codev', 'projects')];
760
+ const buildersDir = path.join(workspaceRoot, '.builders');
761
+ if (fs.existsSync(buildersDir)) {
762
+ for (const wt of fs.readdirSync(buildersDir, { withFileTypes: true })) {
763
+ if (!wt.isDirectory())
764
+ continue;
765
+ projectsDirs.push(path.join(buildersDir, wt.name, 'codev', 'projects'));
766
+ }
767
+ }
768
+ for (const projectsDir of projectsDirs) {
769
+ if (!fs.existsSync(projectsDir))
770
+ continue;
771
+ for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) {
772
+ if (!entry.isDirectory())
773
+ continue;
774
+ const statusPath = path.join(projectsDir, entry.name, 'status.yaml');
775
+ if (!fs.existsSync(statusPath))
776
+ continue;
777
+ const realPath = fs.realpathSync(statusPath);
778
+ if (seen.has(realPath))
779
+ continue;
780
+ seen.add(realPath);
781
+ let state;
782
+ try {
783
+ state = readState(statusPath);
784
+ }
785
+ catch {
786
+ continue; // skip corrupted status files
787
+ }
788
+ for (const [gateName, gateStatus] of Object.entries(state.gates)) {
789
+ if (gateStatus?.status === 'pending' && gateStatus.requested_at) {
790
+ results.push({
791
+ id: state.id,
792
+ title: state.title,
793
+ phase: state.phase,
794
+ gate: gateName,
795
+ requested_at: gateStatus.requested_at,
796
+ statusPath,
797
+ });
798
+ }
799
+ }
800
+ }
801
+ }
802
+ console.log('');
803
+ if (results.length === 0) {
804
+ console.log(chalk.dim('No gates pending approval.'));
805
+ console.log('');
806
+ return;
807
+ }
808
+ // Sort oldest-first so the longest-waiting gates surface at the top.
809
+ results.sort((a, b) => (a.requested_at ?? '').localeCompare(b.requested_at ?? ''));
810
+ console.log(chalk.bold(`${results.length} gate${results.length === 1 ? '' : 's'} pending approval:`));
811
+ console.log('');
812
+ for (const r of results) {
813
+ console.log(` ${chalk.cyan(r.id)} ${chalk.dim('—')} ${r.title}`);
814
+ console.log(` phase: ${r.phase} gate: ${chalk.yellow(r.gate)} requested: ${r.requested_at}`);
815
+ console.log(chalk.dim(` approve: porch approve ${r.id} ${r.gate} --a-human-explicitly-approved-this`));
816
+ console.log('');
817
+ }
818
+ }
819
+ function getArtifactForPhase(workspaceRoot, state, resolver) {
820
+ const baseName = resolveArtifactBaseName(workspaceRoot, state.id, state.title, resolver);
652
821
  switch (state.phase) {
653
822
  case 'specify':
654
823
  return `codev/specs/${baseName}.md`;
@@ -665,7 +834,14 @@ function getArtifactForPhase(workspaceRoot, state) {
665
834
  // ============================================================================
666
835
  export async function cli(args) {
667
836
  const [command, ...rest] = args;
837
+ // Handle --version / -v before any project resolution — version output
838
+ // must work from any directory and never require a project context.
839
+ if (command === '--version' || command === '-v') {
840
+ console.log(version);
841
+ return;
842
+ }
668
843
  const workspaceRoot = process.cwd();
844
+ const resolver = getResolver(workspaceRoot);
669
845
  // Auto-detect project ID for commands that need it
670
846
  function getProjectId(provided) {
671
847
  const { id, source } = resolveProjectId(provided, process.cwd(), workspaceRoot);
@@ -679,6 +855,9 @@ export async function cli(args) {
679
855
  }
680
856
  try {
681
857
  switch (command) {
858
+ case 'pending':
859
+ await pending(workspaceRoot);
860
+ break;
682
861
  case 'next': {
683
862
  const { next: porchNext } = await import('./next.js');
684
863
  const result = await porchNext(workspaceRoot, getProjectId(rest[0]));
@@ -691,28 +870,80 @@ export async function cli(args) {
691
870
  process.exit(1);
692
871
  break;
693
872
  case 'status':
694
- await status(workspaceRoot, getProjectId(rest[0]));
873
+ await status(workspaceRoot, getProjectId(rest[0]), resolver);
695
874
  break;
696
875
  case 'check':
697
- await check(workspaceRoot, getProjectId(rest[0]));
876
+ await check(workspaceRoot, getProjectId(rest[0]), resolver);
698
877
  break;
699
- case 'done':
700
- await done(workspaceRoot, getProjectId(rest[0]));
878
+ case 'done': {
879
+ const doneOpts = {};
880
+ const prIdx = rest.indexOf('--pr');
881
+ const brIdx = rest.indexOf('--branch');
882
+ const mergedIdx = rest.indexOf('--merged');
883
+ if (prIdx !== -1) {
884
+ const val = parseInt(rest[prIdx + 1], 10);
885
+ if (!Number.isInteger(val) || val <= 0)
886
+ throw new Error('--pr requires a positive integer PR number');
887
+ doneOpts.pr = val;
888
+ }
889
+ if (brIdx !== -1) {
890
+ if (!rest[brIdx + 1] || rest[brIdx + 1].startsWith('--'))
891
+ throw new Error('--branch requires a branch name');
892
+ doneOpts.branch = rest[brIdx + 1];
893
+ }
894
+ if (mergedIdx !== -1) {
895
+ const val = parseInt(rest[mergedIdx + 1], 10);
896
+ if (!Number.isInteger(val) || val <= 0)
897
+ throw new Error('--merged requires a positive integer PR number');
898
+ doneOpts.merged = val;
899
+ }
900
+ if (doneOpts.pr !== undefined && doneOpts.merged !== undefined) {
901
+ throw new Error('--pr and --merged are mutually exclusive');
902
+ }
903
+ const hasRecordFlags = doneOpts.pr !== undefined || doneOpts.merged !== undefined;
904
+ // For project ID: use first positional arg, or fall back to auto-detection
905
+ const projectIdArg = rest[0] && !rest[0].startsWith('--') ? rest[0] : undefined;
906
+ await done(workspaceRoot, getProjectId(projectIdArg), resolver, hasRecordFlags ? doneOpts : undefined);
701
907
  break;
908
+ }
702
909
  case 'gate':
703
- await gate(workspaceRoot, getProjectId(rest[0]));
910
+ await gate(workspaceRoot, getProjectId(rest[0]), resolver);
704
911
  break;
705
912
  case 'approve':
706
913
  if (!rest[0] || !rest[1])
707
914
  throw new Error('Usage: porch approve <id> <gate> --a-human-explicitly-approved-this');
708
915
  const hasHumanFlag = rest.includes('--a-human-explicitly-approved-this');
709
- await approve(workspaceRoot, rest[0], rest[1], hasHumanFlag);
916
+ await approve(workspaceRoot, rest[0], rest[1], hasHumanFlag, resolver);
710
917
  break;
711
918
  case 'rollback':
712
919
  if (!rest[0] || !rest[1])
713
920
  throw new Error('Usage: porch rollback <id> <phase>');
714
- await rollback(workspaceRoot, rest[0], rest[1]);
921
+ await rollback(workspaceRoot, rest[0], rest[1], resolver);
922
+ break;
923
+ case 'verify': {
924
+ const verifyProjectId = rest[0] && !rest[0].startsWith('--') ? rest[0] : undefined;
925
+ const skipIdx = rest.indexOf('--skip');
926
+ if (skipIdx === -1)
927
+ throw new Error('Usage: porch verify <id> --skip "reason"');
928
+ const skipReason = rest[skipIdx + 1];
929
+ if (!skipReason || skipReason.startsWith('--'))
930
+ throw new Error('--skip requires a reason');
931
+ const pid = getProjectId(verifyProjectId);
932
+ const sp = findStatusPath(workspaceRoot, pid);
933
+ if (!sp)
934
+ throw new Error(`Project ${pid} not found.`);
935
+ const st = readState(sp);
936
+ if (st.phase !== 'verify') {
937
+ throw new Error(`porch verify --skip can only be used in the verify phase (current: ${st.phase}). The PR must be merged first.`);
938
+ }
939
+ st.phase = 'verified';
940
+ st.context = { ...st.context, verify_skip_reason: skipReason };
941
+ await writeStateAndCommit(sp, st, `chore(porch): ${st.id} verify skipped: ${skipReason}`);
942
+ console.log('');
943
+ console.log(chalk.green(`VERIFIED (skipped): ${st.id}`));
944
+ console.log(` Reason: ${skipReason}`);
715
945
  break;
946
+ }
716
947
  case 'init':
717
948
  if (!rest[0] || !rest[1] || !rest[2]) {
718
949
  throw new Error('Usage: porch init <protocol> <id> <name>');
@@ -727,11 +958,18 @@ export async function cli(args) {
727
958
  console.log(' status [id] Show current state and instructions');
728
959
  console.log(' check [id] Run checks for current phase');
729
960
  console.log(' done [id] Signal build complete (validates checks, advances)');
961
+ console.log(' done [id] --pr N --branch NAME Record PR creation (no phase advancement)');
962
+ console.log(' done [id] --merged N Mark PR as merged (no phase advancement)');
730
963
  console.log(' gate [id] Request human approval');
964
+ console.log(' pending List all gates awaiting approval across projects');
731
965
  console.log(' approve <id> <gate> --a-human-explicitly-approved-this');
966
+ console.log(' verify <id> --skip "reason" Skip verification and mark as verified');
732
967
  console.log(' rollback <id> <phase> Rewind project to an earlier phase');
733
968
  console.log(' init <protocol> <id> <name> Initialize a new project');
734
969
  console.log('');
970
+ console.log('Flags:');
971
+ console.log(' --version, -v Print porch (codev) version');
972
+ console.log('');
735
973
  console.log('Project ID is auto-detected from worktree path or when exactly one project exists.');
736
974
  console.log('');
737
975
  process.exit(command && command !== '--help' && command !== '-h' ? 1 : 0);