@cluesmith/codev 3.3.1 → 3.3.3

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 (248) hide show
  1. package/dashboard-dist/assets/{index-D3XaKpZW.js → index-CUIjS_Vb.js} +41 -41
  2. package/dashboard-dist/assets/index-CUIjS_Vb.js.map +1 -0
  3. package/dashboard-dist/assets/index-DnYLy5C_.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +52 -1
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  9. package/dist/agent-farm/commands/inbox.js +67 -10
  10. package/dist/agent-farm/commands/inbox.js.map +1 -1
  11. package/dist/agent-farm/commands/interrupt.d.ts +4 -1
  12. package/dist/agent-farm/commands/interrupt.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/interrupt.js +6 -3
  14. package/dist/agent-farm/commands/interrupt.js.map +1 -1
  15. package/dist/agent-farm/commands/reset/constants.d.ts +118 -0
  16. package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/reset/constants.js +118 -0
  18. package/dist/agent-farm/commands/reset/constants.js.map +1 -1
  19. package/dist/agent-farm/commands/reset/context.d.ts +18 -0
  20. package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/reset/context.js +41 -0
  22. package/dist/agent-farm/commands/reset/context.js.map +1 -1
  23. package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/reset/receipt.js +16 -5
  25. package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
  26. package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
  27. package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
  28. package/dist/agent-farm/commands/reset/reorient.js +8 -1
  29. package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
  30. package/dist/agent-farm/commands/reset/self.d.ts +335 -0
  31. package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
  32. package/dist/agent-farm/commands/reset/self.js +636 -0
  33. package/dist/agent-farm/commands/reset/self.js.map +1 -0
  34. package/dist/agent-farm/commands/reset.d.ts +4 -1
  35. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  36. package/dist/agent-farm/commands/reset.js +29 -21
  37. package/dist/agent-farm/commands/reset.js.map +1 -1
  38. package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
  39. package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
  40. package/dist/agent-farm/commands/self-refresh.js +506 -0
  41. package/dist/agent-farm/commands/self-refresh.js.map +1 -0
  42. package/dist/agent-farm/commands/send.d.ts +28 -0
  43. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  44. package/dist/agent-farm/commands/send.js +90 -7
  45. package/dist/agent-farm/commands/send.js.map +1 -1
  46. package/dist/agent-farm/commands/spawn.d.ts +7 -0
  47. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  48. package/dist/agent-farm/commands/spawn.js +13 -1
  49. package/dist/agent-farm/commands/spawn.js.map +1 -1
  50. package/dist/agent-farm/db/index.d.ts +3 -1
  51. package/dist/agent-farm/db/index.d.ts.map +1 -1
  52. package/dist/agent-farm/db/index.js +8 -443
  53. package/dist/agent-farm/db/index.js.map +1 -1
  54. package/dist/agent-farm/db/mailbox.d.ts +47 -7
  55. package/dist/agent-farm/db/mailbox.d.ts.map +1 -1
  56. package/dist/agent-farm/db/mailbox.js +56 -12
  57. package/dist/agent-farm/db/mailbox.js.map +1 -1
  58. package/dist/agent-farm/db/migrations.d.ts +57 -0
  59. package/dist/agent-farm/db/migrations.d.ts.map +1 -0
  60. package/dist/agent-farm/db/migrations.js +530 -0
  61. package/dist/agent-farm/db/migrations.js.map +1 -0
  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 -0
  65. package/dist/agent-farm/db/schema.js.map +1 -1
  66. package/dist/agent-farm/db/types.d.ts +22 -0
  67. package/dist/agent-farm/db/types.d.ts.map +1 -1
  68. package/dist/agent-farm/db/types.js.map +1 -1
  69. package/dist/agent-farm/lib/tower-client.d.ts +12 -0
  70. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  71. package/dist/agent-farm/lib/tower-client.js +12 -0
  72. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  73. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  74. package/dist/agent-farm/lib/tunnel-client.js +43 -14
  75. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  76. package/dist/agent-farm/servers/cron-delivery.d.ts +7 -1
  77. package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -1
  78. package/dist/agent-farm/servers/cron-delivery.js +2 -1
  79. package/dist/agent-farm/servers/cron-delivery.js.map +1 -1
  80. package/dist/agent-farm/servers/gate-profiles.d.ts +21 -0
  81. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/gate-profiles.js +36 -4
  83. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  84. package/dist/agent-farm/servers/mailbox-delivery.d.ts +218 -15
  85. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  86. package/dist/agent-farm/servers/mailbox-delivery.js +259 -27
  87. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  88. package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -11
  89. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  90. package/dist/agent-farm/servers/mailbox-wiring.js +202 -48
  91. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  92. package/dist/agent-farm/servers/message-write.d.ts +58 -18
  93. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  94. package/dist/agent-farm/servers/message-write.js +73 -28
  95. package/dist/agent-farm/servers/message-write.js.map +1 -1
  96. package/dist/agent-farm/servers/render-gate.d.ts +37 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  98. package/dist/agent-farm/servers/render-gate.js +71 -12
  99. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  100. package/dist/agent-farm/servers/session-submit.d.ts +220 -27
  101. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  102. package/dist/agent-farm/servers/session-submit.js +333 -42
  103. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  104. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  105. package/dist/agent-farm/servers/tower-cron.js +3 -2
  106. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  107. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  108. package/dist/agent-farm/servers/tower-instances.js +11 -3
  109. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  110. package/dist/agent-farm/servers/tower-routes.d.ts +32 -0
  111. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  112. package/dist/agent-farm/servers/tower-routes.js +245 -36
  113. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  114. package/dist/agent-farm/servers/tower-server.js +15 -2
  115. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  116. package/dist/agent-farm/servers/tower-terminals.d.ts +21 -0
  117. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  118. package/dist/agent-farm/servers/tower-terminals.js +53 -5
  119. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  120. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  121. package/dist/agent-farm/servers/tower-tunnel.js +14 -0
  122. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  123. package/dist/agent-farm/servers/tower-utils.d.ts +35 -0
  124. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  125. package/dist/agent-farm/servers/tower-utils.js +39 -0
  126. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  127. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  128. package/dist/agent-farm/servers/tower-websocket.js +8 -1
  129. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  130. package/dist/agent-farm/types.d.ts +36 -1
  131. package/dist/agent-farm/types.d.ts.map +1 -1
  132. package/dist/agent-farm/utils/message-format.d.ts +136 -4
  133. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  134. package/dist/agent-farm/utils/message-format.js +212 -22
  135. package/dist/agent-farm/utils/message-format.js.map +1 -1
  136. package/dist/agent-farm/utils/server-utils.d.ts +8 -2
  137. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  138. package/dist/agent-farm/utils/server-utils.js +19 -2
  139. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  140. package/dist/cli.d.ts.map +1 -1
  141. package/dist/cli.js +3 -1
  142. package/dist/cli.js.map +1 -1
  143. package/dist/commands/doctor.js +2 -2
  144. package/dist/commands/generate-image.d.ts +20 -1
  145. package/dist/commands/generate-image.d.ts.map +1 -1
  146. package/dist/commands/generate-image.js +188 -1
  147. package/dist/commands/generate-image.js.map +1 -1
  148. package/dist/commands/porch/context-refresh.d.ts +181 -0
  149. package/dist/commands/porch/context-refresh.d.ts.map +1 -0
  150. package/dist/commands/porch/context-refresh.js +270 -0
  151. package/dist/commands/porch/context-refresh.js.map +1 -0
  152. package/dist/commands/porch/index.d.ts +6 -0
  153. package/dist/commands/porch/index.d.ts.map +1 -1
  154. package/dist/commands/porch/index.js +46 -0
  155. package/dist/commands/porch/index.js.map +1 -1
  156. package/dist/commands/porch/next.d.ts +0 -9
  157. package/dist/commands/porch/next.d.ts.map +1 -1
  158. package/dist/commands/porch/next.js +133 -4
  159. package/dist/commands/porch/next.js.map +1 -1
  160. package/dist/commands/porch/prompts.d.ts.map +1 -1
  161. package/dist/commands/porch/prompts.js +18 -2
  162. package/dist/commands/porch/prompts.js.map +1 -1
  163. package/dist/commands/porch/protocol.d.ts.map +1 -1
  164. package/dist/commands/porch/protocol.js +102 -0
  165. package/dist/commands/porch/protocol.js.map +1 -1
  166. package/dist/commands/porch/types.d.ts +61 -0
  167. package/dist/commands/porch/types.d.ts.map +1 -1
  168. package/dist/lib/forge-contracts.d.ts +66 -5
  169. package/dist/lib/forge-contracts.d.ts.map +1 -1
  170. package/dist/lib/forge-contracts.js.map +1 -1
  171. package/dist/lib/forge.d.ts +1 -1
  172. package/dist/lib/forge.d.ts.map +1 -1
  173. package/dist/lib/forge.js +33 -11
  174. package/dist/lib/forge.js.map +1 -1
  175. package/dist/lib/self-refresh-invocation.d.ts +60 -0
  176. package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
  177. package/dist/lib/self-refresh-invocation.js +64 -0
  178. package/dist/lib/self-refresh-invocation.js.map +1 -0
  179. package/dist/lib/test-env.d.ts +50 -0
  180. package/dist/lib/test-env.d.ts.map +1 -1
  181. package/dist/lib/test-env.js +76 -0
  182. package/dist/lib/test-env.js.map +1 -1
  183. package/dist/terminal/pty-manager.d.ts +10 -1
  184. package/dist/terminal/pty-manager.d.ts.map +1 -1
  185. package/dist/terminal/pty-manager.js +38 -9
  186. package/dist/terminal/pty-manager.js.map +1 -1
  187. package/dist/terminal/pty-session.d.ts +52 -4
  188. package/dist/terminal/pty-session.d.ts.map +1 -1
  189. package/dist/terminal/pty-session.js +157 -18
  190. package/dist/terminal/pty-session.js.map +1 -1
  191. package/dist/terminal/shellper-client.d.ts +73 -0
  192. package/dist/terminal/shellper-client.d.ts.map +1 -1
  193. package/dist/terminal/shellper-client.js +151 -0
  194. package/dist/terminal/shellper-client.js.map +1 -1
  195. package/dist/terminal/shellper-main.js +20 -3
  196. package/dist/terminal/shellper-main.js.map +1 -1
  197. package/dist/terminal/shellper-process.d.ts +2 -0
  198. package/dist/terminal/shellper-process.d.ts.map +1 -1
  199. package/dist/terminal/shellper-process.js +17 -1
  200. package/dist/terminal/shellper-process.js.map +1 -1
  201. package/dist/terminal/shellper-protocol.d.ts +28 -0
  202. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  203. package/dist/terminal/shellper-protocol.js.map +1 -1
  204. package/package.json +4 -4
  205. package/scripts/forge/gitea/_lib.sh +214 -0
  206. package/scripts/forge/gitea/issue-comment.sh +8 -1
  207. package/scripts/forge/gitea/issue-view.sh +95 -4
  208. package/scripts/forge/gitea/pr-create.sh +155 -0
  209. package/scripts/forge/gitea/pr-exists.sh +32 -4
  210. package/scripts/forge/gitea/pr-list.sh +40 -31
  211. package/scripts/forge/gitea/pr-view.sh +65 -4
  212. package/scripts/forge/gitea/recently-merged.sh +91 -21
  213. package/scripts/forge/gitea/user-identity.sh +30 -1
  214. package/scripts/forge/github/issue-view.sh +5 -2
  215. package/scripts/forge/github/pr-create.sh +47 -0
  216. package/scripts/forge/github/pr-search.sh +3 -1
  217. package/scripts/forge/gitlab/issue-view.sh +24 -4
  218. package/scripts/forge/gitlab/pr-create.sh +53 -0
  219. package/scripts/forge/gitlab/pr-search.sh +3 -1
  220. package/scripts/forge/linear/issue-view.sh +13 -2
  221. package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
  222. package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
  223. package/skeleton/.claude/skills/generate-image/SKILL.md +22 -1
  224. package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
  225. package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
  226. package/skeleton/.codex/skills/generate-image/SKILL.md +22 -1
  227. package/skeleton/protocol-schema.json +17 -0
  228. package/skeleton/protocols/air/prompts/pr.md +6 -1
  229. package/skeleton/protocols/air/protocol.json +1 -1
  230. package/skeleton/protocols/aspir/prompts/review.md +6 -1
  231. package/skeleton/protocols/aspir/protocol.json +5 -1
  232. package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
  233. package/skeleton/protocols/bugfix/protocol.json +1 -1
  234. package/skeleton/protocols/bugfix/protocol.md +2 -1
  235. package/skeleton/protocols/experiment/protocol.json +1 -1
  236. package/skeleton/protocols/maintain/prompts/review.md +6 -1
  237. package/skeleton/protocols/maintain/protocol.json +1 -1
  238. package/skeleton/protocols/pir/prompts/review.md +10 -7
  239. package/skeleton/protocols/pir/protocol.json +1 -1
  240. package/skeleton/protocols/protocol-schema.json +17 -0
  241. package/skeleton/protocols/research/protocol.json +1 -1
  242. package/skeleton/protocols/spike/protocol.json +1 -1
  243. package/skeleton/protocols/spir/prompts/review.md +6 -1
  244. package/skeleton/protocols/spir/protocol.json +5 -1
  245. package/skeleton/resources/commands/agent-farm.md +1 -1
  246. package/templates/tower.html +16 -1
  247. package/dashboard-dist/assets/index-BZRTEMtH.css +0 -32
  248. package/dashboard-dist/assets/index-D3XaKpZW.js.map +0 -1
