wicked-crew 0.7.40 → 0.7.45

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 (360) hide show
  1. package/dist/api/audit.d.ts +3 -0
  2. package/dist/api/audit.d.ts.map +1 -1
  3. package/dist/api/audit.js +2 -0
  4. package/dist/api/audit.js.map +1 -1
  5. package/dist/api/capture.d.ts +149 -0
  6. package/dist/api/capture.d.ts.map +1 -0
  7. package/dist/api/capture.js +330 -0
  8. package/dist/api/capture.js.map +1 -0
  9. package/dist/api/chat-citations.d.ts +174 -0
  10. package/dist/api/chat-citations.d.ts.map +1 -0
  11. package/dist/api/chat-citations.js +528 -0
  12. package/dist/api/chat-citations.js.map +1 -0
  13. package/dist/api/chat-transcripts.d.ts +20 -1
  14. package/dist/api/chat-transcripts.d.ts.map +1 -1
  15. package/dist/api/chat-transcripts.js +29 -0
  16. package/dist/api/chat-transcripts.js.map +1 -1
  17. package/dist/api/delivery-cache.d.ts +2 -2
  18. package/dist/api/delivery-freeze.d.ts +66 -0
  19. package/dist/api/delivery-freeze.d.ts.map +1 -0
  20. package/dist/api/delivery-freeze.js +104 -0
  21. package/dist/api/delivery-freeze.js.map +1 -0
  22. package/dist/api/delivery-index.d.ts +53 -9
  23. package/dist/api/delivery-index.d.ts.map +1 -1
  24. package/dist/api/delivery-index.js +115 -22
  25. package/dist/api/delivery-index.js.map +1 -1
  26. package/dist/api/demo.d.ts +90 -0
  27. package/dist/api/demo.d.ts.map +1 -0
  28. package/dist/api/demo.js +492 -0
  29. package/dist/api/demo.js.map +1 -0
  30. package/dist/api/diagnostics.d.ts +64 -1
  31. package/dist/api/diagnostics.d.ts.map +1 -1
  32. package/dist/api/diagnostics.js +129 -1
  33. package/dist/api/diagnostics.js.map +1 -1
  34. package/dist/api/eval-sample.d.ts +6 -6
  35. package/dist/api/governance-steering.d.ts +2 -2
  36. package/dist/api/mcp.d.ts +282 -0
  37. package/dist/api/mcp.d.ts.map +1 -0
  38. package/dist/api/mcp.js +521 -0
  39. package/dist/api/mcp.js.map +1 -0
  40. package/dist/api/plan-schema.d.ts +56 -0
  41. package/dist/api/plan-schema.d.ts.map +1 -0
  42. package/dist/api/plan-schema.js +26 -0
  43. package/dist/api/plan-schema.js.map +1 -0
  44. package/dist/api/roster-standing.d.ts +15 -2
  45. package/dist/api/roster-standing.d.ts.map +1 -1
  46. package/dist/api/roster-standing.js +55 -3
  47. package/dist/api/roster-standing.js.map +1 -1
  48. package/dist/api/routes.d.ts +630 -51
  49. package/dist/api/routes.d.ts.map +1 -1
  50. package/dist/api/routes.js +738 -127
  51. package/dist/api/routes.js.map +1 -1
  52. package/dist/api/run-files.d.ts.map +1 -1
  53. package/dist/api/run-files.js +31 -2
  54. package/dist/api/run-files.js.map +1 -1
  55. package/dist/api/run-liveness.d.ts +41 -0
  56. package/dist/api/run-liveness.d.ts.map +1 -0
  57. package/dist/api/run-liveness.js +166 -0
  58. package/dist/api/run-liveness.js.map +1 -0
  59. package/dist/api/seat-health.d.ts.map +1 -1
  60. package/dist/api/seat-health.js +20 -4
  61. package/dist/api/seat-health.js.map +1 -1
  62. package/dist/api/seat-probe.d.ts +141 -0
  63. package/dist/api/seat-probe.d.ts.map +1 -0
  64. package/dist/api/seat-probe.js +257 -0
  65. package/dist/api/seat-probe.js.map +1 -0
  66. package/dist/api/seat-record.d.ts +40 -0
  67. package/dist/api/seat-record.d.ts.map +1 -0
  68. package/dist/api/seat-record.js +218 -0
  69. package/dist/api/seat-record.js.map +1 -0
  70. package/dist/api/seat-signin.d.ts.map +1 -1
  71. package/dist/api/seat-signin.js +3 -1
  72. package/dist/api/seat-signin.js.map +1 -1
  73. package/dist/api/seat-standing.d.ts +24 -4
  74. package/dist/api/seat-standing.d.ts.map +1 -1
  75. package/dist/api/seat-standing.js +50 -3
  76. package/dist/api/seat-standing.js.map +1 -1
  77. package/dist/api/server.d.ts +34 -48
  78. package/dist/api/server.d.ts.map +1 -1
  79. package/dist/api/server.js +283 -107
  80. package/dist/api/server.js.map +1 -1
  81. package/dist/api/skills.d.ts +3 -1
  82. package/dist/api/skills.d.ts.map +1 -1
  83. package/dist/api/skills.js +7 -2
  84. package/dist/api/skills.js.map +1 -1
  85. package/dist/api/stall-watchdog.d.ts +34 -3
  86. package/dist/api/stall-watchdog.d.ts.map +1 -1
  87. package/dist/api/stall-watchdog.js +107 -8
  88. package/dist/api/stall-watchdog.js.map +1 -1
  89. package/dist/api/steering-landing.d.ts +6 -6
  90. package/dist/api/steering-landing.d.ts.map +1 -1
  91. package/dist/api/steering-landing.js +7 -9
  92. package/dist/api/steering-landing.js.map +1 -1
  93. package/dist/api/testing.d.ts +6 -4
  94. package/dist/api/testing.d.ts.map +1 -1
  95. package/dist/api/testing.js +82 -47
  96. package/dist/api/testing.js.map +1 -1
  97. package/dist/campaign/boot-resume.d.ts +34 -0
  98. package/dist/campaign/boot-resume.d.ts.map +1 -0
  99. package/dist/campaign/boot-resume.js +45 -0
  100. package/dist/campaign/boot-resume.js.map +1 -0
  101. package/dist/campaigns/rollup.d.ts +4 -1
  102. package/dist/campaigns/rollup.d.ts.map +1 -1
  103. package/dist/campaigns/rollup.js +6 -1
  104. package/dist/campaigns/rollup.js.map +1 -1
  105. package/dist/campaigns/routes.d.ts +2 -2
  106. package/dist/cli/bearer.d.ts +27 -1
  107. package/dist/cli/bearer.d.ts.map +1 -1
  108. package/dist/cli/bearer.js +62 -2
  109. package/dist/cli/bearer.js.map +1 -1
  110. package/dist/cli/governance.d.ts +11 -0
  111. package/dist/cli/governance.d.ts.map +1 -1
  112. package/dist/cli/governance.js +8 -2
  113. package/dist/cli/governance.js.map +1 -1
  114. package/dist/cli/index.d.ts.map +1 -1
  115. package/dist/cli/index.js +140 -75
  116. package/dist/cli/index.js.map +1 -1
  117. package/dist/cli/mcp.d.ts +11 -0
  118. package/dist/cli/mcp.d.ts.map +1 -1
  119. package/dist/cli/mcp.js +40 -9
  120. package/dist/cli/mcp.js.map +1 -1
  121. package/dist/cli/service.d.ts +78 -0
  122. package/dist/cli/service.d.ts.map +1 -0
  123. package/dist/cli/service.js +334 -0
  124. package/dist/cli/service.js.map +1 -0
  125. package/dist/core/adapter.d.ts +238 -12
  126. package/dist/core/adapter.d.ts.map +1 -1
  127. package/dist/core/adapter.js +619 -157
  128. package/dist/core/adapter.js.map +1 -1
  129. package/dist/core/bridge-reaper.d.ts +4 -1
  130. package/dist/core/bridge-reaper.d.ts.map +1 -1
  131. package/dist/core/bridge-reaper.js +15 -1
  132. package/dist/core/bridge-reaper.js.map +1 -1
  133. package/dist/core/bus-notice.d.ts +27 -0
  134. package/dist/core/bus-notice.d.ts.map +1 -0
  135. package/dist/core/bus-notice.js +24 -0
  136. package/dist/core/bus-notice.js.map +1 -0
  137. package/dist/core/bus.d.ts +99 -0
  138. package/dist/core/bus.d.ts.map +1 -0
  139. package/dist/core/bus.js +176 -0
  140. package/dist/core/bus.js.map +1 -0
  141. package/dist/core/deliver-exclusions.d.ts +71 -0
  142. package/dist/core/deliver-exclusions.d.ts.map +1 -0
  143. package/dist/core/deliver-exclusions.js +110 -0
  144. package/dist/core/deliver-exclusions.js.map +1 -0
  145. package/dist/core/deliver-text.d.ts +61 -14
  146. package/dist/core/deliver-text.d.ts.map +1 -1
  147. package/dist/core/deliver-text.js +178 -30
  148. package/dist/core/deliver-text.js.map +1 -1
  149. package/dist/core/deliver-triage.d.ts +11 -2
  150. package/dist/core/deliver-triage.d.ts.map +1 -1
  151. package/dist/core/deliver-triage.js +19 -1
  152. package/dist/core/deliver-triage.js.map +1 -1
  153. package/dist/core/deliver.d.ts +161 -3
  154. package/dist/core/deliver.d.ts.map +1 -1
  155. package/dist/core/deliver.js +463 -25
  156. package/dist/core/deliver.js.map +1 -1
  157. package/dist/core/deliverable-floor.d.ts.map +1 -1
  158. package/dist/core/deliverable-floor.js +5 -7
  159. package/dist/core/deliverable-floor.js.map +1 -1
  160. package/dist/core/engine-bus.d.ts +40 -0
  161. package/dist/core/engine-bus.d.ts.map +1 -0
  162. package/dist/core/engine-bus.js +27 -0
  163. package/dist/core/engine-bus.js.map +1 -0
  164. package/dist/core/estate-mcp-client.d.ts +87 -5
  165. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  166. package/dist/core/estate-mcp-client.js +90 -11
  167. package/dist/core/estate-mcp-client.js.map +1 -1
  168. package/dist/core/linked-issues.d.ts +46 -0
  169. package/dist/core/linked-issues.d.ts.map +1 -0
  170. package/dist/core/linked-issues.js +123 -0
  171. package/dist/core/linked-issues.js.map +1 -0
  172. package/dist/core/run-identity.d.ts +100 -0
  173. package/dist/core/run-identity.d.ts.map +1 -0
  174. package/dist/core/run-identity.js +178 -0
  175. package/dist/core/run-identity.js.map +1 -0
  176. package/dist/core/shutdown.d.ts +16 -0
  177. package/dist/core/shutdown.d.ts.map +1 -0
  178. package/dist/core/shutdown.js +39 -0
  179. package/dist/core/shutdown.js.map +1 -0
  180. package/dist/core/types.d.ts +15 -1
  181. package/dist/core/types.d.ts.map +1 -1
  182. package/dist/core/types.js +4 -0
  183. package/dist/core/types.js.map +1 -1
  184. package/dist/interactive/bridge-pool.d.ts +67 -3
  185. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  186. package/dist/interactive/bridge-pool.js +129 -13
  187. package/dist/interactive/bridge-pool.js.map +1 -1
  188. package/dist/interactive/bridge-root.d.ts +9 -0
  189. package/dist/interactive/bridge-root.d.ts.map +1 -1
  190. package/dist/interactive/bridge-root.js +11 -4
  191. package/dist/interactive/bridge-root.js.map +1 -1
  192. package/dist/interactive/chat-events.d.ts +14 -9
  193. package/dist/interactive/chat-events.d.ts.map +1 -1
  194. package/dist/interactive/chat-events.js +55 -57
  195. package/dist/interactive/chat-events.js.map +1 -1
  196. package/dist/interactive/council-outcome.d.ts +1 -0
  197. package/dist/interactive/council-outcome.d.ts.map +1 -1
  198. package/dist/interactive/council-outcome.js +33 -0
  199. package/dist/interactive/council-outcome.js.map +1 -1
  200. package/dist/interactive/draft-events.d.ts +36 -17
  201. package/dist/interactive/draft-events.d.ts.map +1 -1
  202. package/dist/interactive/draft-events.js +121 -92
  203. package/dist/interactive/draft-events.js.map +1 -1
  204. package/dist/interactive/draft-skill.d.ts +59 -0
  205. package/dist/interactive/draft-skill.d.ts.map +1 -1
  206. package/dist/interactive/draft-skill.js +233 -0
  207. package/dist/interactive/draft-skill.js.map +1 -1
  208. package/dist/interactive/edit-events.d.ts +1 -14
  209. package/dist/interactive/edit-events.d.ts.map +1 -1
  210. package/dist/interactive/edit-events.js +37 -70
  211. package/dist/interactive/edit-events.js.map +1 -1
  212. package/dist/interactive/proxy-routes.d.ts +2 -2
  213. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  214. package/dist/interactive/proxy-routes.js +4 -13
  215. package/dist/interactive/proxy-routes.js.map +1 -1
  216. package/dist/interactive/ws-relay.d.ts +9 -10
  217. package/dist/interactive/ws-relay.d.ts.map +1 -1
  218. package/dist/interactive/ws-relay.js +23 -50
  219. package/dist/interactive/ws-relay.js.map +1 -1
  220. package/dist/mcp/broker.d.ts +88 -0
  221. package/dist/mcp/broker.d.ts.map +1 -0
  222. package/dist/mcp/broker.js +307 -0
  223. package/dist/mcp/broker.js.map +1 -0
  224. package/dist/mcp/call-records.d.ts +61 -0
  225. package/dist/mcp/call-records.d.ts.map +1 -0
  226. package/dist/mcp/call-records.js +135 -0
  227. package/dist/mcp/call-records.js.map +1 -0
  228. package/dist/mcp/classify.d.ts +45 -0
  229. package/dist/mcp/classify.d.ts.map +1 -0
  230. package/dist/mcp/classify.js +83 -0
  231. package/dist/mcp/classify.js.map +1 -0
  232. package/dist/mcp/discovery.d.ts +24 -0
  233. package/dist/mcp/discovery.d.ts.map +1 -0
  234. package/dist/mcp/discovery.js +78 -0
  235. package/dist/mcp/discovery.js.map +1 -0
  236. package/dist/mcp/invoke.d.ts +36 -0
  237. package/dist/mcp/invoke.d.ts.map +1 -0
  238. package/dist/mcp/invoke.js +80 -0
  239. package/dist/mcp/invoke.js.map +1 -0
  240. package/dist/mcp/policies.d.ts +110 -0
  241. package/dist/mcp/policies.d.ts.map +1 -0
  242. package/dist/mcp/policies.js +400 -0
  243. package/dist/mcp/policies.js.map +1 -0
  244. package/dist/mcp/probe.d.ts +66 -0
  245. package/dist/mcp/probe.d.ts.map +1 -0
  246. package/dist/mcp/probe.js +116 -0
  247. package/dist/mcp/probe.js.map +1 -0
  248. package/dist/mcp/redact.d.ts +34 -0
  249. package/dist/mcp/redact.d.ts.map +1 -0
  250. package/dist/mcp/redact.js +84 -0
  251. package/dist/mcp/redact.js.map +1 -0
  252. package/dist/mcp/registry-store.d.ts +69 -0
  253. package/dist/mcp/registry-store.d.ts.map +1 -0
  254. package/dist/mcp/registry-store.js +82 -0
  255. package/dist/mcp/registry-store.js.map +1 -0
  256. package/dist/mcp/registry.d.ts +147 -0
  257. package/dist/mcp/registry.d.ts.map +1 -0
  258. package/dist/mcp/registry.js +509 -0
  259. package/dist/mcp/registry.js.map +1 -0
  260. package/dist/mcp/rest.d.ts +111 -0
  261. package/dist/mcp/rest.d.ts.map +1 -0
  262. package/dist/mcp/rest.js +606 -0
  263. package/dist/mcp/rest.js.map +1 -0
  264. package/dist/mcp/secrets.d.ts +66 -0
  265. package/dist/mcp/secrets.d.ts.map +1 -0
  266. package/dist/mcp/secrets.js +180 -0
  267. package/dist/mcp/secrets.js.map +1 -0
  268. package/dist/mcp/tool-list.d.ts +25 -0
  269. package/dist/mcp/tool-list.d.ts.map +1 -0
  270. package/dist/mcp/tool-list.js +62 -0
  271. package/dist/mcp/tool-list.js.map +1 -0
  272. package/dist/mcp/upstream-error.d.ts +12 -0
  273. package/dist/mcp/upstream-error.d.ts.map +1 -0
  274. package/dist/mcp/upstream-error.js +14 -0
  275. package/dist/mcp/upstream-error.js.map +1 -0
  276. package/dist/mcp/usage.d.ts +31 -0
  277. package/dist/mcp/usage.d.ts.map +1 -0
  278. package/dist/mcp/usage.js +159 -0
  279. package/dist/mcp/usage.js.map +1 -0
  280. package/dist/presets/routes.d.ts +53 -0
  281. package/dist/presets/routes.d.ts.map +1 -0
  282. package/dist/presets/routes.js +115 -0
  283. package/dist/presets/routes.js.map +1 -0
  284. package/dist/projects/activity.d.ts +6 -18
  285. package/dist/projects/activity.d.ts.map +1 -1
  286. package/dist/projects/activity.js +17 -60
  287. package/dist/projects/activity.js.map +1 -1
  288. package/dist/projects/events.d.ts +6 -5
  289. package/dist/projects/events.d.ts.map +1 -1
  290. package/dist/projects/events.js +33 -58
  291. package/dist/projects/events.js.map +1 -1
  292. package/dist/projects/routes.d.ts.map +1 -1
  293. package/dist/projects/routes.js +19 -0
  294. package/dist/projects/routes.js.map +1 -1
  295. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  296. package/dist/projects/state-home-preflight.js +7 -0
  297. package/dist/projects/state-home-preflight.js.map +1 -1
  298. package/dist/projects/state-home-registry.d.ts.map +1 -1
  299. package/dist/projects/state-home-registry.js +9 -1
  300. package/dist/projects/state-home-registry.js.map +1 -1
  301. package/dist/qe/acceptance.d.ts +28 -28
  302. package/dist/qe/acceptance.d.ts.map +1 -1
  303. package/dist/qe/acceptance.js +60 -61
  304. package/dist/qe/acceptance.js.map +1 -1
  305. package/dist/qe/test-sets.d.ts +3 -5
  306. package/dist/qe/test-sets.d.ts.map +1 -1
  307. package/dist/qe/test-sets.js +5 -7
  308. package/dist/qe/test-sets.js.map +1 -1
  309. package/dist/skills/installed.d.ts +51 -0
  310. package/dist/skills/installed.d.ts.map +1 -0
  311. package/dist/skills/installed.js +46 -0
  312. package/dist/skills/installed.js.map +1 -0
  313. package/dist/standing-orders/evaluator.d.ts +113 -0
  314. package/dist/standing-orders/evaluator.d.ts.map +1 -0
  315. package/dist/standing-orders/evaluator.js +225 -0
  316. package/dist/standing-orders/evaluator.js.map +1 -0
  317. package/dist/standing-orders/history.d.ts +58 -0
  318. package/dist/standing-orders/history.d.ts.map +1 -0
  319. package/dist/standing-orders/history.js +117 -0
  320. package/dist/standing-orders/history.js.map +1 -0
  321. package/dist/standing-orders/parse.d.ts +19 -0
  322. package/dist/standing-orders/parse.d.ts.map +1 -0
  323. package/dist/standing-orders/parse.js +75 -0
  324. package/dist/standing-orders/parse.js.map +1 -0
  325. package/dist/standing-orders/routes.d.ts +180 -0
  326. package/dist/standing-orders/routes.d.ts.map +1 -0
  327. package/dist/standing-orders/routes.js +95 -0
  328. package/dist/standing-orders/routes.js.map +1 -0
  329. package/dist/standing-orders/store.d.ts +154 -0
  330. package/dist/standing-orders/store.d.ts.map +1 -0
  331. package/dist/standing-orders/store.js +189 -0
  332. package/dist/standing-orders/store.js.map +1 -0
  333. package/dist/studio/assets/index-CvV5qITf.css +32 -0
  334. package/dist/studio/assets/index-jCOy6Ns7.js +568 -0
  335. package/dist/studio/index.html +3 -3
  336. package/dist/studio/testid-inventory.json +3274 -894
  337. package/dist/team/key.d.ts +5 -0
  338. package/dist/team/key.d.ts.map +1 -0
  339. package/dist/team/key.js +21 -0
  340. package/dist/team/key.js.map +1 -0
  341. package/dist/team/routes.d.ts +196 -0
  342. package/dist/team/routes.d.ts.map +1 -0
  343. package/dist/team/routes.js +274 -0
  344. package/dist/team/routes.js.map +1 -0
  345. package/dist/team/ws-relay.d.ts +39 -0
  346. package/dist/team/ws-relay.d.ts.map +1 -0
  347. package/dist/team/ws-relay.js +63 -0
  348. package/dist/team/ws-relay.js.map +1 -0
  349. package/endpoint-manifest.json +502 -6
  350. package/package.json +6 -6
  351. package/dist/interactive/demo-events.d.ts +0 -367
  352. package/dist/interactive/demo-events.d.ts.map +0 -1
  353. package/dist/interactive/demo-events.js +0 -1293
  354. package/dist/interactive/demo-events.js.map +0 -1
  355. package/dist/qe/gate-events.d.ts +0 -113
  356. package/dist/qe/gate-events.d.ts.map +0 -1
  357. package/dist/qe/gate-events.js +0 -172
  358. package/dist/qe/gate-events.js.map +0 -1
  359. package/dist/studio/assets/index-Bg7Z3BR-.css +0 -32
  360. package/dist/studio/assets/index-BvOFMjf9.js +0 -554
@@ -85,7 +85,8 @@
85
85
  */
86
86
  import { execFile } from 'node:child_process';
87
87
  import { childEnvWithBootEstateDb } from './governance-store.js';
88
- import { composeEmbeddedDeliverText, factsFromWorkflow, framedDeliverText, runUrlFor, urlPathSegment, } from './deliver-text.js';
88
+ import { DELIVER_STRANDED_SENTINEL } from './deliver-exclusions.js';
89
+ import { composeEmbeddedDeliverText, factsFromWorkflow, framedDeliverText, runUrlFor, configuredPublicOrigin, commitSubject, urlPathSegment, } from './deliver-text.js';
89
90
  /**
90
91
  * The content-address of wicked-core's built-in evidence floor (`builtin_floors::EVIDENCE_FLOOR_PIN`,
91
92
  * criterion "the run left a change in its worktree (done is re-derived from the diff, never
@@ -105,8 +106,10 @@ export const EVIDENCE_FLOOR_PIN = 'e2e7af1db9e48454';
105
106
  export const DELIVER_PHASE_ID = 'deliver';
106
107
  /**
107
108
  * The sentinel the deliver script prints when the RUN'S WORK has been committed but its LIFT did
108
- * not complete (crew#418/#432): a rebase conflict or any failed push. The work is safe on its
109
- * `wicked/<id>` branch; an operator can fix the remote condition and retry delivery.
109
+ * not complete (crew#418): a rebase onto the remote default branch hit a conflict outside the
110
+ * changelog. The work is safe on its `wicked/<id>` branch; an operator resolves the collision and
111
+ * retries delivery. A push the REMOTE refused is not a lift collision and carries
112
+ * {@link DELIVER_PUSH_REJECTED_MARKER} instead (N4).
110
113
  *
111
114
  * crew keys the "stranded, recoverable" reinterpretation on this EXACT substring appearing in
112
115
  * the deliver unit's `denial_reason` (which carries the head+TAIL excerpt of the script's
@@ -116,6 +119,53 @@ export const DELIVER_PHASE_ID = 'deliver';
116
119
  * terminal run failures exactly as before (the crew#400 refusal-vs-infra posture).
117
120
  */