@@ -0,0 +1,155 @@
1
+ #!/bin/sh
2
+ # Forge concept: pr-create (Gitea/Forgejo via tea CLI)
3
+ # forge-executable: tea
4
+ #
5
+ # Input: CODEV_PR_TITLE (required)
6
+ # CODEV_PR_BODY (required, may be empty)
7
+ # CODEV_PR_BASE, CODEV_PR_HEAD, CODEV_PR_REPO, CODEV_PR_DRAFT (optional)
8
+ # CODEV_PR_LOGIN (optional, gitea-only: tea's --login, for multi-login hosts)
9
+ # Output: {"number": <int>, "url": "<web url>"}
10
+ #
11
+ # Creates the PR with the raw REST passthrough (`tea api -X POST …/pulls`),
12
+ # which RETURNS THE CREATED PR — number and `html_url` — in its response body.
13
+ # There is no lookup afterwards, so there is nothing to race and nothing to
14
+ # paginate.
15
+ #
16
+ # Why not `tea pulls create`: it prints a rendered, line-wrapped, ANSI-decorated
17
+ # view of the new PR rather than anything parseable, so it has to be followed by
18
+ # a search for the PR it just made. That search was the bug. #1146 established
19
+ # that Gitea caps every list response at the server's `max_response_items`
20
+ # (default 50 — confirmed against Forgejo 15.0.2: `settings/api` reports 50, and
21
+ # a `?limit=200` request returns exactly 50 items where paging at 50 returns 53),
22
+ # so `tea pulls list --limit 200` silently truncates. On a busy repo the
23
+ # just-created PR falls off the first page and this script would have reported
24
+ # failure for a PR that exists — inviting a duplicate retry.
25
+ #
26
+ # Verified against tea 0.14.2 + Forgejo 15.0.2:
27
+ # - POST repos/{owner}/{repo}/pulls returns the full PR object, incl. `number`
28
+ # and `html_url` (its `url` is the browser page too on create, unlike the GET
29
+ # shape where `url` is the API endpoint — hence `.html_url // .url`).
30
+ # - `{owner}`/`{repo}` are substituted by tea from the repo context, and
31
+ # `--repo <owner>/<name>` supplies that context when the cwd has no Gitea
32
+ # remote. Verified with https and scp-style Gitea remotes.
33
+ # - `base` is REQUIRED by the API (it answers `[Base]: Required` without it),
34
+ # unlike `tea pulls create`, which defaults it client-side. So when
35
+ # CODEV_PR_BASE is unset this resolves the repo's default branch first.
36
+ # - `draft: true` in the payload is SILENTLY IGNORED (the response comes back
37
+ # `draft: false`). Gitea marks a PR draft by a `WIP:` title prefix, which is
38
+ # exactly what `tea pulls create --draft` does, so that is replicated here.
39
+ # - `tea api` EXITS 0 on HTTP errors, printing the error body (JSON with
40
+ # `.message`, or bare text like `404 page not found`). Failure therefore has
41
+ # to be detected by inspecting the response, never by exit status — see the
42
+ # assertion below, which is the load-bearing check in this script.
43
+ set -e
44
+
45
+ if [ -z "$CODEV_PR_TITLE" ]; then
46
+ echo "pr-create: CODEV_PR_TITLE is required" >&2
47
+ exit 2
48
+ fi
49
+
50
+ # An empty body is allowed; an *absent* one is not. Without this, forgetting the
51
+ # variable opens a PR with no body at exit 0 — the silent failure #1455 is about.
52
+ if [ -z "${CODEV_PR_BODY+x}" ]; then
53
+ echo "pr-create: CODEV_PR_BODY is required (set it to \"\" for an empty body)" >&2
54
+ exit 2
55
+ fi
56
+
57
+ head=$CODEV_PR_HEAD
58
+ if [ -z "$head" ]; then
59
+ head=$(git rev-parse --abbrev-ref HEAD)
60
+ fi
61
+
62
+ # Gitea has no draft flag on the API; a "WIP:" title prefix is the draft marker.
63
+ title=$CODEV_PR_TITLE
64
+ if [ "$CODEV_PR_DRAFT" = "1" ]; then
65
+ title="WIP: $CODEV_PR_TITLE"
66
+ fi
67
+
68
+ # Shared tea options: which host/repo to talk to. `{owner}`/`{repo}` in the
69
+ # endpoint are filled from this context.
70
+ set --
71
+ if [ -n "$CODEV_PR_REPO" ]; then set -- "$@" --repo "$CODEV_PR_REPO"; fi
72
+ if [ -n "$CODEV_PR_LOGIN" ]; then set -- "$@" --login "$CODEV_PR_LOGIN"; fi
73
+
74
+ base=$CODEV_PR_BASE
75
+ if [ -z "$base" ]; then
76
+ # The API rejects a missing base, so mirror `tea pulls create`'s default.
77
+ base_response=$(tea api "$@" 'repos/{owner}/{repo}') || base_response=
78
+ base=$(printf '%s' "$base_response" | jq -r '.default_branch // empty' 2>/dev/null) || base=
79
+ if [ -z "$base" ]; then
80
+ # An unresolvable repo (404 — same cause as the POST path below) means the
81
+ # actual remedy is CODEV_PR_REPO, not CODEV_PR_BASE; only a resolvable repo
82
+ # with a genuinely missing default_branch calls for CODEV_PR_BASE.
83
+ case "$base_response" in
84
+ 404*)
85
+ echo "pr-create: Gitea answered 404 — could not resolve owner/repo for this repository." >&2
86
+ echo "pr-create: set CODEV_PR_REPO=owner/repo, or run from a checkout whose remote is a configured Gitea host." >&2
87
+ ;;
88
+ *)
89
+ echo "pr-create: could not resolve the repository's default branch; set CODEV_PR_BASE" >&2
90
+ ;;
91
+ esac
92
+ exit 1
93
+ fi
94
+ fi
95
+
96
+ # Build the payload with jq so an arbitrary body — quotes, backticks, newlines,
97
+ # backslashes, unicode — is JSON-escaped rather than interpolated, and feed it
98
+ # on stdin (`-d @-`) so it never has to survive an argv round-trip.
99
+ response=$(
100
+ jq -n --arg title "$title" --arg body "$CODEV_PR_BODY" \
101
+ --arg head "$head" --arg base "$base" \
102
+ '{title: $title, body: $body, head: $head, base: $base}' \
103
+ | tea api "$@" -X POST -d @- 'repos/{owner}/{repo}/pulls'
104
+ )
105
+
106
+ # `tea api` exits 0 on HTTP errors, so the response body is the ONLY signal that
107
+ # the PR was created. Trusting the exit code here would reintroduce exactly the
108
+ # silent success #1455 exists to kill — this time in the fix for it. So assert
109
+ # the response really is a PR object: an object carrying a numeric `number` AND
110
+ # a non-empty browser URL. Anything else is a failure, however it is dressed.
111
+ result=$(printf '%s' "$response" | jq -c '
112
+ if type != "object" then empty
113
+ elif (.number | type) != "number" then empty
114
+ else
115
+ (.html_url // .url) as $url
116
+ | if ($url | type) == "string" and ($url | length) > 0
117
+ then {number: .number, url: $url}
118
+ else empty
119
+ end
120
+ end
121
+ ' 2>/dev/null) || result=
122
+
123
+ if [ -z "$result" ]; then
124
+ # A numeric `number` with no usable URL means the PR WAS created and only the
125
+ # URL is missing. Say so, and name the number: the operator must not read this
126
+ # as "nothing happened" and retry into a duplicate.
127
+ number=$(printf '%s' "$response" | jq -r '
128
+ if type == "object" and (.number | type) == "number" then .number else empty end
129
+ ' 2>/dev/null) || number=
130
+ if [ -n "$number" ]; then
131
+ echo "pr-create: PR #$number WAS CREATED, but the Gitea response carried no browser URL." >&2
132
+ echo "pr-create: do not retry — that would open a duplicate. Response: $response" >&2
133
+ exit 1
134
+ fi
135
+
136
+ message=$(printf '%s' "$response" | jq -r '.message // empty' 2>/dev/null) || message=
137
+ if [ -n "$message" ]; then
138
+ echo "pr-create: Gitea refused to create the PR: $message" >&2
139
+ else
140
+ case "$response" in
141
+ 404*)
142
+ # `{owner}`/`{repo}` stayed unsubstituted, so tea requested `repos//pulls`.
143
+ # Name the remedy rather than leaving a bare 404 (cf. `_lib.sh#gitea_repo`).
144
+ echo "pr-create: Gitea answered 404 — could not resolve owner/repo for this repository." >&2
145
+ echo "pr-create: set CODEV_PR_REPO=owner/repo, or run from a checkout whose remote is a configured Gitea host." >&2
146
+ ;;
147
+ *)
148
+ echo "pr-create: unexpected response from the Gitea API: $response" >&2
149
+ ;;
150
+ esac
151
+ fi
152
+ exit 1
153
+ fi
154
+
155
+ printf '%s\n' "$result"
@@ -1,6 +1,34 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: pr-exists (Gitea via tea CLI)
3
- # Returns true for open or merged pulls only. Closed-not-merged pulls are excluded.
4
- # --state all fetches pulls in all states; without it, only open pulls are returned.
5
- # Gitea: merged PRs have state="closed" + merged=true; abandoned PRs have state="closed" + merged=false
6
- tea pulls list --state all --fields index --output json | jq "[.[] | select(.head.ref == \"$CODEV_BRANCH_NAME\" and (.state == \"open\" or (.state == \"closed\" and .merged == true)))] | length > 0"
3
+ # forge-executable: tea
4
+ # Input: CODEV_BRANCH_NAME
5
+ # Output: "true" or "false"
6
+ #
7
+ # Returns true for OPEN or MERGED pulls only; closed-not-merged pulls are
8
+ # excluded. `tea pulls list` emits `.head` as a string (not `{ref}`) and reports
9
+ # merged PRs as state "merged" with no `.merged` boolean, so its output can't
10
+ # satisfy the `.head.ref` / `.merged` predicate below. Route through the raw
11
+ # REST passthrough, whose PR objects carry nested `.head.ref` and a `.merged`
12
+ # bool. `tea api` needs an explicit owner/repo in the path (unlike `tea pulls`,
13
+ # which auto-detects it from the local git remote), so resolve it here: honor
14
+ # CODEV_REPO when set, else derive owner/repo from origin's URL (handles https,
15
+ # ssh, and scp-style remotes, with or without a .git suffix).
16
+ #
17
+ # Caveat (Gitea behavior, not a codev bug): for a merged PR whose source branch
18
+ # was deleted, Gitea returns `.head.ref == "refs/pull/N/head"` instead of the
19
+ # original branch name, so a branch-name match won't hit a merged+deleted
20
+ # branch. That doesn't affect the "does an open/merged PR exist for the branch
21
+ # I'm about to push" use case.
22
+ #
23
+ # `state=all` is paginated (Gitea caps a page at max_response_items, default 50)
24
+ # so a branch whose PR isn't in the most recent ~50 would false-negative and
25
+ # block a porch pr_exists gate — tea_api_paged walks every page (see _lib.sh).
26
+ . "$(dirname "$0")/_lib.sh"
27
+ REPO="$(gitea_repo)" || exit 1
28
+ # Capture the paginator's output before piping to jq: in POSIX sh (no
29
+ # pipefail), a `cmd | jq` pipeline reports jq's exit status (0) even when
30
+ # `cmd` failed mid-walk, which would surface a real error as a false-negative
31
+ # "no PR exists" and silently pass a porch pr_exists gate.
32
+ PULLS="$(tea_api_paged "repos/${REPO}/pulls" "state=all")" || exit 1
33
+ printf '%s' "$PULLS" | jq --arg branch "$CODEV_BRANCH_NAME" \
34
+ '[.[] | select(.head.ref == $branch and (.state == "open" or .merged == true))] | length > 0'
@@ -1,36 +1,45 @@
1
1
  #!/bin/sh
2
- # Forge concept: pr-list (Gitea via tea CLI)
2
+ # Forge concept: pr-list (Gitea via tea CLI) — open pulls
3
+ # forge-executable: tea
4
+ # Output: JSON [{number, title, url, reviewDecision, body, createdAt, author,
5
+ # reviewRequests, isDraft}] (PrListItem in forge-contracts.ts)
3
6
  #
4
- # Normalize tea's PR shape to the GitHub-compatible shape codev expects
5
- # (see PrListItem in codev/src/lib/forge-contracts.ts):
6
- # index -> number (int)
7
- # description -> body
8
- # created -> createdAt
9
- # author (string) -> author.login
10
- # reviewDecision -> "" (Gitea has no GitHub-equivalent review-decision summary)
11
- # reviewRequests -> [] (verified against tea 0.14.1: `pulls list` exposes
12
- # no `reviewers` field, and its JSON output is limited
13
- # to the selectable `--fields`, so requested reviewers
14
- # are unreachable here. The VSCode sort silently skips
15
- # the review-requested bucket when empty.)
16
- # isDraft -> false (verified: tea 0.14.1 `pulls list` exposes no
17
- # `draft` field among its selectable `--fields`.)
18
- # The underlying Gitea API PR object does carry `draft` and `requested_reviewers`,
19
- # but only the raw `tea api` passthrough can reach them — populating these two
20
- # fields for Gitea would mean reworking this concept onto `tea api`, which is a
21
- # separate, larger change than #787's scope.
22
- exec tea pulls list --limit 200 \
23
- --fields index,title,state,author,url,created,description \
24
- --output json \
25
- | jq '[.[] | {
26
- number: (.index | tonumber),
7
+ # `tea pulls list --fields …,description` errors ("invalid field 'description'")
8
+ # and its flattened output can't carry a PR body, draft flag, or requested
9
+ # reviewers. Route through the raw REST passthrough instead, whose PR objects
10
+ # expose all of them. `tea api` needs an explicit owner/repo in the path (unlike
11
+ # `tea pulls`, which auto-detects it from the local git remote), so resolve it
12
+ # here: honor CODEV_REPO when set, else derive owner/repo from origin's URL
13
+ # (handles https, ssh, and scp-style remotes, with or without a .git suffix).
14
+ #
15
+ # Field mapping:
16
+ # .number -> number (already an int in the REST shape)
17
+ # .html_url -> url (browser page; Gitea `.url` is the API endpoint)
18
+ # .body -> body
19
+ # .created_at -> createdAt
20
+ # .user.login -> author.login
21
+ # .requested_reviewers[].login -> reviewRequests (user logins; teams have no login → dropped)
22
+ # .draft -> isDraft
23
+ # reviewDecision -> "" (Gitea has no GitHub-equivalent review-decision summary)
24
+ #
25
+ # The open-pulls list is paginated (Gitea caps a page at max_response_items,
26
+ # default 50), so tea_api_paged walks every page rather than silently truncating
27
+ # at ~50 open PRs (see _lib.sh).
28
+ . "$(dirname "$0")/_lib.sh"
29
+ REPO="$(gitea_repo)" || exit 1
30
+ # Capture the paginator's output before piping to jq: in POSIX sh (no
31
+ # pipefail), a `cmd | jq` pipeline reports jq's exit status (0) even when
32
+ # `cmd` failed mid-walk, which would silently truncate the list instead of
33
+ # surfacing the error.
34
+ PULLS="$(tea_api_paged "repos/${REPO}/pulls" "state=open")" || exit 1
35
+ printf '%s' "$PULLS" | jq '[.[] | {
36
+ number,
27
37
  title,
28
- state,
29
- url,
38
+ url: (.html_url // .url),
30
39
  reviewDecision: "",
31
- body: (.description // ""),
32
- createdAt: .created,
33
- author: {login: .author},
34
- reviewRequests: [],
35
- isDraft: false
40
+ body: (.body // ""),
41
+ createdAt: .created_at,
42
+ author: {login: .user.login},
43
+ reviewRequests: [ (.requested_reviewers // [])[] | .login // empty ],
44
+ isDraft: (.draft // false)
36
45
  }]'
@@ -1,6 +1,67 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: pr-view (Gitea via tea CLI)
3
- # Sets `url` to the PR's browser page (`html_url`). Gitea's own `url` field is
4
- # the API endpoint (would render raw JSON in a browser), so we prefer `html_url`
5
- # and fall back to the existing `url` only if `html_url` is absent.
6
- tea pulls view "$CODEV_PR_NUMBER" --output json | jq '.url = (.html_url // .url)'
3
+ # forge-executable: tea
4
+ # Input: CODEV_PR_NUMBER
5
+ # Output: JSON {title, body, state, url, author{login}, baseRefName, headRefName,
6
+ # additions, deletions} (see PrViewResult in forge-contracts.ts)
7
+ #
8
+ # `tea pulls view N --output json` returns a table header / empty list rather
9
+ # than the PR object, so route through the raw REST passthrough. `tea api`
10
+ # needs an explicit owner/repo in the path (unlike `tea pulls`, which
11
+ # auto-detects it from the local git remote), so resolve it here: honor
12
+ # CODEV_REPO when set, else derive owner/repo from origin's URL (handles
13
+ # https, ssh, and scp-style remotes, with or without a .git suffix).
14
+ #
15
+ # `url` is the PR's browser page (`html_url`). Gitea's own `url` field is the
16
+ # API endpoint (would render raw JSON in a browser), so map `html_url` and fall
17
+ # back to `url` only if it's absent — the same choice PIR #1179 made when this
18
+ # concept still went through `tea pulls view`.
19
+ #
20
+ # SHAPE VALIDATION. `tea api` exits 0 on an HTTP error and prints the error
21
+ # BODY, e.g. {"message":"pull does not exist [index: 42]","url":"…/swagger"}.
22
+ # Normalizing that unchecked produced a structurally valid, entirely wrong
23
+ # contract object at exit 0 — every field null except `url`, which took the
24
+ # error body's own `url` (the swagger link) and shipped it to callers as the
25
+ # PR's browser page. Required fields are type-checked first and anything else
26
+ # is a hard failure carrying the server's message on stderr.
27
+ . "$(dirname "$0")/_lib.sh"
28
+ REPO="$(gitea_repo)" || exit 1
29
+ # Capture before piping: in POSIX sh (no pipefail) a `tea api … | jq` pipeline
30
+ # reports jq's exit status, so a failed fetch would surface as jq's exit-0 on
31
+ # empty stdin rather than an error.
32
+ PR="$(tea api "repos/${REPO}/pulls/${CODEV_PR_NUMBER}")" || exit 1
33
+ # An empty body at exit 0 would give jq nothing to work on: it emits no output
34
+ # and exits 0, so the script would "succeed" with empty stdout instead of
35
+ # failing. The validator below never runs on an empty response, so guard here.
36
+ if [ -z "$PR" ]; then
37
+ echo "gitea forge: empty \`tea api\` response for pull ${CODEV_PR_NUMBER}" >&2
38
+ exit 1
39
+ fi
40
+ printf '%s' "$PR" | jq '
41
+ if (type == "object")
42
+ and ((.number | type) == "number")
43
+ and ((.title | type) == "string")
44
+ and ((.state | type) == "string")
45
+ and ((.user.login | type) == "string")
46
+ and ((.base.ref | type) == "string")
47
+ and ((.head.ref | type) == "string")
48
+ then .
49
+ else
50
+ ("gitea forge: unexpected `tea api` response for pull "
51
+ + (env.CODEV_PR_NUMBER // "?") + ": "
52
+ + ((.message // .) | tostring)
53
+ + "\n") | halt_error(1)
54
+ end
55
+ | {
56
+ title,
57
+ body: (.body // ""),
58
+ state,
59
+ url: (.html_url // .url),
60
+ author: {login: .user.login},
61
+ baseRefName: .base.ref,
62
+ headRefName: .head.ref,
63
+ # Type-checked, not just defaulted: the contract types these as numbers, and
64
+ # a non-numeric value here would be passed straight through.
65
+ additions: (if (.additions | type) == "number" then .additions else 0 end),
66
+ deletions: (if (.deletions | type) == "number" then .deletions else 0 end)
67
+ }'
@@ -1,27 +1,97 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: recently-merged (Gitea via tea CLI)
3
+ # forge-executable: tea
4
+ # Input: CODEV_SINCE_DATE (optional, ISO-8601 timestamp)
5
+ # Output: JSON [{number, title, url, body, createdAt, mergedAt, headRefName}]
6
+ # (MergedPrItem in forge-contracts.ts)
3
7
  #
4
- # `tea pulls list --state closed` returns both merged PRs and closed-without-
5
- # merge PRs. Filter to merged only via `.merged == true` (the same predicate
6
- # scripts/forge/gitea/pr-exists.sh already relies on), then map to the
7
- # GitHub-compatible shape:
8
- # index -> number (int)
9
- # created -> createdAt
10
- # updated -> mergedAt (tea exposes no merged_at field via --fields;
11
- # close-then-edit overestimates merged time
12
- # but is acceptable for the 24h overview window)
13
- # head.ref -> headRefName
14
- # description -> body
15
- exec tea pulls list --state closed --limit 1000 \
16
- --fields index,title,state,author,url,created,updated,head,description,merged \
17
- --output json \
18
- | jq '[.[] | select(.merged == true) | {
19
- number: (.index | tonumber),
8
+ # `tea pulls list --fields …,head,description,merged` errors on the `description`
9
+ # field and emits `.head` as a string, so it can't populate `body` or
10
+ # `.head.ref`. Route through the raw REST passthrough instead, whose closed
11
+ # pulls carry `.merged`, `.merged_at`, nested `.head.ref`, and `.body`. Keep
12
+ # only merged pulls (closed-without-merge have `.merged == false`). `tea api`
13
+ # needs an explicit owner/repo in the path (unlike `tea pulls`, which
14
+ # auto-detects it from the local git remote), so resolve it here: honor
15
+ # CODEV_REPO when set, else derive owner/repo from origin's URL (handles https,
16
+ # ssh, and scp-style remotes, with or without a .git suffix).
17
+ #
18
+ # The closed-pulls list is paginated (Gitea caps a page at max_response_items,
19
+ # default 50), so on a busy repo the most-recent merges could push older ones
20
+ # past the first page — tea_api_paged walks every page (see _lib.sh).
21
+ #
22
+ # CODEV_SINCE_DATE BOUNDS THE WALK. This concept feeds a 24h analytics window,
23
+ # but the closed-pulls list is the repo's whole merge history: on an
24
+ # established repo an unbounded walk issues up to GITEA_MAX_PAGES sequential
25
+ # requests inside forge's 30s timeout, and a timeout yields `null` — a worse
26
+ # outcome for the dashboard than truncation was. When CODEV_SINCE_DATE is set
27
+ # we ask the server for update-time-descending order and stop at the first page
28
+ # that reaches back past the cutoff.
29
+ #
30
+ # The stop filter does not take the sort on trust. What it actually needs is
31
+ # the ORDERING, not the parameter, so it checks for the ordering directly and
32
+ # fires only when all of this holds:
33
+ # - the current page is non-increasing in `updated_at`,
34
+ # - the PREVIOUS page was too, and its oldest entry is no older than this
35
+ # page's newest — i.e. the order survives a page boundary, so page-local
36
+ # sorting or a coincidentally-descending first page isn't enough,
37
+ # - some entry on this page predates the cutoff.
38
+ # Never on page 1: with nothing to compare against, one internally-descending
39
+ # page proves nothing about the pages behind it. Costing one extra request is
40
+ # the right trade against dropping a merge.
41
+ #
42
+ # Given that ordering, `updated_at >= merged_at` (a merge updates the PR) means
43
+ # nothing merged after the cutoff can sit beyond the first page whose update
44
+ # times have fallen behind it. A server that ignores `sort=recentupdate` fails
45
+ # these checks and we fall back to the full walk — slower, never wrong.
46
+ #
47
+ # Timestamps go through `gitea_epoch` because Gitea emits RFC3339 in the
48
+ # server's timezone, not necessarily `Z`.
49
+ . "$(dirname "$0")/_lib.sh"
50
+ REPO="$(gitea_repo)" || exit 1
51
+
52
+ if [ -n "$CODEV_SINCE_DATE" ]; then
53
+ QUERY="state=closed&sort=recentupdate"
54
+ # `$prev` is the previous page, bound by tea_api_paged (null on page 1).
55
+ STOP="${GITEA_JQ_LIB}"'
56
+ def descending: . as $a | [ range(($a | length) - 1) | $a[.] >= $a[. + 1] ] | all;
57
+ def times: [ .[] | (.updated_at | gitea_epoch) ];
58
+
59
+ times as $t
60
+ | ($prev | if . == null then null else times end) as $p
61
+ | (env.CODEV_SINCE_DATE | gitea_epoch) as $since
62
+ | ($since != null)
63
+ and ($p != null)
64
+ and (($t | length) > 0) and (($p | length) > 0)
65
+ and ([ $t[] | . != null ] | all) and ([ $p[] | . != null ] | all)
66
+ and ($t | descending) and ($p | descending)
67
+ and (($p | min) >= ($t | max))
68
+ and (($t | min) < $since)
69
+ '
70
+ else
71
+ QUERY="state=closed"
72
+ STOP=""
73
+ fi
74
+
75
+ # Capture the paginator's output before piping to jq: in POSIX sh (no
76
+ # pipefail), a `cmd | jq` pipeline reports jq's exit status (0) even when
77
+ # `cmd` failed mid-walk, which would silently truncate the list instead of
78
+ # surfacing the error.
79
+ PULLS="$(tea_api_paged "repos/${REPO}/pulls" "$QUERY" "$STOP")" || exit 1
80
+ printf '%s' "$PULLS" | jq "${GITEA_JQ_LIB}"'
81
+ (env.CODEV_SINCE_DATE | gitea_epoch) as $since
82
+ | [ .[]
83
+ | select(.merged == true)
84
+ # Drop merges older than the cutoff. An unparseable/absent timestamp on
85
+ # either side keeps the item — the caller filters the window again, so
86
+ # over-reporting is harmless where dropping a real merge is not.
87
+ | select($since == null
88
+ or ((.merged_at | gitea_epoch) as $m | $m == null or $m >= $since))
89
+ | {
90
+ number,
20
91
  title,
21
- state,
22
- url,
23
- body: (.description // ""),
24
- createdAt: .created,
25
- mergedAt: .updated,
92
+ url: (.html_url // .url),
93
+ body: (.body // ""),
94
+ createdAt: .created_at,
95
+ mergedAt: .merged_at,
26
96
  headRefName: (.head.ref // "")
27
97
  }]'
@@ -1,3 +1,32 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: user-identity (Gitea via tea CLI)
3
- tea whoami --output json | jq -r ".login"
3
+ # forge-executable: tea
4
+ # Output: plain text username
5
+ #
6
+ # `tea whoami` has no `--output json` flag (its only documented option is
7
+ # --help), so it can't feed a jq pipeline. Route through the raw REST
8
+ # passthrough instead: `tea api user` returns the Gitea `User` object, whose
9
+ # `.login` is the authenticated username (mirrors `gh api user --jq .login`).
10
+ #
11
+ # SHAPE VALIDATION. `tea api` exits 0 on an HTTP error and prints the error
12
+ # BODY, which has no `.login` — `jq -r .login` then printed the literal string
13
+ # "null" at exit 0, and callers took that as the current user's handle. Check
14
+ # for a non-empty string login first and fail with the server's message
15
+ # otherwise. The response is captured before the pipe because POSIX sh has no
16
+ # pipefail: `tea api user | jq` would report jq's status, not tea's.
17
+ USER_JSON="$(tea api user)" || exit 1
18
+ # jq given empty input emits nothing and exits 0, so an empty body at exit 0
19
+ # would leave the script "succeeding" with no username at all.
20
+ if [ -z "$USER_JSON" ]; then
21
+ echo "gitea forge: empty \`tea api user\` response" >&2
22
+ exit 1
23
+ fi
24
+ printf '%s' "$USER_JSON" | jq -r '
25
+ if (type == "object") and ((.login | type) == "string")
26
+ and ((.login | test("^\\s*$")) | not)
27
+ then .login
28
+ else
29
+ ("gitea forge: unexpected `tea api user` response: "
30
+ + ((.message // .) | tostring)
31
+ + "\n") | halt_error(1)
32
+ end'
@@ -1,5 +1,8 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: issue-view (GitHub via gh CLI)
3
3
  # Input: CODEV_ISSUE_ID
4
- # Output: JSON {title, body, state, url, comments[]}
5
- exec gh issue view "$CODEV_ISSUE_ID" --json title,body,state,url,comments
4
+ # Output: JSON {title, body, state, url, author, createdAt, assignees, labels, milestone, comments[]}
5
+ # author/createdAt/assignees/labels/milestone are optional by contract; `gh`
6
+ # emits them in the neutral shapes the IssueView contract expects (author.login,
7
+ # assignees[].login, labels[].name, milestone.title or null when unset).
8
+ exec gh issue view "$CODEV_ISSUE_ID" --json title,body,state,url,author,createdAt,assignees,labels,milestone,comments
@@ -0,0 +1,47 @@
1
+ #!/bin/sh
2
+ # Forge concept: pr-create (GitHub via gh CLI)
3
+ # forge-executable: gh
4
+ #
5
+ # Input: CODEV_PR_TITLE (required)
6
+ # CODEV_PR_BODY (required, may be empty)
7
+ # CODEV_PR_BASE, CODEV_PR_HEAD, CODEV_PR_REPO, CODEV_PR_DRAFT (optional)
8
+ # Output: {"number": <int>, "url": "<web url>"}
9
+ #
10
+ # `gh pr create` prints the new PR's URL on stdout; the number is its last path
11
+ # segment. Anything gh writes that isn't a URL (hints, prompts) is ignored.
12
+ set -e
13
+
14
+ if [ -z "$CODEV_PR_TITLE" ]; then
15
+ echo "pr-create: CODEV_PR_TITLE is required" >&2
16
+ exit 2
17
+ fi
18
+
19
+ # An empty body is allowed; an *absent* one is not. Without this, forgetting the
20
+ # variable opens a PR with no body at exit 0 — the silent failure #1455 is about.
21
+ if [ -z "${CODEV_PR_BODY+x}" ]; then
22
+ echo "pr-create: CODEV_PR_BODY is required (set it to \"\" for an empty body)" >&2
23
+ exit 2
24
+ fi
25
+
26
+ set -- --title "$CODEV_PR_TITLE" --body "$CODEV_PR_BODY"
27
+ if [ -n "$CODEV_PR_BASE" ]; then set -- "$@" --base "$CODEV_PR_BASE"; fi
28
+ if [ -n "$CODEV_PR_HEAD" ]; then set -- "$@" --head "$CODEV_PR_HEAD"; fi
29
+ if [ -n "$CODEV_PR_REPO" ]; then set -- "$@" --repo "$CODEV_PR_REPO"; fi
30
+ if [ "$CODEV_PR_DRAFT" = "1" ]; then set -- "$@" --draft; fi
31
+
32
+ url=$(gh pr create "$@" | grep -E '^https?://' | tail -n 1)
33
+
34
+ if [ -z "$url" ]; then
35
+ echo "pr-create: gh pr create returned no PR URL" >&2
36
+ exit 1
37
+ fi
38
+
39
+ number=${url##*/}
40
+ case "$number" in
41
+ '' | *[!0-9]*)
42
+ echo "pr-create: could not parse a PR number from '$url'" >&2
43
+ exit 1
44
+ ;;
45
+ esac
46
+
47
+ printf '{"number":%s,"url":"%s"}\n' "$number" "$url"
@@ -2,4 +2,6 @@
2
2
  # Forge concept: pr-search (GitHub via gh CLI)
3
3
  # Input: CODEV_SEARCH_QUERY
4
4
  # Output: JSON [{number, headRefName, baseRefName}]
5
- exec gh pr list --search "$CODEV_SEARCH_QUERY" --json number,headRefName,baseRefName
5
+ # --state all is required so the search includes merged/closed PRs; without it
6
+ # `gh pr list` defaults to --state open and post-merge lookups return nothing (#759).
7
+ exec gh pr list --state all --search "$CODEV_SEARCH_QUERY" --json number,headRefName,baseRefName
@@ -1,6 +1,26 @@
1
1
  #!/bin/sh
2
2
  # Forge concept: issue-view (GitLab via glab CLI)
3
- # Adds a `url` field mapped from GitLab's `web_url` (the issue's browser page);
4
- # all other fields glab emits are passed through unchanged. `. + {url: …}` is
5
- # non-destructive — if `web_url` is absent, `url` is null (optional by contract).
6
- glab issue view "$CODEV_ISSUE_ID" --output json | jq '. + {url: .web_url}'
3
+ # Maps GitLab's fields into the forge-neutral IssueView shape. `. + {…}` is
4
+ # non-destructive for keys we don't touch; the mapped keys are optional by
5
+ # contract, so a shape mismatch degrades to an omitted line rather than a crash
6
+ # (jq returns null for missing keys, and the guards below tolerate null):
7
+ # web_url -> url
8
+ # author.username -> author.login
9
+ # created_at -> createdAt
10
+ # assignees[].username -> assignees[].login
11
+ # labels[] -> labels[].name (newer glab emits label objects; older emits strings)
12
+ # milestone.title -> milestone.title (null when unset)
13
+ # Best-effort preset (Spec 589): fields GitLab doesn't expose stay absent. The
14
+ # contract's optional fields are added only when present (via `+ {…}` merges) so
15
+ # an absent field is OMITTED, not emitted as null — `author?`/`createdAt?` are not
16
+ # nullable (only `milestone` is). `exec` is dropped: with a pipe it would replace
17
+ # only the glab subshell, not the whole pipeline, so it's misleading here.
18
+ glab issue view "$CODEV_ISSUE_ID" --output json | jq '
19
+ .
20
+ + {url: .web_url}
21
+ + {assignees: [(.assignees // [])[] | {login: .username}]}
22
+ + {labels: [(.labels // [])[] | if type == "string" then {name: .} else {name: .name} end]}
23
+ + (if .author.username then {author: {login: .author.username}} else {} end)
24
+ + (if .created_at then {createdAt: .created_at} else {} end)
25
+ + (if .milestone then {milestone: {title: .milestone.title}} else {} end)
26
+ '