118
121
  export const DELIVER_LIFT_CONFLICT_MARKER = 'deliver: LIFT-CONFLICT';
122
+ /**
123
+ * The sentinel the deliver script prints LAST when the REMOTE REFUSED the push after the run's work
124
+ * was committed and its lift came out clean (N4, ship re-proof): a pre-receive hook, a protected
125
+ * ref, an auth 403, a transport failure, or a non-fast-forward on the run branch. It used to carry
126
+ * {@link DELIVER_LIFT_CONFLICT_MARKER}, which mislabelled a refusal as a lift collision — the lift
127
+ * read `unchanged` — and, because the engine EXEMPTS that marker from its deterministic
128
+ * deliver-refusal arm, sent the failure to the LLM triage judge, which re-ran the push once with no
129
+ * gate. Without the lift marker the failure takes the engine's deterministic arm: the run parks at
130
+ * an `escalation` gate on the deliver unit, and nothing is pushed again until a human approves.
131
+ * The untracked recovery sentinel ({@link DELIVER_STRANDED_SENTINEL}) still keeps the worktree.
132
+ */
133
+ export const DELIVER_PUSH_REJECTED_MARKER = 'deliver: PUSH-REJECTED';
134
+ /**
135
+ * The machine line the deliver script prints LAST on a successful PUSH-ONLY delivery (N1, the half
136
+ * of F2 that did not hold): the branch is on the remote, and no pull request was opened because gh
137
+ * could not resolve the origin to a GitHub repository. `deliver: PUSHED-NO-PR <branch> <remote>` —
138
+ * the branch has no spaces (git forbids them in a ref), and the remote is the rest of the line.
139
+ * The daemon reads it from the APPROVED deliver unit's output ({@link pushedOnlyFrom}) into the
140
+ * durable `run.delivered` record, so the run reads `delivery: 'pushed'` instead of `'stranded'`.
141
+ */
142
+ export const DELIVER_PUSHED_NO_PR_MARKER = 'deliver: PUSHED-NO-PR';
143
+ /** A remote URL with its userinfo removed — `https://tok@host/r` → `https://host/r`. A scp-like
144
+ * `git@host:path` keeps its user (it names an SSH login, not a secret). */
145
+ export function remoteWithoutUserinfo(url) {
146
+ return url.trim().replace(/^([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^@/]*@/, '$1');
147
+ }
148
+ /**
149
+ * The push-only delivery a deliver transcript records, or `null`. The LAST
150
+ * {@link DELIVER_PUSHED_NO_PR_MARKER} line wins. Whether it outranks a PR URL in the same transcript
151
+ * is decided by position in `deliveryRecordFrom` (`api/delivery-index.ts`): the script's verdict is
152
+ * the last of the two, and anything earlier may be the remote's echo.
153
+ */
154
+ export function pushedOnlyFrom(text) {
155
+ let found = null;
156
+ for (const line of text.split('\n')) {
157
+ const m = /^deliver: PUSHED-NO-PR (\S+) (.+)$/.exec(line.trimEnd());
158
+ if (m !== null)
159
+ found = { branch: m[1], remote: remoteWithoutUserinfo(m[2]) };
160
+ }
161
+ return found;
162
+ }
163
+ /** A GitHub login as GitHub itself allows it — alphanumerics and single hyphens, ≤ 39 chars. The
164
+ * ONE validator for the deliver identity: the setting's 400 and the script's splice share it, so
165
+ * a value that passed the route can always be baked into a single-quoted shell literal. */
166
+ export function isGitHubLogin(login) {
167
+ return /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(login);
168
+ }
119
169
  /** A branch name the script may splice into a single-quoted literal and hand to git: the ref
120
170
  * charset git accepts for `wicked/<run>`-style heads, no `..`, no leading `-`, no quotes. */
121
171
  const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
@@ -131,7 +181,7 @@ const SAFE_PR_URL = /^https:\/\/[A-Za-z0-9.-]+\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+
131
181
  * (≤ 90 ASCII bytes) on purpose — the PTY carrier's whole prompt is 1000 B and core's budget test
132
182
  * keeps ≥ 300 B of intent headroom. */
133
183
  export const BUG_FIX_SWEEP_INSTRUCTIONS = 'Update every consumer of behaviour this fix retires or changes: tests, docs, comments.';
134
- const defaultGhExec = (args, opts) => new Promise((resolve) => {
184
+ export const defaultGhExec = (args, opts) => new Promise((resolve) => {
135
185
  // The daemon's governance-store variables never ride into a child (crew#495): the boot value is
136
186
  // restored and the exported store URL is stripped, the same helper every other spawn site uses.
137
187
  execFile('gh', args, { cwd: opts.cwd, timeout: opts.timeoutMs, encoding: 'utf8', env: childEnvWithBootEstateDb(process.env) }, (err, stdout, stderr) => {
@@ -139,6 +189,39 @@ const defaultGhExec = (args, opts) => new Promise((resolve) => {
139
189
  resolve({ stdout: String(stdout ?? ''), stderr: String(stderr ?? ''), code });
140
190
  });
141
191
  });
192
+ /**
193
+ * `git remote get-url --push origin` in `rootPath` — the deliver gate's origin preflight (F2).
194
+ *
195
+ * `--push` because the sentence is about the PUSH: `git push origin` uses `remote.origin.pushurl`
196
+ * when one is configured, so a repo whose FETCH url is GitHub while its PUSH url is not (or the
197
+ * reverse) would otherwise get a card describing a destination the branch never reaches (codex
198
+ * review of this PR, HIGH). With no `pushurl` configured git answers the fetch url, so the common
199
+ * case is unchanged.
200
+ *
201
+ * - the trimmed URL when git answered one;
202
+ * - `''` when git said there is NO such remote — the one answer that licenses the card to claim
203
+ * "this repository has no origin";
204
+ * - `null` for everything else (git missing, a timeout, a root that is not a checkout). "Could
205
+ * not read it" is not a licence to claim anything about it either way, so the card keeps its
206
+ * generic sentence.
207
+ *
208
+ * A run worktree shares its repository's remotes and does not exist yet at compose time, so this
209
+ * reads the REGISTERED repo root. Bounded to 5 s, once per delivering launch.
210
+ */
211
+ export async function readDeliverOriginUrl(rootPath) {
212
+ const res = await new Promise((resolve) => {
213
+ execFile('git', ['remote', 'get-url', '--push', 'origin'], { cwd: rootPath, timeout: 5_000, encoding: 'utf8', env: childEnvWithBootEstateDb(process.env) }, (err, stdout, stderr) => {
214
+ const code = err === null ? 0 : typeof err.code === 'number' ? err.code : null;
215
+ resolve({ stdout: String(stdout ?? ''), stderr: String(stderr ?? ''), code });
216
+ });
217
+ });
218
+ if (res.code === 0) {
219
+ const url = res.stdout.trim();
220
+ // git can exit 0 with nothing for a remote configured with an empty URL — unknown, not absent.
221
+ return url === '' ? null : url;
222
+ }
223
+ return /no such remote/i.test(res.stderr) ? '' : null;
224
+ }
142
225
  /**
143
226
  * Resolve `revisesPr` (a PR number) to the head branch the run will base on and push to — the
144
227
  * daemon's job, since the engine has no GitHub client (DES-L9 §2). Bounded to 5 s; every failure
@@ -300,6 +383,13 @@ export function deliverPrScript(intent, opts = {}) {
300
383
  const api = apiOriginLiteral(opts.apiOrigin);
301
384
  const fallbackLines = heredocLines(framedDeliverText(fallback));
302
385
  const heredoc = heredocDelimiter(fallbackLines);
386
+ // crew#550: the PR title may run to GitHub's 256 characters, the commit subject stays at git's 72.
387
+ // The subject is composed here from the same title; the script puts it above the full text when
388
+ // the two differ, so the commit body opens with the whole title.
389
+ const subjectLines = heredocLines(commitSubject(fallback.title));
390
+ const fallbackTitleLines = heredocLines(fallback.title);
391
+ // Never the text heredoc's own delimiter, so each heredoc has exactly one closing line.
392
+ const subjectHeredoc = heredocDelimiter([...fallbackLines, ...subjectLines, ...fallbackTitleLines, heredoc]);
303
393
  // The run id for the daemon URL: the LAUNCH id when the composer knows it, pre-encoded as one
304
394
  // strict path segment (only `[A-Za-z0-9._~%-]` survive, so the single-quoted literal is safe);
305
395
  // otherwise derived from the branch at run time and percent-encoded byte-wise by the script.
@@ -315,6 +405,20 @@ export function deliverPrScript(intent, opts = {}) {
315
405
  if (!SAFE_PR_URL.test(revises.url))
316
406
  throw new Error(`revisesPr #${revises.number}: not a pull request URL: ${JSON.stringify(revises.url)}`);
317
407
  }
408
+ // (crew#549) The configured deliver identity, baked as a single-quoted literal. Fail CLOSED at
409
+ // compose time on anything that is not a GitHub login: a run that silently delivered under an
410
+ // unchecked identity is the defect, and a value that reached a shell literal unvalidated would
411
+ // be worse than either.
412
+ const identity = (opts.deliverIdentity ?? '').trim();
413
+ if (identity !== '' && !isGitHubLogin(identity)) {
414
+ throw new Error(`deliver identity: not a GitHub login: ${JSON.stringify(identity)}`);
415
+ }
416
+ // R1: the repository the consent card names IS the one `gh pr create` opens on. Without `--repo`
417
+ // gh resolves the base itself (a `gh repo set-default`, an `upstream` remote), so "opens a pull
418
+ // request there" could name one repository while the PR opened on another (codex review, HIGH).
419
+ // `githubRepoOf` admits only `[A-Za-z0-9._-]+/[A-Za-z0-9._-]+`, safe in a single-quoted literal;
420
+ // a non-github.com origin bakes '' and gh keeps resolving it (the push-only path keys on that).
421
+ const ghRepo = githubRepoOf(opts.originUrl) ?? '';
318
422
  const prNum = revises === null ? '' : String(revises.number);
319
423
  const target = revises === null ? '' : revises.headRef;
320
424
  const prUrl = revises === null ? '' : revises.url;
@@ -332,17 +436,95 @@ export function deliverPrScript(intent, opts = {}) {
332
436
  // token's login) holds, disclosed on the gate card, never switched at push time. Unset ⇒
333
437
  // today's behaviour, now said aloud. No account name is baked into crew code (env-driven).
334
438
  'L=$(gh api user -q .login 2>/dev/null || true)',
439
+ // (crew#549) The CONFIGURED identity is the system setting `deliverIdentityLogin` — baked
440
+ // here at compose time, validated against the GitHub login charset before it is spliced —
441
+ // and the `GH_ACCOUNT` env var when the setting is empty (still supported; the setting wins
442
+ // so a daemon started without the export is not silently unpinned). `GH_ACCOUNT` is then the
443
+ // ONE name the refusals use, whichever source set it.
444
+ `CFG='${identity}'`,
445
+ 'if [ -n "$CFG" ]; then GH_ACCOUNT="$CFG"; fi',
335
446
  'if [ -n "${GH_ACCOUNT:-}" ]; then',
336
447
  ' [ "$L" = "$GH_ACCOUNT" ] || { echo "deliver: identity mismatch — GH_ACCOUNT is $GH_ACCOUNT but gh\'s active login is ${L:-unreadable}; nothing was staged, committed or pushed. Fix the daemon\'s gh login (switch gh\'s active account, or export GH_TOKEN in the daemon environment) and approve to retry the deliver phase"; exit 1; }',
337
448
  ' if [ -n "${GH_TOKEN:-}" ]; then echo "deliver: pushing as $L (GH_ACCOUNT pinned by GH_TOKEN)"; else echo "deliver: pushing as $L (GH_ACCOUNT from the gh keyring — export GH_TOKEN to pin it)"; fi',
338
449
  'elif [ -n "$L" ]; then echo "deliver: pushing as $L (GH_ACCOUNT not set — not pinned)"',
339
450
  'else echo "deliver: pushing as an unknown login (gh not authenticated; GH_ACCOUNT not set — not pinned)"; fi',
451
+ // (crew#549 / F-RC1-010) THE CREDENTIAL CROSS-CHECK. `gh api user` says who gh is; it says
452
+ // NOTHING about the credential `git push` will use. On the RC1 rig those two disagreed —
453
+ // `git credential fill` handed git one account while `gh api user` reported another — and the
454
+ // run pushed under an identity the operator had not chosen (or took a 403 that hard-failed
455
+ // it). So ask git itself, for the remote's own host, BEFORE anything is fetched or staged,
456
+ // and REFUSE when both logins are concrete and differ — naming both, whether or not an
457
+ // identity is configured. `GIT_TERMINAL_PROMPT=0` keeps a helperless host from prompting (it
458
+ // errors instead, which reads as unresolved). Only an https remote has a git credential; an
459
+ // ssh or local remote authenticates another way, so the check does not apply and says so.
460
+ // A placeholder username (`x-access-token`, `oauth2`, `token`) identifies a TOKEN, not an
461
+ // account: unresolved, disclosed, never a refusal (the token's own login is what `gh api
462
+ // user` already reported).
463
+ //
464
+ // THE REMOTE'S OWN USERNAME COMES FIRST (codex review on #727). A remote spelled
465
+ // `https://other-bot@github.com/o/r.git` tells git which account to authenticate as, and git
466
+ // uses it whatever the helper would have answered for the bare host. Stripping it and asking
467
+ // only about the host left exactly the defect this guard exists for alive: gh reports
468
+ // `release-bot`, the URL says `other-bot`, the helper agrees with gh, and the push goes out as
469
+ // `other-bot`. So the URL username is read, refused against gh's login when both are concrete,
470
+ // and passed INTO the credential query so the helper answers for the account git will use.
471
+ 'RU=$(git remote get-url origin 2>/dev/null || true)',
472
+ 'case "$RU" in',
473
+ ' https://*)',
474
+ ' RH=${RU#https://}; RH=${RH%%/*}; RU_USER=""',
475
+ ' case "$RH" in *@*) RU_USER=${RH%@*}; RH=${RH##*@};; esac',
476
+ // A `user:password@host` remote carries the secret in the URL; take the user, never the rest.
477
+ ' case "$RU_USER" in *:*) RU_USER=${RU_USER%%:*};; esac',
478
+ ' case "${RU_USER:-}" in',
479
+ ' ""|x-access-token|oauth2|token|PRIVATE-TOKEN) ;;',
480
+ ' *) if [ -n "$L" ] && [ "$RU_USER" != "$L" ]; then echo "deliver: identity mismatch — gh\'s active login is $L but origin\'s URL authenticates as $RU_USER (https://$RU_USER@$RH/…); nothing was staged, committed or pushed. The push would use $RU_USER, not $L. Point origin at https://$RH/… and pin ONE identity (export GH_TOKEN in the daemon environment, or fix the credential helper for $RH), then approve to retry the deliver phase"; exit 1; fi',
481
+ ' echo "deliver: origin authenticates as $RU_USER (agrees with gh)";; esac',
482
+ ' GC=$(printf "protocol=https\\nhost=%s\\n%s\\n" "$RH" "${RU_USER:+username=$RU_USER}" | GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null | sed -n "s/^username=//p" | head -1 || true)',
483
+ ' case "${GC:-}" in',
484
+ ' "") echo "deliver: git\'s credential identity for $RH is unresolved (no credential helper answered) — the push uses whatever the helper hands git at push time";;',
485
+ ' x-access-token|oauth2|token|PRIVATE-TOKEN) echo "deliver: git\'s credential for $RH is a token ($GC), so it names no account — gh reports ${L:-an unknown login}";;',
486
+ ' *)',
487
+ ' if [ -n "$L" ] && [ "$GC" != "$L" ]; then echo "deliver: identity mismatch — gh\'s active login is $L but git\'s credential for $RH is $GC; nothing was staged, committed or pushed. The push would use $GC, not $L. Pin ONE identity (export GH_TOKEN in the daemon environment, or fix the credential helper for $RH) and approve to retry the deliver phase"; exit 1; fi',
488
+ ' echo "deliver: git\'s credential for $RH is $GC (agrees with gh)";;',
489
+ ' esac;;',
490
+ ' "") echo "deliver: this worktree has no origin remote — no git credential applies";;',
491
+ ' *) echo "deliver: origin is not an https remote — no git credential applies (ssh keys or a local path authenticate the push)";;',
492
+ 'esac',
340
493
  // REVISION MODE inputs (DES-L9 / crew#550) — baked at compose time from the resolved PR, each
341
494
  // validated against a strict charset before it is spliced into a single-quoted literal. Empty
342
495
  // TARGET ⇒ today's new-PR delivery.
343
496
  `PRNUM='${prNum}'`,
344
497
  `TARGET='${target}'`,
345
498
  `PRURL='${prUrl}'`,
499
+ `GHREPO='${ghRepo}'`,
500
+ // R1 (Copilot on crew#736): the card the operator approved named GHREPO; if origin was
501
+ // re-pointed since (the run can wait at its gate for hours), the push would leave for another
502
+ // repository than the one consented to. Refuse before anything is fetched, staged or pushed.
503
+ // The URL is PARSED, not globbed (codex review, HIGH: `*github.com/o/r` also matched
504
+ // `https://evil.example/github.com/o/r` and `git@evilgithub.com:o/r`): the host — userinfo and
505
+ // port stripped, lowercased — must be exactly github.com and the path exactly GHREPO (a
506
+ // trailing `/` or `.git` dropped). The URL itself (it may carry a token) is never printed.
507
+ // EVERY push URL is checked (`--all`): `git push origin` pushes to each `remote.origin.pushurl`,
508
+ // so a second one would receive the branch unnamed (Copilot on crew#736). The host must be
509
+ // github.com or ssh.github.com — the same two `githubRepoOf` names a repository for.
510
+ 'if [ -n "$GHREPO" ]; then',
511
+ " GL=$(printf '%s' \"$GHREPO\" | tr 'A-Z' 'a-z'); ODRIFT=''; OANY=''",
512
+ ' while IFS= read -r OU; do',
513
+ ' [ -n "$OU" ] || continue; OANY=1',
514
+ ' case "$OU" in',
515
+ ' *://*) OH=${OU#*://}; OH=${OH%%/*}; OH=${OH##*@}; OH=${OH%%:*}; OP=${OU#*://*/};;',
516
+ ' *) OH=${OU%%:*}; OH=${OH##*@}; OP=${OU#*:};;',
517
+ ' esac',
518
+ // GitHub owner/repo names are case-insensitive: compare lowercased, keep GHREPO's spelling
519
+ // for `--repo` (codex review, MEDIUM). An absolute scp path (`git@github.com:/o/r`) is the same
520
+ // repository: the leading `/` goes.
521
+ " OH=$(printf '%s' \"$OH\" | tr 'A-Z' 'a-z'); OP=$(printf '%s' \"$OP\" | tr 'A-Z' 'a-z'); OP=${OP#/}; OP=${OP%/}; OP=${OP%.git}",
522
+ ' case "$OH" in github.com|ssh.github.com) [ "$OP" = "$GL" ] || ODRIFT=1;; *) ODRIFT=1;; esac',
523
+ ' done <<GHREPO_URLS',
524
+ '$(git remote get-url --push --all origin 2>/dev/null || true)',
525
+ 'GHREPO_URLS',
526
+ ' if [ -n "$ODRIFT" ] || [ -z "$OANY" ]; then echo "deliver: origin no longer points at $GHREPO (or also pushes elsewhere), the repository this delivery was approved for — nothing was staged, committed or pushed. Point origin back at $GHREPO alone, or reject and relaunch against the new remote"; exit 1; fi',
527
+ 'fi',
346
528
  // (a) The run branch: wicked/<worktree-basename> (the engine names run worktrees by run id),
347
529
  // falling back to the currently checked-out branch when that ref does not exist.
348
530
  'R=$(basename "$PWD")',
@@ -404,7 +586,7 @@ export function deliverPrScript(intent, opts = {}) {
404
586
  // A failed PUSH happens after the product was committed. Keep its worktree from being reaped
405
587
  // by leaving this reserved, untracked recovery sentinel; it is removed HERE (before staging)
406
588
  // so a normal delivery never sees it, and a retry removes it before another attempt (crew#432).
407
- 'S=.wicked-crew-delivery-stranded',
589
+ `S=${DELIVER_STRANDED_SENTINEL}`,
408
590
  'rm -f -- "$S"',
409
591
  // (c1) COMMIT THE RUN'S WORK (crew#317). Agents write files; they do not commit — which is
410
592
  // the premise of core#291 and the reason `d1bc72c2` pushed a branch identical to origin/main.
@@ -459,6 +641,18 @@ export function deliverPrScript(intent, opts = {}) {
459
641
  heredoc,
460
642
  'fi',
461
643
  'TITLE=$(sed -n 1p "$TD/text")',
644
+ `cat > "$TD/subject" <<'${subjectHeredoc}'`,
645
+ ...subjectLines,
646
+ subjectHeredoc,
647
+ `cat > "$TD/fbtitle" <<'${subjectHeredoc}'`,
648
+ ...fallbackTitleLines,
649
+ subjectHeredoc,
650
+ // The subject for the title actually used: the composed one when the daemon's title is the
651
+ // embedded fallback's; else (the daemon titled it differently) a plain word-boundary cut at 72.
652
+ 'if [ "$(cat "$TD/fbtitle")" != "$TITLE" ]; then printf \'%s\\n\' "$TITLE" | awk \'{ if (length($0) <= 72) print; else { s = substr($0, 1, 71); sub(/ [^ ]*$/, "", s); print s "…" } }\' > "$TD/subject"; fi',
653
+ // The commit message: the text verbatim when its title fits the subject width, else the 72-column
654
+ // subject, a blank line, then the text (whose first line is the full title) — crew#550.
655
+ 'if [ "$(cat "$TD/subject")" = "$TITLE" ]; then cp "$TD/text" "$TD/commit"; else { cat "$TD/subject"; echo; cat "$TD/text"; } > "$TD/commit"; fi',
462
656
  "sed '1,2d' \"$TD/text\" > \"$TD/body\"",
463
657
  // (c0) DELIVER PREFLIGHT (crew#426) — a governed run that bumps an internal WORKSPACE package's
464
658
  // version (e.g. packages/crew-api-types) leaves its version-derived codegen AND the lockfile
@@ -554,6 +748,9 @@ export function deliverPrScript(intent, opts = {}) {
554
748
  'while IFS= read -r -d "" F; do',
555
749
  ' [ -n "$F" ] || continue',
556
750
  ' BN=${F##*/}; RN=""',
751
+ // The recovery sentinel is removed above, so this arm is the ONE predicate's first rule, kept
752
+ // here so `deliverExclusionByName` and this classifier stay the same ladder (N2).
753
+ ' [ "$F" = "$S" ] && RN="delivery-sentinel"',
557
754
  // Classify on a LOWERCASED basename so DEPLOY.KEY / .ENV / SOCKET.PATH cannot bypass the
558
755
  // denylist by case (review, #439).
559
756
  ' LBN=$(printf "%s" "$BN" | tr "[:upper:]" "[:lower:]")',
@@ -571,7 +768,7 @@ export function deliverPrScript(intent, opts = {}) {
571
768
  // `--cleanup=whitespace`, NOT git's default for `-F`: an operator/repo `commit.cleanup=strip`
572
769
  // would otherwise treat every `## Intent` / `## Run` / `## Phases` heading as a `#` comment and
573
770
  // strip it from the commit body (review W3-K1).
574
- 'git diff --cached --quiet || git commit -q --cleanup=whitespace -F "$TD/text"',
771
+ 'git diff --cached --quiet || git commit -q --cleanup=whitespace -F "$TD/commit"',
575
772
  // (c2) NOTHING TO DELIVER — no staged work AND no commits of its own. Fail LOUDLY before the
576
773
  // remote is touched: an empty ref pushed under a run id is worse than a failed phase.
577
774
  'if [ -n "$TARGET" ]; then A=$(git rev-list --count "origin/$TARGET..$B"); [ "$A" -ge 1 ] || { echo "deliver: nothing to deliver — the run added no commit on top of PR #$PRNUM"; exit 1; }; else',
@@ -637,17 +834,20 @@ export function deliverPrScript(intent, opts = {}) {
637
834
  '[ "$A" -ge 1 ] || { echo "deliver: nothing to deliver — the run produced no committed change (after rebasing onto $D, $B carries no commit of its own); nothing was pushed"; exit 1; }',
638
835
  'fi',
639
836
  // (d) Push. Any push failure happens AFTER the work was committed and its branch was proven
640
- // ahead. It is therefore a recoverable lift failure, whether the remote branch moved, auth
641
- // returned 403, the transport is down, or a hook rejected it. Preserve git's own output AND
642
- // print the marker last, so crew strands the run and POST /runs/:id/deliver can retry it.
837
+ // ahead, whether the remote branch moved, auth returned 403, the transport is down, or a hook
838
+ // rejected it. It is a REFUSAL BY THE REMOTE, not a lift collision (N4): the lift already came
839
+ // out clean. Preserve git's own output, leave the recovery sentinel, and print
840
+ // DELIVER_PUSH_REJECTED_MARKER last — never the LIFT-CONFLICT marker — so the engine takes its
841
+ // deterministic deliver-refusal arm and parks the run at a gate; nothing pushes again until a
842
+ // human approves the retry.
643
843
  // A revision pushes the run branch ONTO the PR's head branch (`$B:refs/heads/$TARGET`) — the
644
844
  // PR gains exactly the run's commits; a rejection there is the same recoverable strand.
645
845
  '_push() { if [ -n "$TARGET" ]; then git push origin "$B:refs/heads/$TARGET"; else git push -u origin "$B"; fi; }',
646
846
  'if PUSHOUT=$(_push 2>&1); then echo "$PUSHOUT"; else',
647
847
  ' echo "$PUSHOUT"',
648
848
  ' case "$PUSHOUT" in',
649
- ` *non-fast-forward*|*"fetch first"*|*"[rejected]"*|*"Updates were rejected"*) : > "$S"; echo "${DELIVER_LIFT_CONFLICT_MARKER} — push of $B was rejected because the remote branch moved (non-fast-forward); rebase and re-run; nothing was pushed"; exit 1;;`,
650
- ` *) : > "$S"; PUSHERR="\${PUSHOUT:0:96} ... \${PUSHOUT: -128}"; PUSHERR=\${PUSHERR//$'\\n'/ }; echo "deliver: git push of $B failed after commit: $PUSHERR; retry POST /runs/:id/deliver; nothing was pushed; ${DELIVER_LIFT_CONFLICT_MARKER}"; exit 1;;`,
849
+ ` *non-fast-forward*|*"fetch first"*|*"[rejected]"*|*"Updates were rejected"*) : > "$S"; echo "deliver: the remote refused the push of $B because its branch moved (non-fast-forward); the work is committed on $B and nothing was pushed — approve to retry the deliver phase; ${DELIVER_PUSH_REJECTED_MARKER}"; exit 1;;`,
850
+ ` *) : > "$S"; PUSHERR="\${PUSHOUT:0:96} ... \${PUSHOUT: -128}"; PUSHERR=\${PUSHERR//$'\\n'/ }; echo "deliver: the remote refused the push of $B after commit: $PUSHERR; the work is committed on $B and nothing was pushed — fix the remote condition, then approve to retry the deliver phase; ${DELIVER_PUSH_REJECTED_MARKER}"; exit 1;;`,
651
851
  ' esac',
652
852
  'fi',
653
853
  // (e) Open the PR with gh's OUTPUT and EXIT STATUS captured separately (crew#317). The old
@@ -669,7 +869,41 @@ export function deliverPrScript(intent, opts = {}) {
669
869
  ' if COUT=$(gh pr comment "$PRNUM" --body-file "$TD/body" 2>&1); then echo "deliver: run record commented on pull request #$PRNUM"; else echo "$COUT"; echo "deliver: could not comment on pull request #$PRNUM — the commits landed; the record is in the commit message"; fi',
670
870
  ' URL="$PRURL"',
671
871
  'else',
672
- ' if ! OUT=$(gh pr create --head "$B" --title "$TITLE" --body-file "$TD/body" 2>&1); then echo "$OUT"; echo "deliver: gh pr create failed for $B — no PR was opened"; exit 1; fi',
872
+ // (e2) A NON-GITHUB ORIGIN DELIVERS THE BRANCH (F2). `gh pr create` failing used to be
873
+ // `exit 1` with no fallback and no push-only mode, so on a local, SSH, GitLab, ADO or Gitea
874
+ // origin the phase PUSHED THE BRANCH — step (d), above, already happened — and then died on
875
+ // "none of the git remotes configured for this repository point to a known GitHub host".
876
+ // Approving the retry re-ran the identical refusal; rejecting cancelled the run. The run could
877
+ // never reach a terminal state while the one irreversible side effect was already done.
878
+ //
879
+ // gh is the AUTHORITY on whether a remote is a GitHub remote — it resolves the remotes itself
880
+ // and that refusal IS its verdict — so the push-only success path opens on that message and on
881
+ // nothing else. The pattern requires BOTH halves of the diagnostic, in order, rather than one
882
+ // fragment (codex review of this PR, MEDIUM), while skipping the leading "none of the" and the
883
+ // point/points verb so a gh wording tweak cannot silently re-open the F2 hole. Every other gh failure (auth, validation, rate limit, a gh that is not
884
+ // installed) stays exactly as loud as it was, which is what keeps a GitHub Enterprise Server
885
+ // origin — where gh succeeds — on the pull-request path. No forge integration is invented
886
+ // here: the pushed branch IS the delivery, and the phase says so and says where to take it.
887
+ //
888
+ // Done is still RE-DERIVED before the claim: the remote ref must be ahead of the base. And
889
+ // nothing may read a pull request out of this output — there is no URL in it, so `prUrlFrom`
890
+ // answers null and the run's `delivery` never reads `delivered`.
891
+ ' if ! OUT=$(gh pr create ${GHREPO:+--repo "$GHREPO"} --head "$B" --title "$TITLE" --body-file "$TD/body" 2>&1); then',
892
+ ' echo "$OUT"',
893
+ ' case "$OUT" in',
894
+ ' *"git remotes configured for this repository"*"known GitHub host"*)',
895
+ ' P=$(git rev-list --count "$D..origin/$B")',
896
+ ' [ "$P" -ge 1 ] || { echo "deliver: $B is not ahead of $D on the remote after the push — refusing to report a delivery with no commits"; exit 1; }',
897
+ // The PUSH url, with any `user[:password]@` userinfo stripped before it reaches the transcript
898
+ // (codex review of N1: a push URL can carry a deploy token).
899
+ ' R=$(git remote get-url --push origin | sed -E "s#^([A-Za-z][A-Za-z0-9+.-]*://)[^@/]*@#\\1#")',
900
+ ' echo "deliver: pushed $B to origin ($R) with $P commit(s) on top of $D, and no pull request was opened because that remote is not a GitHub host gh can resolve. The branch IS the delivery — open the pull/merge request for $B on your forge; merge stays human.";',
901
+ // N1: the machine line the daemon records the push-only delivery from — LAST, one line.
902
+ ` echo "${DELIVER_PUSHED_NO_PR_MARKER} $B $R";`,
903
+ ' exit 0;;',
904
+ ' esac',
905
+ ' echo "deliver: gh pr create failed for $B — no PR was opened"; exit 1',
906
+ ' fi',
673
907
  ' echo "$OUT"',
674
908
  // (f) DONE IS RE-DERIVED, NOT ASSERTED — twice, from two independent facts, before the phase
675
909
  // is allowed to report a delivery:
@@ -735,25 +969,225 @@ export function deliverPrPhase(dependsOn = [], intent, opts = {}) {
735
969
  validator_pin: EVIDENCE_FLOOR_PIN,
736
970
  };
737
971
  }
972
+ /** The host of a git remote URL — `https://`/`ssh://` URLs and the scp-like `git@host:owner/repo`
973
+ * spelling — or `null` when the value names no host (a path). */
974
+ export function originRemoteHost(url) {
975
+ const trimmed = url.trim();
976
+ if (trimmed === '')
977
+ return null;
978
+ // Any `scheme://…` is decided HERE, authority or not: `file:///p` has an empty authority and
979
+ // names no host, and so does any other schemed URL written without one.
980
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(trimmed);
981
+ if (scheme !== null) {
982
+ if (scheme[1].toLowerCase() === 'file')
983
+ return null;
984
+ const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(?:[^@/]*@)?([^/:]+)/.exec(trimmed);
985
+ return authority !== null ? authority[1].toLowerCase() : null;
986
+ }
987
+ // A Windows drive is a PATH, excluded by shape rather than by "a host is at least two letters"
988
+ // — which also rejected the single-letter host `h:path` (codex review of this PR, LOW).
989
+ if (/^[A-Za-z]:[\\/]/.test(trimmed))
990
+ return null;
991
+ // scp-like: `[user@]host:path`. The path may be ABSOLUTE (`git@example.com:/srv/git/repo.git`
992
+ // is valid scp syntax) — an earlier `(?!\/)` guard rejected exactly that and called a real
993
+ // remote a local path (codex review, LOW).
994
+ const scp = /^(?:[^@/\\]+@)?([A-Za-z0-9._-]+):/.exec(trimmed);
995
+ return scp !== null ? scp[1].toLowerCase() : null;
996
+ }
997
+ /** {@link DeliverOriginKind} for an origin URL read at compose time. */
998
+ export function classifyDeliverOrigin(url) {
999
+ if (url === null || url === undefined || url.trim() === '')
1000
+ return 'none';
1001
+ const host = originRemoteHost(url);
1002
+ if (host === null)
1003
+ return 'local';
1004
+ return host === 'github.com' || host.endsWith('.github.com') ? 'github' : 'other';
1005
+ }
1006
+ /**
1007
+ * The gate card's first sentence for a NEW-PR delivery — what will ACTUALLY happen (F2).
1008
+ *
1009
+ * It used to promise "pushes its branch → opens a pull request" unconditionally. On a local, SSH,
1010
+ * GitLab, ADO or Gitea origin that was a false promise twice over: the phase pushed the branch and
1011
+ * then died on `gh pr create`, so the operator consented to a pull request that could not exist and
1012
+ * got an irreversible push plus a run that could not reach a terminal state. The push-only success
1013
+ * path in the script fixes the outcome; this fixes the consent.
1014
+ *
1015
+ * `null`/absent origin ⇒ the generic sentence, unchanged: "could not read it" is not a licence to
1016
+ * claim anything about it either way.
1017
+ */
1018
+ /** The hosts a GitHub repository is pushed to: the web host and the ssh-over-443 endpoint. */
1019
+ const GITHUB_PUSH_HOSTS = new Set(['github.com', 'ssh.github.com']);
738
1020
  /**
739
- * What the deliver gate card says (DES-L9 §4): the push target — onto the revised PR's branch, or a
740
- * new PR — and the push identity with its pin source: `GH_ACCOUNT` pinned by an exported `GH_TOKEN`
741
- * (then `gh api user` IS the token's login and the script refuses on a difference), `GH_ACCOUNT`
742
- * from the gh keyring (the login can flip between this check and the push — export `GH_TOKEN` to
743
- * pin it), or unset (pushes as whatever login gh holds). Never the token, never a live probe.
1021
+ * `owner/repo` of a github.com origin URL — `https://[user[:token]@]github.com/owner/repo(.git)`,
1022
+ * `ssh://git@github.com/owner/repo`, `git@github.com:owner/repo.git` — or `null` when the value is
1023
+ * not a github.com URL with exactly an owner/name path. Credentials never survive: only the path is
1024
+ * read (R1, ship-prove-3).
744
1025
  */
1026
+ export function githubRepoOf(url) {
1027
+ if (classifyDeliverOrigin(url) !== 'github')
1028
+ return null;
1029
+ const trimmed = (url ?? '').trim();
1030
+ // Only the two hosts a repository is pushed to (`ssh.github.com` is GitHub's ssh-over-443
1031
+ // endpoint). Another `*.github.com` (gist, api) names no repository `--repo` could open on —
1032
+ // the card keeps its generic sentence and gh resolves the remote itself. The script's drift
1033
+ // check accepts exactly these two hosts too (Copilot on crew#736).
1034
+ if (!GITHUB_PUSH_HOSTS.has(originRemoteHost(trimmed) ?? ''))
1035
+ return null;
1036
+ const schemed = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(?:[^@/]*@)?[^/]+\/(.*)$/.exec(trimmed);
1037
+ const scp = schemed === null ? /^(?:[^@/\\]+@)?[A-Za-z0-9._-]+:(.*)$/.exec(trimmed) : null;
1038
+ const path = (schemed ?? scp)?.[1] ?? null;
1039
+ if (path === null)
1040
+ return null;
1041
+ const segs = path.replace(/[?#].*$/, '').split('/').filter((x) => x !== '');
1042
+ if (segs.length !== 2)
1043
+ return null;
1044
+ const owner = segs[0];
1045
+ const name = segs[1].replace(/\.git$/i, '');
1046
+ const part = /^[A-Za-z0-9._-]+$/;
1047
+ return part.test(owner) && part.test(name) && name !== '' ? `${owner}/${name}` : null;
1048
+ }
1049
+ /** The run branch the deliver script pushes — `wicked/<run id>` (the engine names run worktrees by
1050
+ * run id and the script pushes `wicked/<worktree basename>`) — or `null` when the run is unknown. */
1051
+ export function deliverRunBranch(runId) {
1052
+ // The id as the engine receives it — NOT trimmed: `" run "` takes the engine's hash tier, and a
1053
+ // trim here would announce `wicked/run` (Copilot on crew#736).
1054
+ const id = runId ?? '';
1055
+ // The charset alone is not a git ref component: `.run`, `run..next`, `run.lock`, `run.` are
1056
+ // illegal refs the engine must sanitize, so they name no branch here (Copilot on crew#736).
1057
+ // NTFS reserved device stems (`CON`, `con.txt`, `COM1`…) are rewritten and hashed by the
1058
+ // engine's `sanitize_worktree_id` too, so they name no branch either (Copilot on crew#736).
1059
+ const reserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
1060
+ const refSafe = (x) => /^[A-Za-z0-9._-]+$/.test(x) && !x.startsWith('.') && !x.startsWith('-') && !x.includes('..') && !x.endsWith('.') && !x.endsWith('.lock') && !reserved.test(x);
1061
+ if (refSafe(id))
1062
+ return `wicked/${id}`;
1063
+ // The engine's hashless COLON tier (core#345/#347): an id whose only illegal char is `:` (a
1064
+ // campaign-shaped `<label>:<repo>:a0`) names its worktree and branch with each `:` as `-`
1065
+ // (Copilot on crew#736). Any other shape takes the engine's hash-suffixed tier, which is not
1066
+ // re-derived here — the card then says "the run branch" rather than guess.
1067
+ if (/^[A-Za-z0-9._-]+(?::[A-Za-z0-9._-]+)+$/.test(id) && refSafe(id.replaceAll(':', '-'))) {
1068
+ return `wicked/${id.replaceAll(':', '-')}`;
1069
+ }
1070
+ return null;
1071
+ }
1072
+ /**
1073
+ * The gate card's first sentence for a NEW-PR delivery — what will ACTUALLY happen (F2). `runId`
1074
+ * names the branch (R2: the card used to print the literal placeholder `wicked/<run>`); without
1075
+ * one — the LAUNCH composer, before the run exists — it says "the run branch". The same sentence
1076
+ * is what `GET /repos/:id/deliver-target` answers, so launch and gate cannot disagree (R3).
1077
+ */
1078
+ export function newPrTargetSentence(originUrl, runId) {
1079
+ const kind = classifyDeliverOrigin(originUrl);
1080
+ const runBranch = deliverRunBranch(runId);
1081
+ const branch = runBranch !== null ? `branch ${runBranch}` : 'the run branch';
1082
+ if (kind === 'github') {
1083
+ // R1: name the GitHub repository the pull request opens on, read off the origin URL (never
1084
+ // crew's registry label, never a credential).
1085
+ const repo = githubRepoOf(originUrl);
1086
+ return repo !== null
1087
+ ? `Pushes ${branch} to ${repo} on GitHub and opens a pull request there; merge stays human.`
1088
+ : `Pushes ${branch} to origin and opens a pull request; merge stays human.`;
1089
+ }
1090
+ if (kind === 'local') {
1091
+ // NOT a flat "no pull request is opened": `gh pr create` resolves every configured remote, not
1092
+ // only `origin`, so a checkout whose origin is a path while some other remote is a GitHub
1093
+ // repository can still get one (codex review of this PR, MEDIUM). The push DESTINATION is what
1094
+ // this read establishes; the pull request is stated as the condition it actually is.
1095
+ return (`Pushes ${branch} to origin (${(originUrl ?? '').trim()}) — a local path, so no pull request ` +
1096
+ 'can be opened against it: unless another remote in this checkout is a GitHub repository gh ' +
1097
+ 'resolves, the pushed branch IS the delivery.');
1098
+ }
1099
+ if (kind === 'other') {
1100
+ const host = originRemoteHost((originUrl ?? '').trim()) ?? 'that host';
1101
+ return (`Pushes ${branch} to origin (${host}) and opens a pull request only if gh resolves ${host} ` +
1102
+ 'as a GitHub host it is logged in to; otherwise no pull request is opened and the pushed ' +
1103
+ 'branch IS the delivery. Merge stays human.');
1104
+ }
1105
+ if (originUrl === '') {
1106
+ return (`Pushes ${branch} to origin — but this repository has no \`origin\` remote, so the push ` +
1107
+ 'will fail and nothing will be delivered. Add the remote first.');
1108
+ }
1109
+ return `Pushes ${branch} to origin and opens a pull request; merge stays human.`;
1110
+ }
1111
+ /** The launch-time reading of the same origin preflight the deliver gate uses (R3, crew#730). */
1112
+ export function deliverTargetView(originUrl) {
1113
+ const unread = originUrl === null || originUrl === undefined;
1114
+ return {
1115
+ origin: unread ? 'unknown' : classifyDeliverOrigin(originUrl),
1116
+ githubRepo: githubRepoOf(originUrl),
1117
+ sentence: newPrTargetSentence(originUrl),
1118
+ };
1119
+ }
1120
+ /** The registered repo a launch's `repoRef` names — by id, then name, then checkout basename: the
1121
+ * ONE matcher the launch's origin read and `GET /repos/:id/deliver-target` share. */
1122
+ export function deliverRepoFor(repos, repoRef) {
1123
+ // Three passes, so the precedence is real: an exact id never loses to an earlier repo whose
1124
+ // name or checkout basename collides with it (codex review, MEDIUM).
1125
+ return (repos.find((r) => r.id === repoRef) ??
1126
+ repos.find((r) => r.name === repoRef) ??
1127
+ repos.find((r) => r.root_path.split(/[\\/]/).filter((x) => x !== '').pop() === repoRef));
1128
+ }
745
1129
  export function deliverGateInstructions(opts) {
746
1130
  const pr = opts.revisesPr ?? null;
747
1131
  const target = pr !== null
748
- ? `Pushes wicked/<run> onto pull request #${pr.number} (branch ${pr.headRef}); no new PR.`
749
- : 'Pushes the run branch wicked/<run> to origin and opens a pull request; merge stays human.';
750
- const account = opts.ghAccount ?? null;
1132
+ ? `Pushes ${deliverRunBranch(opts.runId) ?? 'the run branch'} onto pull request #${pr.number} (branch ${pr.headRef}); no new PR.`
1133
+ : newPrTargetSentence(opts.originUrl, opts.runId);
1134
+ // (crew#549) The SETTING wins over the env var, and the card names which one it read, because
1135
+ // "pin it now if it must differ" is not an instruction an operator can follow without knowing
1136
+ // where the pin lives.
1137
+ const configured = (opts.deliverIdentity ?? '').trim();
1138
+ const account = configured !== '' ? configured : (opts.ghAccount ?? null);
1139
+ const source = configured !== '' ? 'the deliver identity setting' : 'GH_ACCOUNT';
751
1140
  const who = account !== null && account !== ''
752
1141
  ? opts.ghTokenPinned === true
753
- ? `Push identity: GH_ACCOUNT=${account}, pinned by GH_TOKEN — the phase refuses if gh's login differs at push time.`
754
- : `Push identity: GH_ACCOUNT=${account} from the gh keyring — the login can change between the check and the push; export GH_TOKEN to pin it. The phase refuses if gh's login differs.`
755
- : 'Push identity: GH_ACCOUNT is not set — pushes as whatever login gh holds.';
756
- return `${target} ${who}`;
1142
+ ? `Push identity: ${account} (${source}), pinned by GH_TOKEN — the phase refuses if gh's login differs at push time.`
1143
+ : `Push identity: ${account} (${source}) against the gh keyring — the login can change between the check and the push; export GH_TOKEN to pin it. The phase refuses if gh's login differs.`
1144
+ : 'Push identity: none configured — pushes as whatever login gh holds (set the deliver identity in system settings to pin it).';
1145
+ // Whatever is configured, the phase also asks git which credential IT would use for the
1146
+ // remote's host and refuses when the two logins disagree — the flip that pushed under an
1147
+ // unintended account (F-RC1-010).
1148
+ return `${target} ${who} It refuses if gh's login and git's credential for the remote disagree.`;
1149
+ }
1150
+ /**
1151
+ * The `deliver` PLAN STEP a delivering PRESET launch hands the engine (DES-TEAMING-002 T3,
1152
+ * `LaunchOptions.deliverStepJson`): catalog `deliver`, id `deliver`, the same hardened push-and-PR
1153
+ * Tool command, gate-card instructions and evidence-floor pin as the composed phase below. The
1154
+ * engine appends it to the preset's plan and puts `deliver` in the floor, so the run stays ONE
1155
+ * team plan behind its `plan_approval` gate — crew never composes a def over a preset. The step
1156
+ * omits `depends_on` (compose gives `deliver` the step before it) and carries no
1157
+ * `verified_evidence` (a plan step cannot; DES rev 12 note 4: a recorded difference with no
1158
+ * runtime effect). `phases` only feeds the embedded fallback text.
1159
+ */
1160
+ export function deliverPresetStep(
1161
+ /** The preset launched, or `null` for a user-composed plan. */
1162
+ presetName, phases, runId, intent, launch = {}) {
1163
+ const apiOrigin = launch.apiOrigin ?? null;
1164
+ const revisesPr = launch.revisesPr ?? null;
1165
+ const facts = factsFromWorkflow({
1166
+ runId,
1167
+ intent,
1168
+ workflowId: presetName,
1169
+ repoRef: launch.repoRef ?? null,
1170
+ phases,
1171
+ runUrl: runUrlFor(configuredPublicOrigin(), runId),
1172
+ revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
1173
+ });
1174
+ const phase = deliverPrPhase([], intent, {
1175
+ runId,
1176
+ facts,
1177
+ apiOrigin,
1178
+ revisesPr,
1179
+ ghAccount: launch.ghAccount ?? null,
1180
+ ghTokenPinned: launch.ghTokenPinned === true,
1181
+ originUrl: launch.originUrl ?? null,
1182
+ deliverIdentity: launch.deliverIdentity ?? null,
1183
+ });
1184
+ return {
1185
+ catalog: 'deliver',
1186
+ id: DELIVER_PHASE_ID,
1187
+ instructions: phase.instructions ?? '',
1188
+ executor: phase.executor,
1189
+ validator_pin: EVIDENCE_FLOOR_PIN,
1190
+ };
757
1191
  }
758
1192
  export function composeDeliverWorkflow(base, runId, intent, launch = {}) {
759
1193
  if (base.phases.some((p) => p.id === DELIVER_PHASE_ID)) {
@@ -774,7 +1208,7 @@ export function composeDeliverWorkflow(base, runId, intent, launch = {}) {
774
1208
  workflowId: base.id,
775
1209
  repoRef: launch.repoRef ?? null,
776
1210
  phases: base.phases,
777
- runUrl: runUrlFor(apiOrigin, runId),
1211
+ runUrl: runUrlFor(configuredPublicOrigin(), runId),
778
1212
  revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
779
1213
  });
780
1214
  return {
@@ -790,6 +1224,10 @@ export function composeDeliverWorkflow(base, runId, intent, launch = {}) {
790
1224
  revisesPr,
791
1225
  ghAccount: launch.ghAccount ?? null,
792
1226
  ghTokenPinned: launch.ghTokenPinned === true,
1227
+ // F2 — the origin the push will actually go to, so the gate card cannot promise a pull
1228
+ // request on a remote that can never carry one.
1229
+ originUrl: launch.originUrl ?? null,
1230
+ deliverIdentity: launch.deliverIdentity ?? null,
793
1231
  }),
794
1232
  ],
795
1233
  };